Merge pull request #206 from CRTao/translate-tw

Translate curriculum to zh-tw
pull/1273/head
Jen Looper 4 years ago committed by GitHub
commit 796b437dc9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,193 @@
# 程式語言概論與必備工具
這堂課程解釋程式語言的基礎。這項主題能應用到當代多數的程式語言。關於必備工具的部分,你會學到許多實用的開發者軟體。
![關於程式語言](../webdev101-programming.png)
> 由[Tomomi Imura](https://twitter.com/girlie_mac) 繪製。
## 課前測驗
[課前測驗](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/1?loc=zh_tw)
## 大綱
在這堂課中,包含:
- 什麼是程式設計?
- 程式語言的種類
- 程式的基本架構
- 給專業開發者的實用軟體與工具
## 什麼是程式設計?
程式設計又稱作Coding是編寫電腦或手機裝置的指令過程。我們以裝置看得懂的程式語言來編寫這些指令這些指令集涉及到常見的*程式*、*電腦程式*、*應用程式(App)*或*執行檔*。
一個*程式*以程式碼任意創作出來,網頁、遊戲、手機應用都是程式的一種。雖然我們可以在不編寫程式碼的情況下建出程式,但裝置底下的邏輯概念還是以程式碼為主。一個*執行中*、*執行編碼*的程式都是仰賴著指令。你眼前正閱讀的文字就是由程式輸出到螢幕。
✅ 課外研究: 誰被認為是世界上第一位電腦工程師?
## 撰寫程式語言
編寫程式語言的主要目的是讓開發者得以指示裝置。裝置只能讀懂二元格式 ( 1 與 0 ),對於*大多數*的開發者而言,這並不是個很好理解溝通的方式。程式語言就像人類與電腦之間溝通的橋梁。
程式語言有不同的格式與滿足的目的。舉例來說JavaScript 常被用在網頁應用上,而 Bash 主要是用在作業系統上。
*低階語言(Low level languages)* 通常比 *高階語言(high level languages)* 要求更少的裝置指示步數。然而高階語言的閱讀性與支援性讓它成為最普及的程式語言。JavaScript 即是一種高階語言。
下列程式碼說明高階語言(JavaScript)與低階語言(ARM assembly code)的差異:
```javascript
let number = 10
let n1 = 0, n2 = 1, nextTerm;
for (let i = 1; i <= number; i++) {
console.log(n1);
nextTerm = n1 + n2;
n1 = n2;
n2 = nextTerm;
}
```
```c
area ascen,code,readonly
entry
code32
adr r0,thumb+1
bx r0
code16
thumb
mov r0,#00
sub r0,r0,#01
mov r1,#01
mov r4,#10
ldr r2,=0x40000000
back add r0,r1
str r0,[r2]
add r2,#04
mov r3,r0
mov r0,r1
mov r1,r3
sub r4,#01
cmp r4,#00
bne back
end
```
事實上,*他們都做一樣的事情* ── 印出前十項費氏數列。
✅ 費氏數列的[定義](https://zh.wikipedia.org/zh-tw/%E6%96%90%E6%B3%A2%E9%82%A3%E5%A5%91%E6%95%B0%E5%88%97)為該數是由之前的兩數相加而得出。初始值為 0 與 1。
## 程式的基本架構
程式中的一行指令(instruction)被稱做*陳述式(statement)*,通常會由特定符號或分行來決定結尾處,或是執行*程式終止*。程式終止的方式會依不同程式語言而有所不同。
大多數程式需要使用用戶或其他位置的資料,陳述式依照這些資料決定指令。資料會影響程式的運作方式,編寫程式語言便是一種暫時性儲存資料的途徑。這些資料稱為*變數(Variables)*。變數會指引資料儲存到裝置上的位置,它們就像代數一樣:有自己的名稱、數值隨時間改變。
有些情況裝置不會執行到陳述式。通常是開發者故意的選擇或意外性地輸入錯誤,讓應用程式變得更豐富且需要被維護。常見的發生情況在決定條件的時候,如當代程式語言以 `if..else` 條件式來決定程式的執行方式。
✅ 往後的課程會講解各式各樣的陳述式型態。
## 工欲善其事,必先利其器
[![謀生工具 Tools of the Trade](https://img.youtube.com/vi/69WJeXGBdxg/0.jpg)](https://youtube.com/watch?v=69WJeXGBdxg "謀生工具 Tools of the Trade")
在這個章節,你會學習到一些實用的軟體來開始你的開發旅程。
一個**開發環境**包含許多開發者撰寫程式需要用到的工具。它們會依照開發者的需求而不同,隨著時間、專案大小、程式語言而有所調整。每個開發者都有自己獨特的開發環境。
### 文字編輯器
文字編輯器可說是最重要的軟體開發工具。開發者可以在其中撰寫並執行程式。
還有許多原因讓開發者選擇文字編輯器:
- *偵錯(Debugging)* 在程式碼中一行一行地找尋錯誤。有些文字編輯器有偵錯的功能,可以依照不同的程式語言而調整。
- *語法突顯(Syntax highlighting)* 將程式碼加上顏色並自動排版,方便開發者閱讀。文字編輯器也支援語法突顯的客製化。
- *整合擴充插件(Extensions and Integrations)* 擴充插件不包含在預設的文字編輯器當中,開發者依照自己的需求建立並新增到文字編輯器當中。舉例來說,許多開發者需要統整程式文檔並註解這些檔案,他們就會加裝檢查拼字的插件。有些插件功能只支援特定的文字編輯器,文字編輯器也提供搜尋擴充插件的功能。
- *客製化(Customization)* 大多數的文字編輯器都允許開發者做客製化,開發者依照自己的習慣,建立自己順手的開發環境。其中也包含建立自己的擴充插件。
#### 常見文字編輯器與網頁開發插件
- [Visual Studio Code](https://code.visualstudio.com/)
- [Code Spell Checker](https://marketplace.visualstudio.com/items?itemName=streetsidesoftware.code-spell-checker)
- [Live Share](https://marketplace.visualstudio.com/items?itemName=MS-vsliveshare.vsliveshare-pack)
- [Prettier - Code formatter](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)
- [Atom](https://atom.io/)
- [spell-check](https://atom.io/packages/spell-check)
- [teletype](https://atom.io/packages/teletype)
- [atom-beautify](https://atom.io/packages/atom-beautify)
### 瀏覽器
另一款重要的工具是瀏覽器。網頁開發人員會藉由瀏覽器觀察程式的執行情況,瀏覽器也是常見的網頁編輯與檢視器,顯示來自檔案的可見物件,如 HTML 檔。
許多瀏覽器都附加*開發者工具(DevTools)*,內含實用的功能協助開發者蒐集與取得重要的程式資訊。假設有一頁網頁出現錯誤,了解它出錯的時間與狀況就很重要,配置開發者工具即可蒐集這些資訊。
#### 常見瀏覽器與網頁開發工具
- [Edge](https://docs.microsoft.com/microsoft-edge/devtools-guide-chromium?WT.mc_id=academic-13441-cxa)
- [Chrome](https://developers.google.com/web/tools/chrome-devtools/)
- [Firefox](https://developer.mozilla.org/docs/Tools)
### 常用指令
有些開發者偏好較少的介面,通常會使用指令來完成工作;撰寫程式碼要求大量的文字輸入,有些開發者偏好以不中斷文字輸入為首要條件,常用快捷鍵做視窗與檔案的切換。多數工作能以滑鼠操作,但為了減少鍵盤與滑鼠間的切換,指令輸入會是實踐上較合適的方式。另一項指令輸入的好處是它們彈性很高,隨時可以更新設定,甚至移植到其他機器上。每一位開發者有各自的開發習慣,開發環境也有所不同。
### 常用指令選項
指令選項(Command Line Options)會依不同的作業系統而有所不同。
*💻 表示預設已安裝在作業系統上。*
#### Windows
- [Powershell](https://docs.microsoft.com/powershell/scripting/overview?view=powershell-7?WT.mc_id=academic-13441-cxa) 💻
- [Command Line](https://docs.microsoft.com/windows-server/administration/windows-commands/windows-commands?WT.mc_id=academic-13441-cxa) (又稱作 CMD) 💻
- [Windows Terminal](https://docs.microsoft.com/windows/terminal/?WT.mc_id=academic-13441-cxa)
- [mintty](https://mintty.github.io/)
#### MacOS
- [Terminal](https://support.apple.com/guide/terminal/open-or-quit-terminal-apd5265185d-f365-44cb-8b09-71a064a42125/mac) 💻
- [iTerm](https://iterm2.com/)
- [Powershell](https://docs.microsoft.com/powershell/scripting/install/installing-powershell-core-on-macos?view=powershell-7?WT.mc_id=academic-13441-cxa)
#### Linux
- [Bash](https://www.gnu.org/software/bash/manual/html_node/index.html) 💻
- [KDE Konsole](https://docs.kde.org/trunk5/en/applications/konsole/index.html)
- [Powershell](https://docs.microsoft.com/powershell/scripting/install/installing-powershell-core-on-linux?view=powershell-7?WT.mc_id=academic-13441-cxa)
#### 其他常用指令
- [Git](https://git-scm.com/) (💻 已支援大多數的作業系統。)
- [NPM](https://www.npmjs.com/)
- [Yarn](https://classic.yarnpkg.com/en/docs/cli/)
### 技術文件
若開發人員想學新的事物,技術文件會是很好的幫手。他們會參照文件來學習如何使用工具與新的程式語言,並瞭解如何鑽研更深入的用法。
#### 常用的網頁開發文件
- [Mozilla Developer Network](https://developer.mozilla.org/docs/Web)
- [Frontend Masters](https://frontendmasters.com/learn/)
✅ 研究項目: 現在你已經了解基本的網頁開發環境了。請比較「網頁開發環境」與「網頁設計環境」之間的差異。
---
## 🚀 挑戰
比較不同的程式語言: JavaScript 與 Java 間有什麼獨特的特徵? 那 COBOL 與 Go 呢?
## 課後測驗
[課後測驗](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/2?loc=zh_tw)
## 複習與自學
學習不同的程式語言。試著在三種不同的程式語言寫幾行程式碼。你學到了什麼?
## 作業
[閱讀技術文件](assignment.zh-tw.md)

@ -0,0 +1,11 @@
# 閱讀技術文件
## 說明
網頁開發人員所需要使用的工具套件可以參考 [MDN Client端技術開發文件](https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Understanding_client-side_tools/Overview)。 請選擇三種未被本課程提及的工具,試解釋為何網頁開發人員使用它,並搜尋其相關的工具與它的技術文件。這些資料不能出現在前述的 MDN 文件當中。
## 學習評量
| 優良 | 普通 | 待改進 |
| ------------------------------ | -------------------- | ------------------------------ |
| 解釋為何網頁開發人員會用此工具 | 只解釋工具的使用方法 | 未提及工具的使用意義與使用方法 |

@ -0,0 +1,319 @@
# GitHub 簡介
這堂課程講述一個提供加設與管理程式碼的平台 ── Github的基本功能。
![GitHub 簡介](../images/webdev101-github.png)
> 由[Tomomi Imura](https://twitter.com/girlie_mac) 繪製
## 課前測驗
[課前測驗](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/3?loc=zh_tw)
## 大綱
在這堂課中,包含:
- 追蹤裝置上的工作專案
- 與其他人共同開發專案
- 如何貢獻網路上的開源軟體
### 開始之前
在你開始課程之前,你需要安裝 Git 這套套件。在你的終端機上輸入:
`git --version`
若你的裝置上沒有安裝 Git[請下載並安裝 Git](https://git-scm.com/downloads)。 安裝完之後,請設定裝置本地 Git 的使用者設定。
* `git config --global user.name "your-name"`
* `git config --global user.email "your-email"`
要確認 Git 使用者設定是否完成,你可以輸入:
`git config --list`
此外,你需要一組 GitHub 的帳戶、一款文字編輯器 (如Visual Studio Code) 與你的終端機 (或 command prompt)。
若你缺少 GitHub 帳戶,請前往 [github.com](https://github.com/)建立並登入一組帳戶,遵循指示完成資料的填寫。
✅ GitHub 不是唯一的程式碼數據庫,但 GitHub 是家喻戶曉的。
### 課前準備
你需要在裝置(筆電或電腦)上建立程式專案的資料夾,與 GitHub 公共的數據庫(Public Repository)。之後的例子會使用到此公共數據庫來與他人分享程式碼。
---
## 程式碼管理
假設你的本地端資料夾存放著一些程式專案,你想利用 Git 來作專案追蹤與版本控制,甚至是對未來的你寫一封情書。在一天、一週甚至是一個月後閱讀你的提交紀錄,了解當初你的決定,回想之前的更動。前提是當初你有寫一條完整的提交紀錄。
### 課題:建立數據庫並提交程式碼
1. **在 GitHub 上建立數據庫** 在 GitHub.com 上,在 "Repositories" 的標籤或導航欄的右上方,找到 **new repo** 的按鈕。
1. 為你的數據庫資料夾取個名字。
1. 選擇 **建立數據庫(create repository)**.
1. **調查本地的專案資料夾** 在終端機中開啟儲存程式碼的資料夾,在你想追蹤的目錄下輸入:
```bash
cd [資料夾名稱]
```
1. **初始化 git 數據庫(repo)** 在目錄下輸入:
```bash
git init
```
1. **檢查狀態** 若想檢查目前數據庫的狀態,輸入:
```bash
git status
```
它會輸出類似以下的訊息:
```output
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: file.txt
modified: file2.txt
```
指令 `git status` 會回報那些檔案已經準備 _被存到_ 數據庫或是被更動過但不想更新上去。
1. **開始追蹤所有檔案**
新增檔案到暫存區(staging area)。
```bash
git add .
```
`git add` 加上路徑 `.` 表示追蹤該路徑下所有的檔案。
1. **只追蹤選擇的檔案**
```bash
git add [檔案或資料夾路徑]
```
上述指令幫助你只追蹤被選取的檔案,允許使用者分批提交。
1. **取消追蹤所有檔案**
```bash
git reset
```
上述指令能取消追蹤(unstage)暫存區的檔案。
1. **取消追蹤特定的檔案**
```bash
git reset [檔案或資料夾路徑]
```
上述指令只取消追蹤單一檔案,在下一次提交時不被圈選在裡面。
1. **保存工作狀態** 現在,已經有一些檔案被標記在 _暫存區(staging area)_。 Git 會追蹤區域內的檔案。若要保存這些檔案的狀態,你需要 _提交(commit)_ 這些檔案。 _提交_ 會記錄當下數據庫中檔案的狀態到歷史紀錄中。 你需要指令`git commit` 完成這項工作。
```bash
git commit -m "first commit"
```
這會提交暫存區內的檔案,"first commit"即提交紀錄。提交紀錄最好能識別出這次的提交主要做了那些更動。
1. **連接本地數據庫到 GitHub 遠端數據庫** 現在 Git 數據庫已經成功運行在你的本地裝置上,但有時候你希望能將檔案備份到其他地方,或是邀請他人參與這項程式專案。 GitHub 便是一個好地方。先前我們已經建立遠端數據庫在 GitHub 上,現在我們只要連接本地數據庫到 GitHub上。指令 `git remote add` 能完成這項課題:
> 注意,在輸入指令前,你需要取得 GitHub 遠端數據庫的 URL 位置。請將下列的 `repository_name` 替換為你的遠端數據庫路徑。
```bash
git remote add origin https://github.com/username/repository_name.git
```
這會在 GitHub 遠端數據庫上建立一個名叫 "origin" 的 _遠端位置(remote)_ ,或稱 _連接(connection)_
1. **上傳本地檔案到 GitHub** 現在已經建好了遠端與本地的 _連接_。 利用指令 `git push` 可以將本地檔案上傳到遠端數據庫當中:
```bash
git push -u origin main
```
所有的提交都會加到 GitHub 上 "main" 的分支當中。
1. **增加更多的更動** 若之後再對程式碼有所更動、提交並上傳到 GitHub 上,只要輸入:
```bash
git add .
git commit -m "type your commit message here"
git push
```
> 提示:建立 `.gitignore` 檔案可以讓你自動排除的特定檔案項目不被 GitHub 追蹤。好比是有一個在同一個目錄下的筆記檔不想被上傳到遠端數據庫。以下是 `.gitignore` 的參考版型: [.gitignore 參考版型](https://github.com/github/gitignore)。
#### 提交紀錄(Commit Messages)
一條好的 Git 提交標題行最好滿足下列條件:
提交完後,提交紀錄會顯示 <你的標題>
標題使用祈使語句,如使用 "change" 而非 "changed" 或 "changes"。
同理地,內文(選擇性)也使用祈使語句。內文須包含改動的動機與改動前後的差異。你需要解釋「為什麼改」而非「怎麼改」。
✅ 花點時間在 GitHub 上閒晃。你能找到很棒的提交紀錄嗎? 你能找到簡潔的提交紀錄嗎? 哪些資訊是你認為一個提交紀錄要有的重要資訊?
### 課題:多人合作
另一個將專案上傳到 GitHub 的主要原因是讓其他開發者能參與其中。
## 與其他人共同開發專案
在你的遠端資料庫中,前往 `Insights > Community` 來對比你的專案與其他推薦社群專案。
以下是一些你的 GitHub 數據庫需要精進的地方:
- **專案描述(Description)** 你有為你的專案新增描述嗎?
- **README** 你有新增 README 嗎? GitHub 提供編寫 README 的指引與參考: [README](https://docs.github.com/articles/about-readmes/)
- **開發指引(Contributing guideline)** 你的專案內有[開發指引](https://docs.github.com/articles/setting-guidelines-for-repository-contributors/)嗎?
- **行為準則(Code of Conduct)** [行為準則](https://docs.github.com/articles/adding-a-code-of-conduct-to-your-project/)
- **授權條款License** 這或許是最重要的:[授權條款](https://docs.github.com/articles/adding-a-license-to-a-repository/)
這些資源對剛加入到專案的新成員有所幫助。這些是新的合作夥伴比看程式碼還優先查詢的地方。完善它們能有效縮減他人消化的時間。
✅ README 檔,雖然多數人都會配置,但忙碌的開發者都會疏於管理。 你能在社群中找到相關的例子嗎? 這邊有[關於建立 READMEs 的有利工具](https://www.makeareadme.com/)可以嘗試。
### 課題:合併程式碼
開發指引文件幫助他人了解如何共同開發專案。它提供專案需要被貢獻的部分與該如何運作。共同開發者需要經過下列步驟來與他人在 GitHub 共同開發專案:
1. **分叉(Fork)專案** 你或許希望別人能 _分叉(fork)_ 你的專案。 分叉代表別人建立一份你的專案副本到他人的 GitHub 數據庫中。
1. **複製(Clone)** 複製專案到他人的本地裝置中。
1. **建立分支(branch)** 依照工作需求建立 _分支(branch)_
1. **專注在他人投入的工作範圍** 要求他人只專注在單一課題上,這樣能提升他們工作 _合併(merge)_ 的機會。想像他們在修正錯誤,同時又新增新功能、更新測試機制......這時如果你只想合併其中的一個、或者是兩個功能呢?
✅ 想像一個情況:一個重要的分支是編輯與分享的主軸,它能被如何應用?
> 注意,在做更動前,記得建立新的分支。任何提交都會在你所在的分支上,指令 `git status` 可以檢查你現在所在的分支。
現在,我們以共同開發者的角度來看。假設開發者已經 _分支__複製_ 了他人的數據庫到自己的 Git 數據庫上,準備開始編輯檔案:
1. **建立新的分支** 利用指令 `git branch` 來建立新的分支,只做相關的工作改動。
```bash
git branch [分支名稱]
```
1. **切換到該工作分支** 使用指令 `git checkout` 來切換到特定分支,更新分支的檔案狀態:
```bash
git checkout [分支名稱]
```
1. **程式設計** 記得追蹤你所更改的地方,利用下列的指令來告訴 Git
```bash
git add .
git commit -m "my changes"
```
請確保提交都有適當的名稱,對管理者與你自己都有好處。
1. **將工作分支與 `main` 分支進行合併** 當工作完成時,你會需要將工作分支與 `main` 分支進行合併。 `main` 分支可能會被他人更新,在合併之前記得更新主分支:
```bash
git checkout main
git pull
```
這項步驟可能會面臨到 _衝突(conflicts)_,代表 Git 無法將本地的更動作 _合併(combine)_ 。此時你需要執行下列的指令:
```bash
git checkout [分支名稱]
git merge main
```
這會將所有 `main` 分支的改動加入到你的本地目錄中。若出現狀況VS Code會告訴你 Git 會對衝突的檔案感到 _困惑(confused)_ 你需要判斷哪一項檔案或程式碼才是最適當的選擇。
1. **將你的成果上傳到 GitHub** 這代表著兩件事:將分支推到你的遠端數據庫以及準備建立 Pull Request(PR)。
```bash
git push --set-upstream origin [分支名稱]
```
上述的指令會在分叉的數據庫中新增分支。
1. **建立 PR** 藉由造訪分叉的數據庫中建立 PRGitHub 會指示你是否要建立 PR之後要填寫提交紀錄以及編寫詳細的說明。讓管理者了解你做了哪些更動並進行 _交叉比對(fingers crossed)_。 他們會感激你的貢獻並 _合併(merge)_ 你的 PR。完成這步後你就成為了專案貢獻者恭喜
1. **清理專案** 在 PR 被成功合併後, _清除專案(clean up)_ 會是一個好習慣。 你需要清除你的本地分支以及你的遠端數據庫分支。首先,你可以利用下列的指令清除本地分支:
```bash
git branch -d [分支名稱]
```
之後,請確保在 GitHub 頁面上刪除遠端分支。
`Pull request` 要求更新更動到「自己」的專案數據庫,這看起來很蠢。但管理者與核心組員必須謹慎地考量你的更動才能合併到專案的主分支中。這便是向管理者請求上傳許可。
一個 PR 提供比對以及討論的地方,解釋分支的意義、確認程式的合理性、留言與測試……等等。一個好的 PR 必須參照前述所說的提交紀錄準則。若你的 PR 有解決特定的 issue記得標記在 PR 當中。使用 `#` 接在數字前面來標記 issue 編號,如 `#97`
🤞 交叉比對每個程式環節都正確無誤後,專案管理者才合併你所作的更動 🤞
若要從 GitHub 遠端數據庫更新到目前的本地工作分支,使用:
`git pull`
## 如何貢獻網路上的開源軟體
首先,尋找一個你感興趣的數據庫,你會複製一份副本到自己的裝置上。
✅ 對新手而言,尋找「適合新手」的數據庫可以[搜尋 'good-first-issue' 標籤](https://github.blog/2020-01-22-browse-good-first-issues-to-start-contributing-to-open-source/)。
![複製數據庫到本地](../images/clone_repo.png)
有許多方式來複製數據庫。 一種是利用 "clone" 整個數據庫的內容。可以使用 HTTPS、SSH 或是 GitHub CLI (Command Line Interface)。
打開終端機並輸入下列指令來複製數據庫:
`git clone https://github.com/ProjectURL`
複製完後記得切換到正確的資料夾當中:
`cd ProjectURL`
你也可以利用[Codespaces](https://github.com/features/codespaces)來打開專案,一款嵌入在 GitHub 中的雲端開發環境,或是使用[GitHub Desktop](https://desktop.github.com/)。
最後,你也可以下載數據庫的壓縮檔。
### 有關 GitHub 的小知識
你可以為別人打星星(star)、追蹤(watch)或分叉(fork)任何一個 GitHub 上的共享數據庫。打上星星的數據庫會出現在右上方的導航欄中。就像是書籤,但是是給程式碼用的。
專案內會有 issue 追蹤器。大多數的 issue 會在 GitHub "Issue" 的標籤內(有些Issue會由作者另外說明),供大家進行討論。 Pull Requests 標籤內會有正在討論與審核的程式更動。
專案也會有討論區、寄信功能以及聊天室如 Slack、Discord 或 IRC。
✅ 花點時間觀察你的新專案,試著更新設定、新增描述、或架構成一個大型專案(像個大看板一樣!)。你可以創造出任何東西!
---
## 🚀 挑戰
找朋友一起編輯彼此的程式。共同建立一項專案、分叉程式、建立分支、合併更動。
## 課後測驗
[課後測驗](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/4?loc=zh_tw)
## 複習與自學
了解更多:
[貢獻開源軟體](https://opensource.guide/how-to-contribute/#how-to-submit-a-contribution)
[Git cheatsheet](https://training.github.com/downloads/github-git-cheat-sheet/)
練習,練習,再練習! GitHub 有提供很好的學習管道:[lab.github.com](https://lab.github.com/)
- [第一週在 GitHub 上](https://lab.github.com/githubtraining/first-week-on-github)
你能找到更資深的實驗內容。
## 作業
完成[第一週在 GitHub 上](https://lab.github.com/githubtraining/first-week-on-github)

@ -0,0 +1,231 @@
# 建立無障礙網頁
![關於網頁親和力](../webdev101-a11y.png)
> 由 [Tomomi Imura](https://twitter.com/girlie_mac) 繪製
## 課前測驗
[課前測驗](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/5?loc=zh_tw)
> 網路的強大在於它的普遍性。無論用戶是否有殘疾,讓大家無差別地使用網路是必要的。
>
> The power of the Web is in its universality. Access by everyone regardless of disability is an essential aspect.
>
> \- Tim Berners-Lee 爵士 ── 全球資訊網協會總監暨網際網路創立者
這段話完美地詮釋建立無障礙網頁的重要性。應用程式若無法服務所有人是建立在排他性設計。身為一位網頁開發員,我們需要時刻警惕著網頁親和力。從此時開始,你接下來創建的網頁要能服務給任何人。在這堂課中,你會學習開發無障礙網頁相關的工具與觀念。
## 相關工具
### 螢幕報讀器 (Screen readers)
螢幕報讀器可說是最知名的無障礙工具之一。
[螢幕報讀器](https://zh.wikipedia.org/wiki/%E8%9E%A2%E5%B9%95%E9%96%B1%E8%AE%80%E5%99%A8)已被廣泛地幫助視覺障礙者。就像我們花了不少時間確保瀏覽器能呈現網頁內容,我們也要確保螢幕報讀器能完整地實現相同的工作。
基本上,螢幕報讀器會一行一行地報讀網頁的內容。若網頁都是以文字建構而成,那螢幕報讀器就能達到與瀏覽器相同的功效。當然,網頁不可能只有文字:裡面可能會有連結、圖像、顏色或是其他視覺物件。我們必須確保螢幕報讀器也能呈現出上述物件。
任何一位網頁開發者需要熟悉螢幕報讀器。它們是用戶的好幫手,就像你的瀏覽器一樣,你需要學習螢幕報讀器是如何運作的。幸運的是,它們都內建在大部分的作業系統當中。
有些瀏覽器內建相關工具與擴充插件,報讀出文字甚至是可互動物件:例如[這些以網頁親和力為目標的 Edge 瀏覽器工具](https://support.microsoft.com/zh-tw/microsoft-edge/microsoft-edge-%E7%9A%84%E5%8D%94%E5%8A%A9%E5%B7%A5%E5%85%B7%E5%8A%9F%E8%83%BD-4c696192-338e-9465-b2cd-bd9b698ad19a)。 它們是重要的輔助工具,或許與螢幕報讀器有所差異,但基本上能測試螢幕報讀器的基本功能。
✅ 嘗試使用螢幕報讀器或瀏覽器朗讀工具。 在 Windows 中,[Narrator](https://support.microsoft.com/zh-tw/windows/%E5%AE%8C%E6%95%B4%E7%9A%84%E6%9C%97%E8%AE%80%E7%A8%8B%E5%BC%8F%E6%8C%87%E5%8D%97-e4397a0d-ef4f-b386-d8ae-c172f109bdb1)內建在系統中,使用者也可以選擇[JAWS](https://webaim.org/articles/jaws/)與[NVDA](https://www.nvaccess.org/about-nvda/)等額外安裝程式。 macOS 與 iOS 平台上則內建[VoiceOver](https://support.apple.com/guide/voiceover/welcome/10)。
### 放大器
另一項為視覺障礙者提供的輔助程式為放大器。最常見的定點放大功能,可以藉由 `Control + 加號(+)` 或降低螢幕解析度來完成。這個步驟會重新縮放整個網頁,確保網頁的[互動式設計](https://developer.mozilla.org/en-US/docs/Learn/CSS/CSS_layout/Responsive_Design)是縮放頁面重要的一環。
其他放大功能會專注在小部分的區域上,這些額外軟體提供類似於實體放大器的功能。 Windows 內建[放大器](https://support.microsoft.com/zh-tw/windows/%E4%BD%BF%E7%94%A8%E6%94%BE%E5%A4%A7%E9%8F%A1%E4%BB%A5%E8%AE%93%E8%9E%A2%E5%B9%95%E4%B8%8A%E7%9A%84%E5%85%A7%E5%AE%B9%E6%9B%B4%E5%AE%B9%E6%98%93%E7%9C%8B%E5%88%B0-414948ba-8b1c-d3bd-8615-0e5e32204198)功能;第三方程式[ZoomText](https://www.freedomscientific.com/training/zoomtext/getting-started/)則受到廣泛用戶的使用。 macOS 與 iOS 內建[Zoom](https://www.apple.com/accessibility/mac/vision/)軟體。
### 對比度檢查器
我們必須謹慎地挑選網頁的顏色對比度,讓色盲人士或低視能障礙者有辦法閱讀文章。
✅ 利用相關的瀏覽器擴充插件來測試看看你所喜愛的網頁,例如[WCAG's color checker](https://microsoftedge.microsoft.com/addons/detail/wcag-color-contrast-check/idahaggnlnekelhgplklhfpchbfdmkjp?hl=zh-tw)。 你學到了什麼?
### Lighthouse
在瀏覽器的開發者工具中,你能找到一款工具名為 Lighthouse。這項工具可以有效的確認網頁親和力。這邊不強制網頁都要經過 Lighthouse 審核,但 100% 的分數是網頁完美的基礎。
✅ 在你的瀏覽器開發工具中找尋 Lighthouse ,對任何網頁做分析。 你發現了什麼?
## 提升網頁親和力
網頁親和力是項相對龐大的主體。要尋求幫助,網路上有許多資源可以參考。
- [Accessible U - 明尼蘇達大學](https://accessibility.umn.edu/your-role/web-developers)
我們無法在此說明建立無障礙網頁的所有重點,但下列是開發時重要的核心觀念。從零開始就留意網頁親和力**一定會比**從完成的網頁來增進網頁親和力來的容易。
## 良好的呈現方式
### 安全的配色
每個人的觀點都有所不同,包含觀看顏色。如何選擇配色也是網頁親和力的課題之一。下列是一款實用的[安全配色生產工具](http://colorsafe.co/).
✅ 舉出在配色上有問題的一個網頁,告訴大家為什麼?
### 使用合理的 HTML 語法
在 CSS 與 JavaScript 的支援下,我們無法判斷網頁物件的控制目的。 好比 `<span>` 也可以建立 `<button>` `<b>` 可以代表超連結。 在造型變化上或許較為容易,但可能會對螢幕報讀器產生混淆。我們必須確保對應的控制有相同的標籤,例如超連結只使用 `<a>`。這才能完整的表現 HTML 的語義化。
✅ 去任何一個網頁分析開發員與設計員是否使用正確的 HTML 語法。 你能找到語法將連結取代成按鈕嗎? 小提示:在瀏覽器中點擊「右鍵」 > 「檢視網頁原始碼」來確認程式碼。
### 建立分級的描述性標題的
螢幕報讀器使用者[非常仰賴文字標題](https://webaim.org/projects/screenreadersurvey8/#finding),確認並查詢網頁資訊。使用描述性標題並有層次的標籤分別得以創建出較易閱讀的網頁。
### 使用合理的視覺指引
CSS 提供完整的網頁造型控制,你可以讓文字框框線隱藏或是讓超連結去除底線。然而去除這些視覺指引會造成視覺障礙者閱讀上的困擾,難以辨別它們原本的功能性。
## 連結文字的重要性
超連結是網頁中重點物件。請確保螢幕報讀器能正確的辨識網頁中的超連結。
### 螢幕報讀器處理連結
可想而知,螢幕報讀器對於連結與文字的朗讀方式是相同的。因此,判斷下列文章例子,哪個對於網頁親和力上較為合理?
> 小藍企鵝,又稱做仙企鵝,是世界上最小的企鵝。[點擊這裡](https://zh.wikipedia.org/zh-tw/%E5%B0%8F%E8%97%8D%E4%BC%81%E9%B5%9D)了解更多資訊。
> 小藍企鵝,又稱做仙企鵝,是世界上最小的企鵝。造訪 https://zh.wikipedia.org/zh-tw/%E5%B0%8F%E8%97%8D%E4%BC%81%E9%B5%9D 了解更多資訊。
> **注意** 讀完之後,請盡量**避免**上方的連結例子。
記得,螢幕報讀器處理物件的方式會與瀏覽器有所不同。
### 使用 URL 的問題
螢幕報讀器會朗讀文字。如果 URL 出現在文章中,它也會被裝置朗讀。綜觀來說,朗讀 URL 不含有任何意義,而且聽來不悅耳。你可能曾在手機上聽過含有網址的文章朗讀。
### 使用「點擊這裡」的問題
螢幕報讀器有能力去處理超連結文字,就像我們尋找網頁中的連結一樣。想像所有的超連結文字都表示成「點擊這裡」,用戶就會重複聽到「點擊這裡!點擊這裡!點擊這裡!點擊這裡......」,會造成別人的困擾。
### 合理的連結文字
合理的連結文字為連結與文字找到了平衡。像上方例子中的小藍企鵝,連結是導向維基百科的物種頁面。文字*小藍企鵝*就會是完美的連結文字,讓用戶了解點擊後會取得關於小藍企鵝的相關資訊。
> [小藍企鵝](https://zh.wikipedia.org/zh-tw/%E5%B0%8F%E8%97%8D%E4%BC%81%E9%B5%9D),又稱做仙企鵝,是世界上最小的企鵝。
✅ 花點時間逛逛網頁,觀察它們如何設定連結的導向。比較較合適的連結文字,你學到了什麼?
#### 關於搜尋引擎
網頁中的搜尋引擎對網頁親和力有加分的效果,搜尋引擎也能從連結找到你的網頁,了解網頁的主題。所以連結文字對大家都有幫助!
### ARIA
想像下方的網頁:
| 產品 | 描述 | 數量 |
| ------------ | ----------- | ----------- |
| 物品 | [描述]('#') | [數量]('#') |
| 超優物品 | [描述]('#') | [數量]('#') |
在這項舉例中,瀏覽器用戶可以清楚地辨別重複性的「描述」與「數量」;但對於螢幕報讀器用戶而言,他們只能重複地聽取*「描述」與「數量」*這兩個單詞。
為了處理這種情況HTML 提供一套屬性名為[Accessible Rich Internet Applications (ARIA)](https://developer.mozilla.org/docs/Web/Accessibility/ARIA)。它能為螢幕報讀器附加額外的朗讀資訊。
> **注意** 就如 HTML 的各式物件,瀏覽器與螢幕報讀器能支援的項目也不盡相同。然而,客群主體基本上都支援 ARIA 屬性。
若網頁不支援部分格式語法,你可以使用 `aria-label` 來描述連結。就如下方物件被標註上 "Widget description" 的標籤。
``` html
<a href="#" aria-label="Widget description">description</a>
```
✅ 總體而言,使用語義化標籤得以取代 ARIA 功能,但是這些標籤無法全面性地覆蓋 HTML 的各式物件。樹(Tree)就是一種例子。沒有 HTML 物件能處理一棵樹,所以你可以在 `<div>` 元素中加上 aria 數值。[關於 ARIA 的 MDN 技術文件](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA)說明許多有用的資訊。
```html
<h2 id="tree-label">File Viewer</h2>
<div role="tree" aria-labelledby="tree-label">
<div role="treeitem" aria-expanded="false" tabindex="0">Uploads</div>
</div>
```
## 圖片
螢幕報讀器用戶無法直接的了解圖片內容。為了確保圖片也能服務給視覺障礙者,這就是 `alt` 屬性存在的目的。所有重要的圖片都需要有 `alt` 描述圖片的內容。
裝飾性圖片的 `alt` 欄位就需要留空字串:`alt=""`。 這樣能避免螢幕報讀器朗讀不必要的裝飾性圖片敘述。
✅ 你可能會猜到,搜尋引擎也有可能無法辨別圖片。它們也是使用 alt 敘述。因此,無障礙網頁有許多加分要素!
## 鍵盤輸入
有些用戶無法使用滑鼠或觸控板,他們需要使用鍵盤中的 "tab" 鍵來切換網頁元素讓網頁物件能順序性地排列與互動就很重要。如果網頁有語義化標籤並以CSS造型化視覺畫面網頁基本上就能以鍵盤來切換當然手動測試還是必要的。了解更多關於[鍵盤調查的策略](https://webaim.org/techniques/keyboard/)。
✅ 前往任何一個網頁,試著只以鍵盤瀏覽網頁。哪些能成功運作,你又遭遇了哪些困難? 你有辦法解釋嗎?
## 結論
能連上網頁並不是網際網路的真諦。從現在起確保網頁能以無障礙的方式呈現。雖然還有很多未被提及的開發資訊,熟習你知道的無障礙開發知識一定會對網頁有所幫助。
---
## 🚀 挑戰
看看下列的 HTML 程式碼。利用你所學到的知識,試著增進它的網頁親和力。
```html
<!DOCTYPE html>
<html>
<head>
<title>
Example
</title>
<link href='../assets/style.css' rel='stylesheet' type='text/css'>
</head>
<body>
<div class="site-header">
<p class="site-title">Turtle Ipsum</p>
<p class="site-subtitle">The World's Premier Turtle Fan Club</p>
</div>
<div class="main-nav">
<p class="nav-header">Resources</p>
<div class="nav-list">
<p class="nav-item nav-item-bull"><a href="https://www.youtube.com/watch?v=CMNry4PE93Y">"I like turtles"</a></p>
<p class="nav-item nav-item-bull"><a href="https://en.wikipedia.org/wiki/Turtle">Basic Turtle Info</a></p>
<p class="nav-item nav-item-bull"><a href="https://en.wikipedia.org/wiki/Turtles_(chocolate)">Chocolate Turtles</a></p>
</div>
</div>
<div class="main-content">
<div>
<p class="page-title">Welcome to Turtle Ipsum.
<a href="">Click here</a> to learn more.
</p>
<p class="article-text">
Turtle ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum
</p>
</div>
</div>
<div class="footer">
<div class="footer-section">
<span class="button">Sign up for turtle news</span>
</div><div class="footer-section">
<p class="nav-header footer-title">
Internal Pages
</p>
<div class="nav-list">
<p class="nav-item nav-item-bull"><a href="../">Index</a></p>
<p class="nav-item nav-item-bull"><a href="../semantic">Semantic Example</a></p>
</div>
</div>
<p class="footer-copyright">&copy; 2016 Instrument</span>
</div>
</body>
</html>
```
## 課後測驗
[課後測驗](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/6?loc=zh_tw)
## 複習與自學
許多政府機關立法要求無障礙需求。查詢你的國家是否有這些法規。它們涉及了那些,那些又沒包含在其中? 看看[英國政府機關網頁](https://accessibility.blog.gov.uk/)的例子。
## 作業
[分析一個非無障礙網頁](assignment.zh-tw.md)
參考出處: [Turtle Ipsum](https://github.com/Instrument/semantic-html-sample)

@ -0,0 +1,11 @@
# 分析一個非無障礙網頁
## 說明
辨別一個你認為對視覺障礙者不友善的網頁,列出網頁改進的策略。你需要先發掘各個網頁,不使用分析工具的情況下判斷網頁的親和力,再套用到 Lighthouse 審核工具。將網頁的審核結果轉化成至少十點的改進計畫。
## 學習評量
| 作業內容 | 優良 | 普通 | 待改進 |
| -------- | ----------------------------------------------------------------------------------- | --------------- | --------------- |
| 書面報告 | 文章解釋為何網頁需要被改進,將 Lighthouse 結果轉成 pdf 檔,列出十點詳細的改進計畫。 | 缺少 20% 的內容 | 缺少 50% 的內容 |

@ -0,0 +1,17 @@
# 網頁開發入門
這段課程會說明重要的程式開發觀念,不會以程式碼為導向,讓你成為出色的開發人員。
### 主題
1. [程式語言概論與必備工具](../1-intro-to-programming-languages/translations/README.zh-tw.md)
2. [GitHub 簡介](../2-github-basics/translations/README.zh-tw.md)
3. [建立無障礙網頁](../3-accessibility/translations/README.zh-tw.md)
### 參與人員
文章「無障礙網頁」是由 [Christopher Harrison](https://twitter.com/geektrainer) 用滿滿的 ♥️ 來編寫。
文章「GitHub 簡介」是由 [Floor Drees](https://twitter.com/floordrees) 用滿滿的 ♥️ 來編寫。
文章「建立無障礙網頁」是由 [Jasmine Greenaway](https://twitter.com/paladique) 用滿滿的 ♥️ 來編寫。

@ -0,0 +1,197 @@
# JavaScript 入門 - 資料型態
![JavaScript 入門 - 資料型態](../images/webdev101-js-datatypes.png)
> 由 [Tomomi Imura](https://twitter.com/girlie_mac) 繪製
## 課前測驗
[課前測驗](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/7?loc=zh_tw)
這堂課會講述 Javascript 的基礎 ── 一款建立互動性網頁的程式語言。
[![JavaScript 的資料型態](https://img.youtube.com/vi/JNIXfGiDWM8/0.jpg)](https://youtube.com/watch?v=JNIXfGiDWM8 "JavaScript 的資料型態")
讓我們從程式語言的基礎 ── 「變數」與「資料型態」開始吧!
## 變數 (Variable)
變數會依照程式碼調整並儲存數值。
建立並**宣告**變數的語法有一定的格式: **[關鍵字(keyword)] [變數名稱(name)]**。下列開始分別敘述:
- **關鍵字 (Keyword)** 關鍵字可以是 `let` 或者是 `var`
> 注意,關鍵字 `let` 在 ES6 被導入進去,為變數限制 _區塊範疇(block scope)_。 建議上使用 `let` 勝過於 `var`。我們在往後的章節會講述什麼是區塊範疇。
- **變數名稱 (Variable Name)** 你可以自己定義變數名稱。
### 課題:變數的操作
1. **宣告變數** 利用關鍵字 `let` 來宣告新的變數:
```javascript
let myVariable;
```
`myVariable` 已經被關鍵字 `let` 宣告出來了。它現在並沒有被賦予數值。
1. **賦予數值** 以運算子 `=` 來為變數新增數值,後面加上預期的數字。
```javascript
myVariable = 123;
```
> 注意:本堂課程中的運算子 `=` 只作為「指派運算子」,處理賦予數值的功用。它與「等於」並沒有關係。
變數 `myVariable` 現在已經被*初始化*為數值 123。
1. **代碼重構 (Refactor)** 改寫你的程式碼為:
```javascript
let myVariable = 123;
```
合併「宣告變數」與「賦予數值」為一條程式碼的步驟稱為 _顯式初始化 (explicit initialization)_
1. **更改變數數值** 透過下列的方式更改參數數值:
```javascript
myVariable = 321;
```
一旦變數被宣告後,你在任何往後的程式碼利用運算子 `=` 賦予新的數值。
✅ 動手試試看! 在瀏覽器中撰寫 JavaScript開啟瀏覽器並前往開發者工具你會在 Console 頁面找到命令提示字元。輸入 `let myVariable = 123` 並按下 Enter。輸入 `myVariable`。它出現了什麼? 往後,你會學到更多這些觀念的課程。
## 常數 (Constants)
宣告與初始化常數的行為與變數相似,只差在關鍵字 `const`。通常常數會以全大寫的方式命名。
```javascript
const MY_VARIABLE = 123;
```
常數與變數非常類似,主要的兩大差別為:
- **一定要附帶數值** 常數一定要被初始化過,否則在執行程式時會產生錯誤。
- **參考(Reference)不能被改變** 變數與常數都能根據其他變數或常數作為定義,但是常數的參考在初始化後,就不能再被更改,否則在執行程式時會產生錯誤。我們來看下列兩種例子:
- **簡單數值** 下列程式碼是不被允許的:
```javascript
const PI = 3;
PI = 4; // 錯誤
```
- **有限存取的參考物件** 下列程式碼是不被允許的:
```javascript
const obj = { a: 3 };
obj = { b: 5 } // 錯誤
```
- **參考物件** 下列程式碼是「允許」的:
```javascript
const obj = { a: 3 };
obj.a = 5; // OK
```
上述只改變物件的數值而非物件本身,這是可以被允許的。
> 注意,`const` 代表參考物件在重新賦值上有存取的限制。數值並不是永遠 _不可變的_,如果數值是規劃在其他物件上的話。
## 資料型態 (Data types)
變數可以儲存不一樣的數值型態,好比是數字或是文字。這些多樣的數值型態被稱為**資料型態**。資料型態是軟體開發上重要的環節,它能幫助開發者釐清程式該如何被填寫以及運作。除此之外,有些獨特的資料型態能改變或賦加而外的數值資訊。
✅ 資料型態也存在於 JavaScript 的原始結構中它們是程式語言中最低階的描述方式。六種資料型態string、number、bigint、boolean、undefined 和 symbol。 花點時間了解它們的含義。資料 `zebra` 屬於哪一類? 那 `0` 呢? `true` 呢?
### 數字 (Numbers)
在前一個段落中,`myVariable`的數值屬於數字型。
`let myVariable = 123;`
變數能儲存所有類型的數字,包含小數點與負數。數字也可以被套用在四則運算上,這被放在[下一個段落](#operators)中。
### 算術運算子 (Arithmetic Operators)
這些是做四則運算時會使用到的算術運算子,下列舉出了幾項例子:
| 符號 | 描述 | 舉例 |
| ------ | ------------------------------| --------------------- |
| `+` | **加法** 對兩數做相加 | `1 + 2 // 答案為 3` |
| `-` | **減法** 對兩數做相減 | `1 - 2 // 答案為 -1` |
| `*` | **乘法** 對兩數做相乘 | `1 * 2 // 答案為 2` |
| `/` | **除法** 對兩數做相除 | `1 / 2 // 答案為 0.5` |
| `%` | **餘數** 取得兩數相除的餘數 | `1 % 2 // 答案為 1` |
✅ 試試看!在瀏覽器命令欄中使用算數運算子。你得到了什麼結果?
### 字串 (Strings)
字串由多組字元組成,會以單引號或雙引號匡列起來。
- `'這是一組字串'`
- `"這也是一組字串"`
- `let myString = '這是被存在變數中的字串';`
記得使用引號來編寫字串,否則 JavaScript 會把字串內容當作是變數名稱。
### 字串格式化
字串由文字組成,自然需要隨時間而做修正。
要**串接**兩個以上的字串,可以使用運算子 `+`
```javascript
let myString1 = "Hello";
let myString2 = "World";
myString1 + myString2 + "!"; //HelloWorld!
myString1 + " " + myString2 + "!"; //Hello World!
myString1 + ", " + myString2 + "!"; //Hello, World!
```
✅ 在 JavaScript 中,為什麼 `1 + 1 = 2`,但是 `'1' + '1' = 11` 呢? 想想看。那 `'1' + 1` 呢?
**樣板字面值(Template literals)**是另一種格式化字串的方式,它不使用引號,而是使用反引號。任何非純文字字串必須放在 `${ }` 中。這會包入字串型態的任何變數。
```javascript
let myString1 = "Hello";
let myString2 = "World";
`${myString1} ${myString2}!` //Hello World!
`${myString1}, ${myString2}!` //Hello, World!
```
這樣就可以達到字串格式化的目的,但要注意樣板字面會遵守變數中的空格與分行符號。
✅ 什麼情況下該使用樣板字面,或者是純文字字串呢?
### 布林 (Booleans)
布林有兩種數值:`true` 或 `false`,使用在程式碼做條件決定的時候。在多數情況下,[運算子](#operators)可以套用在布林上,你會在初始化或更新數值時使用。
- `let myTrueBool = true`
- `let myFalseBool = false`
✅ 布林值 `true` 亦有廣義的 'truthy' 數值。有趣的是,在 JavaScript 中,[除非被定義為 falsy其餘的數值都會被當作是 truthy](https://developer.mozilla.org/en-US/docs/Glossary/Truthy)。
---
## 🚀 挑戰
JavaScript 在處理資料結構時有許多種方法,有些場合為人詬病。在這方面做一些調查,例如:大小寫敏感性的問題!在命令欄中輸入看看: `let age = 1; let Age = 2; age == Age`,輸出結果是 `false`,為什麼? 你能找到其他問題嗎?
## 課後測驗
[課後測驗](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/8?loc=zh_tw)
## 複習與自學
試試看[這些 JavaScript 練習題](https://css-tricks.com/snippets/javascript/)。 你學到了什麼?
## 作業
[練習資料型態](assignment.zh-tw.md)

@ -0,0 +1,11 @@
# 練習資料型態
## 簡介
想像你要建立一條購物清單,寫下你需要使用的資料型態。你是怎麼做出這些選擇的?
## 學習評量
| 作業內容 | 優良 | 普通 | 待改進 |
| -------- | ---------------------------------- | ---------------------------------- | ---------------------------------- |
| | 使用六種資料型態並解釋用途以及用法 | 使用四種資料型態並解釋用途以及用法 | 使用兩種資料型態並解釋用途以及用法 |

@ -0,0 +1,195 @@
# JavaScript 入門 - 函式與方法
![JavaScript 入門 - 函式](../images/webdev101-js-functions.png)
> 由 [Tomomi Imura](https://twitter.com/girlie_mac) 繪製
## 課前測驗
[課前測驗](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/9?loc=zh_tw)
撰寫程式碼時,我們必須確保程式碼的閱讀性。聽來不太直覺,理解程式碼的時間遠比撰寫時間來的久。裡面最需要被管理的程式項目就是**函式**。
[![函式與方法](https://img.youtube.com/vi/XgKsD6Zwvlc/0.jpg)](https://youtube.com/watch?v=XgKsD6Zwvlc "函式與方法")
> 點擊上方圖片觀看關於函式的影片。
## 函式 (Function)
函式是程式碼區塊,會在程式執行時被呼叫運行。有些時候我們需要重複性的執行同一項作業,比起複製整個邏輯到其他區塊,函式是較完美的處理方式。不只方便維護,也可以在任何地方、任何時間被其他函式呼叫執行。
另一項重點是函式的名稱,聽來不太重要,但它能直接地解釋程式碼的內容。你可以想像它是按鈕上的文字,若按鈕上寫著「停止計時」,你會預期按壓按鈕後會終止計時器的運作。
## 建立並呼叫函式
函式的語法格式如下:
```javascript
function nameOfFunction() { // 函式的定義
// 函式的說明與內容
}
```
如果你想建立一個打招呼的函式,它可能會以下列的格式呈現:
```javascript
function displayGreeting() {
console.log('Hello, world!');
}
```
如果你想呼叫這個函式,我們使用函式的名稱加上 `()`。我們不需要考慮函式是在被呼叫地方的前面或後面才被定義出來JavaScript 的編譯器會幫你尋找它的定義為置。
```javascript
// 呼叫函式
displayGreeting();
```
> **注意** 另一個你正使用的函式類型稱做 **方法(method)**。事實上,我們能在執行 `console.log` 的 demo 時能找到它。它與函式的差異在於它需要接續在物件後面,在這個例子中就是 `console`,而函式並沒有強制要求的。你會發現許多開發者在兩者之間做切換。
### 函式的重點觀念
在建立函式時,你需要注意一些重點:
- 我們反覆提到的,函式的名字要能了解函式的主要功能。
- 使用**駝峰式大小寫(camelCasing)**來連接單字。
- 單一函式只專一在單一功能。
## 向含式傳遞資料
為了讓函式能被重複利用,你會需要餵給函式不同的資料。以上述 `displayGreeting` 的例子中,它只能輸出文字 **Hello, world!**。這並不是個實用的函式。要增加函式的彈性,例如打招呼的對象,我們可以增加新的**參數(parameter/argument)**。它提供額外的資料給函式使用。
參數會寫在定義函式的地方,以括號與逗號標記與分隔:
```javascript
function name(param, param2, param3) {
}
```
現在我們更新函式 `displayGreeting`,讓它支援打招呼的對象:
```javascript
function displayGreeting(name) {
const message = `Hello, ${name}!`;
console.log(message);
}
```
當我們要呼叫函式時,輸入需要的參數在括號中:
```javascript
displayGreeting('Christopher');
// 呼叫完,印出字串 "Hello, Christopher!"
```
## 預設值(Default values)
我們利用參數增加了函式的彈性。但如果我們不想每次都要指定參數給函式使用呢? 繼續之前的例子,保留對象的名稱外,我們增加招呼語的種類。我們可以定義招呼語的預設值,若使用者沒有指定哪一種招呼語時,就使用預設值。它的方法就與賦予變數數值一樣 ── `parameterName = 'defaultValue'`。例如:
```javascript
function displayGreeting(name, salutation='Hello') {
console.log(`${salutation}, ${name}`);
}
```
當我們呼叫函式時,我們可以選擇是否要指定招呼語到 `salutation` 中。
```javascript
displayGreeting('Christopher');
// 輸出字串 "Hello, Christopher"
displayGreeting('Christopher', 'Hi');
// 輸出字串 "Hi, Christopher"
```
## 回傳值(Return values)
目前為止,我們的函式只能輸出字串到[console](https://developer.mozilla.org/en-US/docs/Web/API/console)上。這或許是我們希望的結果,尤其是需要呼叫其他服務的時候。萬一今天我想建立一個額外的函式負責做資料處理與運算呢?
此時,我們可以利用**回傳值**。回傳值由函式輸出,就像變數一樣儲存像是字串或是數字的結果。
如果函式有定義回傳值,那就需要使用關鍵字 `return` 。關鍵字 `return` 需要附帶回傳的數值或是參考物件在後方,如:
```javascript
return myVariable;
```
我們建立一個函式專門建立招呼訊息並回傳給呼叫者:
```javascript
function createGreetingMessage(name) {
const message = `Hello, ${name}`;
return message;
}
```
當函式被呼叫時,變數會儲存函式回傳的數值。這就像我們給變數定值一樣: `const name = 'Christopher'`
```javascript
const greetingMessage = createGreetingMessage('Christopher');
```
## 將函式作為函式參數使用
在你的程式旅程中,你會見到有函式將其他函式當作參數使用。這個俐落的手法常被用在一種情況:我們不知道 A 事件什麼時候發生與完成,但我們要在 A 事件後執行 B 事件。
舉例來說,考慮函式[setTimeout](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout),它會啟動計時機制,並在倒數完後執行下一個程式。我們需要告訴函式哪一個函式要在時間到後執行,一個完美的例子!
執行下方的程式,三秒鐘之後你會看到訊息**已經過三秒鐘**。
```javascript
function displayDone() {
console.log('已經過三秒鐘');
}
// 計時單位為毫秒。
setTimeout(displayDone, 3000);
```
### 不記名函式(Anonymous functions)
回顧我們所建的函式,這些函式都只被執行了一次。當程式越來越複雜,我們可能建了許多的函式,但他們可能都只被呼叫了一次。這並不是理想的方式,那不如,不要給它函式名稱!
我們可以傳遞函式作為參數使用,也可以直接在參數裡建立新的函式。同樣使用關鍵字 `function`,但我們寫在參數欄當中。
試著以不記名函式的方式改寫程式碼:
```javascript
setTimeout(function() {
console.log('3 seconds has elapsed');
}, 3000);
```
執行上述程式後可以得到相同的結果。我們建立了一個函式,一個沒有名字的函式!
### 箭頭函式(Fat arrow functions)
許多程式語言,包含 JavaScript都有一個常見的快捷語法稱作**箭頭(arrow/fat arrow)**函式。 它使用 `=>` 表示法,就像是箭頭一樣,如同它的名稱!使用 `=>` 可以省略關鍵字 `function`
再一次改寫程式碼,這次我們使用箭頭函式:
```javascript
setTimeout(() => {
console.log('3 seconds has elapsed');
}, 3000);
```
### 使用不同策略的時機
現在你已經學會了三種將函式作為參數的方法了。你可能會好奇使用它們的時機點為何。如果你知道你會重複使用一個函式,請使用正常的方法;如果你知道函式只用在特定的函式內一次,這就是用無記名函式的時機;箭頭函式與傳統 `function` 語法則是取決與你自己,但多數的開發者比較偏好使用 `=>`
---
## 🚀 挑戰
你能用一句話清楚地說明這些函式與方法的差別嗎? 試試看吧!
## 課後測驗
[課後測驗](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/10?loc=zh_tw)
## 複習與自學
這很值得去閱讀[關於箭頭函式的資料](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions),它們越來越常被用在程式碼上。試著寫個函式,再改寫成箭頭語法。
## 作業
[把玩函式](assignment.zh-tw.md)

@ -0,0 +1,13 @@
# 把玩函式
## 簡介
建立不同的函式,有的回傳數值,有的不回傳數值。
看看你是否能讓函式有多樣的參數輸入與參數預設值。
## 學習評量
| 作業內容 | 優良 | 普通 | 待改進 |
| -------- | ------------------------------------------ | -------------------------- | -------------- |
| | 建立兩個以上多樣參數輸入且功能豐富的函式。 | 建立一個有參數輸入的函式。 | 函式出現問題。 |

@ -0,0 +1,176 @@
# JavaScript 入門 - 做出決定
![JavaScript 入門 - 做出決定](../images/webdev101-js-decisions.png)
> 由 [Tomomi Imura](https://twitter.com/girlie_mac) 繪製
## 課前測驗
[課前測驗](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/11?loc=zh_tw)
為程式碼做決定與控制順序以提高其重複利用性與豐富性。這堂課程提供 JavaScript 的資料流控制語法與布林資料型的重點。
[![做出決定](https://img.youtube.com/vi/SxTp8j-fMMY/0.jpg)](https://youtube.com/watch?v=SxTp8j-fMMY "做出決定")
> 點擊上方圖片以觀賞關於程式做決定的影片。
## 回顧布林資料型
布林只能有兩種數值: `true``false`。 布林幫助程式碼間在特定的條件下做出決定。
設定布林值的方式如下:
`let myTrueBool = true`
`let myFalseBool = false`
✅ 布林的命名來源為一位英格蘭數學家、哲學家暨邏輯學家喬治·布爾 George Boole (18151864)。
## 關係運算子與布林
運算子會被使用在執行比較上,並回傳布林值。以下是常用的運算子列表。
| 符號 | 描述 | 舉例 |
| ----- | -------------------------------------------------------------------------- | ------------------ |
| `<` | **小於** 比較兩數,當左數小於右數時,回傳 `true`。 | `5 < 6 // true` |
| `<=` | **小於等於** 比較兩數,當左數小於右數或左數等於右數時,回傳 `true`。 | `5 <= 6 // true` |
| `>` | **大於** 比較兩數,當左數大於右數時,回傳 `true`。 | `5 > 6 // false` |
| `>=` | **大於等於** 比較兩數,當左數大於右數或左數等於右數時,回傳 `true`。 | `5 >= 6 // false` |
| `===` | **嚴格相等** 比較兩數,當左數等於右數且有相同的資料型態時,回傳 `true`。 | `5 === 6 // false` |
| `!==` | **嚴格不等於** 比較兩數,回傳值與嚴格相等恰恰相反。 | `5 !== 6 // true` |
✅ 藉由在瀏覽器命令欄中撰寫關係運算子來驗證你學到的知識。你有什麼意外發現嗎?
## If 條件式
當條件成立時If 條件式會執行區塊內的程式碼。
```javascript
if (condition){
//Condition was true. Code in this block will run.
}
```
邏輯運算子常被用在建立條件上。
```javascript
let currentMoney;
let laptopPrice;
if (currentMoney >= laptopPrice){
//Condition was true. Code in this block will run.
console.log("Getting a new laptop!");
}
```
## IF..Else 條件式
當條件不成立時,`else` 條件式會執行區塊內的程式碼。它可以和 `if` 條件式選擇性地使用。
```javascript
let currentMoney;
let laptopPrice;
if (currentMoney >= laptopPrice){
// if 條件成立時,這段程式碼會被執行。
console.log("Getting a new laptop!");
}
else{
// else 條件成立時,這段程式碼會被執行。
console.log("Can't afford a new laptop, yet!");
}
```
✅ 在瀏覽器命令欄中測試你是否了解上述程式碼的意義。改變變數 currentMoney 和 laptopPrice 的數值來取得不同的回傳值 `console.log()`
## 邏輯運算子與布林
有時候條件內容包含兩個以上的比較,邏輯運算子可以協助串接比較式來回傳布林值。
| 符號 | 描述 | 舉例 |
| ------ | -------------------------------------------------------------------------- | --------------------------------------------------------- |
| `&&` | **邏輯和** 比較兩個布林條件式,當兩者**皆**回傳 `true` 時,回傳 `true`。 | `(5 > 6) && (5 < 6 ) //左為false、右為true回傳 false。` |
| `\|\|` | **邏輯或** 比較兩個布林條件式,當其中一個回傳 `true` 時,回傳 `true`。 | `(5 > 6) \|\| (5 < 6) //左為false、右為true回傳 true。` |
| `!` | **邏輯非** 回傳布林條件式的相反回傳值。 | `!(5 > 6) // 5 不大於 6但 "!" 讓回傳值為 true。` |
## 邏輯運算子下的決定與條件
邏輯運算子可以用在建立 if..else 條件式中的條件。
```javascript
let currentMoney;
let laptopPrice;
let laptopDiscountPrice = laptopPrice - (laptopPrice * .20) //Laptop price at 20 percent off
if (currentMoney >= laptopPrice || currentMoney >= laptopDiscountPrice){
// if 條件成立時,這段程式碼會被執行。
console.log("Getting a new laptop!");
}
else {
// else 條件成立時,這段程式碼會被執行。
console.log("Can't afford a new laptop, yet!");
}
```
### 否定運算子
你已經了解如何在 `if...else` 條件式中建立條件。任何 `if` 條件需要決定它的是與否。利用運算子 `!`_否定_ 結果。它以下列方式呈現:
```javascript
if (!condition) {
// condition 為 false 時,執行。
} else {
// condition 為 true 時,執行。
}
```
### 條件運算子
`if...else` 並不是唯一表達條件式的方式。你也可以使用名為條件運算子的符號。語法如下:
```javascript
let variable = condition ? <條件成立時回傳 A> : <條件否定時回傳 B>
```
下列是較明確的例子:
```javascript
let firstNumber = 20;
let secondNumber = 10
let biggestNumber = firstNumber > secondNumber ? firstNumber: secondNumber;
```
✅ 花點時間閱讀這段程式碼。你能了解這些運算子的運作方式嗎?
上述的狀態為:
- 若 `firstNumber` 大於 `secondNumber`
- 則賦予 `firstNumber` 數值 `biggestNumber`
- 否則賦予為數值 `secondNumber`
條件運算子的另一種表達方式為:
```javascript
let biggestNumber;
if (firstNumber > secondNumber) {
biggestNumber = firstNumber;
} else {
biggestNumber = secondNumber;
}
```
---
## 🚀 挑戰
建立一個程式,使用邏輯運算式來做判斷,再將程式碼改寫成條件運算子。你喜歡用哪一種語法?
---
## 課後測驗
[課後測驗](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/12?loc=zh_tw)
## 複習與自學
閱讀更多可被使用的運算子:[MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators)。
瀏覽 Josh Comeau 所編制的[運算子查詢表](https://joshwcomeau.com/operator-lookup/)
## 作業
[運算子](assignment.zh-tw.md)

@ -0,0 +1,41 @@
# 運算子
## 簡介
把玩並熟悉運算子。以下是建議的練習程式模板:
你有兩套提供給同一組學生的成績系統。
### 第一套成績系統
定義分數為 1 到 5。當分數 3 以上時通過測驗。
### 第二套成績系統
定義分數為 `A, A-, B, B-, C, C-` `A` 為最高分而 `C` 為最低分,皆為通過測驗。
### 課題
使用下列矩陣 `allStudents` 表示所有學生的成績,試著建立新的矩陣 `studentsWhoPass` 包含所有通過學生的編號。
> 提示,使用 for-loop 迴圈、 if...else 條件式與比較運算子:
```javascript
let allStudents = [
'A',
'B-',
1,
4,
5,
2
]
let studentsWhoPass = [];
```
## 學習評量
| 作業內容 | 優良 | 普通 | 待改進 |
| -------- | ---------------------- | ------------------------ | ---------- |
| | 提供完整的解答與解釋。 | 只提供部分的解答與解釋。 | 解答有誤。 |

@ -0,0 +1,126 @@
# JavaScript 入門 - 矩陣與迴圈
![JavaScript 入門 - 矩陣](../images/webdev101-js-arrays.png)
> 由 [Tomomi Imura](https://twitter.com/girlie_mac) 繪製
## 課前測驗
[課前測驗](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/13?loc=zh_tw)
這堂課會包含程式語言 JavaScript建立互動式網頁的基礎。課程中你會學到用來操作資料的矩陣與迴圈。
[![矩陣與迴圈](https://img.youtube.com/vi/Q_CRM2lXXBg/0.jpg)](https://youtube.com/watch?v=Q_CRM2lXXBg "矩陣與迴圈")
> 點擊以上的圖片來觀賞關於矩陣與迴圈的影片。
## 矩陣(Arrays)
處理資料是任何程式語言中最常見的任務,如果能將程式碼有條理地編排成如矩陣的形式,處理任務來會更加輕鬆。矩陣的資料儲存格式就像串列,其中一項好處是矩陣可以儲存不同的資料型態資料。
✅ 矩陣其實圍繞在我們身邊!你能想到現實中那些應用是矩陣的形式,例如太陽能板矩陣嗎?
矩陣的語法需要用到中括號。
`let myArray = [];`
上述為一個空矩陣,矩陣也能在宣告時設定內容資料,資料會以逗號來做區隔。
`let iceCreamFlavors = ["Chocolate", "Strawberry", "Vanilla", "Pistachio", "Rocky Road"];`
矩陣數值位置會以**索引(index)**來決定,標記從矩陣的開頭相隔多少元素。在上述例子中,字串 "Chocolate" 的索引為 0"Rocky Road" 為 4。以中括號帶入索引來接收、修改或加入特定元素數值。
✅ 索引從 0 開始有超出你的預料之外嗎?在部分程式語言中,索引會從 1 開始。這有一段有趣的歷史,你可以[閱讀維基百科](https://zh.wikipedia.org/wiki/%E5%BE%9E%E9%9B%B6%E9%96%8B%E5%A7%8B%E7%9A%84%E7%B7%A8%E8%99%9F)來了解它。
```javascript
let iceCreamFlavors = ["Chocolate", "Strawberry", "Vanilla", "Pistachio", "Rocky Road"];
iceCreamFlavors[2]; //"Vanilla"
```
你可以指定索引改變其中的數值,如:
```javascript
iceCreamFlavors[4] = "Butter Pecan"; //Changed "Rocky Road" to "Butter Pecan"
```
或者是指定索引加入新數值,如:
```javascript
iceCreamFlavors[5] = "Cookie Dough"; //Added "Cookie Dough"
```
✅ 另一個常用加入矩陣元素的方式為使用矩陣運算子如 array.push()。
要得知矩陣內有多少元素,可以使用矩陣屬性 `length`
```javascript
let iceCreamFlavors = ["Chocolate", "Strawberry", "Vanilla", "Pistachio", "Rocky Road"];
iceCreamFlavors.length; //5
```
✅ 自己動手試試看!使用瀏覽器命令欄,自由地建立並操控矩陣。
## 迴圈(Loops)
迴圈幫助你處理重複性與**迭代(iterative)**任務,這可以省下許多程式碼行數與時間。每一次迭代可以有不同的變數、數值或條件。在 JavaScript 中有許多種迴圈的樣式,彼此都有些微的差異,但主要功能都一樣:對資料做迴圈。
### For 迴圈(For Loop)
`for` 迴圈有三個條件:
- `計數器(counter)` 一個初始化變數決定目前迭代的次數。
- `條件式(condition)` 一個以比較運算子表示的陳述式,當結果為 `true` 時終止迴圈。
- `迭代陳述式(iteration-expression)` 在每一次迭代完後,改變計數器的數值。
```javascript
// 從 0 加到 10
for (let i = 0; i < 10; i++) {
console.log(i);
}
```
✅ 在瀏覽器命令欄中運行這段迴圈。在改變計數器、條件式與迭代陳述式後有什麼改變?你能讓它變成從 10 減到 0 嗎?
### While 迴圈(While Loop)
不同於 `for` 迴圈語法,`while` 迴圈只要求一個條件式,當條件式回傳 `true` 時即終止迴圈。迴圈中的條件是通常需要依賴像計數器的變數,這些變數必須被合理的處理。計數器的起始化需要在迴圈外面,之後的條件陳述式與計數器的變化則需要在迴圈當中。
```javascript
// 從 0 加到 10
let i = 0;
while (i < 10) {
console.log(i);
i++;
}
```
✅ 你會選擇 for 迴圈還是 while 迴圈呢? 一萬七千個 StackOverflow 用戶問過相同的問題,有些回應[可能會勾起你的興趣](https://stackoverflow.com/questions/39969145/while-loops-vs-for-loops-in-javascript)。
## 矩陣與迴圈
矩陣通常會與迴圈一起出現,大多數的條件式需要矩陣的長度來決定迴圈次數,矩陣的索引值也與計數器的數值相同。
```javascript
let iceCreamFlavors = ["Chocolate", "Strawberry", "Vanilla", "Pistachio", "Rocky Road"];
for (let i = 0; i < iceCreamFlavors.length; i++) {
console.log(iceCreamFlavors[i]);
} // 當所有冰淇淋口味表示完時,結束迴圈。
```
✅ 做個實驗,在瀏覽器命令欄中製作一個使用矩陣的迴圈。
---
## 🚀 挑戰
除了 for 迴圈與 while 迴圈外,仍有許多使用矩陣與迴圈的方法:[forEach](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach)、[for-of](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...of)與[map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map)。 用上列其中一種語法改寫你的迴圈。
## 課後測驗
[課後測驗](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/14?loc=zh_tw)
## 複習與自學
在 JavaScript 中,矩陣有許多控制的方法,它們在處理資料上有很大的幫助。
[學習這些方法](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array),如 push、pop、slice 和 splice試著套用在你所創造的矩陣上。
## 作業
[給矩陣的迴圈](assignment.zh-tw.md)

@ -0,0 +1,13 @@
# 給矩陣的迴圈
## 簡介
建立一個程式,列出數字每第三個從 1 到 20 的值(3,6,9......),並輸出在 console 上。
> 提示:使用 for-loop 迴圈並改寫它的迭代陳述式。
## 學習評量
| 作業內容 | 優良 | 普通 | 待改進 |
| -------- | -------------------------------------- | ---------------------- | -------------- |
| | 程式完成簡介裡的課題並解釋程式碼意義。 | 程式完成簡介裡的課題。 | 程式出現問題。 |

@ -0,0 +1,14 @@
# JavaScript 入門
JavaScript 編輯網頁的一種程式語言。在這四堂課中,你會學到它最基本的知識。
### 主題
1. [變數與資料型態](../1-data-types/translations/README.zh-tw.md)
2. [函式與方法](../2-functions-methods/translations/README.zh-tw.md)
3. [在 JavaScript 做出決定](../3-making-decisions/translations/README.zh-tw.md)
4. [矩陣與迴圈](../4-arrays-loops/translations/README.zh-tw.md)
### 參與人員
這些文章是由 [Jasmine Greenaway](https://twitter.com/paladique)、[Christopher Harrison](https://twitter.com/geektrainer)和[Chris Noring](https://twitter.com/chris_noring) 用滿滿的 ♥️ 來編寫。

@ -0,0 +1,231 @@
# 盆栽盒專案 Part 1 HTML 簡介
![HTML 簡介](../images/webdev101-html.png)
> 由 [Tomomi Imura](https://twitter.com/girlie_mac) 繪製
## 課前測驗
[課前測驗](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/15?loc=zh_tw)
### 大綱
HTML (HyperText Markup Language) 可說是網頁的骨架。若說 CSS 打扮你的 HTML 而 JavaScript 讓它活起來HTML 則是網頁應用的身體。HTML 的語法甚至佐證前行說明,它包含了 "head"、 "body" 和 "footer" 的標籤。
在這堂課中,我們會使用 HTML 去構建我們盆栽盒的虛擬介面。包含一個標題、三個欄位:左右各一的可拖曳植物欄位與中間的玻璃盆栽罐。這堂課程後,你會看到欄位中有許多盆栽,但介面可能會有點奇怪。不用擔心,往後的課程會講述 CSS 語法來造型化你的介面。
### 課題
在你的電腦上,建立專案資料夾 'terrarium' 並在其中建立檔案 'index.html'。你可以在 Visual Studio Code 中新增 VS Code 視窗建立你的資料夾,點擊「開啟資料夾」來檢視其中。在探索面板中點擊小小的「檔案」按鈕來建立新的檔案:
![VS Code 介面](../images/vs-code-index.png)
或者是
在 git bash 中使用下列指令:
* `mkdir terrarium`
* `cd terrarium`
* `touch index.html`
* `code index.html``nano index.html`
> index.html 檔案預設會連接到你的瀏覽器上,以網頁的方式呈現。網址像 `https://anysite.com/test` 會以資料夾結構的方式包含資料夾 `test` 與資料夾內的 `index.html`。`index.html` 不需要出現在網址當中。
---
## 文件型別宣告(DocType) 與 html 標籤
HTML 檔案的第一行是文件型別宣告。你會訝異這一行必須存在在檔案首行存在的意義,但這行能告訴舊款瀏覽器這份檔案開啟的模式以及它的定義方式。
> 提示:在 VS Code 中, 你可以把游標放在標籤上從MDN文件庫中取得標籤的相關資訊。
檔案第二行會是 `<html>` 起始標籤,之後接續著 `</html>` 結束標籤。這項標籤使整個介面的根本。
### 課題
新增這些行到你的 `index.html` 檔案開頭:
```HTML
<!DOCTYPE html>
<html></html>
```
✅ 有許多不一樣的文件型別模式,你可以用 query string 做設定:[怪異模式與標準模式](https://developer.mozilla.org/en-US/docs/Web/HTML/Quirks_Mode_and_Standards_Mode)。這些模式用來支援非常古老的瀏覽器,現在可能都不會見到的瀏覽器(Netscape Navigator 4 與 Internet Explorer 5)。 你可以觀看他們的文件型別模式宣告定義。
---
## 文件的 'head'
HTML 文件中 'head' 的區域包含很多網頁的重要資訊,也被稱作[元資訊(metadata)](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta)。我們告訴網頁伺服器我們需要哪些檔案與資訊,主要有四個:
- 網頁標題
- 網頁元資訊,包含:
- 字元集(Character Set),解釋網頁字元的編碼方式。
- 瀏覽器資訊,包含 `x-ua-compatible` 讓 Internet Explorer Edge 瀏覽器支援的資訊。
- 關於裝置的視窗資訊(Viewport)與後續視窗行為的依據。 設定視窗資訊的初始比例為 1 並控制讀取時的縮放大小。
### 課題
在網頁檔中,新增 'head' 區塊在 `<html>` 的標籤之間。
```html
<head>
<title>Welcome to my Virtual Terrarium</title>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
```
✅ 你認為設定下列視窗元資訊 `<meta name="viewport" content="width=600">` 會發生什麼事? 請閱讀更多有關於[Viewport](https://developer.mozilla.org/en-US/docs/Mozilla/Mobile/Viewport_meta_tag)的資訊。
---
## 文件的 `body`
### HTML 標籤
在 HTML 中,新增標籤到你的 .html 檔案中。每個標籤都要有起始標籤與結束標籤,像是: `<p>hello</p>` 來增加文章。 在 `<html>` 標籤中利用一組 `<body>` 標籤建立新的介面內容,現在你的程式呈現如下:
### 課題
```html
<!DOCTYPE html>
<html>
<head>
<title>Welcome to my Virtual Terrarium</title>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body></body>
</html>
```
現在,你可以開始豐富你的網頁內容。通常,你會使用 `<div>` 標籤來建立網頁中不同的元素。我們在一連串的 `<div>` 元素當中新增圖片。
### 圖片
其中一個不需要結束標籤的 html 標籤為 `<img>` 標籤,因為它含有 `src` 元素來提供所有網頁需要取得的資訊。
在專案資料夾中新增一個名叫 `images` 的資料夾,複製[我們提供的圖片](../../solution/images)到資料夾裡面,一共有十四張圖片。
### 課題
新增這些植物圖片到 `<body></body>` 標籤之間:
```html
<div id="page">
<div id="left-container" class="container">
<div class="plant-holder">
<img class="plant" alt="plant" id="plant1" src="./images/plant1.png" />
</div>
<div class="plant-holder">
<img class="plant" alt="plant" id="plant2" src="./images/plant2.png" />
</div>
<div class="plant-holder">
<img class="plant" alt="plant" id="plant3" src="./images/plant3.png" />
</div>
<div class="plant-holder">
<img class="plant" alt="plant" id="plant4" src="./images/plant4.png" />
</div>
<div class="plant-holder">
<img class="plant" alt="plant" id="plant5" src="./images/plant5.png" />
</div>
<div class="plant-holder">
<img class="plant" alt="plant" id="plant6" src="./images/plant6.png" />
</div>
<div class="plant-holder">
<img class="plant" alt="plant" id="plant7" src="./images/plant7.png" />
</div>
</div>
<div id="right-container" class="container">
<div class="plant-holder">
<img class="plant" alt="plant" id="plant8" src="./images/plant8.png" />
</div>
<div class="plant-holder">
<img class="plant" alt="plant" id="plant9" src="./images/plant9.png" />
</div>
<div class="plant-holder">
<img class="plant" alt="plant" id="plant10" src="./images/plant10.png" />
</div>
<div class="plant-holder">
<img class="plant" alt="plant" id="plant11" src="./images/plant11.png" />
</div>
<div class="plant-holder">
<img class="plant" alt="plant" id="plant12" src="./images/plant12.png" />
</div>
<div class="plant-holder">
<img class="plant" alt="plant" id="plant13" src="./images/plant13.png" />
</div>
<div class="plant-holder">
<img class="plant" alt="plant" id="plant14" src="./images/plant14.png" />
</div>
</div>
</div>
```
> 筆記: Spans 和 Divs。 Divs 是種「塊」元素,而 Spans 是種「內聯」元素。替換它們會發生什麼事情?
更新這些程式碼後,現在植物出現在畫面上了。他看起來很糟,因為我們還沒用 CSS 來美化它,我們會在下堂課中處理。
每張圖片都有 alt 文字,即使是隱藏起來或沒取得的圖片。它是圖片提供網頁親和力很重要的屬性。你可以在之後學習更多關於網頁親和力的課程。現在,只需要記得 alt 屬性提供而外的圖片資訊,處理用戶無法觀看圖片時的資訊(連線太慢、src 屬性有誤、使用者使用螢幕報讀器......)。
✅ 你有發現每張圖片都有相同的 alt 標籤嗎?這樣合理嗎?為什麼?你能改善它嗎?
---
## 語義化標籤
綜觀來說,最好使用普遍意義的標籤名稱。這是什麼意思?這代表你的 HTML 標籤必須表現它的資料型態或是它的功能性。舉例來說,網頁上的標題文字就應該使用 `<h1>` 標籤。
新增下列程式碼到你的 `<body>` 標籤中:
```html
<h1>My Terrarium</h1>
```
使用語義化標籤如:標題 `<h1>` 和未排序串列 `<ul>` ,能幫助螢幕報讀器理解網頁的內容。普遍來說,按鈕為 `<button>` 而串列為 `<li>`。我們當然 _可以_ 用自訂義包含按鈕事件的 `<span>` 元素來替代按鈕,但這對障礙者而言,無法直接地理解語法功用會是一種負擔。基於這項原因,盡量只使用語義化標籤。
✅ 看看螢幕報讀器是[如何去處理一個網頁](https://www.youtube.com/watch?v=OUDV1gqs9GA)。 你可以看出非語義化標籤會怎麼阻礙用戶的嗎?
## 盆栽盒
最後一項介面的要素,就是建立一個盆栽盒的雛型。
### 課題:
加入這段程式碼在 `</div>` 之前:
```html
<div id="terrarium">
<div class="jar-top"></div>
<div class="jar-walls">
<div class="jar-glossy-long"></div>
<div class="jar-glossy-short"></div>
</div>
<div class="dirt"></div>
<div class="jar-bottom"></div>
</div>
```
✅ 即使你在檔案中新增了程式碼,你卻沒看到任何東西。為什麼?
---
## 🚀 挑戰
這邊有一些「古老」的 HTML 標籤。雖然[這些標籤](https://developer.mozilla.org/en-US/docs/Web/HTML/Element#Obsolete_and_deprecated_elements)被歸為不推薦使用的標籤,但仍值得去嘗試的。你可以用 `<marquee>` 標籤來讓 h1 標題文字變成縱向呈現嗎?實驗完後,記得要移除這些標籤喔。
## 課後測驗
[課後測驗](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/16?loc=zh_tw)
## 複習與自學
HTML 是一種「行之有效」的構築系統,建立了現今的各種網頁。從標籤來學習關於 HTML 的歷史,你能了解為什麼有的標籤被排除,而有的被新增上去嗎?有什麼標籤會在未來被建立上去呢?
學習更多關於建立網頁的資訊:[Microsoft Learn](https://docs.microsoft.com/learn/modules/build-simple-website/?WT.mc_id=academic-13441-cxa)。
## 作業
[練習 HTML建立部落格雛形](assignment.zh-tw.md)

@ -0,0 +1,11 @@
# 練習 HTML建立部落格雛形
## 簡介
想像你正在設計,或重新設計你自己的專屬網頁。建立網頁的圖像設計雛形,並用 HTML 語法建立這些網頁元素。你可以建在紙上並掃描它,或者是建在軟體中,只要確保有 HTML 語法在裡面。
## 學習評量
| 作業內容 | 優良 | 普通 | 待改進 |
| -------- | ------------------------------------ | ------------------------------- | --------------------------------- |
| | 可以見到部落格編排包含10種以上的元素 | 見到部落格編排大約有 5 種的元素 | 見到部落格編排最多只有 3 種的元素 |

@ -0,0 +1,263 @@
# 盆栽盒專案 Part 2 CSS 簡介
![CSS 簡介](../images/webdev101-css.png)
> 由 [Tomomi Imura](https://twitter.com/girlie_mac) 繪製
## 課前測驗
[課前測驗](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/17?loc=zh_tw)
### 大綱
階層式樣式表CSS (Cascading Style Sheets)解決了網頁開發重要的問題:如何讓網頁變得漂亮。為你的應用造型化可以讓網頁更好用、更好看。你也可以使用 CSS 建立回應式網頁設計(Responsive Web Design - RWD),依據你的視窗大小改變網頁的呈現方式。 CSS 不只讓網頁變美麗它允許加入動畫以呈現更生動的互動體驗。CSS 工作組持續維護 CSS 的規格書,你可以在[全球資訊網協會官網](https://www.w3.org/Style/CSS/members)追蹤他們的作業。
> 筆記CSS 是一種程式語言,但就像任何在網路上的東西一樣,並不是所有瀏覽器都支援最新的規格。請時常利用[CanIUse.com](https://caniuse.com)檢查你的設計是否支援相關瀏覽器。
這堂課中,我們會為我們的線上盆栽盒增加造型,學習更多 CSS 的概念:串接(Cascade)、繼承(Inheritance)、選擇器(Selectors)、定位(Positioning)與建立布局(Layout)。我們會規劃盆栽盒的布局,建立實際的盆栽盒。
### 開始之前
你需要確保你有盆栽盒的 HTML 程式碼,準備被造型化。
### 課題
在盆栽盒專案中,我們新增檔案 `style.css`。 在 HTML 檔案中匯入該檔案在 `<head>` 區塊中:
```html
<link rel="stylesheet" href="./style.css" />
```
---
## 串接(Cascade)
串接造型表單體現了造型依照表單上的優先度「串接」在網頁應用上。網頁作者利用程式碼設定造型優先度,行內樣式(inline styles)的優先度會比外部造型表單來的高。
### 課題
新增行內造型 "color: red" 到 `<h1>` 標籤中:
```HTML
<h1 style="color: red">My Terrarium</h1>
```
之後,也新增下列程式碼在 `style.css` 檔案中:
```CSS
h1 {
color: blue;
}
```
✅ 你的網頁顯示了哪一種顏色?為什麼?你能找到方法覆蓋這個造型嗎?何時會讓你想用這套做法呢?又為什麼不呢?
---
## 繼承(Inheritance)
從父關係標籤到子關係標籤上繼承造型,如被嵌套的物件會繼承容器物件的造型。
### 課題
我們設定 body 的字體為特定字型,確認嵌套物件的字型:
```CSS
body {
font-family: helvetica, arial, sans-serif;
}
```
開啟你的瀏覽器命令欄到 'Elements' 標籤中,觀察 H1 的字型。它繼承了 body 的字型,表現在瀏覽器上:
![inherited font](../images/1.png)
✅ 你能讓被嵌套元素繼承其他格式嗎?
---
## CSS 選擇器(Selectors)
### 標籤
到目前為止,你的 `style.css` 檔案只有一部份標籤被造型化,這讓程式看起來很怪:
```CSS
body {
font-family: helvetica, arial, sans-serif;
}
h1 {
color: #3a241d;
text-align: center;
}
```
這種造型方法只能控制被指定的元素,但如果你需要套用在每一種盆栽盒內的植物。你需要利用 CSS 選擇器。
### Ids
新增左容器與右容器造型布局。因為網頁內只有一個左容器與右容器,我們就這樣命名 id 標記。要造型化它們,使用 `#`
```CSS
#left-container {
background-color: #eee;
width: 15%;
left: 0px;
top: 0px;
position: absolute;
height: 100%;
padding: 10px;
}
#right-container {
background-color: #eee;
width: 15%;
right: 0px;
top: 0px;
position: absolute;
height: 100%;
padding: 10px;
}
```
這裡,你已經將容器擺在絕對位置上了,一個位在左側,一個位在右側。容器寬度使用百分比以確保它們在小螢幕裝置上也能運作正常。
✅ 這兩段樣式已經重複了,請不要照抄。你能找到更好的方式來造型化這些 ids 嗎? 或許你可以從 id 或 class 來下手。讓 CSS 套用在容器上,我們需要改寫 HTML 程式碼:
```html
<div id="left-container" class="container"></div>
```
### Classes
在上述例子中,你成功地為兩樣物件新增造型。如果你想一次套用在多樣物件上,你就需要 CSS classes。利用這個方法來布局兩個容器。
注意每個植物的標記都有 ids 與 classes。JavaScript 使用 Id 標記來控制植物的擺放; class 則是被 CSS 套用特定的造型。
```html
<div class="plant-holder">
<img class="plant" alt="plant" id="plant1" src="./images/plant1.png" />
</div>
```
新增下列程式碼到 `style.css` 檔案中:
```CSS
.plant-holder {
position: relative;
height: 13%;
left: -10px;
}
.plant {
position: absolute;
max-width: 150%;
max-height: 150%;
z-index: 2;
}
```
片段開頭是 CSS 的定位屬性,分為相對與絕對定位,我們會在下一個段落進行解述。我們來看看百分比高度的方式:
你設定了植物架高度為 13%,確保所有植物都能在不需要滾動容器的情況下,在每一個垂直的容器中顯示出來。
你設定了植物架向左移 10 像素,讓植物能在容器的正中間。圖片上亦有大區域的透明區域需要被拖曳過來,往左位移更適合呈現在畫面上。
之後,植物設定寬度為 150%。 當瀏覽器調整比例時,也能同時將植物圖片作大小的調整。試著改變瀏覽器檢視比例,植物依舊會保持在容器中。
我們換看 z-index控制物件的相對高度讓植物坐落在容器上方且在盆栽盒內部。
✅ 為什麼需要分為植物架與植物 CSS 選擇器?
## 定位(Positioning)
多樣的定位屬性,包含靜態(static)、相對(relative)、固定(fixed)、絕對(absolute)和黏貼(sticky),有時候讓人難以駕馭,但成功設定完後,可以讓你完整地掌握元素坐落的位置。
絕對定位元素會依照他的父關係物件來決定定位位置,若沒有關係物件,整個文件的 body 就會成為定位依據。
相對定位元素則依照 CSS 指定的方向來調整他的起始位置。
在我們的樣本中,`plant-holder` 是相對定位元素,坐落在絕對定位的容器當中。因此,容器被定義在左側與右側,而被嵌入的植物架會調整它在容器的位置,保持植物之間的間隔。
> `plant` 本身也擁有絕對定位,為了讓圖片被拖曳,你能在下段課程中發現更多資訊。
✅ 試著改變容器與植物架的定位模式。發生了什麼事?
## 布局(Layouts)
現在,你已經善用你所學的,只用 CSS 建出盆栽盒!
首先,對 `.terrarium` 的 div 子關係物件加上圓邊矩形:
```CSS
.jar-walls {
height: 80%;
width: 60%;
background: #d1e1df;
border-radius: 10%;
position: absolute;
bottom: 0.5%;
left: 20%;
opacity: 0.5;
z-index: 1;
}
.jar-top {
width: 50%;
height: 5%;
background: #d1e1df;
position: absolute;
bottom: 80.5%;
left: 25%;
opacity: 0.7;
z-index: 1;
}
.jar-bottom {
width: 50%;
height: 1%;
background: #d1e1df;
position: absolute;
bottom: 0%;
left: 25%;
opacity: 0.7;
}
.dirt {
width: 58%;
height: 5%;
background: #3a241d;
position: absolute;
border-radius: 0 0 4rem 4rem;
bottom: 1%;
left: 21%;
opacity: 0.7;
z-index: -1;
}
```
注意這邊百分比的用法,即使是 `border-radius` 也請留意。 當瀏覽器調整檢視比例時,你會發現玻璃罐也會受到調整。 其他值得注意的地方為:玻璃罐的寬度與高度百分比,每個元素絕對定位在中心與視窗的下方。
✅ 試著改變罐子的顏色與透明度,觀察泥土與罐子的關係。發生了什麼事?為什麼?
---
## 🚀 挑戰
新增「氣泡反光」在罐子左下方的位置,讓玻璃材質更擬真一些。你需要編輯 `.jar-glossy-long``.jar-glossy-short` 造型集來模擬罐子反光。下面是成果圖:
![盆栽盒成果圖](../images/terrarium-final.png)
在做課後測驗前,請先前往下列的學習頁面:[用 CSS 造型化你的網頁應用](https://docs.microsoft.com/en-us/learn/modules/build-simple-website/4-css-basics?WT.mc_id=academic-13441-cxa)
## 課後測驗
[課後測驗](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/18?loc=zh_tw)
## 複習與自學
CSS 看似很好上手但要在所有瀏覽器與螢幕大小上運作正常也會面臨到許多挑戰。CSS-Grid 與 Flexbox 這兩種工具讓上述作業變得比較好規劃與調整。藉由遊玩[Flexbox Froggy](https://flexboxfroggy.com/)與[Grid Garden](https://codepip.com/games/grid-garden/)來學習它們。
## 作業
[重構 CSS](assignment.zh-tw.md)

@ -0,0 +1,11 @@
# 重構 CSS
## 簡介
使用 Flexbox 或 CSS Grid 重新規劃盆栽盒,拍幾張在不同瀏覽器上運作的畫面。重新規劃時,你可能需要改變程式碼中版本的標記。不需要去考慮植物拖曳的問題,我們只重構 HTML 與 CSS 的部分。
## 學習評量
| 作業內容 | 優良 | 普通 | 待改進 |
| -------- | --------------------------------------- | ---------------- | -------------------- |
| | 使用 Flexbox 或 CSS Grid 呈現新的盆栽盒 | 重新取代部分元素 | 無法更新原有的盆栽盒 |

@ -0,0 +1,219 @@
# 盆栽盒專案 Part 3 - DOM 元素控制與閉包
![DOM 元素與閉包](../images/webdev101-js.png)
> 由 [Tomomi Imura](https://twitter.com/girlie_mac) 繪製
## 課前測驗
[課前測驗](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/19?loc=zh_tw)
### 大綱
操作 DOM (Document Object Model) 是網頁開發的一項關鍵。根據[MDN 文件](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Introduction) 「Document Object Model (DOM) 元素能根據網頁文件的結構與內容來呈現物件」。藉由使用 JavaScript 框架而非原始的 JavaScript 程式碼來管理 DOM在網頁上操作 DOM 的挑戰已經不比以前困難了,但這裡我們要自己來管理它們!
此外,這堂課也會介紹有關[JavaScript 閉包(Closure)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures)的概念,你可以想像成一個函式被包在另一個函式中,以訪問外面函式範圍中的變數。
> JavaScript 閉包是個廣闊且複雜的主題。本堂課只觸及建立盆栽盒需要的最基礎概念。你能得知一個閉包為:內部函式和外部函式建立一項關係,允許內部函式存取外部函式的變數等作用域。要得知更多關於閉包的原理,請造訪觀看[額外的文件](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures)。
我們會使用閉包來操控 DOM。
想像 DOM 就像一棵樹,表現出所有操作網頁的方式。多樣的 APIs (Application Program Interfaces) 提供程式開發者,依照自己使用的程式語言,以存取、編輯、編排等方式管理 DOM 元素。
![DOM 樹的表達](../images/dom-tree.png)
> HTML 語法會參考 DOM 的呈現方式。出自 [Olfa Nasraoui](https://www.researchgate.net/publication/221417012_Profile-Based_Focused_Crawler_for_Social_Media-Sharing_Websites)。
在這堂課中,我們會完成我們的盆栽盒專案,建立 JavaScript 來對網頁中的植物進行互動式操作。
### 開始之前
確保盆栽盒的 HTML 與 CSS 已經編輯完成。這堂課會新增拖曳植物進出盆栽罐的功能。
### 課題
在專案資料夾中,新增檔案 `script.js`。 匯入該檔案在 HTML 檔 `<head>` 的部分:
```html
<script src="./script.js" defer></script>
```
> 筆記:匯入外部 JavaScript 檔案到 HTML 檔案須使用 `defer`,讓 JavaScript 檔案只有在 HTML 被完全載入時才被執行。你也可以使用 `async` 的屬性,允許 JavaScript 在解析 HTML 檔時就被執行。這項專案中,我們必須確保 HTML 的元件被完整建立後才允許使用拖曳功能。
---
## DOM 元素
我們要做的第一件事是建立 DOM 下,要被操控的物件的連結。在專案例子中,我們有罐子外的十四株植物等著被拖曳。
### 課題
```html
dragElement(document.getElementById('plant1'));
dragElement(document.getElementById('plant2'));
dragElement(document.getElementById('plant3'));
dragElement(document.getElementById('plant4'));
dragElement(document.getElementById('plant5'));
dragElement(document.getElementById('plant6'));
dragElement(document.getElementById('plant7'));
dragElement(document.getElementById('plant8'));
dragElement(document.getElementById('plant9'));
dragElement(document.getElementById('plant10'));
dragElement(document.getElementById('plant11'));
dragElement(document.getElementById('plant12'));
dragElement(document.getElementById('plant13'));
dragElement(document.getElementById('plant14'));
```
發生了什麼事?你正以 DOM 搜尋網頁檔內的物件,藉由 Id 作為依據來搜尋。回想第一堂 HTML 課中,我們可每一株植物一個專屬的 Id (`id="plant1"`),現在你就可以使用它。在辨別完每一株植物物件後,傳遞給待編輯的函式 `dragElement`,讓 HTML 物件可以被拖曳。
✅ 為什麼我們要以 Id 作為物件的參考?為什麼不以 CSS 的 class 作為參考?請參考以前的 CSS 課程回答此問題。
---
## 閉包(Closure)
現在,你已經準備好要建立 dragElement 閉包,建立包在外部函式內的內部函式組,在我們的例子中,會用上三個函式。
閉包在一或多個以上函式要存取外部函式時非常好用。看看下面的例子:
```javascript
function displayCandy(){
let candy = ['jellybeans'];
function addCandy(candyType) {
candy.push(candyType)
}
addCandy('gumdrops');
}
displayCandy();
console.log(candy)
```
這項例子中,函式 displayCandy 包住另一個函式 addCandy新增新的糖果樣式到已存在的矩陣當中。當執行這段程式時矩陣 `candy` 會被認作是未定義,因為它是函式的本地變數。
✅ 你能讓矩陣 `candy` 被存取嗎?試著將它移到閉包外面。這時,矩陣會變成全域變數,取消閉包內的存取限制。
### 課題
在檔案 `script.js` 的元素宣告下方,新增函式:
```javascript
function dragElement(terrariumElement) {
//set 4 positions for positioning on the screen
let pos1 = 0,
pos2 = 0,
pos3 = 0,
pos4 = 0;
terrariumElement.onpointerdown = pointerDrag;
}
```
`dragElement` 藉由程式定義的參數取得 `terrariumElement` 物件。之後,設定一些位置 `0` 的變數給函式內的物件使用。它們是本地變數,給每一個進到拖曳函式內的物件操控。盆栽盒會被這些拖曳物件填充,我們的網頁應用必須要持續追蹤這些物件的位置。
此外,進到函式的 terrariumElement 也被新增了 `pointerdown` 事件,它是管理 DOM 的其中一項[網頁 APIs](https://developer.mozilla.org/en-US/docs/Web/API)。當按鈕按下時,或是在我們案例中,一個拖曳物件被點擊時,`onpointerdown` 事件就會被觸發。這個事件處理器(event handler)皆運作在[網頁與行動瀏覽器](https://caniuse.com/?search=onpointerdown)上,只有少部分的例外。
✅ [事件處理器 `onclick`](https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/onclick)支援更多的瀏覽器。為什麼我們不在這邊使用它? 想想看我們在這此建立的視窗互動類型。
---
## 函式 pointerDrag
terrariumElement 已經準備好被拖曳了。當觸發 `onpointerdown` 事件時,函式 pointerDrag 會參與其中。新增這項函式在程式碼 `terrariumElement.onpointerdown = pointerDrag;` 下方:
### 課題
```javascript
function pointerDrag(e) {
e.preventDefault();
console.log(e);
pos3 = e.clientX;
pos4 = e.clientY;
}
```
許多事情會發生。首先,你使用 `e.preventDefault();` 取消掉 pointerdown 原先的預設事件。這樣你可以操作更多的介面行為。
> 回到你建立的程式碼中,試著刪除 `e.preventDefault()` 並執行看看,發生了什麼事?
第二,用瀏覽器打開 `index.html` 並調查我們的介面。當你點擊植物時,你可以發現 'e' 事件被觸發了。專研一下,一個 pointerdown 事件會產生多少資訊!
接下來,紀錄本地變數 `pos3``pos4` 被設定為 e.clientX 和 e.clientY。你可以在觀察面板中會發現 `e` 的數值。這項數值取得按下植物瞬間的 x 與 y 座標資訊。為了全面的控制植物行為,在拖曳植物時,我們會持續更新座標資訊。
✅ 將整個網頁應用建立在一個大閉包下,會讓程式碼變得比較清楚嗎?如果沒有,你有其他方法管理這十四株可拖曳的植物嗎?
增加初始化函式,在程式碼 `pos4 = e.clientY` 下方加上下列兩行事件處理:
```html
document.onpointermove = elementDrag;
document.onpointerup = stopElementDrag;
```
現在,在游標拖曳時,你的植物能跟著你的游標走,而在你取消點擊時停下來。`onpointermove` 和 `onpointerup` 也是 `onpointerdown` 類型相同的 API。然而現在介面會出現錯誤訊息因為我們還沒建立函式 `elementDrag``stopElementDrag`
## 函式 elementDrag 與 stopElementDrag
新增兩條內部函式在閉包中,它們會處理拖曳植物與停止拖曳的事件。你希望你可以拖曳任何一株植物且放在螢幕上的任一地方。介面並沒有強制你盆栽盒的配置格式,你可以自由地增加、移除與移動盆栽罐內的植物。
### 課題
新增函式 `elementDrag` 在函式閉包 `pointerDrag` 宣告列的正下方:
```javascript
function elementDrag(e) {
pos1 = pos3 - e.clientX;
pos2 = pos4 - e.clientY;
pos3 = e.clientX;
pos4 = e.clientY;
console.log(pos1, pos2, pos3, pos4);
terrariumElement.style.top = terrariumElement.offsetTop - pos2 + 'px';
terrariumElement.style.left = terrariumElement.offsetLeft - pos1 + 'px';
}
```
在這條函式之前,你編輯了四個本地變數位置的初始值在外部函式中。這邊又做了什麼事?
當你拖曳物件時,你更新數值 `pos1``pos3` 減去現在的 `e.clientX`,而 `pos3` 在之前被初始化為為 `e.clientX`。同樣的行為套用在 `pos2`上。之後,你更新 `pos3``pos4` 到新的 XY 座標點位置。你能在 console 下看到數值在拖曳下更新的情況。我們也更新植物的 CSS 造型中的定位點為 `pos1``pos2`,比較植物左上方座標點與新座標點的關係。
> `offsetTop``offsetLeft` 是 CSS 的屬性,決定物件與它父關係物件的定位關係。父關係物件可以是任何元素,只要它的定位屬性不為 `static`
這些座標點的計算式讓你成功校整了植物與盆栽盒之間的行為。
### 課題
最後的課題是在介面上新增 `stopElementDrag` 函式,我們將它加在函式閉包 `elementDrag` 的正下方:
```javascript
function stopElementDrag() {
document.onpointerup = null;
document.onpointermove = null;
}
```
這條小函式重制 `onpointerup``onpointermove` 事件,這樣你可以重新開始該植物的拖曳事件,或是拖曳新的植物。
✅ 如果不將這些事件設為空值時,會發生什麼事?
我們終於完成了這項專案!
🥇 恭喜你!你建立了一個漂亮的盆栽盒。![盆栽盒成果圖](../images/terrarium-final.png)
---
## 🚀 挑戰
新增新的事件處理器到你的閉包中,讓你能對植物做更多的事情。舉例來說,雙擊植物讓它排列到最上層。發揮你的創意吧!
## 課後測驗
[課後測驗](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/20?loc=zh_tw)
## 複習與自學
在螢幕上拖曳物件看似簡單,但依照不同的目的與實現方法會遭遇到不同的問題。事實上,這邊有一份關於你可以嘗試的[拖曳 API](https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API)。我們沒在專案中使用是為了建立不一樣的實現方法,試著使用這些 API 到專案中,看看你能完成什麼。
在[W3C 文件](https://www.w3.org/TR/pointerevents1/) 和 [MDN 網頁文件](https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events)上取得更多關於 pointer 的事件。
記得習慣性用[CanIUse.com](https://caniuse.com/)檢查網頁的瀏覽器兼容性。
## 作業
[用 DOM 做更多事](assignment.zh-tw.md)

@ -0,0 +1,11 @@
# 用 DOM 做更多事
## 簡介
調查其中一項 DOM 的元素。造訪 MSN 關於[DOM 介面的清單](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model)挑選其中一項。在網路上找尋一個使用這項元素的網頁,並解釋如何使用它。
## 學習評量
| 作業內容 | 優良 | 普通 | 待改進 |
| -------- | ---------------------- | ---------------- | ---------------- |
| | 完整的評論文章附帶例子 | 評論文章不帶例子 | 評論文章並不完整 |

@ -0,0 +1,34 @@
# 我的盆栽盒:一個關於 HTML、CSS 與 JavaScript DOM 控制的專案 🌵🌱
深思一項小型互動式拖放程式專案,在 HTML、JS 與 CSS 的帶領下,你可以建立網頁介面,美化它,並增加互動功能。
![我的盆栽盒](../images/screenshot_gray.png)
# 課程
1. [HTML 簡介](../1-intro-to-html/translations/README.zh-tw.md)
2. [CSS 簡介](../2-intro-to-css/translations/README.zh-tw.md)
3. [DOM 簡介與閉包](../translations/3-intro-to-DOM-and-closures/README.zh-tw.md)
## 參與人員
由 [Jen Looper](https://www.twitter.com/jenlooper) 用滿滿的 ♥️ 來編寫。
用 CSS 建立盆栽盒的發想來自於 Jakub Mandra 的玻璃罐 [CodePen](https://codepen.io/Rotarepmi/pen/rjpNZY)。
手繪插圖由 [Jen Looper](http://jenlooper.com) 使用 Procreate 繪製。
## 建置你的盆栽盒
你可以利用 Azure Static Web Apps 建置、發布你的盆栽盒到網路上。
1. 分叉這個數據庫
2. 按下下方按鈕
[![Deploy to Azure button](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/?feature.customportal=false&WT.mc_id=academic-13441-cxa#create/Microsoft.StaticApp)
3. 遵循指示建立你的網頁應用。請確保你的程式根目錄為 `/solution` 或者是你自己的專案位置。這項專案並不包含任何 API你不需要考慮額外匯入的問題。 .github 資料夾會建立在你的分叉數據庫中,它會幫助 Azure Static Web Apps 的組建服務並發布你的應用到新的網址。

@ -0,0 +1,30 @@
# 事件驅動程式設計 ── 建立一款打字遊戲
## 大綱
打字可說是開發者被嚴重低估的技能之一,將腦中的想法快速地轉換到編輯器中,讓你流暢地發揮你的想像力。其中一個訓練方法就是遊玩遊戲!
> 因此,讓我們來開發一款打字遊戲吧!
你會使用到你學到的 JavaScript、HTML 與 CSS 技法來建立打字遊戲。這款遊戲提供隨機的引文作為玩家的目標(使用[夏洛克·福爾摩斯](https://zh.wikipedia.org/wiki/%E6%AD%87%E6%B4%9B%E5%85%8B%C2%B7%E7%A6%8F%E5%B0%94%E6%91%A9%E6%96%AF)的引文),計算玩家準確輸入所需要花費的時間。
![demo](../images/demo.gif)
## 開始之前
這堂課會假設你已經熟悉下列的概念:
- 建立文字輸入及按鈕控制
- CSS 與 class 的造型設定
- JavaScript 基礎觀念
- 建立矩陣
- 建立隨機數
- 取得目前時間
## 課程
[使用事件驅動程式設計,建立一款打字遊戲](../typing-game/translations/README.zh-tw.md)
## 參與人員
由 [Christopher Harrison](http://www.twitter.com/geektrainer) 用滿滿的 ♥️ 來編寫。

@ -0,0 +1,339 @@
# 使用事件建立遊戲
## 課前測驗
[課前測驗](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/21?loc=zh_tw)
## 事件驅動程式設計
當我們建立專為瀏覽器設計的應用程式時,我們會提供 Graphical User Interface (GUI) 給用戶使用,在我們建立的格式上進行互動。最常見的互動方式是透過點擊或輸入在多樣的物件。開發者面臨的問題是,我們不了解用戶會何時對這些物件產生互動!
[事件驅動程式設計](https://zh.wikipedia.org/zh-tw/%E4%BA%8B%E4%BB%B6%E9%A9%85%E5%8B%95%E7%A8%8B%E5%BC%8F%E8%A8%AD%E8%A8%88)是一種程式設計的方式,以建立我們的 GUI。若拆解該名詞的話我們知道主軸關鍵會是**事件(Event)**。根據 Merriam-Webster[事件](https://www.merriam-webster.com/dictionary/event)according定義為「將發生的事」。它能有效地解決我們面臨的問題。我們知道當用戶產生互動時什麼程式必須回應其要求只差在我們不知道用戶會何時產生互動。
藉由建立新的函式,我們可以標記這段將被運行的程式碼。我們回顧一下[程序式程式設計](https://zh.wikipedia.org/wiki/%E8%BF%87%E7%A8%8B%E5%BC%8F%E7%BC%96%E7%A8%8B),函式會依照順序一行一行的被運行。這同樣也會被實踐在事件驅動程式設計上,差別在於**如何**去呼叫這些函式。
要處理這些事件:點擊按鈕、輸入字串等等,我們需註冊**事件監聽者(Event Listeners)**。事件監聽者是函式之一,負責回應當事件觸發時,提供相對應的回應。事件監聽者可以根據用戶的行為,更新使用者介面,呼叫伺服器,或是任何你想要它做的事。我們利用[addEventListener](https://developer.mozilla.org/docs/Web/API/EventTarget/addEventListener)新增事件監聽者,提供要被運行的函式。
> **注意** 值得注意我們有許多建立事件監聽者的方式。你可以使用匿名函式(anonymous functions),或是有名字的;你可以使用多種的快捷,好比直接設定 `click` 屬性,或使用 `addEventListener`。在我們練習過程中,主要專注在 `addEventLister` 與匿名函式上,它們可能是開發者最常見的網頁開發技巧。同時,也是彈性最高的: `addEventListener` 作用在任何事件,任何以參數方式輸入的事件名稱。
### 常見事件
創造應用時,這邊有[數種事件](https://developer.mozilla.org/docs/Web/Events)提供給你監聽。基本上,使用者在網頁上做的任何行為都會觸發事件,你需要花大量時間、大量精力確保它們有相對應的使用者體驗。幸運的是,你只需要處理少部分的事件類型。這邊是一些常見的事件類型,我們會使用其中兩種來建立遊戲:
- [點擊](https://developer.mozilla.org/en-US/docs/Web/API/Element/click_event) 使用者點擊物件,通常會是按鈕或是連結。
- [右鍵選單](https://developer.mozilla.org/en-US/docs/Web/API/Element/contextmenu_event) 使用者點擊滑鼠右鍵。
- [選取](https://developer.mozilla.org/en-US/docs/Web/API/Element/select_event) 使用者標記特定文字。
- [輸入](https://developer.mozilla.org/en-US/docs/Web/API/Element/input_event) 使用者輸入文字。
## 建立遊戲
現在我們藉由建立遊戲,了解事件是如何在 JavaScript 上運作的。我們的遊戲會測試玩家的打字技巧,一項程式開發員被忽略的技能之一。我們應該時刻練習打字技術!大致的遊戲流程如下:
- 玩家點擊「開始」按鈕並產生一行要被輸入的引文
- 玩家盡快地輸入這段文字到文字框中
- 當單字輸入完畢時,立即標記下一個單字。
- 當玩家打錯字時,將文字框轉為紅色。
- 當玩家完成引文輸入時,顯示祝賀語與花費的時間。
讓我們開始建立遊戲,學習事件驅動吧!
### 檔案結構
我們總共需要三個檔案:**index.html**、**script.js**與**style.css**。我們來設定它們,以完成後續的步驟。
- 建立新的資料夾存放我們的遊戲,開啟 Console 或是終端機,輸入下列指令:
```bash
# Linux 或 macOS
mkdir typing-game && cd typing-game
# Windows
md typing-game && cd typing-game
```
- 打開文字編輯器 Visual Studio Code
```bash
code .
```
- 現在,在 Visual Studio Code 中新增三個檔案到資料夾中,分別為:
- index.html
- script.js
- style.css
## 建立使用者介面
藉由回顧我們的需求,我們在 HTML 頁面上新增一些元素。這就像是看一份食譜,你需要對應的食材:
- 一個地方呈現將被輸入的引文
- 一個地方呈現任何訊息,好比祝賀文
- 一個玩家輸入的文字框
- 一個開始按鈕。
每一個物件都需要 ID ,讓 JavaScript 程式能控制它們。另外,在 HTML 檔案匯入 CSS 與 JavaScript 檔,我們等一下會編輯它們。
在新的 **index.html** 檔案中,加入下列程式碼:
```html
<!-- inside index.html -->
<html>
<head>
<title>Typing game</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>Typing game!</h1>
<p>Practice your typing skills with a quote from Sherlock Holmes. Click **start** to begin!</p>
<p id="quote"></p> <!-- This will display our quote -->
<p id="message"></p> <!-- This will display any status messages -->
<div>
<input type="text" aria-label="current word" id="typed-value" /> <!-- The textbox for typing -->
<button type="button" id="start">Start</button> <!-- To start the game -->
</div>
<script src="script.js"></script>
</body>
</html>
```
### 執行應用程式
最好的逐段開發模式是定期的確認程式結果。讓我們來執行現在的應用程式。Visual Studio Code 上有一個好用的擴充套件為[Live Server](https://marketplace.visualstudio.com/items?itemName=ritwickdey.LiveServer),它會在你儲存網頁檔案時,同時架設並更新瀏覽器上的網頁。
- 安裝[Live Server](https://marketplace.visualstudio.com/items?itemName=ritwickdey.LiveServer),點擊連結中的 **Install**
- 瀏覽器要求開啟 Visual Studio CodeVisual Studio Code 會執行後續的安裝流程
- 安裝完後,重啟 Visual Studio Code
- 一旦安裝完成,在 Visual Studio Code 下按下 Ctrl-Shift-P (或 Cmd-Shift-P) 開啟指令視窗。
- 輸入 **Live Server: Open with Live Server**
- Live Server 會架設並發布你的網頁成果
- 開啟瀏覽器,前往 **https://localhost:5500**
- 現在你能看到你所做的網頁!
讓我們來為網頁增加更多功能。
## 加入 CSS
建立完 HTML 檔,現在我們為了造型加入 CSS。我們需要標記玩家需要輸入的單字若單字輸入錯誤時需要改變文字框的顏色。利用兩組 class 來完成:
在檔案 **style.css** 加入下列語法:
```css
/* 在 style.css 中 */
.highlight {
background-color: yellow;
}
.error {
background-color: lightcoral;
border: red;
}
```
✅ 處理 CSS 時,你可以規劃任何你想要的介面布局。花點時間讓你的網頁更迷人:
- 變更其他字型
- 改變標題顏色
- 改變物件大小
## JavaScript
建立完使用者介面後,我們要專注在 JavaScript 上,提供網頁邏輯處理的能力。我們將工作分為下列步驟:
- [建立常數](#建立常數)
- [事件監聽者 - 開始遊戲](#加入開始邏輯)
- [事件監聽者 - 輸入文字](#加入打字邏輯)
首先,我們先編輯檔案 **script.js**
### 建立常數
加入一些變數給程式使用。同樣地,就像食譜一樣,我們需要的食材如下:
- 矩陣,儲存所有引文
- 空矩陣,儲存單一引文的所有單字
- 變數,儲存空矩陣的索引,標記玩家現在面對的單字
- 變數,紀錄玩家點擊開始時的時間
我們也需要將使用者介面上的物件做連結:
- 文字框 (**typed-value**)
- 顯示引文 (**quote**)
- 訊息欄 (**message**)
```javascript
// 在檔案 script.js 中
// 所有的引文內容
const quotes = [
'When you have eliminated the impossible, whatever remains, however improbable, must be the truth.',
'There is nothing more deceptive than an obvious fact.',
'I ought to know by this time that when a fact appears to be opposed to a long train of deductions it invariably proves to be capable of bearing some other interpretation.',
'I never make exceptions. An exception disproves the rule.',
'What one man can invent another can discover.',
'Nothing clears up a case so much as stating it to another person.',
'Education never ends, Watson. It is a series of lessons, with the greatest for the last.',
];
// 儲存單字列表及目前要輸入的單字索引
let words = [];
let wordIndex = 0;
// 開始時間
let startTime = Date.now();
// 網頁物件連結
const quoteElement = document.getElementById('quote');
const messageElement = document.getElementById('message');
const typedValueElement = document.getElementById('typed-value');
```
✅ 試著加入更多的引文到你的遊戲中。
> **筆記** 我們可以接收任何物件,只要使用程式碼 `document.getElementById`。因為我們需要定期參考這些元素,所以使用常數來確認是否有單字輸入錯誤的問題。框架如[Vue.js](https://vuejs.org/)或[React](https://reactjs.org/)可以幫助你更好管理你的程式碼。
花點時間觀看下列關於 `const`、`let` 與 `var` 的影片。
[![變數類型](https://img.youtube.com/vi/JNIXfGiDWM8/0.jpg)](https://youtube.com/watch?v=JNIXfGiDWM8 "變數類型")
> 點擊上方圖片以觀賞關於變數的影片。
### 加入開始邏輯
為了開始我們的遊戲,玩家會點擊開始按鈕。當然,我們不知道何時玩家會開始遊戲,這就是為什麼我們使用[事件監聽者](https://developer.mozilla.org/docs/Web/API/EventTarget/addEventListener)到程式中。一個事件監聽者允許我們監看事件的觸發與對應的回應程式。在這個例子,我們希望當使用者點擊開始時,執行某些程式。
當玩家點擊 **start** 按鈕後,我們需要挑選一段引文、設定使用者介面並追蹤現在玩家的要輸入的單字與時間。下列為我們需要新增的程式碼,我們會在之後逐行解釋。
```javascript
// 在 script.js 末端
document.getElementById('start').addEventListener('click', () => {
// 取得一行引文
const quoteIndex = Math.floor(Math.random() * quotes.length);
const quote = quotes[quoteIndex];
// 將引文分成許多單字,存在矩陣中。
words = quote.split(' ');
// 重制單字索引來做追蹤
wordIndex = 0;
// 更新使用者介面
// 建立 span 元素的矩陣,設定 class 用。
const spanWords = words.map(function(word) { return `<span>${word} </span>`});
// 轉換成字串並以 innerHTML 顯示引文
quoteElement.innerHTML = spanWords.join('');
// 標記第一個單字
quoteElement.childNodes[0].className = 'highlight';
// 清除訊息欄之前的訊息
messageElement.innerText = '';
// 設定文字框
// 清除文字框
typedValueElement.value = '';
// 設定 focus
typedValueElement.focus();
// 設定事件驅動程式
// 開始計時器
startTime = new Date().getTime();
});
```
我們來分解程式碼吧!
- 設定單字追蹤
- 使用[Math.floor](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Math/floor)和[Math.random](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Math/random)讓我們能隨機從矩陣 `quotes` 中挑選一行引文
- 轉換 `quote``words` 組成的矩陣,追蹤目前玩家正在輸入的單字
- `wordIndex` 設定為 0玩家會從第一的單字開始輸入
- 設定使用者介面
- 建立矩陣 `spanWords`,將每一個單字包在 `span` 元素中
- 這讓我們能高光標記單字
- `join` 矩陣來建立字串,我們可以在 `quoteElement` 上更新 `innerHTML`
- 這會顯示引文給玩家檢視
- 設定第一個 `span` 元素的 `className``highlight`,來標記單字呈黃色
- 修改 `messageElement``innerText``''`,這會清除訊息欄的內容
- 設定文字框
- 清除目前 `typedValueElement``value`
- 設定 `typedValueElement``focus`
- 呼叫 `getTime` 來啟始計時器
### 加入打字邏輯
當玩家開始打字時,`input` 事件會被觸發。對應的事件監聽者需要檢查玩家是否輸入正確的單字,監控目前的遊戲狀況。回到檔案 **script.js**,加入下方程式碼到檔案最下方。我們會在後續解釋程式碼。
```javascript
// script.js 最末端
typedValueElement.addEventListener('input', () => {
// 取得目前的單字
const currentWord = words[wordIndex];
// 取得目前輸入的數值
const typedValue = typedValueElement.value;
if (typedValue === currentWord && wordIndex === words.length - 1) {
// 句子最末端
// 顯示成功
const elapsedTime = new Date().getTime() - startTime;
const message = `CONGRATULATIONS! You finished in ${elapsedTime / 1000} seconds.`;
messageElement.innerText = message;
} else if (typedValue.endsWith(' ') && typedValue.trim() === currentWord) {
// 單字最末端
// 清除輸入的數值,準備給新的單字使用
typedValueElement.value = '';
// 移動到下一個單字
wordIndex++;
// 重設所有引文子元素的 class 名稱
for (const wordElement of quoteElement.childNodes) {
wordElement.className = '';
}
// 標記新單字
quoteElement.childNodes[wordIndex].className = 'highlight';
} else if (currentWord.startsWith(typedValue)) {
// 單字目前輸入正確
// 標記下一個單字
typedValueElement.className = '';
} else {
// 單字輸入錯誤
typedValueElement.className = 'error';
}
});
```
讓我們分解程式碼吧!我們開始取得目前的單字與玩家輸入的數值。我們建立一系列的邏輯,檢查引文是否輸入完成,單字是否輸入完成,單字是否正確、是否錯誤。
- 引文完成,檢查 `typedValue``currentWord` 相等且 `wordIndex``words``length` 減一相等。
- 計算 `elapsedTime` ,利用目前時間減去 `startTime` 取得遊戲時長
- `elapsedTime` 除以 1,000 ,轉化毫秒單位為秒單位
- 顯示成功訊息
- 單字完成,以 `typedValue` 間的空白為界,檢查 `typedValue` 是否與 `currentWord` 相等
- 設定 `typedElement``value``''` ,準備給下一個單字輸入進來
- 增加 `wordIndex` 到下一個單字
- 進迴圈,每一個 `quoteElement``childNodes` ,它們的 `className` 都被設為 `''` ,代表預設的單字呈現規則
- 設定單字的 `className``highlight` 來標記為下一個被輸入的單字
- 單字目前輸入正確但未完成,從 `typedValue` 開始檢查 `currentWord`
- 確保清除 `typedValueElement``className`,顯示預設的呈現方式。
- 若此時輸入錯誤,我們加上錯誤規則
- 設定 `typedValueElement``className``error`
## 測試你的應用程式
我們做到最後了!最後一步就是確保我們的應用程式運作正常。試試看!不要擔心程式出現錯誤,**所有的開發者**都會面臨錯誤。有需要時,檢查程式訊息並偵錯。
點擊按鈕 **start**,馬上開始輸入單字!你可以看看這預覽動畫。
![遊戲中的動畫](../../4-typing-game/images/demo.gif)
---
## 🚀 挑戰
加入更多功能。
- 在完成遊戲時,關閉 `input` 事件監聽者;遊戲重新開始時,再重新開啟它。
- 當玩家完成引文時,關閉文字框
- 以對話窗格的方式顯示恭賀訊息
- 利用[localStorage](https://developer.mozilla.org/docs/Web/API/Window/localStorage)儲存最高分的資料
## 課後測驗
[課後測驗](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/22?loc=zh_tw)
## 複習與自學
在瀏覽器上閱讀[所有開發者可運用的事件](https://developer.mozilla.org/en-US/docs/Web/Events),想想你能在什麼樣的場合使用各個事件。
## 作業
[建立一款新的鍵盤遊戲](assignment.zh-tw.md)

@ -0,0 +1,11 @@
# 建立一款新的鍵盤遊戲
## 簡介
建立一款使用鍵盤事件的小遊戲。它可以是不同的鍵盤輸入遊戲:使用鍵盤在視窗上繪製像素點的繪圖遊戲。激發你的創意吧!
## 學習評量
| 作業內容 | 優良 | 普通 | 待改進 |
| -------- | ------------------ | ------------ | ------------ |
| | 呈現完整的遊戲內容 | 遊戲內容單調 | 遊戲出現問題 |

@ -0,0 +1,167 @@
# 瀏覽器擴充功能專案 Part 1關於瀏覽器
![瀏覽器繪圖筆記](../images/sketchnote.jpg)
> 由 [Wassim Chegham](https://dev.to/wassimchegham/ever-wondered-what-happens-when-you-type-in-a-url-in-an-address-bar-in-a-browser-3dob) 繪製
## 課前測驗
[課前測驗](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/23?loc=zh_tw)
### 大綱
瀏覽器擴充功能新增額外的功能給瀏覽器。在你建立之前,你應該學習瀏覽器是如何運作的。
### 關於瀏覽器
在這一系列的課程中,你會學習如何建立瀏覽器擴充功能,運作在 Chrome、Firefox 與 Edge 瀏覽器上。在這一章中,你會探索瀏覽器是如何運作,建立瀏覽器擴充功能的內容。
但到底何謂瀏覽器?它是幫助用戶顯示伺服器內容到網頁上的程式軟體。
✅ 小歷史:第一個網頁瀏覽器為 'WorldWideWeb',由 Timothy Berners-Lee 爵士於 1990 年建立。
![早期的瀏覽器](../images/earlybrowsers.jpg)
> 這邊有一些早期的瀏覽器,請參考[Karen McGrane](https://www.slideshare.net/KMcGrane/week-4-ixd-history-personal-computing)
用戶使用網址 URL (Uniform Resource Locator) 位置連上網路,通常以 `http``https` 位置開頭使用超文本傳輸協定(Hypertext Transfer Protocol),瀏覽器便能與該伺服器溝通並抓取網頁的資料。
這時,瀏覽器轉譯引擎會呈現到用戶的裝置上,可以是手機、桌機或是筆記型電腦。
瀏覽器也有能力暫存內容,不需要每一次都向伺服器請求內容。瀏覽器儲存用戶的瀏覽紀錄、儲存 'cookies',一種包含用戶活動資訊的小型資料。
請記得一件重要的事,各家瀏覽器並不會相同!每一種瀏覽器都有各自的長處短處,專業的網頁開發人員必須了解如何讓網頁在不同瀏覽器上運作正常。這包含處理手機的小視窗,處理離線用戶的行為。
這邊有一個值得加到你書籤的實用網頁:[caniuse.com](https://www.caniuse.com)。當你在建構網頁時,你可以查詢 caniuse 技術支援清單,確保你能提供用戶最佳的使用體驗。
✅ 你知道你的網頁用戶最常使用什麼瀏覽器嗎?檢查你的分析程式,你可以安裝各種分析程式當作是你開發的一種環節,它們會告訴你那些瀏覽器最常被使用。
## 瀏覽器擴充功能
為什麼你需要建立瀏覽器擴充功能?它能附加在瀏覽器上,讓你快速地重複執行部分功能。舉例來說,如果你需要在網頁中檢查你所互動的顏色,你或許需要顏色選擇器擴充功能;如果你有記憶帳號密碼的困擾,你可能需要密碼管理擴充功能。
瀏覽器擴充功能在開發上也很有趣。它們有效地管理並執行少部分任務課題。
✅ 你最喜歡哪一項瀏覽器擴充功能?它們提供了什麼功能?
### 安裝擴充功能
在你建立擴充功能以前先看看建制與安裝瀏覽器擴充功能的流程。每一種瀏覽器在管理套件上可能有些不同Edge上的管理過程就與 Chrome 與 Firefox 相似:
![Edge 瀏覽器開啟 edge://extensions 中的設定選單截圖](../images/install-on-edge.png)
大體而言,過程為:
- 指令 `npm run build` 建制你的管理套件
- 在瀏覽器中的延伸模組區點擊右上方的「更多設定」按鈕
- 如果這是新的套件,選擇 `load unpacked` 從資料夾上傳新的擴充套件(在我們的例子中, `/dist` )
- 如果這是已安裝的套件,點擊 `reload` 按鈕
✅ 上述教學步驟讓你導入自己建立的擴充功能;若要安裝已公開的套件,你可以前往瀏覽器擴充功能商店,逛逛這些[商店](https://microsoftedge.microsoft.com/addons/Microsoft-Edge-Extensions-Home)並安裝你選擇的套件。
### 展開行動
你打算寫一套擴充功能來顯示你國家的碳足跡,顯示國家的能源使用量與可用能源量。套件內會有 API Key 來存取網頁 CO2 Signal 的 API。
**你需要:**
- [一組 API key](https://www.co2signal.com/):在網頁上輸入你的電子信箱,它會寄一組鑰匙給你
- 給[Electricity Map](https://www.electricitymap.org/map)使用的[國家區域代碼](http://api.electricitymap.org/v3/zones) (舉個例子,在波士頓使用'US-NEISO')
- [程式碼](../../start),下載 `start` 資料夾,你需要修改裡面的程式碼檔案。
- [NPM](https://www.npmjs.com)NPM 是一套軟體包管理工具,在本地安裝的軟體包會被列在 `package.json` 檔案中,成為網頁利用的資源。
✅ 從[這個優質的學習套件](https://docs.microsoft.com/en-us/learn/modules/create-nodejs-project-dependencies/?WT.mc_id=academic-13441-cxa)中,學習更多關於軟體包管理。
花點時間看一下程式檔案結構
dist
-|manifest.json (defaults set)
-|index.html (前端 HTML)
-|background.js (background JS)
-|main.js (built JS)
src
-|index.js (你的 JS 程式碼)
✅ 當你取得你的 API Key 與國家區域代碼後,紀錄在筆記中給之後的課程使用。
### 建立給擴充功能使用的 HTML
這套擴充功能有兩個重點。一個是取得 API Key 與國家區域代碼:
![在瀏覽器擴充功能中,顯示 API key與國家區域代碼的輸入欄截圖](../images/1.png)
與顯示國家的碳排放量:
![在瀏覽器擴充功能中,顯示 US-NEISO 地區碳排放量與石化燃料比例截圖](../images/2.png)
讓我們開始建立輸入欄位的 HTML 與它的 CSS 吧。
在資料夾 `/dist` 中,建立輸入表單與結果顯示區域。在檔案 `index.html` 中,規劃表單區域:
```HTML
<form class="form-data" autocomplete="on">
<div>
<h2>New? Add your Information</h2>
</div>
<div>
<label for="region">Region Name</label>
<input type="text" id="region" required class="region-name" />
</div>
<div>
<label for="api">Your API Key from tmrow</label>
<input type="text" id="api" required class="api-key" />
</div>
<button class="search-btn">Submit</button>
</form>
```
這個表單儲存你的輸入資訊並儲存到 Local Storage 中。
接下來,建立結果輸出區。在 form tag 後面新增一些 divs
```HTML
<div class="result">
<div class="loading">loading...</div>
<div class="errors"></div>
<div class="data"></div>
<div class="result-container">
<p><strong>Region: </strong><span class="my-region"></span></p>
<p><strong>Carbon Usage: </strong><span class="carbon-usage"></span></p>
<p><strong>Fossil Fuel Percentage: </strong><span class="fossil-fuel"></span></p>
</div>
<button class="clear-btn">Change region</button>
</div>
```
這時,你可以試著建制這個專案。請確保安裝擴充套建的軟體依賴套件,輸入:
```
npm install
```
這項指令會使用 NPM (Node Package Manager)安裝 webpack 給你的擴充套件建制過程中使用。Webpack 是一個處理程式編譯的工具組合包。你可以在 `/dist/main.js` 看到它的執行後的結果 ── 程式碼已經被打好包了。
到目前為止,擴充套件已經被建制,如果你導入此套件到 Edge 中也能完整地呈現出來。
恭喜你,你已經達成建立擴充套件的第一步驟。在接下來的課程中,你會新增更多功能,讓它更加的實用。
---
## 🚀 挑戰
逛逛瀏覽器擴充商店,安裝一套擴充功能到你的瀏覽器中。你可以查看它的檔案群。你發現了什麼?
## 課後測驗
[課後測驗](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/24?loc=zh_tw)
## 複習與自學
這堂課中你學到了一些瀏覽器的歷史。趁這個機會閱讀更多它的歷史,學習網際網路的發明者是如何構思網路的應用。這邊有一些實用的網頁:
[瀏覽器的歷史](https://www.mozilla.org/en-US/firefox/browsers/browser-history/)
[網路的歷史](https://webfoundation.org/about/vision/history-of-the-web/)
[與 Tim Berners-Lee 的訪談](https://www.theguardian.com/technology/2019/mar/12/tim-berners-lee-on-30-years-of-the-web-if-we-dream-a-little-we-can-get-the-web-we-want)
## 作業
[重新造型你的套件](assignment.zh-tw.md)

@ -0,0 +1,11 @@
# 重新造型你的套件
## 簡介
本課程的擴充套件已經包含了造型設定,但你不需要非得使用它們。改寫它的 CSS 檔來重新構築擴充插件的造型。
## 學習評量
| 作業內容 | 優良 | 普通 | 待改進 |
| -------- | -------------------------- | -------------- | ------------ |
| | 新造型能正常地套用在程式中 | 造型規劃不完整 | 套件出現問題 |

@ -0,0 +1,224 @@
# 瀏覽器擴充功能專案 Part 1呼叫 API使用 Local Storage
## 課前測驗
[課前測驗](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/25?loc=zh_tw)
### 大綱
在這堂課中,藉由傳遞你的擴充功能表單並顯示結果來呼叫 API。此外你會了解如何儲存資料到瀏覽器的 Local Storage 中給未來使用。
✅ 請參考下列程式碼段,加入程式碼到檔案適當的位置
### 設定控制擴充功能的元素:
現在你有已建好的 HTML 表單與結果區 `<div>`。接下來,你需要在 `/src/index.js` 做一些處理,一點一點地構築出你的擴充功能。參考[前一堂課程](../../1-about-browsers/translations/README.zh-tw.md)來設置你的專案與了解建制過程。
處理 `index.js` 檔案,建立一些 `const` 變數來儲存不同用途的數值:
```JavaScript
// 表單區域
const form = document.querySelector('.form-data');
const region = document.querySelector('.region-name');
const apiKey = document.querySelector('.api-key');
// 結果區域
const errors = document.querySelector('.errors');
const loading = document.querySelector('.loading');
const results = document.querySelector('.result-container');
const usage = document.querySelector('.carbon-usage');
const fossilfuel = document.querySelector('.fossil-fuel');
const myregion = document.querySelector('.my-region');
const clearBtn = document.querySelector('.clear-btn');
```
這些區域會被 CSS class 給參考,它們在前一堂課中已經被你設定好了。
### 新增監聽者
接下來,新增提交與重置表單的事件監聽者與按鈕,讓使用者能提交表單或是點擊重置鈕時,事件會發生。新增初始化呼叫處理到應用中,在檔案的最下方新增:
```JavaScript
form.addEventListener('submit', (e) => handleSubmit(e));
clearBtn.addEventListener('click', (e) => reset(e));
init();
```
✅ 注意提交事件與點擊事件的寫法,事件是如何被傳入到 handleSubmit 或是 reset 函式中的。你能在不改變功能的情況下,改寫成較長的格式嗎?你比較喜歡哪一種寫法?
### 建立 init() 函式與 reset() 函式:
現在你需要建立函式 init(),處理應用程式的初始化部分:
```JavaScript
function init() {
//如果任何東西存在 localStorage 中,取出來
const storedApiKey = localStorage.getItem('apiKey');
const storedRegion = localStorage.getItem('regionName');
//設定 icon 為通用綠色
//todo
if (storedApiKey === null || storedRegion === null) {
//如果沒有 keys顯示表單
form.style.display = 'block';
results.style.display = 'none';
loading.style.display = 'none';
clearBtn.style.display = 'none';
errors.textContent = '';
} else {
//localStorage 有 saved keys/regions顯示結果
displayCarbonUsage(storedApiKey, storedRegion);
results.style.display = 'none';
form.style.display = 'none';
clearBtn.style.display = 'block';
}
};
function reset(e) {
e.preventDefault();
//只清除 local storage 國家區域代碼
localStorage.removeItem('regionName');
init();
}
```
在函式中,有一些有趣的邏輯。閱讀它們,你看出發生什麼事嗎?
- 兩個 `const` 被設定為檢查用戶是否有儲存 APIKey 與國家區域代碼在 local storage 中。
- 若兩者皆為 null將造型設為 'block' 來顯示表單
- 隱藏 results、loading 與 clearBtn設定 error 文字為空字串
- 若存在 key 與代碼,開始新的流程:
- 呼叫 API 取得碳排放資訊
- 隱藏結果區域
- 隱藏表單
- 顯示重置按鈕
在下一步之前,你可以學習一些瀏覽器的重要成員:[LocalStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage)。 LocalStorage 是瀏覽器儲存字串的有效方法,以 `key-value` 配對兩兩一組。這種儲存型態可以被 JavaScript 管理並控制瀏覽器的資料。LocalStorage 沒有期限,而另一款網頁儲存 SessionStorage 會在瀏覽器關閉時清除內容。不同的儲存方式有各自的優缺點。
> 注意 ── 你的瀏覽器擴充套件有自己的 local storage。主瀏覽器視窗是不同的個體兩者會做各自的行為。
你設定 APIKey 紀錄字串數值。你可以在 Edge 瀏覽器上「檢查」一個網頁 (右鍵瀏覽器來檢查),在 Applications 標籤中觀察儲存區的使用情況。
![Local storage 區域](../images/localstorage.png)
✅ 想想那些情況你不需要儲存資料到 LocalStorage 中。總體而言,將 API Keys 放在 LocalStorage 是個很糟糕的想法!你知道為什麼嗎?在我們的例子中,我們的應用程式是以教學為目的,並不會發布在應用程式商店中,所以我們選擇此中處理方式。
你可以發現網頁 API 能處理 LocalStorage使用 `getItem()`、`setItem()` 或是 `removeItem()`。它們廣泛地支援不同的瀏覽器。
在建立函式 `init()` 中的函式 `displayCarbonUsage()` 之前,我們先建立表單提交初始化的功能。
### 處理表單提交
建立函式 `handleSubmit`,接收事件參數 `(e)`。終止網頁移轉的事件(在本例子中,我們終止瀏覽器刷新的處理)並呼叫新的函式 `setUpUser`,傳送參數 `apiKey.value``region.value`。藉由這個方式,你能將兩個初始表單的數值正確地移轉到適合的位置。
```JavaScript
function handleSubmit(e) {
e.preventDefault();
setUpUser(apiKey.value, region.value);
}
```
✅ 刷新你的記憶 ── 上堂課中的 HTML 檔案開頭有兩個輸入區域,它們的 `values` 被存到 `const` 中,並且被定為 `required`,表示瀏覽器禁止使用者輸入空值。
### 設定使用者
來到函式 `setUpUser`,這裡你能找到 apiKey 與 regionName 被存到 Local Storage 中。新增函式:
```JavaScript
function setUpUser(apiKey, regionName) {
localStorage.setItem('apiKey', apiKey);
localStorage.setItem('regionName', regionName);
loading.style.display = 'block';
errors.textContent = '';
clearBtn.style.display = 'block';
//建立初始化呼叫
displayCarbonUsage(apiKey, regionName);
}
```
這個函式設定當 API 被呼叫時,顯示讀取訊息。到這裡,你即將建立這個擴充功能專案最重要的函式!
### 顯示碳排放量
最後,是時候查詢 API 了!
在前往下一步前,我們先來討論何謂 API。API[Application Programming Interfaces](https://www.webopedia.com/TERM/A/API.html),是網頁開發者工具箱內最重要的成員。它們提供程式標準的互動模式與溝通介面,舉例來說,如果你建立一個需要存取資料庫的網頁,資料庫方可能就有人建立了 API 供你使用。API 有各式各樣的種類,最普遍使用的為[REST API](https://www.smashingmagazine.com/2018/01/understanding-using-rest-api/)。
✅ 'REST' 全名為 'Representational State Transfer',提供各式各樣 URL 形式來抓取資料。對網路開發者的 API 種類做一點研究,什麼形式的 API 最吸引你?
這條函式中有一個重要到值得紀錄的事情。第一點為[關鍵字 `async`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function)。讓你的函式非同步地執行,在行為完成前做等待,譬如資料被回傳。
這裡有一個簡短的影片介紹 `async`
[![Async 與 Await 處理 promises 物件](https://img.youtube.com/vi/YwmlRkrxvkk/0.jpg)](https://youtube.com/watch?v=YwmlRkrxvkk "Async 與 Await 處理 promises 物件")
> 點擊上方圖片以觀賞關於 async/await 的影片。
建立新的函式來詢問 C02Signal 的 API
```JavaScript
import axios from '../node_modules/axios';
async function displayCarbonUsage(apiKey, region) {
try {
await axios
.get('https://api.co2signal.com/v1/latest', {
params: {
countryCode: region,
},
headers: {
'auth-token': apiKey,
},
})
.then((response) => {
let CO2 = Math.floor(response.data.data.carbonIntensity);
//calculateColor(CO2);
loading.style.display = 'none';
form.style.display = 'none';
myregion.textContent = region;
usage.textContent =
Math.round(response.data.data.carbonIntensity) + ' grams (grams C02 emitted per kilowatt hour)';
fossilfuel.textContent =
response.data.data.fossilFuelPercentage.toFixed(2) +
'% (percentage of fossil fuels used to generate electricity)';
results.style.display = 'block';
});
} catch (error) {
console.log(error);
loading.style.display = 'none';
results.style.display = 'none';
errors.textContent = 'Sorry, we have no data for the region you have requested.';
}
}
```
這是一個挺大的函式,發生了什麼事?
- 遵循程式實踐過程,你使用關鍵字 `async` 讓函式非同步地作行為。函式內的 `try/catch` 區塊會在 API 回傳資料時回傳 promise 物件。因為我們無法控制 API 會多快地回應訊息(甚至無法回應訊息!),你需要處理這種不確定性的時序關係。
- 藉由提供 API Key 訪問 co2signal API 以取得你的地區資料。要使用這把鑰匙,你必須在網頁標頭中新增認證參數。
- 當 API 回應時,你將各種物件填入回傳的數值,並輸出到畫面上中。
- 如果發生錯誤,或沒有結果產生,輸出錯誤訊息。
✅ 非同步程式設計是一種實用的工具。閱讀[更多使用方法](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function)設定非同步程式的程式碼。
恭喜你!當你建制你的專案(`npm run build`)並在瀏覽器上刷新功能,你有個可以運作的應用套件了!現在只差圖示無法正常顯示,我們會在下一堂課中修正它。
---
## 🚀 挑戰
我們在課程中討論了不同種類的 API。選擇一樣網頁 API 並做更深度的研究。舉例來說,看看瀏覽器內支援的 API 如 [HTML Drag and Drop API](https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API)。依你看,什麼決定了 API 的優劣?
## 課後測驗
[課後測驗](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/26?loc=zh_tw)
## 複習與自學
這堂課你學會關於 LocalStorage 與 API它們對資深網頁開發者提供很大的幫助。你能想想這兩樣東西如何彼此相互合作呢想想你會如何建構你的網頁讓 API 得以使用你所儲存的資料。
## 作業
[認領一項 API](assignment.zh-tw.md)

@ -0,0 +1,11 @@
# 認領一項 API
## 大綱
API 可以是很好玩的。這裡有[許多公開 API 的清單](https://github.com/public-apis/public-apis)。挑選一項 API建立一個網頁擴充功能來解決問題。問題可以很小如找不到足夠的寵物照片這時你可以嘗試使用[dog CEO API](https://dog.ceo/dog-api/));或是解決更大問題。好好享受吧!
## 學習評量
| 作業內容 | 優良 | 普通 | 待改進 |
| -------- | --------------------------------------------- | ------------------------ | ------------ |
| | 使用上述清單內的 API 建立完整的瀏覽器擴充功能 | 建立部分的瀏覽器擴充功能 | 套件存在問題 |

@ -0,0 +1,160 @@
# 瀏覽器擴充功能專案 Part 1學習背景工作與效能
## 課前測驗
[課前測驗](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/27?loc=zh_tw)
### 大綱
在前兩堂課程中,你學會如何建立表單、顯示 API 回覆的資料在結果區塊中,這是網頁處理網頁資訊的標準行為。你甚至學會了如何非同步性地抓取資料。你的擴充套件就快完成了。
它只剩管理背景工作:包括刷新套件的圖示顏色,我們來討論瀏覽器是如何處理這類的工作。也讓我們探討你所建立的網頁,瀏覽器會多有效地處理其中的內容。
## 網頁處理效能的基礎
> "網頁處理效能攸關兩件事:網頁多快地載入,與程式多快地執行。" -- [Zack Grossbart](https://www.smashingmagazine.com/2012/06/javascript-profiling-chrome-developer-tools/)
關於如何讓你的網頁能快速地運作在各類裝置、各個使用者以及各種情況,是一件難以想像的龐大主題。這裡有一些要點確保你在開發網頁或是擴充功能時,銘記在心。
第一件事為確保網頁收集關於網頁效能的資料,在瀏覽器的開發者工具中可以實現它。在 Edge 中,選擇「設定及更多」按鈕(瀏覽器上三個點的圖示),並選擇更多工具 > 開發人員工具並開啟 Performance 分頁。你也可以使用鍵盤快捷鍵Windows 上的 `Ctrl` + `Shift` + `I` 與 Mac 上的 `Option` + `Command` + `I` 來開啟開發人員工具。
Performance 分頁包括了效能分析工具。開啟一個網頁,例如 https://www.microsoft.com點擊 'Record' 按鈕並重新整理網頁。停止錄製後你就能取得網頁的 'script'、'render' 與 'paint' 的過程與資訊:
![Edge 性能分析工具](../images/profiler.png)
✅ 造訪[Microsoft 文件](https://docs.microsoft.com/en-us/microsoft-edge/devtools-guide/performance?WT.mc_id=academic-13441-cxa)觀看 Edge 的 Performance 分頁資訊
> 提示:要取得真正的網頁開啟時間,記得清除你的瀏覽器快取。
選擇一樣網頁在載入時,時間列中出現的事件物件。
觀看它的總覽面板並截圖你的網頁效能。
![Edge 性能分析工具截圖](../images/snapshot.png)
檢查 Event Log 面板,是否有網頁事件花超過 15 毫秒:
![Edge event log](../images/log.png)
✅ 了解你的性能分析工具!在這個網頁中,開啟開發者工具,檢查是否有任何 bottleneck。什麼是載入最久的物件哪個又是最快的
## 效能分析
總體而言,每一位網頁開發者一定要注意一些「有問題的地方」,避免在發布作品時有令人意想不到的驚喜。
**資產(Asset)大小**:過去幾年來,網頁「變重」了,也因此變慢了。有些負擔來自於圖片的使用。
✅ 查詢[Internet Archive](https://httparchive.org/reports/page-weight),看看過去的網頁負擔等資訊。
一個好的習慣是確保你的圖片有做最佳化,呈現合理的檔案大小及解析度影像給你的使用者。
**DOM 查找元素(Traversal)**:瀏覽器必須依照你的程式碼建立 Document Object Model請確保你的 tags 最小化,網頁只使用必須的功能與造型。另外,過量的網頁 CSS 也可以被最佳化,舉例來說,造型樣板只用在單頁上,而非全域上。
**JavaScript**:每一位 JavaScript 開發者會觀察 'render-blocking' 腳本,它會在 DOM 查找與瀏覽器呈現前被載入好。請考慮使用 `defer` 在你的程式碼中,我們的盆栽盒專案就有實踐這行。
✅ 在[網頁測速網](https://www.webpagetest.org/)上測試一些網頁,學習確認網頁效能的基本檢查。
現在你了解瀏覽器如何呈現你所提供的資產,我們來看看我們的擴充功能最後需要補齊的項目:
### 建立函式計算顏色
編輯 `/src/index.js`,新增函式 `calculateColor()` 在一系列為了 DOM 存取的 `const` 變數之後:
```JavaScript
function calculateColor(value) {
let co2Scale = [0, 150, 600, 750, 800];
let colors = ['#2AA364', '#F5EB4D', '#9E4229', '#381D02', '#381D02'];
let closestNum = co2Scale.sort((a, b) => {
return Math.abs(a - value) - Math.abs(b - value);
})[0];
console.log(value + ' is closest to ' + closestNum);
let num = (element) => element > closestNum;
let scaleIndex = co2Scale.findIndex(num);
let closestColor = colors[scaleIndex];
console.log(scaleIndex, closestColor);
chrome.runtime.sendMessage({ action: 'updateIcon', value: { color: closestColor } });
}
```
發生了什麼事?你傳遞了 API 回傳的二氧化碳濃度數值,計算出它最適合對應的顏色矩陣索引位置。之後,你將這個顏色數值傳給了 chrome runtime。
chrome.runtime 有[一個 API](https://developer.chrome.com/extensions/runtime)處理所有的背景工作,你的擴充套件借助了此功能:
> "在應用程式中,使用 chrome.runtime API 來接收背景頁面,回傳關於 manifest 的資訊,監聽並回應事件。你也可以利用此 API 轉換 URL 的相對路徑成絕對路徑。"
✅ 如果你正打算開發此專案給 Edge 瀏覽器上使用,你會訝異你使用的是 chrome API。新的 Edge 瀏覽器執行在 Chromium browser 引擎上,所以你也能使用這些工具。
> 注意,如果你想要剖析瀏覽器擴充功能,請在擴充套件上執行開發者工具,它與瀏覽器主視窗為不同的個體。
### 設定圖示預設顏色
現在,在函式 `init()` 中,利用呼叫 chrome `updateIcon` 設定圖示顏色為通用綠:
```JavaScript
chrome.runtime.sendMessage({
action: 'updateIcon',
value: {
color: 'green',
},
});
```
### 呼叫函式、執行呼叫
接下來,在 C02Signal API 回傳的 promise 物件下方呼叫函式:
```JavaScript
//let CO2...
calculateColor(CO2);
```
最後,在檔案 `/dist/background.js` 中,新增事件監聽者給這些背景行為的呼叫:
```JavaScript
chrome.runtime.onMessage.addListener(function (msg, sender, sendResponse) {
if (msg.action === 'updateIcon') {
chrome.browserAction.setIcon({ imageData: drawIcon(msg.value) });
}
});
//參考 energy lollipop extension很好的程式
function drawIcon(value) {
let canvas = document.createElement('canvas');
let context = canvas.getContext('2d');
context.beginPath();
context.fillStyle = value.color;
context.arc(100, 100, 50, 0, 2 * Math.PI);
context.fill();
return context.getImageData(50, 50, 100, 100);
}
```
在此程式中,你建立了事件監聽者給任何前到背景工作管理者的訊息。若 'updateIcon' 被呼叫,則接下來的程式會被執行,利用 Canvas API 繪製出對應顏色的圖示。
✅ 你會學習更多關於 Canvas API 在往後的[太空遊戲課程](../../6-space-game/2-drawing-to-canvas/translations/README.zh-tw.md)。
現在,重新建制你的擴充功能(`npm run build`),刷新並運行你的套件,觀察圖示的顏色變化。現在是時候去跑腿或是洗碗嗎?現在你知道了!
恭喜你,你已經建立了一款實用的瀏覽器擴充功能,並學到更多瀏覽器的運作方式與監測它的效能分析。
---
## 🚀 挑戰
調查一些悠久的開源網站,並根據它們的 GitHub 歷史,你能分辨它們過去幾年以來效能上的調整嗎?什麼它們是共同的痛點?
## 課後測驗
[課後測驗](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/28?loc=zh_tw)
## 複習與自學
請考慮註冊[performance newsletter](https://perf.email/)
調查瀏覽器測量網頁效能的方法,查看開發者工具內的 Performance 分頁。你能找到什麼巨大的差別嗎?
## 作業
[分析網頁效能](assignment.zh-tw.md)

@ -0,0 +1,9 @@
# 分析網頁效能
請提供一份詳細的報告,點出一個網頁效能上的問題點。分析網頁緩慢的原因並提供改善它的方案。不要只依賴瀏覽器工具,做一點研究尋找更多幫助你的工具。
## 學習評量
| 作業內容 | 優良 | 普通 | 待改進 |
| -------- | ---------------------------------- | -------------- | ------------ |
| | 詳細的報告包括非瀏覽器的第三方工具 | 呈現出標準報告 | 報告內容有限 |

@ -0,0 +1,28 @@
# 建立瀏覽器擴充功能
建立瀏覽器擴充功能是個好玩且有趣的方式來思考應用程式的執行效能,包含各式各樣類型的網頁資產。這堂學習模組介紹了瀏覽器運作方式、如何架設擴充功能、建立表單、呼叫 API、使用 Local Storage 和測量網頁效能的方法並增進它。
你會建立一個支援在 Edge、Chrome 與 Firefox 的瀏覽器擴充功能。這個擴充功能就像小型的網頁,專門滿足特定課題:利用[C02 Signal API](https://www.co2signal.com)檢查地區的電力使用與碳排濃度,回傳地區的碳足跡。
這款特設擴充功能允許使用者在輸入完 API Key 與國家地區代碼到表單後,取得當地電力使用量與其他資訊,決定使用者後續的行為。舉例來說,在地區高電力用量時,你可能會延後烘衣機的使用(增加碳排)。
### 主題
1. [關於瀏覽器](../1-about-browsers/translations/README.zh-tw.md)
2. [表單與 Local Storage](../2-forms-browsers-local-storage/translations/README.zh-tw.md)
3. [背景工作與效能](../3-background-tasks-and-performance/translations/README.zh-tw.md)
### 成就
![綠能瀏覽器擴充功能](../extension-screenshot.png)
## 參與人員
網頁碳排放追蹤的發想出自於 Asim Hussain微軟綠能雲端倡導小組的領導人與[Green Principles](https://principles.green/)的作者。這源自於一個[網頁專案](https://github.com/jlooper/green)。
擴充功能的結構受[Adebola Adeniran 的 COVID 擴充功能](https://github.com/onedebos/covtension)啟發。
「點」圖示系統的概念參考[Energy Lollipop](https://energylollipop.com/)的加州排放擴充功能。
這些課程由 [Jen Looper](https://www.twitter.com/jenlooper) 用滿滿的 ♥️ 來編寫。

@ -0,0 +1,224 @@
# 建立太空遊戲 Part 1簡介
![影片](../../images/pewpew.gif)
## 課前測驗
[課前測驗](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/29?loc=zh_tw)
### 遊戲開發中的繼承(Inheritance)與組合(Composition)
在之前的課程中,因為專案較小的規模,我們不需要去擔憂應用程式的設計結構。然而,當你的應用程式規模越來越大時,結構的選擇就是一大課題。在 JavaScript 中,有兩種大方向來建立龐大的應用程式:*組合(Composition)*與*繼承(Inheritance)*。它們有各自的優缺點,我們會藉由遊戲內容來進行說明。
✅ 其中一本有名的程式設計用書是有關於[設計模式](https://zh.wikipedia.org/wiki/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%EF%BC%9A%E5%8F%AF%E5%A4%8D%E7%94%A8%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E8%BD%AF%E4%BB%B6%E7%9A%84%E5%9F%BA%E7%A1%80)。
在遊戲中你會有`遊戲物件`,顯示在畫面中。這代表它們在笛卡爾座標系中有各自的位置,以 `x``y` 座標點定義。當你在開發遊戲時,你會注意到所有的遊戲物件都有一套標準的規範,和大多數的遊戲相似,通常會有這些元素:
- **適地性** 大多數遊戲元素都是建立在位置上的。這代表他們有各自的所在處,一組 `x``y`
- **可移動的** 這些物件可以移動到新的位置。典型來說有英雄、怪物或是 NPC(Non Player Character),但有些例外,好比是樹這種常駐物件。
- **可自毀的** 這些物件只能存在於一小段時間,接著它們就會自我刪除。通常這是`死亡`或是`被摧毀`的布林訊號傳遞給遊戲引擎,告知物件不再需要被描繪出來。
- **冷卻時間** 「冷卻時間」是存活週期短的典型物件屬性。好比是一段文字、爆炸的視覺特效,只能呈現數毫秒的時間。
✅ 想想看遊戲小精靈(Pac-Man)。你能辨別出符合上述清單的其中四種物件嗎?
### 行為表達
以上的敘述皆在表達遊戲物件所進行的行為。那我們該如何去編寫它們呢?我們可以使用方法(methods)連接 classes 或是物件(objects)來表達這些行為。
**Classes**
這個想法是結合 `classes` 與`繼承`的方式來在 class 中添加特定行為。
✅ 繼承是一個重要概念。在[有關繼承的 MDN 文章中](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Inheritance_and_the_prototype_chain)學習更多內容。
以程式碼來表達的話,一個遊戲物件通常會呈現這種形式:
```javascript
//設定 class GameObject
class GameObject {
constructor(x, y, type) {
this.x = x;
this.y = y;
this.type = type;
}
}
//這個 class 會繼承 GameObject 中 class 內容
class Movable extends GameObject {
constructor(x,y, type) {
super(x,y, type)
}
//這個可移動物件可以在畫面上移動
moveTo(x, y) {
this.x = x;
this.y = y;
}
}
//這是特定的 class 繼承 Movable class它能使用所有繼承到的屬性內容
class Hero extends Movable {
constructor(x,y) {
super(x,y, 'Hero')
}
}
//另一方面,這個 class 只繼承到 GameObject 的內容
class Tree extends GameObject {
constructor(x,y) {
super(x,y, 'Tree')
}
}
//英雄可以移動......
const hero = new Hero();
hero.moveTo(5,5);
//但樹木卻不能
const tree = new Tree();
```
✅ 花點時間重新構思小精靈(Pac-Man)的主角,或是 Inky、Pinky 與 Blinky 這幾隻鬼魂。它們該如何以 JavaScript 表現?
**組合**
另一種處理物件繼承的方式為*組合(Composition)*。物件以這種方式呈現它們的行為:
```javascript
//建立常數 gameObject
const gameObject = {
x: 0,
y: 0,
type: ''
};
//...與常數 movable
const movable = {
moveTo(x, y) {
this.x = x;
this.y = y;
}
}
//常數 movableObject 是 gameObject 與 movable 的組合
const movableObject = {...gameObject, ...movable};
//利用函式建立新的英雄,繼承 movableObject 的內容
function createHero(x, y) {
return {
...movableObject,
x,
y,
type: 'Hero'
}
}
//...與常駐物件只繼承 gameObject 的屬性
function createStatic(x, y, type) {
return {
...gameObject
x,
y,
type
}
}
//建立可以移動的英雄
const hero = createHero(10,10);
hero.moveTo(5,5);
//和建立只能佇立於此的樹木
const tree = createStatic(0,0, 'Tree');
```
**我該使用哪一種設計模式?**
這都取決於你選擇何種設計模式。JavaScript 支援這兩種範例。
--
另一種在遊戲開發中常見的設計模式負責處理玩家的遊戲表現與遊戲體驗。
## 發布訂閱設計模式
✅ Pub/Sub 全名為 'publish-subscribe'
這個設計模式將應用程式內不同的模組分開處理,讓彼此不知道彼此的行為。為何要這樣做?這讓我們總觀上更輕易地了解各個模組的行為。也可以在你想要時輕易地改變模組的行為模式。我們該如何實踐它呢?我們先建立這幾個概念:
- **訊息** 一個訊息通常會以文字字串與額外的負載(payload) ── 一組定義訊息內容的資料 ── 呈現。遊戲中典型的訊息可以是 `KEY_PRESSED_ENTER`
- **發布者** 這個元素*發布*訊息給所有的訂閱者。
- **訂閱者** 這個元素*監聽*特定的訊息,並藉由執行某些任務以作為訊息的回應,例如發射雷射光。
實踐方法雖小,但這是功能強大的設計方式。這是它的建立方式:
```javascript
//設定 EventEmitter class 容納監聽者
class EventEmitter {
constructor() {
this.listeners = {};
}
//當訊息接收時,讓監聽者處理它的負載
on(message, listener) {
if (!this.listeners[message]) {
this.listeners[message] = [];
}
this.listeners[message].push(listener);
}
//當訊息發出時,附上負載發給監聽者
emit(message, payload = null) {
if (this.listeners[message]) {
this.listeners[message].forEach(l => l(message, payload))
}
}
}
```
利用上述程式我們建立一套小型實作內容:
```javascript
//設定訊息種類
const Messages = {
HERO_MOVE_LEFT: 'HERO_MOVE_LEFT'
};
//調用你設定的 eventEmitter
const eventEmitter = new EventEmitter();
//設定英雄
const hero = createHero(0,0);
//讓 eventEmitter 監聽有關英雄往左移的訊息,並執行動作
eventEmitter.on(Messages.HERO_MOVE_LEFT, () => {
hero.move(5,0);
});
//設定遊戲視窗來監聽鍵盤事件,當左方向鍵按壓時,發出英雄往左移的訊息
window.addEventListener('keyup', (evt) => {
if (evt.key === 'ArrowLeft') {
eventEmitter.emit(Messages.HERO_MOVE_LEFT)
}
});
```
我們連接了鍵盤事件 `ArrowLeft` 並傳遞 `HERO_MOVE_LEFT` 訊息。我們監聽該訊息並移動 `hero` 作為結果。這種開發方式讓事件監聽者與英雄區隔開來。你也可以將 `ArrowLeft` 換成 `A` 鍵。此外,我們能修改 eventEmitter 的 on 函式,讓 `ArrowLeft` 事件產生截然不同的行為。
```javascript
eventEmitter.on(Messages.HERO_MOVE_LEFT, () => {
hero.move(5,0);
});
```
當遊戲越來越豐富、物件越來越複雜時,這套設計方式能維持程式碼的整潔。由衷建議善用這套設計模式。
---
## 🚀 挑戰
想想看發布訂閱模式可以如何增進一款遊戲。哪一個部份該發送事件,而遊戲又該如何回應事件?現在你有機會發揮你的創意,思考一款新遊戲和它運作的模組。
## 課後測驗
[課後測驗](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/30?loc=zh_tw)
## 複習與自學
藉由[閱讀此連結](https://docs.microsoft.com/en-us/azure/architecture/patterns/publisher-subscriber?WT.mc_id=academic-13441-cxa)來認識更多關於發布與訂閱的設計模式。
## 作業
[建立遊戲雛形](assignment.zh-tw.md)

@ -0,0 +1,11 @@
# 建立遊戲雛形
## 簡介
使用本課程中的程式案例,編寫一款你喜歡的遊戲呈現方式。這是一款簡單小規模的遊戲,目的是要能以 class、組合模式與發布訂閱模式呈現遊戲的運作方式。發揮你的創意
## 學習評量
| 作業內容 | 優良 | 普通 | 待改進 |
| -------- | -------------------------- | -------------------------- | ---------------------------- |
| | 畫面上有三個元素且能被控制 | 畫面上有兩個元素且能被控制 | 畫面上只有一個元素且能被控制 |

@ -0,0 +1,216 @@
# 建立太空遊戲 Part 2在畫布上繪製英雄與怪物
## 課前測驗
[課前測驗](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/31?loc=zh_tw)
## Canvas
Canvas 是 HTML 中的元素,預設上不帶有任何內容,就如一塊白板。你需要自己彩繪上去。
✅ 在 MDN 上閱讀[更多關於 Canvas API](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API)。
這是它典型的宣告方式,位在頁面的 body 中:
```html
<canvas id="myCanvas" width="200" height="100"></canvas>
```
上面我們設定了 `id`、`width` 和 `height`
- `id`:讓你在處理物件時,能快速地取得參考位置。
- `width`:物件的寬度。
- `height`:物件的高度。
## 繪製簡單幾何圖樣
Canvas 使用了笛卡爾座標系繪製圖案。因此有 x 軸與 y 軸來表達物件的所在地點。座標點 `0,0` 位在畫布的左上方;而右下方則是我們定義畫布的寬度與高度。
![畫布網格](../canvas_grid.png)
> 圖片出自於 [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Drawing_shapes)
要在 Canvas 物件上繪製圖案,你需要執行下列步驟:
1. **取得 Canvas 物件的參考位置**。
1. **取得 Context 物件的參考位置**,定義在 Canvas 元素中。
1. 使用 context 元素**進行繪製動作**。
以程式碼表達上述步驟會呈現成:
```javascript
// 繪製紅色矩形
//1. 取得 canvas 參考點
canvas = document.getElementById("myCanvas");
//2. 設定 context 為 2D 以繪製基本圖形
ctx = canvas.getContext("2d");
//3. 填入色彩紅色
ctx.fillStyle = 'red';
//4. 利用這些參數決定位置與大小,繪製矩形
ctx.fillRect(0,0, 200, 200) // x,y,width, height
```
✅ Canvas API 主要是處理 2D 圖形,但你也可以在網頁中繪製 3D 圖形。要完成這個需求,你可以使用 [WebGL API](https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API)。
你可以使用 Canvas API 繪製出這些物件:
- **幾何圖形**,我們已經展示繪製矩形的流程,還有許多種形狀可以使用。
- **文字**,你可以繪製文字,決定你想要的字型及顏色。
- **圖片**,你可以依據圖片檔繪製圖案,舉例來說像是 .jpg 或是 .png 檔。
✅ 試試看!你知道如何繪製矩形,你能在頁面中繪製圓形嗎?看看在 CodePen 上有趣的 Canvas 塗鴉。這邊有一樣[特別令人驚豔的例子](https://codepen.io/dissimulate/pen/KrAwx)。
## 讀取並繪製圖片檔
建立 `Image` 物件並設定其 `src` 屬性,你可以讀取圖片檔。接著監聽 `load` 事件,了解圖片何時已經可以被使用。程式碼如下:
### 讀取檔案
```javascript
const img = new Image();
img.src = 'path/to/my/image.png';
img.onload = () => {
// 圖片載入完成,準備使用
}
```
### 讀取檔案之模式
建議上可以將上述程式打包起來,建立成完整的結構,判斷圖片是否載入完成,也方便未來的使用:
```javascript
function loadAsset(path) {
return new Promise((resolve) => {
const img = new Image();
img.src = path;
img.onload = () => {
// 圖片載入完成,準備使用
resolve(img);
}
})
}
// 實際用法
async function run() {
const heroImg = await loadAsset('hero.png')
const monsterImg = await loadAsset('monster.png')
}
```
要在畫面上繪製遊戲物件,你的程式碼會如下所示:
```javascript
async function run() {
const heroImg = await loadAsset('hero.png')
const monsterImg = await loadAsset('monster.png')
canvas = document.getElementById("myCanvas");
ctx = canvas.getContext("2d");
ctx.drawImage(heroImg, canvas.width/2,canvas.height/2);
ctx.drawImage(monsterImg, 0,0);
}
```
## 是時候來建立你的遊戲了
### 建立目標
你需要建立包含 Canvas 元素的網頁。它會是 `1024*768` 的黑色畫面。我們提供了兩張圖片:
- 英雄艦艇
![英雄艦艇](../solution/assets/player.png)
- 5*5 隻怪物
![敵軍艦艇](../solution/assets/enemyShip.png)
### 開始開發的建議步驟
在你的 `your-work` 子資料夾中,確認檔案是否建立完成。它應該包括:
```bash
-| assets
-| enemyShip.png
-| player.png
-| index.html
-| app.js
-| package.json
```
在 Visual Studio Code 中開啟這個資料夾的副本。你需要建立本地端的開發環境,建議為 Visual Studio Code 與安裝好的 NPM 與 Node。如果你的電腦中還沒設定好 `npm`[這是它的設定流程]](https://www.npmjs.com/get-npm)。
前往 `your_work` 資料夾,開始你的專案:
```bash
cd your-work
npm start
```
這會啟動 HTTP 伺服器,網址為 `http://localhost:5000`。開啟瀏覽器並輸入該網址。目前會是空白的頁面,但不久後就會不一樣了。
> 筆記:想觀察畫面的改變,請重新整理你的頁面。
### 加入程式碼
`your-work/app.js` 中加入程式碼以解決下列目標:
1. 在 Canvas **繪製**黑色背景
> 要點:在 `/app.js` 中,加入兩行程式在 TODO 下方:設定 `ctx` 元素為黑色,左上方座標點為 0,0 且大小與 Canvas 相等。
2. **讀取**材質
> 要點:使用 `await loadTexture` 導入圖片位置以新增玩家與敵軍圖片。你還沒辦法在畫面上看到它們!
3. 在畫面的正下方**繪製**英雄
> 要點:使用 `drawImage` API 來繪製 heroImg 到畫面上,設定位置為 `canvas.width / 2 - 45``canvas.height - canvas.height / 4)`
4. **繪製** 5*5 隻怪物
> 要點:現在移除註解,在畫面上繪製敵人。接著編輯函式 `createEnemies`
首先,設定幾個常數:
```javascript
const MONSTER_TOTAL = 5;
const MONSTER_WIDTH = MONSTER_TOTAL * 98;
const START_X = (canvas.width - MONSTER_WIDTH) / 2;
const STOP_X = START_X + MONSTER_WIDTH;
```
接著,利用迴圈在畫面上繪製矩陣型態的怪物:
```javascript
for (let x = START_X; x < STOP_X; x += 98) {
for (let y = 0; y < 50 * 5; y += 50) {
ctx.drawImage(enemyImg, x, y);
}
}
```
## 結果
完成後的成果應該如下所示:
![黑畫面上有英雄與 5*5 隻怪物](../partI-solution.png)
## 解答
試著自己先完成程式碼,但如果你遭遇到困難,請參考[解答](../solution/app.js)。
---
## 🚀 挑戰
你已經學會繪製 2D 圖形的 Canvas API。看看 [WebGL API](https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API),試著繪製 3D 物件。
## 課後測驗
[課後測驗](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/32?loc=zh_tw)
## 複習與自學
[閱讀更多資料](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API),學習更多有關 Canvas API 的用法。
## 作業
[把玩 Canvas API](assignment.zh-tw.md)

@ -0,0 +1,11 @@
# 把玩 Canvas API
## 簡介
挑選一款 Canvas API 上的元素,為它建立一些有趣的設定。你能利用重複的星星建立銀河嗎?你能建立有特殊材質的線條嗎?你可以觀察 CodePen 上的範本激發想法,但請不要抄襲。
## 學習評量
| 作業內容 | 優良 | 普通 | 待改進 |
| -------- | -------------------------- | ---------------------------- | ------------ |
| | 程式碼呈現有趣的材質與圖案 | 有提交程式碼,但無法正常執行 | 未提交程式碼 |

@ -0,0 +1,388 @@
# 建立太空遊戲 Part 3加入動作
## 課前測驗
[課前測驗](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/33?loc=zh_tw)
有外星人在移動的遊戲才會好玩!在這款遊戲中,我們會建立兩種移動模式:
- **鍵盤滑鼠的移動**:當使用者控制鍵盤或滑鼠時,能移動畫面上的物件。
- **遊戲內建的移動**:遊戲能自動地在一定時間內,移動其中的物件。
那我們該如何移動畫面上的物件呢?這都取決於笛卡爾座標系:我們改變物件的座標 (x,y),並在畫面上重新繪製出來。
通常你需要下列流程來*移動*畫面上的物件:
1. **設定物件的新地點**,你才能察覺到物件有所移動。
2. **清除畫面**,每一次的繪製間都需要將畫面清除乾淨。我們可以繪製一張背景色的矩形來覆蓋畫面。
3. **在新地點重新繪製物件**,我們就能移動物件,從 A 點移動到 B 點。
合理的程式碼如下所示:
```javascript
// 設定英雄位置
hero.x += 5;
// 利用矩形清除英雄
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 重新繪製背景與英雄
ctx.fillRect(0, 0, canvas.width, canvas.height)
ctx.fillStyle = "black";
ctx.drawImage(heroImg, hero.x, hero.y);
```
✅ 你能了解為什麼在同一秒內多次重新繪製英雄會影響效能的原因嗎?閱讀[其他種同目的之設計模式](https://www.html5rocks.com/en/tutorials/canvas/performance/)。
## 處理鍵盤事件
連接特定事件到程式中,你就能處理遊戲事件。鍵盤事件可以在視窗被選擇時觸發,而滑鼠事件如 `click`,則要點擊特定的物件。我們會在這個專案中,使用鍵盤物件。
要處理一種事件,需要使用視窗的 `addEventListener()` 方法,並提供給它兩個參數。第一個參數是事件的名稱,例如: `keyup`。第二個參數是回應事件結果的被呼叫函式。
下列是一種例子:
```javascript
window.addEventListener('keyup', (evt) => {
// `evt.key` = 按鍵字串
if (evt.key === 'ArrowUp') {
// 做某事
}
})
```
鍵盤事件有兩個屬性來判別被按壓的按鍵:
- `key`,使用字串名稱表達該按鍵,例如: `ArrowUp`
- `keyCode`,使用數字呈現,例如 `37` 會對應到 `ArrowLeft`
✅ 除了遊戲開發以外,鍵盤事件也是十分實用的功能。你能想到其他使用相同技術的應用嗎?
### 特殊按鍵之限制
有許多*特殊*按鍵會影響視窗。這代表若我們正監聽著 `keyup` 事件,這個按鍵同時也會執行視窗的滾動行為。某些時候你會需要*關閉*這些瀏覽器中預設的行為,好比是建立這款遊戲時。你需要下列的程式:
```javascript
let onKeyDown = function (e) {
console.log(e.keyCode);
switch (e.keyCode) {
case 37:
case 39:
case 38:
case 40: // 方向鍵
case 32:
e.preventDefault();
break; // 空白鍵
default:
break; // 不阻止其他按鍵
}
};
window.addEventListener('keydown', onKeyDown);
```
上述的程式碼能確保方向鍵與空白鍵關閉*預設*的行為。這個*關閉*機制會在我們呼叫 `e.preventDefault()` 時觸發。
## 遊戲內建的移動
我們可以讓物件自己移動,利用計時器如 `setTimeout()` 或是 `setInterval()` 這兩個函式,隨著秒數間隔更新物件的位置。如下方呈現:
```javascript
let id = setInterval(() => {
// 在 y 軸上移動敵人
enemy.y += 10;
})
```
## 遊戲迴圈
遊戲迴圈是個重要概念,定期地呼叫必須執行的函式。之所以被稱作遊戲迴圈也是基於所有東西會在一個迴圈中呈現給玩家。遊戲迴圈會利用到所有的遊戲物件,並依據各個情況與理由決定是否要繪製出它們。舉例來說,當一個敵人被雷射擊中,爆炸了。他就不應該存在於現在的遊戲迴圈中。你會在後續的課程學到更多此概念。
這是一個遊戲迴圈的基本格式,以程式碼表達如下:
```javascript
let gameLoopId = setInterval(() =>
function gameLoop() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "black";
ctx.fillRect(0, 0, canvas.width, canvas.height);
drawHero();
drawEnemies();
drawStaticObjects();
}, 200);
```
上述的迴圈會每 `200` 毫秒重新繪製 Canvas。你能自由地判斷哪種時長更適合套用在你的遊戲中。
## 繼續我們的太空遊戲
你會利用現有的程式碼來擴增我們的專案。你可以使用你在 Part I 完成的程式,或是使用 [Part II - Starter](../your-work) 這包程式。
- **移動英雄**:你需要加入程式,確保你可以使用方向鍵來移動主角。
- **移動敵人**:你也需要加入程式,確保敵人能定期地由上往下移動。
## 建議步驟
在你的 `your-work` 子資料夾中,確認檔案是否建立完成。它應該包括:
```bash
-| assets
-| enemyShip.png
-| player.png
-| index.html
-| app.js
-| package.json
```
開始 `your_work` 資料夾中的專案,輸入:
```bash
cd your-work
npm start
```
這會啟動 HTTP 伺服器並發布網址 `http://localhost:5000`。開啟瀏覽器並輸入該網址,現在它能呈現英雄以及所有的敵人,但它們還沒辦法移動!
### 加入程式碼
1. **加入特定物件** `hero`、`enemy` 和 `game object`,它們皆有 `x``y` 位置屬性。(記得課程[繼承與組合](../../README.zh-tw.md)中的片段)。
*提示* `game object` 要有 `x``y`,以及繪製到畫布上的能力。
>要點:開始建立 GameObject class ,結構如下所示,再繪製到畫布上:
```javascript
class GameObject {
constructor(x, y) {
this.x = x;
this.y = y;
this.dead = false;
this.type = "";
this.width = 0;
this.height = 0;
this.img = undefined;
}
draw(ctx) {
ctx.drawImage(this.img, this.x, this.y, this.width, this.height);
}
}
```
現在,延伸 GameObject 來建立英雄與敵人。
```javascript
class Hero extends GameObject {
constructor(x, y) {
...it needs an x, y, type, and speed
}
}
```
```javascript
class Enemy extends GameObject {
constructor(x, y) {
super(x, y);
(this.width = 98), (this.height = 50);
this.type = "Enemy";
let id = setInterval(() => {
if (this.y < canvas.height - this.height) {
this.y += 5;
} else {
console.log('Stopped at', this.y)
clearInterval(id);
}
}, 300)
}
}
```
2. **加入鍵盤事件處理器**以處理鍵盤輸入(移動英雄的上下左右)
*記住* 這是笛卡爾座標系,左上方為 `0,0`。也請記得關閉鍵盤的*預設行為*
>要點:建立函式 onKeyDown 並連接到視窗中:
```javascript
let onKeyDown = function (e) {
console.log(e.keyCode);
...add the code from the lesson above to stop default behavior
}
};
window.addEventListener("keydown", onKeyDown);
```
這時候檢查你的瀏覽器命令欄,看看是否能偵測到鍵盤輸入。
3. **建立**[發布訂閱模式](../../README.zh-tw.md),這能讓剩下的程式段落保持乾淨。
要做到此步驟,你可以:
1. **建立視窗的事件監聽者**
```javascript
window.addEventListener("keyup", (evt) => {
if (evt.key === "ArrowUp") {
eventEmitter.emit(Messages.KEY_EVENT_UP);
} else if (evt.key === "ArrowDown") {
eventEmitter.emit(Messages.KEY_EVENT_DOWN);
} else if (evt.key === "ArrowLeft") {
eventEmitter.emit(Messages.KEY_EVENT_LEFT);
} else if (evt.key === "ArrowRight") {
eventEmitter.emit(Messages.KEY_EVENT_RIGHT);
}
});
```
1. **建立 EventEmitter class** 以發布及訂閱訊息:
```javascript
class EventEmitter {
constructor() {
this.listeners = {};
}
on(message, listener) {
if (!this.listeners[message]) {
this.listeners[message] = [];
}
this.listeners[message].push(listener);
}
emit(message, payload = null) {
if (this.listeners[message]) {
this.listeners[message].forEach((l) => l(message, payload));
}
}
}
```
1. **建立常數**並設定 EventEmitter
```javascript
const Messages = {
KEY_EVENT_UP: "KEY_EVENT_UP",
KEY_EVENT_DOWN: "KEY_EVENT_DOWN",
KEY_EVENT_LEFT: "KEY_EVENT_LEFT",
KEY_EVENT_RIGHT: "KEY_EVENT_RIGHT",
};
let heroImg,
enemyImg,
laserImg,
canvas, ctx,
gameObjects = [],
hero,
eventEmitter = new EventEmitter();
```
1. **初始化遊戲**
```javascript
function initGame() {
gameObjects = [];
createEnemies();
createHero();
eventEmitter.on(Messages.KEY_EVENT_UP, () => {
hero.y -=5 ;
})
eventEmitter.on(Messages.KEY_EVENT_DOWN, () => {
hero.y += 5;
});
eventEmitter.on(Messages.KEY_EVENT_LEFT, () => {
hero.x -= 5;
});
eventEmitter.on(Messages.KEY_EVENT_RIGHT, () => {
hero.x += 5;
});
}
```
1. **設定遊戲迴圈**
重構函式 window.onload 來初始化遊戲,設定遊戲迴圈的定時間隔。你還需要加入雷射光:
```javascript
window.onload = async () => {
canvas = document.getElementById("canvas");
ctx = canvas.getContext("2d");
heroImg = await loadTexture("assets/player.png");
enemyImg = await loadTexture("assets/enemyShip.png");
laserImg = await loadTexture("assets/laserRed.png");
initGame();
let gameLoopId = setInterval(() => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "black";
ctx.fillRect(0, 0, canvas.width, canvas.height);
drawGameObjects(ctx);
}, 100)
};
```
5. **加入程式**來定期地移動敵人
重構函式 `createEnemies()` 以建立敵人們,接到 gameObjects 中:
```javascript
function createEnemies() {
const MONSTER_TOTAL = 5;
const MONSTER_WIDTH = MONSTER_TOTAL * 98;
const START_X = (canvas.width - MONSTER_WIDTH) / 2;
const STOP_X = START_X + MONSTER_WIDTH;
for (let x = START_X; x < STOP_X; x += 98) {
for (let y = 0; y < 50 * 5; y += 50) {
const enemy = new Enemy(x, y);
enemy.img = enemyImg;
gameObjects.push(enemy);
}
}
}
```
新增函式 `createHero()` 來為英雄做相同的事情。
```javascript
function createHero() {
hero = new Hero(
canvas.width / 2 - 45,
canvas.height - canvas.height / 4
);
hero.img = heroImg;
gameObjects.push(hero);
}
```
最後,建立函式 `drawGameObjects()` 以開始繪製:
```javascript
function drawGameObjects(ctx) {
gameObjects.forEach(go => go.draw(ctx));
}
```
你的敵人開始會朝你的英雄艦艇前進!
---
## 🚀 挑戰
如你所見,在加入零零總總的函式、變數與 class 後,你的程式變成了「麵條式代碼(spaghetti code)」。你能有效的編排你的程式,讓它更容易被閱讀?勾劃出一個系統來組織你的程式碼,即使所有東西都在一個檔案中。
## 課後測驗
[課後測驗](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/34?loc=zh_tw)
## 複習與自學
我們並沒有使用框架(frameworks)來編寫我們的遊戲,現在有許多 JavaScript 基底的 Canvas 框架,提供給遊戲開發使用。花點時間[閱讀這些框架](https://github.com/collections/javascript-game-engines)。
## 作業
[為你的程式做註解](assignment.zh-tw.md)

@ -0,0 +1,11 @@
# 為你的程式做註解
## 簡介
打開遊戲資料夾中目前的 /app.js 檔案,試著幫它做上註解並整理乾淨。程式碼很容易脫離掌控,現在是個好機會來確保你的程式是容易去閱讀的,在未來還可以被使用。
## 學習評量
| 作業內容 | 優良 | 普通 | 待改進 |
| -------- | ----------------------------- | ----------------------- | ----------------------- |
| | `app.js` 完整地註解且分塊整理 | `app.js` 有做充分的註解 | `app.js` 凌亂且缺乏註解 |

@ -0,0 +1,297 @@
# 建立太空遊戲 Part 4加入雷射與碰撞偵測
## 課前測驗
[課前測驗](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/35?loc=zh_tw)
這堂課中,你會學會如何在 JavaScript 上發射雷射光!我們需要在遊戲中新增兩項東西:
- **雷射光**:這束雷射會從英雄艦艇垂直往上移動
- **碰撞偵測**,除了完成*射擊*這項能力,我們還會新增幾項遊戲規則:
- **雷射擊中敵人**:被雷射擊中的敵人會死亡
- **雷射擊中畫面最上方**:雷射擊中到畫面最上方會消失
- **敵人碰觸到英雄**:敵人與英雄在互相碰觸時皆會被摧毀
- **敵人碰觸到畫面最下方**:敵人碰觸到畫面最下方時,該敵人與英雄會被摧毀
簡單來說,你這位*英雄*需要在敵人到達畫面最下方之前,使用雷射擊毀它們。
✅ 做點關於第一款電腦遊戲的研究。它有哪些功能?
讓我們成為英雄吧!
## 碰撞偵測
我們該如何進行碰撞偵測呢?我們需要將遊戲物件視為移動中的矩形。你或許會問為什麼?這是因為,繪製遊戲物件的圖片皆為矩形:它有一組 `x``y`、`width` 與 `height`
若兩個矩形,舉例來說:英雄與敵人*相交*了,這就是一次碰撞。至於會發生什麼事取決於遊戲規則。要製作碰撞偵測,你需要這些步驟:
1. 取得表達遊戲物件為矩形的方法,好比是:
```javascript
rectFromGameObject() {
return {
top: this.y,
left: this.x,
bottom: this.y + this.height,
right: this.x + this.width
}
}
```
2. 一個比較函式,這個函式可以如下:
```javascript
function intersectRect(r1, r2) {
return !(r2.left > r1.right ||
r2.right < r1.left ||
r2.top > r1.bottom ||
r2.bottom < r1.top);
}
```
## 我們該如何摧毀物件
要摧毀遊戲物件,你需要讓遊戲知道,它不再需要在定期的遊戲迴圈中繪製該物件。一種方法是在情況發生時,我們可以標記遊戲物件為*死亡*,例如:
```javascript
// 碰撞發生
enemy.dead = true
```
接著,你在重新繪製畫面前,排除掉這些*死亡*的物件,例如:
```javascript
gameObjects = gameObject.filter(go => !go.dead);
```
## 我們該如何發射雷射
發射雷射可以被視作回應一件鍵盤事件,並建立往特定方向移動的物件。因此我們需要列出下列步驟:
1. **建立雷射物件**:從英雄艦艇的正上方,建立往畫面上方移動的物件。
2. **連接該程式到鍵盤事件**:我們需要在鍵盤中挑選一個按鍵,表達玩家發射雷射光。
3. 在按鍵按壓時,**建立看起來像雷射光的遊戲物件**。
## 雷射的冷卻時間
在每次按壓按鍵時,好比說*空白鍵*,雷射光都需要被發射出來。為了讓遊戲不要在短時間內發射太多組雷射光,我們需要修正它。修法為建立*冷卻時間* ── 一個計時器確保雷射在一定期間內只能被發射一次。你可以藉由下列方式建立:
```javascript
class Cooldown {
constructor(time) {
this.cool = false;
setTimeout(() => {
this.cool = true;
}, time)
}
}
class Weapon {
constructor {
}
fire() {
if (!this.cooldown || this.cooldown.cool) {
// 產生雷射光
this.cooldown = new Cooldown(500);
} else {
// 什麼事都不做,冷卻中。
}
}
}
```
✅ 根據太空遊戲系列課程的第一章,回想關於*冷卻時間*。
## 建立目標
你會利用上一堂課中現成的程式碼(你應該有整理並重構過)做延伸。使用來自 Part II 的檔案或是使用 [Part III - Starter](../your-work)。
> 要點:你需要使用的雷射光已經在資料夾中,並已匯入到程式碼中。
- **加入碰撞偵測**,建立下列規則給各個雷射碰觸到東西的情況:
1. **雷射擊中敵人**:被雷射擊中的敵人會死亡
2. **雷射擊中畫面最上方**:雷射擊中到畫面最上方會消失
3. **敵人碰觸到英雄**:敵人與英雄在互相碰觸時皆會被摧毀
4. **敵人碰觸到畫面最下方**:敵人碰觸到畫面最下方時,該敵人與英雄會被摧毀
## 建議步驟
在你的 `your-work` 子資料夾中,確認檔案是否建立完成。它應該包括:
```bash
-| assets
-| enemyShip.png
-| player.png
-| laserRed.png
-| index.html
-| app.js
-| package.json
```
開始 `your_work` 資料夾中的專案,輸入:
```bash
cd your-work
npm start
```
這會啟動 HTTP 伺服器並發布網址 `http://localhost:5000`。開啟瀏覽器並輸入該網址,現在它能呈現英雄以及所有的敵人,但它們還沒辦法移動!
### 建立程式碼
1. **設定表達遊戲物件為矩形的方法,以處理碰撞狀況** 下列的程式表達 `GameObject` 的矩形呈現方式。編輯你的 GameObject class
```javascript
rectFromGameObject() {
return {
top: this.y,
left: this.x,
bottom: this.y + this.height,
right: this.x + this.width,
};
}
```
2. **加入程式碼來檢查碰撞** 這會是新函式來測試兩矩形是否相交:
```javascript
function intersectRect(r1, r2) {
return !(
r2.left > r1.right ||
r2.right < r1.left ||
r2.top > r1.bottom ||
r2.bottom < r1.top
);
}
```
3. **加入雷射發射功能**
1. **加入鍵盤事件訊息**。 *空白鍵*要能在英雄艦艇上方建立雷射光。加入三個常數到 Messages 物件中:
```javascript
KEY_EVENT_SPACE: "KEY_EVENT_SPACE",
COLLISION_ENEMY_LASER: "COLLISION_ENEMY_LASER",
COLLISION_ENEMY_HERO: "COLLISION_ENEMY_HERO",
```
1. **處理空白鍵**。 編輯 `window.addEventListener` 的 keyup 函式來處理空白鍵:
```javascript
} else if(evt.keyCode === 32) {
eventEmitter.emit(Messages.KEY_EVENT_SPACE);
}
```
1. **加入監聽者**。 編輯函式 `initGame()` 來確保英雄可以在空白鍵按壓時,發射雷射光:
```javascript
eventEmitter.on(Messages.KEY_EVENT_SPACE, () => {
if (hero.canFire()) {
hero.fire();
}
```
建立新的函式 `eventEmitter.on()` 確保敵人碰觸到雷射光時,能更新死亡狀態:
```javascript
eventEmitter.on(Messages.COLLISION_ENEMY_LASER, (_, { first, second }) => {
first.dead = true;
second.dead = true;
})
```
1. **移動物件**。 確保雷射逐步地向畫面上方移動。建立新的 class Laser 延伸自 `GameObject`,你應該有做過:
```javascript
class Laser extends GameObject {
constructor(x, y) {
super(x,y);
(this.width = 9), (this.height = 33);
this.type = 'Laser';
this.img = laserImg;
let id = setInterval(() => {
if (this.y > 0) {
this.y -= 15;
} else {
this.dead = true;
clearInterval(id);
}
}, 100)
}
}
```
1. **處理碰撞**。 建立雷射的碰撞規則。加入函式 `updateGameObjects()` 來確認被碰撞的物件。
```javascript
function updateGameObjects() {
const enemies = gameObjects.filter(go => go.type === 'Enemy');
const lasers = gameObjects.filter((go) => go.type === "Laser");
// 雷射擊中某物
lasers.forEach((l) => {
enemies.forEach((m) => {
if (intersectRect(l.rectFromGameObject(), m.rectFromGameObject())) {
eventEmitter.emit(Messages.COLLISION_ENEMY_LASER, {
first: l,
second: m,
});
}
});
});
gameObjects = gameObjects.filter(go => !go.dead);
}
```
記得在 `window.onload` 裡的遊戲迴圈中加入 `updateGameObjects()`
4. **設定雷射的冷卻時間**,它只能在定期內發射一次。
最後,編輯 Hero class 來允許冷卻:
```javascript
class Hero extends GameObject {
constructor(x, y) {
super(x, y);
(this.width = 99), (this.height = 75);
this.type = "Hero";
this.speed = { x: 0, y: 0 };
this.cooldown = 0;
}
fire() {
gameObjects.push(new Laser(this.x + 45, this.y - 10));
this.cooldown = 500;
let id = setInterval(() => {
if (this.cooldown > 0) {
this.cooldown -= 100;
} else {
clearInterval(id);
}
}, 200);
}
canFire() {
return this.cooldown === 0;
}
}
```
到這裡,你的遊戲有了些功能!你可以測試方向鍵,使用空白鍵發射雷射。當你擊中敵人時它們會消失。幹得漂亮!
---
## 🚀 挑戰
加入爆炸特效! 看看 [Space Art Repo](../solution/spaceArt/readme.txt) 中的檔案,試著在雷射擊中外星人時,加入爆炸畫面。
## 課後測驗
[課後測驗](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/36?loc=zh_tw)
## 複習與自學
對遊戲中的迴圈定時做點實驗。當你改變數值時,發生了什麼事?閱讀更多關於 [JavaScript 計時事件](https://www.freecodecamp.org/news/javascript-timing-events-settimeout-and-setinterval/)。
## 作業
[探索碰撞](assignment.zh-tw.md)

@ -0,0 +1,11 @@
# 探索碰撞
## 簡介
為了更了解碰撞是如何運作的,建立一款小遊戲包含物件的碰撞。利用鍵盤或是滑鼠來移動物件,當物件碰撞時執行某些行為。它可以像是彗星撞地球,或是碰碰車。發揮你的創意!
## 學習評量
| 作業內容 | 優良 | 普通 | 待改進 |
| -------- | -------------------------------------------------------------- | ------------------ | ---------- |
| | 建立出完整的程式:有在畫面上繪製物件、有基本的碰撞與對應的行為 | 程式有部分尚未完成 | 程式有瑕疵 |

@ -0,0 +1,189 @@
# 建立太空遊戲 Part 5分數與生命數
## 課前測驗
[課前測驗](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/37?loc=zh_tw)
在這堂課中,你會學習如何為遊戲加入計分功能與計算性命數。
## 在畫面上繪製文字
為了在畫面上顯示遊戲分數,你需要了解如何配置文字。答案是在 Canvas 物件上使用方法 `fillText()`。你也可以控制其他特徵,例如文字字型、文字顏色甚至文字對齊方向(左、右、置中)。下面是在畫面中繪製一些文字。
```javascript
ctx.font = "30px Arial";
ctx.fillStyle = "red";
ctx.textAlign = "right";
ctx.fillText("show this on the screen", 0, 0);
```
✅ 閱讀更多關於[在畫布上建立文字](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Drawing_text),你可以自由地豐富你的文字!
## 性命,一個遊戲概念
遊戲中的生命概念只是一個數字。在太空遊戲中,在你的船艦受到攻擊時,扣除生命值是一種常見的方式。如果能以圖像的方式顯示生命值也是很好的方式,例如船艦圖示、心臟圖像,而非單純使用數字。
## 建立目標
我們在遊戲中新增下列功能:
- **遊戲分數**:當每一艘敵軍艦艇被摧毀時,英雄應該取得一些獎勵分數,我們建議一艘敵艦一百分。遊戲總分應該顯示在畫面的左下角。
- **生命值**:你的船艦有三條性命。在每一次敵軍艦艇撞擊你時,你會損失一條性命。生命數應該顯示在畫面的右下角,以下列圖像顯示出來。 ![性命圖片](../solution/assets/life.png)
## 建議步驟
在你的 `your-work` 子資料夾中,確認檔案是否建立完成。它應該包括:
```bash
-| assets
-| enemyShip.png
-| player.png
-| laserRed.png
-| index.html
-| app.js
-| package.json
```
開始 `your_work` 資料夾中的專案,輸入:
```bash
cd your-work
npm start
```
這會啟動 HTTP 伺服器並發布網址 `http://localhost:5000`。開啟瀏覽器並輸入該網址,現在它能顯示出英雄與所有的敵人,在你操作方向鍵後,英雄能移動並擊落敵人。
### 加入程式碼
1. 從資料夾 `solution/assets/` **複製你需要的檔案** 到資料夾 `your-work` 中。你會加入檔案 `life.png`。在函式 window.onload 中加入 lifeImg
```javascript
lifeImg = await loadTexture("assets/life.png");
```
1. 在檔案清單中加入 `lifeImg`
```javascript
let heroImg,
...
lifeImg,
...
eventEmitter = new EventEmitter();
```
2. **新增變數**。 加入程式碼表達你的遊戲總分(0)和剩餘性命(3),並顯示在畫面上。
3. **擴增函式 `updateGameObjects()`**。 擴增函式 `updateGameObjects()` 來處理敵軍碰撞:
```javascript
enemies.forEach(enemy => {
const heroRect = hero.rectFromGameObject();
if (intersectRect(heroRect, enemy.rectFromGameObject())) {
eventEmitter.emit(Messages.COLLISION_ENEMY_HERO, { enemy });
}
})
```
4. **加入 `life``points`**.
1. **初始化變數**。 在 `Hero` class 的 `this.cooldown = 0` 下方,設定性命與分數:
```javascript
this.life = 3;
this.points = 0;
```
1. **在畫面上顯示變數內容**。 在畫面上繪製這些數值:
```javascript
function drawLife() {
// TODO, 35, 27
const START_POS = canvas.width - 180;
for(let i=0; i < hero.life; i++ ) {
ctx.drawImage(
lifeImg,
START_POS + (45 * (i+1) ),
canvas.height - 37);
}
}
function drawPoints() {
ctx.font = "30px Arial";
ctx.fillStyle = "red";
ctx.textAlign = "left";
drawText("Points: " + hero.points, 10, canvas.height-20);
}
function drawText(message, x, y) {
ctx.fillText(message, x, y);
}
```
1. **在遊戲迴圈中加入呼叫**。 請確保你加入這些函式到 `updateGameObjects()` 下方的 window.onload 內:
```javascript
drawPoints();
drawLife();
```
1. **制定遊戲規則**。 制定下列的遊戲規則:
1. **在英雄與敵人發生碰撞時**,扣除一條生命。
擴增 `Hero` class 來執行這段減法:
```javascript
decrementLife() {
this.life--;
if (this.life === 0) {
this.dead = true;
}
}
```
2. **當雷射擊中敵人時**,增加遊戲總分一百分。
擴增 Hero class 來執行這段加法:
```javascript
incrementPoints() {
this.points += 100;
}
```
加入這些函式到碰撞事件發送器中:
```javascript
eventEmitter.on(Messages.COLLISION_ENEMY_LASER, (_, { first, second }) => {
first.dead = true;
second.dead = true;
hero.incrementPoints();
})
eventEmitter.on(Messages.COLLISION_ENEMY_HERO, (_, { enemy }) => {
enemy.dead = true;
hero.decrementLife();
});
```
✅ 做點研究,探索其他使用到 JavaScript 與 Canvas 的遊戲。他們有什麼共同特徵?
在這個工作的尾聲,你應該能在右下方看到「性命」小船;左下方看到遊戲總分。當你碰撞到敵人時會扣除生命;當你擊落敵人時會增加分數。做得好!你的遊戲就快完成了。
---
## 🚀 挑戰
你的程式就快完成了。你能預測到下一步嗎?
## 課後測驗
[課後測驗](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/38?loc=zh_tw)
## 複習與自學
研究其他種增加與減少分數與生命數的方法。這邊有一些有趣的遊戲引擎,例如:[PlayFab](https://playfab.com)。使用這些引擎能如何增進你的遊戲?
## 作業
[建立計分遊戲](assignment.zh-tw.md)

@ -0,0 +1,11 @@
# 建立計分遊戲
## 簡介
建立一款遊戲,能有創意地顯示生命值與分數。建議上能在畫面的正下方,以心型圖示顯示生命值,或是斗大的數字顯示分數。看看這些[免費的遊戲資源](https://www.kenney.nl/)。
## 學習評量
| 作業內容 | 優良 | 普通 | 待改進 |
| -------- | ---------------- | ---------------- | ------------ |
| | 呈現出完整的遊戲 | 遊戲提供部分內容 | 遊戲存在問題 |

@ -0,0 +1,222 @@
# 建立太空遊戲 Part 6結束與重來
## 課前測驗
[課前測驗](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/39?loc=zh_tw)
有許多方式可以表達遊戲中的*結束狀態*。這都取決於你這位遊戲開發者,定義遊戲結束的理由。假設我們討論這款已經開發許久的太空遊戲,以下是遊戲結束的理由:
- **`N` 艘敵軍艦艇被擊毀**:如果你想將遊戲分成許多關卡,一種常見的方式是將每一關的破關門檻,定為擊毀 `N` 艘敵軍艦艇。
- **你的船艦已被擊毀**:一定有遊戲,只要你的船艦被擊毀一次時,便判定你輸了這場遊戲。另一種可行概念是加入生命值系統。每次你的船艦被擊毀時,會扣除一條生命。一但你損失了所有性命,你便輸了這場遊戲。
- **你已經取得 `N` 點分數**:另一種常見的結束狀態為分數門檻。取得分數的機制取決在你,常見的條件為摧毀敵艦、或是收集敵艦所*掉落*的道具。
- **完成關卡**:這或許會涉及到許多種狀態,好比說: `X` 艘艦艇已被擊毀、已取得 `Y` 點分數或是收集特定的道具。
## 重新遊戲
如果玩家很享受你的遊戲,他們會想再重新遊玩一次。一旦因任何原因結束遊戲時,你應該要提供重新遊戲的方法。
✅ 想想看,什麼條件下會結束一款遊戲,而它們又是如何提示你重新遊玩。
## 建立目標
你需要為你的遊戲新增這些規則:
1. **贏得遊戲**。 一旦所有敵軍艦艇被擊毀時,你便贏得這場遊戲。請額外地顯示勝利訊息。
1. **重新開始**。 一旦你損失了所有性命,或是贏得了勝利,你應該提供方法來重新遊戲。記住!你需要重新初始化你的遊戲,所有遊戲的歷史紀錄會被移除。
## 建議步驟
在你的 `your-work` 子資料夾中,確認檔案是否建立完成。它應該包括:
```bash
-| assets
-| enemyShip.png
-| player.png
-| laserRed.png
-| life.png
-| index.html
-| app.js
-| package.json
```
開始 `your_work` 資料夾中的專案,輸入:
```bash
cd your-work
npm start
```
這會啟動 HTTP 伺服器並發布網址 `http://localhost:5000`。開啟瀏覽器並輸入該網址。你的遊戲應該能被遊玩。
> 要點: 要避免在 Visual Studio Code 裡出現警告訊息,編輯函式 `window.onload` 以 is而非 let 的方式呼叫 `gameLoopId`;並在檔案正上方獨立地宣告 gameLoopId `let gameLoopId;`
### 加入程式碼
1. **追蹤結束狀態**。 新增程式碼來追蹤敵人的數量,利用下列函式判斷英雄艦艇是否被擊毀:
```javascript
function isHeroDead() {
return hero.life <= 0;
}
function isEnemiesDead() {
const enemies = gameObjects.filter((go) => go.type === "Enemy" && !go.dead);
return enemies.length === 0;
}
```
1. **加入訊息處理器**。 編輯 `eventEmitter` 以處理這些狀態:
```javascript
eventEmitter.on(Messages.COLLISION_ENEMY_LASER, (_, { first, second }) => {
first.dead = true;
second.dead = true;
hero.incrementPoints();
if (isEnemiesDead()) {
eventEmitter.emit(Messages.GAME_END_WIN);
}
});
eventEmitter.on(Messages.COLLISION_ENEMY_HERO, (_, { enemy }) => {
enemy.dead = true;
hero.decrementLife();
if (isHeroDead()) {
eventEmitter.emit(Messages.GAME_END_LOSS);
return; // 遊戲失敗,提前結束
}
if (isEnemiesDead()) {
eventEmitter.emit(Messages.GAME_END_WIN);
}
});
eventEmitter.on(Messages.GAME_END_WIN, () => {
endGame(true);
});
eventEmitter.on(Messages.GAME_END_LOSS, () => {
endGame(false);
});
```
1. **加入新的訊息**。 新增這些訊息到 Messages 常數中:
```javascript
GAME_END_LOSS: "GAME_END_LOSS",
GAME_END_WIN: "GAME_END_WIN",
```
2. **加入重新開始的功能** 在按下特定按鈕後,程式會重新開始遊戲。
1. **監聽 `Enter` 按鈕之按壓**。 編輯視窗的 eventListener ,監聽按鍵的按壓:
```javascript
else if(evt.key === "Enter") {
eventEmitter.emit(Messages.KEY_EVENT_ENTER);
}
```
1. **加入重新遊戲的訊息**。 加入這段訊息到 Messages 常數中:
```javascript
KEY_EVENT_ENTER: "KEY_EVENT_ENTER",
```
1. **制定遊戲規則**。 編制下列的遊戲規則:
1. **玩家勝利條件**。 當所有敵軍艦艇被擊毀時,顯示勝利訊息。
1. 首先,建立函式 `displayMessage()`
```javascript
function displayMessage(message, color = "red") {
ctx.font = "30px Arial";
ctx.fillStyle = color;
ctx.textAlign = "center";
ctx.fillText(message, canvas.width / 2, canvas.height / 2);
}
```
1. 建立函式 `endGame()`
```javascript
function endGame(win) {
clearInterval(gameLoopId);
// 設定延遲以確保所有圖像皆繪製完成
setTimeout(() => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "black";
ctx.fillRect(0, 0, canvas.width, canvas.height);
if (win) {
displayMessage(
"Victory!!! Pew Pew... - Press [Enter] to start a new game Captain Pew Pew",
"green"
);
} else {
displayMessage(
"You died !!! Press [Enter] to start a new game Captain Pew Pew"
);
}
}, 200)
}
```
1. **重新遊戲的邏輯**。 當玩家損失所有的性命,或是贏下這場遊戲,顯示遊戲重來的提示。此外,在*重新遊玩*按鍵被按壓時,重新遊戲(你可以自己決定任一個鍵盤按鍵)。
1. 建立函式 `resetGame()`
```javascript
function resetGame() {
if (gameLoopId) {
clearInterval(gameLoopId);
eventEmitter.clear();
initGame();
gameLoopId = setInterval(() => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "black";
ctx.fillRect(0, 0, canvas.width, canvas.height);
drawPoints();
drawLife();
updateGameObjects();
drawGameObjects(ctx);
}, 100);
}
}
```
1. 在 `initGame()` 內呼叫 `eventEmitter` 來重新設定遊戲:
```javascript
eventEmitter.on(Messages.KEY_EVENT_ENTER, () => {
resetGame();
});
```
1. 在 EventEmitter 加入函式 `clear()`
```javascript
clear() {
this.listeners = {};
}
```
👽 💥 🚀 恭喜你,艦長!你的遊戲已經完成了!幹得好! 🚀 💥 👽
---
## 🚀 挑戰
加入遊戲音效!你能加入音效來提升遊戲品質嗎?或許在雷射擊中敵人,或是在英雄死亡、勝利時發出音效。看看這套[沙盒](https://www.w3schools.com/jsref/tryit.asp?filename=tryjsref_audio_play),了解如何使用 JavaScript 播放音效。
## 課後測驗
[課後測驗](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/40?loc=zh_tw)
## 複習與自學
你的功課是建立一款新的小遊戲。去探索一些有趣的遊戲,決定你想建造的遊戲類型。
## 作業
[建立一款遊戲](assignment.zh-tw.md)

@ -0,0 +1,19 @@
# 建立一款遊戲
## 簡介
試著建立一款小遊戲,實際應用不同的終止狀態。做一些變化,如取得的分數、英雄血量或是被擊敗的怪物。可以是很簡單的類型,例如文字冒險遊戲。請參考下列的遊戲流程作為啟發:
```
英雄> 使用寶劍攻擊 - 獸人受到了 3 點傷害
獸人> 使用棍棒攻擊 - 英雄受到了 2 點傷害
英雄> 踢擊 - 獸人受到了 1 點傷害
遊戲訊息> 獸人已被擊敗 - 英雄取得 2 枚硬幣
遊戲訊息> ****已殲滅所有怪獸,你已佔領了邪惡堡壘****
```
## 學習評量
| 作業內容 | 優良 | 普通 | 待改進 |
| -------- | ---------------- | ---------------- | ------------ |
| | 呈現出完整的遊戲 | 遊戲提供部分內容 | 遊戲存在問題 |

@ -0,0 +1,31 @@
# 建立一款太空遊戲
一款太空遊戲能教會你更多 JavaScript 的進階概念
這系列課程中,你會學習如何建立屬於自己的太空遊戲。如果你遊玩過遊戲「太空侵略者」,這款遊戲有相同的套路:操控太空船並擊落由上接近的怪物。這是遊戲完成後的模樣:
![遊戲成品](../images/pewpew.gif)
這六堂課程中,你會學習:
- **使用** Canvas 元素來在畫面上繪製物件
- **了解**笛卡爾座標系
- **學習**發布與訂閱設計模式,建立容易維護及擴增的遊戲結構
- **利用** Async/Await 來讀取遊戲資源
- **處理**鍵盤事件
## 總覽
- 理論
- [利用 JavaScript 設計遊戲](../1-introduction/translations/README.zh-tw.md)
- 實作
- [在畫布上繪製](../2-drawing-to-canvas/translations/README.zh-tw.md)
- [移動畫面上之物件](../3-moving-elements-around/translations/README.zh-tw.md)
- [碰撞偵測](../4-collision-detection/translations/README.zh-tw.md)
- [持續得分](../5-keeping-score/translations/README.zh-tw.md)
- [結束與重新遊戲](../6-end-condition/translations/README.zh-tw.md)
## 參與人員
遊戲資源皆來自於 https://www.kenney.nl/。
如果你熱愛設計遊戲,這邊有許多實用的資源,許多資源是免費的,有些則是付費內容。

@ -0,0 +1,307 @@
# 建立銀行網頁應用程式 Part 1HTML 模板與網頁路由
## 課前測驗
[課前測驗](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/41?loc=zh_tw)
### 大綱
自從 JavaScript 出現在瀏覽器後,網頁開始變得更複雜、更多互動。網頁技術已經普遍地用於建立功能齊全的應用程式,執行在瀏覽器上,我們稱之為[網頁應用程式](https://zh.wikipedia.org/zh-tw/%E7%BD%91%E7%BB%9C%E5%BA%94%E7%94%A8%E7%A8%8B%E5%BA%8F)。基於網頁應用程式的高互動性,使用者不會想在互動後做所有頁面載入所需的等待。這也是為什麼 JavaScript 使用 DOM 來更新 HTML提供使用者更流暢的網頁體驗。
在這堂課程中,我們會譜出銀行網頁應用程式的基礎,使用 HTML 模板建立不同的畫面,各自顯示並更新內容,而不必每次都需要載入整個 HTML 頁面。
### 開始之前
你需要一個網頁伺服器來測試我們要建的專案。如果你還沒有,你可以安裝 [Node.js](https://nodejs.org) 並在你的專案資料夾中使用指令 `npx lite-server`。這會建立一個本地端的網頁伺服器,在瀏覽器上開啟你的網頁程式。
### 準備
在你的電腦上,建立資料夾 `bank`,並在裡面建立檔案 `index.html`。我們以這個 HTML [樣板](https://en.wikipedia.org/wiki/Boilerplate_code)來開始:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bank App</title>
</head>
<body>
<!-- This is where you'll work -->
</body>
</html>
```
---
## HTML 模板(templates)
如果你想在同一個網頁上建立不同的畫面,其中一種方法是各自建立一個 HTML 檔給每一個你想呈現的子畫面。然而,這個方式有許多不便之處:
- 你需要在切換頁面時,重新載入整個網頁。這會很花時間。
- 在不同子頁面上共享數據會是一大難題。
另一個解決方案是只有一個 HTML 檔案,並使用 `<template>` 元素定義多個 [HTML 模板](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template)。
一個模板提供可重複利用的 HTML 區塊,它不會顯示在瀏覽器上,而在需要之時由 JavaScript 以呈現出來。
### 課題
我們會建立一個銀行網頁應用程式,其中包含兩個子畫面:登入頁面與儀表板頁面。首先,我們在網頁應用程式的 HTML body 上,建立放置區來放置模板的子頁面。
```html
<div id="app">Loading...</div>
```
我們給它 `id`,以利後續 JavaScript 對它的追蹤。
> 提示:因為它裡面元素的內容會被置換,我們可以建立載入中訊息或提示,在應用程式載入時顯示出來。
接下來,我們加入下列的 HTML 模板,給登入畫面使用。現在我們只加入一行標題與一個有連結的區塊,進行簡單的功能。
```html
<template id="login">
<h1>Bank App</h1>
<section>
<a href="/dashboard">Login</a>
</section>
</template>
```
接著,加入另一個 HTML 模板給儀表板頁面。這個頁面就會包含不同的區塊:
- 包含標題與登出連結的網頁標頭
- 現在的銀行帳戶餘額
- 一個歷史交易清單的表格
```html
<template id="dashboard">
<header>
<h1>Bank App</h1>
<a href="/login">Logout</a>
</header>
<section>
Balance: 100$
</section>
<section>
<h2>Transactions</h2>
<table>
<thead>
<tr>
<th>Date</th>
<th>Object</th>
<th>Amount</th>
</tr>
</thead>
<tbody></tbody>
</table>
</section>
</template>
```
> 提示:在建立 HTML 模板時,如果你想知道它的呈現樣子為何,你可以註解掉 `<template>``</template>`。使用 `<!-- -->` 來註解它們。
✅ 你知道為什麼我們需要使用模板的 `id` 屬性嗎?我們可以使用別的屬性,例如 classes 嗎?
## 利用 JavaScript 顯示模板
現在,如果你使用瀏覽器打開你的應用程式,你會看到畫面卡在 `Loading...` 的畫面。那是因為我們需要為它新增一些 JavaScript 的程式碼來顯示出這些 HTML 模板。
展現模板通常需要三個步驟:
1. 在 DOM 內接收模板元素,舉例來說,使用 [`document.getElementById`](https://developer.mozilla.org/en-US/docs/Web/API/Document/getElementById)。
2. 複製模板元素,使用 [`cloneNode`](https://developer.mozilla.org/en-US/docs/Web/API/Node/cloneNode)。
3. 將複製元素接到 DOM 的顯示元素上,例如使用 [`appendChild`](https://developer.mozilla.org/en-US/docs/Web/API/Node/appendChild)。
✅ 我們為什麼需要在接到 DOM 前,複製一份模板?你能想像如果我們省略了此步驟後,會發生什麼事嗎?
### 課題
在資料夾中建立新檔案 `app.js`,並在你的 HTML 檔案的 `<head>` 區塊中中匯入這個新檔案:
```html
<script src="app.js" defer></script>
```
`app.js` 中,我們建立新函式 `updateRoute`
```js
function updateRoute(templateId) {
const template = document.getElementById(templateId);
const view = template.content.cloneNode(true);
const app = document.getElementById('app');
app.innerHTML = '';
app.appendChild(view);
}
```
這裡做的事情就是我們上述提及過的三個步驟。我們使用 `templateId` 展現了模板,並將複製的內容接在我們的放置區中。注意我們需要使用 `cloneNode(true)` 來複製整個模板的子樹。
現在我們呼叫這個函式,指定特定的模板並觀察結果。
```js
updateRoute('login');
```
✅ 程式碼中 `app.innerHTML = '';` 的目的為何?如果刪去它會發生什麼事?
## 建立網頁路由(Routing)
當提及網頁應用程式時,我們稱呼 *路由(Routing)* 來連接**網址(URLs)**到特定的畫面上,呈現相關內容。一個含有多個 HTML 檔的網頁,網址又象徵著檔案路徑,這能自動地完成網址與檔案的轉換。舉例來說,專案資料夾內有這些檔案:
```
mywebsite/index.html
mywebsite/login.html
mywebsite/admin/index.html
```
若我們建立網路伺服器,根目錄為 `mywebsite`,則 URL 路由為:
```
https://site.com --> mywebsite/index.html
https://site.com/login.html --> mywebsite/login.html
https://site.com/admin/ --> mywebsite/admin/index.html
```
然而,在我們的網頁應用中,我們使用單一個 HTML 檔包含所有的子畫面到其中,所以預設的路由行為並不能幫助到本次專案。我們需要手動進行連接,使用 JavaScript 更新該被顯示出來的模板。
### 課題
我們使用簡單的物件來達成 URL 網址與模板的[關聯實體關係](https://en.wikipedia.org/wiki/Associative_array)。加入這個物件到 `app.js` 檔案的最上方。
```js
const routes = {
'/login': { templateId: 'login' },
'/dashboard': { templateId: 'dashboard' },
};
```
現在,我們對函式 `updateRoute` 做一些更動。我們不直接將 `templateId` 作為參數傳遞,而是接收現在的 URL 網址,在使用關聯表來取得相對應的模板 ID 數值。我們可以使用 [`window.location.pathname`](https://developer.mozilla.org/en-US/docs/Web/API/Location/pathname) 來取得網址的部分路徑。
```js
function updateRoute() {
const path = window.location.pathname;
const route = routes[path];
const template = document.getElementById(route.templateId);
const view = template.content.cloneNode(true);
const app = document.getElementById('app');
app.innerHTML = '';
app.appendChild(view);
}
```
這邊我們建立了模板的路由關係。你可以藉由修改網址,來測試你的網頁是否正確的轉移。
✅ 如果你輸入了不存在的網址,它會發生什麼事?我們該如何解決呢?
## 加入網頁訪問
下一個步驟為在不更改網址的情況下,新增網頁訪問的途徑。這會做出兩件事情:
1. 更新現在的網址
2. 更新要被顯示的模板到新的網址中
我們已經完成了第二點,藉由使用函式 `updateRoute` 來完成,所以我們需要釐清該如何更新現在的網址。
我們需要使用 JavaScript詳細來看為 [`history.pushState`](https://developer.mozilla.org/en-US/docs/Web/API/History/pushState),更新網址位置並建立瀏覽紀錄,同時不更新整個 HTML 頁面。
> 筆記:網頁超連結元素 [`<a href>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a) 可以建立不同網址的連接,但它預設上會讓瀏覽器重新載入 HTML 檔。我們需要手動新增 JavaScript 處理路由以避免此行為發生,在點擊事件中使用函式 preventDefault() 。
### 課題
我們來建立新的函式,讓應用程式得以做網頁的訪問:
```js
function navigate(path) {
window.history.pushState({}, path, window.location.origin + path);
updateRoute();
}
```
這個方法根據導入的路徑位置,更新了現在的網址位置,再更新模板上去。`window.location.origin` 回傳了網址的根路徑,允許我們重新構築完整的網址。
現在,藉由上述的函式,我們可以解決找不到網頁路徑的問題。我們修改函式 `updateRoute`,在找不到該網頁時強制轉移到一個存在的網頁。
```js
function updateRoute() {
const path = window.location.pathname;
const route = routes[path];
if (!route) {
return navigate('/login');
}
...
```
如果找不到網頁路由時,我們會導往 `login` 的頁面。
現在,我們建立新的函式,在連結被點擊時取得網址位置,並避免瀏覽器進行預設上的重新載入:
```js
function onLinkClick(event) {
event.preventDefault();
navigate(event.target.href);
}
```
現在我們完成應用程式的網頁訪問系統,在 HTML 檔的 *Login**Logout* 連結加入此函式。
```html
<a href="/dashboard" onclick="onLinkClick()">Login</a>
...
<a href="/login" onclick="onLinkClick()">Logout</a>
```
使用 [`onclick`](https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/onclick) 屬性會將 `click` 事件連接到 JavaScript 程式碼中,這邊會再呼叫函式 `navigate()`
試著點擊這些連結,你應該能造訪網頁中不同的的畫面了。
`history.pushState` 這個方法是 HTML5 標準的一部份,支援在[所有當代的瀏覽器](https://caniuse.com/?search=pushState)上。如果你要為舊款的瀏覽器設計網頁應用程式的話,這邊有一個技巧來加在這個 API 上:在路徑前面加上 [hash (`#`)](https://en.wikipedia.org/wiki/URI_fragment),你可以完成網頁路由與不須重載網頁的功能,它的目的就是在同一個網頁中做內部連結的切換。
## 處理瀏覽器的「上一頁」與「下一頁」
使用 `history.pushState` 會建立瀏覽器的瀏覽紀錄。你可以使用瀏覽器的*上一頁*來確認,它應該要能呈現像這樣的畫面:
![瀏覽歷史的截圖](../history.png)
點擊上一頁數次,你會看到網址會改變且歷史紀錄也更新上去了,但同一個模板還是被顯示出來。
這是因為網頁不知道該如何依據歷史紀錄來呼叫 `updateRoute()`。如果你閱讀了 [`history.pushState` 技術文件](https://developer.mozilla.org/en-US/docs/Web/API/History/pushState),你會發現如果狀態改變 ── 同時代表著網址改變 ── [`popstate`](https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event) 事件就會被觸發。我們會利用此特徵來修復這個問題。
### 課題
為了在瀏覽器歷史改變時更新該被顯示的模板,我們會以新函式來呼叫 `updateRoute()`。我們在 `app.js` 檔最下方加入:
```js
window.onpopstate = () => updateRoute();
updateRoute();
```
> 筆記:我們在這裡使用[箭頭函式](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions),簡短地宣告 `popstate` 事件處理器。它與正規的函式的功能是一樣的。
這是關於箭頭函式的回想影片:
[![箭頭函式](https://img.youtube.com/vi/OP6eEbOj2sc/0.jpg)](https://youtube.com/watch?v=OP6eEbOj2sc "箭頭函式")
> 點擊上方圖片以觀看關於箭頭函式的影片。
現在,試著點擊瀏覽器上的上一頁與下一頁,檢查這次模板是否正確地更新出來。
---
## 🚀 挑戰
加入新的模板與對應的關聯表,顯示出本應用程式第三頁的功能 ── 帳戶餘額。
## 課後測驗
[課後測驗](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/42?loc=zh_tw)
## 複習與自學
網頁路由是網頁開發中很棘手的部分,特別是將網頁切換轉變為單一頁面應用程式(Single Page Application)。閱讀關於[Azure Static Web App 提供服務的方式](https://docs.microsoft.com/en-us/azure/static-web-apps/routes?WT.mc_id=academic-13441-cxa)以處理網頁路由。你能解釋為什麼文件上的某些決定會如此重要呢?
## 作業
[增進網頁路由](assignment.zh-tw.md)

@ -0,0 +1,14 @@
# 增進網頁路由
## 簡介
我們的網頁路由的定義只包含模板的 ID。但當顯示新的網頁頁面時我們或許會用到更多東西。讓我們來增進我們的網頁路由方式新增兩項功能
- 給各個模板標題,在模板切換後同時更新網頁視窗的標題。
- 加入額外選項,在模板切換後執行特定程式。我們希望在切換到儀表板頁面時,在開發者命令欄顯示 `'Dashboard is shown'`
## 學習評量
| 作業內容 | 優良 | 普通 | 待改進 |
| -------- | -------------------------------------------------------- | --------------------------------------------------------------------------------------------- | ------------------------ |
| | 兩個新功能運作正常,標題與程式也能執行新的 `routes` 規則 | 兩個新功能運作正常,但行為是 hardcoded 上去而非使用 `routes` 規則。新的路由規則無法完整地運作 | 新功能不完全或不正常運行 |

@ -0,0 +1,297 @@
# 建立銀行網頁應用程式 Part 2登入與註冊表單
## 課前測驗
[課前測驗](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/43?loc=zh_tw)
### 大綱
在大多數當代網頁應用程式中,你可以建立自己的帳戶來擁有自己的私人空間。許多用戶在同一時間可以存取同一個網頁應用程式,你就必須有一套機制分開儲存不同用戶的資料並顯示適當的資訊。我們不會涉及到如何管理[用戶個資的安全](https://zh.wikipedia.org/wiki/%E8%BA%AB%E4%BB%BD%E9%AA%8C%E8%AF%81),它是個相當廣泛的主題,我們僅會確保每個用戶能在這款銀行應用上建立一到多個數位帳戶。
在這單元中,我們會使用 HTML 表單來新增登入與註冊的功能。我們會看到如何使用伺服器 API 傳遞資料,定義基本的用戶字串輸入之檢查機制。
### 開始之前
你需要完成第一單元 [HTML 模板與網頁路由](../../1-template-route/translations/README.zh-tw.md)的應用程式。你還需要安裝 [Node.js](https://nodejs.org) 與在本地端[運行伺服器 API](../../api/translations/README.zh-tw.md)以傳輸建立帳戶所需的資料。
你可以測試伺服器是否運作正常,在終端機內輸入指令:
```sh
curl http://localhost:5000/api
# -> 會回傳結果 "Bank API v1.0.0"
```
---
## 表單與其控制
`<form>` 元素打包了 HTML 文件中使用者輸入與提交資料的地方。有許多種使用者介面(UI)以表單的方式呈現,最常見的內容會包含 `<input>``<button>` 元素。
有許多種 `<input>` 的[種類](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input),舉例來說,若要建立使用者輸入使用者名稱的地方,你可以:
```html
<input id="username" name="username" type="text">
```
`name` 屬性同時亦是表單傳輸資料的名稱。`id` 屬性是用來與 `<label>` 做表單控制(form control)的連接。
> 花點時間看看 [`<input>` 種類](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input)的清單與[其他表單控制](https://developer.mozilla.org/en-US/docs/Learn/Forms/Other_form_controls),讓你在建立使用者介面時,有全部供你使用的原生 UI 元素可以參考。
✅ 紀錄一下 `<input>` 是種[空元素](https://developer.mozilla.org/en-US/docs/Glossary/Empty_element),你*不應該*在它後面加上對應的結束標籤。然而,你仍然可以在它的後面使用 `<input/>`,這沒有強制規定。
表單中的 `<button>` 元素是有些特別。如果你沒有指定它的 `type` 屬性,它會在你輸入文字時,自動地提交表單內容給伺服器。這邊有一些你可以設定的 `type` 內容:
- `submit` `<form>` 內的預設型態,按鈕會觸發表單提交這項行為。
- `reset` 按鈕會重置所有表單控制回初始狀態。
- `button` 在按鈕按下時不執行預設行為。你可以藉由 JavaScript 自由定義之後的動作。
### 課題
`login` 模板內加入表單。我們需要*使用者名稱(username)*的輸入框與*登入(Login)*的按鈕。
```html
<template id="login">
<h1>Bank App</h1>
<section>
<h2>Login</h2>
<form id="loginForm">
<label for="username">Username</label>
<input id="username" name="user" type="text">
<button>Login</button>
</form>
</section>
</template>
```
如果你仔細地看,你會注意到我們在這裡還加了 `<label>` 元素。`<label>` 元素被用來新增文字到 UI 上,譬如說我們的使用者名稱。為了讓表單得以被閱讀,標籤是很重要的,此外它還有額外的優點:
- 連結標籤到表單控制上,它能幫助使用者的額外工具,好比說螢幕報讀器,理解接下來該提供何種資料。
- 你可以點擊標籤,它會跳轉到相對應的輸入框,讓使用觸控型裝置的用戶更容易操作。
> [網頁親和力](https://developer.mozilla.org/en-US/docs/Learn/Accessibility/What_is_accessibility)是非常重要但常被忽視的主題。感謝[語義化 HTML 元素](https://developer.mozilla.org/en-US/docs/Learn/Accessibility/HTML)的幫助,建立無障礙的網頁內容變得更加容易。你可以[閱讀更多有關網頁親和力的文章](https://developer.mozilla.org/en-US/docs/Web/Accessibility),避免觸犯到常見的錯誤並成為負責任的開發者。
現在,我們加入第二張表單給用戶註冊使用,就像前一張一樣:
```html
<hr/>
<h2>Register</h2>
<form id="registerForm">
<label for="user">Username</label>
<input id="user" name="user" type="text">
<label for="currency">Currency</label>
<input id="currency" name="currency" type="text" value="$">
<label for="description">Description</label>
<input id="description" name="description" type="text">
<label for="balance">Current balance</label>
<input id="balance" name="balance" type="number" value="0">
<button>Register</button>
</form>
```
藉由 `value` 屬性,我們可以定義輸入框內的預設值。
注意一下 `balance` 的輸入類型為 `number`。它看起來與其他輸入框不一樣嗎?試著與它互動看看。
✅ 你能只利用鍵盤造訪表格,與表格互動嗎?你是如何做到的?
## 提交資料給伺服器
現在我們有可以使用的 UI 了,下一個步驟要將資料送給我們的伺服器。讓我們來快速地測試一下程式:在點擊 *Login**Register* 按鈕後,發生了什麼事?
你有注意到瀏覽器的網址列改變了嗎?
![截圖:點擊 Register 按鈕後,瀏覽器網址列改變](../images/click-register.png)
`<form>` 預設的行為:使用 [GET 方法](https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.3)提交表格,將表格資料接在網址後面,傳送給目前網址的伺服器。然而這個方法有一些缺點:
- 資料大小有上限限制(大約 2000 字元)
- 可以直接在網址內看到資料(對密碼而言,這並不恰當)
- 它不能做檔案的上傳
這也是為什麼你需要將它轉換為 [POST 方法](https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.5),將表單資料存在 HTTP 請求的內容中。這樣就不會遇到上述的限制。
> POST 是常見的資料傳輸方法,[在一些特別的情況下](https://www.w3.org/2001/tag/doc/whenToUseGet.html),使用 GET 方法相對起來比較恰當。例如進行搜尋的時候。
### 課題
加入 `action``method` 屬性到註冊表單之中:
```html
<form id="registerForm" action="//localhost:5000/api/accounts" method="POST">
```
現在,試著以你的名字申請新的帳戶。在點擊 *Register* 按鈕後,你應該能看到像這樣的畫面:
![瀏覽器網址為 localhost:5000/api/accounts並顯示 JSON 的資料字串。](../images/form-post.png)
若所有事情都運作正常,伺服器應該會回應你的請求,附帶 [JSON](https://www.json.org/json-en.html) 包含著你剛建立的帳戶資料。
✅ 試著以相同名字再註冊一次。發生了什麼事?
## 不重新載入地提交資料
你可能會注意到,這些行動間出現了一個小問題:在提交表單時,我們離開了網頁應用,瀏覽器又重新導回到伺服器的網址。我們試著避免網頁應用重新載入所有的頁面,做出[單一頁面應用程式 (SPA)](https://zh.wikipedia.org/zh-tw/%E5%8D%95%E9%A1%B5%E5%BA%94%E7%94%A8)。
為了讓傳遞資料給伺服器時,不發生頁面重新載入的情況,我們需要使用 JavaScript。
比起直接在 `<form>` 元素的 `action` 屬性加入網址,你可以使用 `javascript:` 字串接在程式語句前頭來執行自訂的行為。使用這方法也意味著你需要額外修改一些原本瀏覽器會做的行為。
- 接收表單資料
- 轉換並編碼表單資料成合適的格式
- 建立 HTTP 請求並傳遞給伺服器
### 課題
將註冊表單的 `action` 替換為:
```html
<form id="registerForm" action="javascript:register()">
```
開啟 `app.js`,加入新的函式 `register`
```js
function register() {
const registerForm = document.getElementById('registerForm');
const formData = new FormData(registerForm);
const data = Object.fromEntries(formData);
const jsonData = JSON.stringify(data);
}
```
我們使用 `getElementById()` 蒐集表單的元素,使用 [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) 協助從表單控制中取出 key/value 的數據對。
之後,利用 [`Object.fromEntries()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/fromEntries) 轉換資料成正規物件,最後再將檔案轉成 [JSON](https://www.json.org/json-en.html) ── 一個在網路上常見的資料交換格式。
現在資料已經準備提交給伺服器了。建立新函式 `createAccount`
```js
async function createAccount(account) {
try {
const response = await fetch('//localhost:5000/api/accounts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: account
});
return await response.json();
} catch (error) {
return { error: error.message || 'Unknown error' };
}
}
```
這個函式做了什麼?首先,注意關鍵字 `async`,代表著函式包含了[**非同步化程式**](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function)。在與關鍵字 `await` 一起使用時,它會在繼續運行程式前,等待非同步的程式被執行,就像等待伺服器回應一樣。
這邊有關於使用 `async/await` 的影片:
[![Async 與 Await 管理 promises](https://img.youtube.com/vi/YwmlRkrxvkk/0.jpg)](https://youtube.com/watch?v=YwmlRkrxvkk "Async 與 Await 管理 promises")
> 點擊上方圖片以觀看關於 async/await 的影片。
我們使用 API `fetch()` 來傳送 JSON 資料給伺服器。這個方法需要使用兩個參數:
- 伺服器的網址,在此使用 `//localhost:5000/api/accounts`
- 網頁請求的設定,就是我們定義 `POST` 方法與提供請求的 `body`。當我們傳輸 JSON 資料給伺服器,我們還需要在標頭的 `Content-Type` 定為 `application/json`,伺服器才知道該如何解讀裡面的內容。
當伺服器以 JSON 回應請求後,我們可以使用 `await response.json()` 來取得 JSON 的內容並回傳結果。注意在此為非同步程式的方法,我們使用關鍵字 `await` 回傳任何在解讀封包時產生的錯誤訊息。
現在,在函式 `register` 中呼叫 `createAccount()`
```js
const result = await createAccount(jsonData);
```
因為我們在這此使用了關鍵字 `await`,我們需要在註冊函式前新增關鍵字 `async`
```js
async function register() {
```
最後,我們儲存一些紀錄來檢查結果。最後的函式應該會如下方格式:
```js
async function register() {
const registerForm = document.getElementById('registerForm');
const formData = new FormData(registerForm);
const jsonData = JSON.stringify(Object.fromEntries(formData));
const result = await createAccount(jsonData);
if (result.error) {
return console.log('An error occured:', result.error);
}
console.log('Account created!', result);
}
```
過程有些冗長,但我們達成了!當你開啟[瀏覽器開發者工具](https://developer.mozilla.org/en-US/docs/Learn/Common_questions/What_are_browser_developer_tools),試著註冊新的帳戶,你應該能看到網頁並沒有改變,但命令欄中會顯示帳戶成功註冊的訊息。
![瀏覽器命令欄中顯示紀錄訊息之截圖](../images/browser-console.png)
✅ 你覺得傳給伺服器的資料是安全的嗎?其他人有辦法攔截網頁請求嗎?你可以閱讀 [HTTPS](https://en.wikipedia.org/wiki/HTTPS),了解更多關於安全的資料傳輸。
## 資料驗證
試著在註冊新帳戶時,不輸入你的使用者名稱,你會發現伺服器回傳了錯誤狀態訊息:[400 (Bad Request)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/400#:~:text=The%20HyperText%20Transfer%20Protocol%20(HTTP,%2C%20or%20deceptive%20request%20routing).)。
在傳輸資料給伺服器之前,最好先[驗證表單資料](https://developer.mozilla.org/en-US/docs/Learn/Forms/Form_validation),以確保我們傳送合法的網頁請求。 HTML5 表單控制內建包含了驗證方法,使用了多樣的屬性: controls provides built-in validation using various attributes:
- `required` 輸入框必須被填寫,否則表單不能被提交。
- `minlength``maxlength` 定義輸入框的文字下限與文字上限。
- `min``max` 定義輸入框的數字下限與數字上限。
- `type` 定義輸入框內的資料格式,例如`數字`、`email`、`檔案`或是[其他內建的格式](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input)。這個屬性可能會改變表單控制的表現方法。
- `pattern` 允許定義[正規表示法](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions)的字串,測試輸入的內容是否滿足它。
> 提示:你可以自定義表單控制的呈現方法,利用 CSS pseudo-classes `:valid``:invalid` 判斷內容是否合理。
### 課題
在建立新的合法帳戶時,有兩個必須被填寫的輸入框:使用者名稱與資產狀態,而其他選項則是可有可無。現在更新表單的 HTML 語法,使用 `required` 屬性並標記提示在標籤中:
```html
<label for="user">Username (required)</label>
<input id="user" name="user" type="text" required>
...
<label for="currency">Currency (required)</label>
<input id="currency" name="currency" type="text" value="$" required>
```
伺服器並沒設定輸入框的文字上限,定義合理的文字輸入上限是必要的。
在文字框內加入 `maxlength` 屬性:
```html
<input id="user" name="user" type="text" maxlength="20" required>
...
<input id="currency" name="currency" type="text" value="$" maxlength="5" required>
...
<input id="description" name="description" type="text" maxlength="100">
```
現在,如果文字框並沒有滿足我們所定義的規則時,在點擊了 *Register* 按鈕後,你會看到:
![傳輸錯誤表單而出現驗證失敗的截圖](../images/validation-error.png)
這類在傳輸資料給伺服器*之前*的驗證系統稱之為**用戶端(client-side)**驗證。但注意有些資料是沒有辦法在傳輸前被驗證的。舉例來說,我們沒辦法在發出請求前,確認是否已經存在著一組相同姓名的帳戶。伺服器上額外的驗證措施就稱之為**伺服器端(server-side)**驗證。
通常這兩個驗證都需要去編寫,用戶端驗證能及時回饋給用戶,提升使用者體驗;伺服器端驗證確保你要處理的用戶資料是合理且安全的。
---
## 🚀 挑戰
當相同使用者名稱的帳戶已經存在時,在 HTML 上顯示錯誤訊息。
這邊有做過一些造型的最終登入頁面範本。
![加上 CSS 造型的登入頁面截圖](../images/result.png)
## 課後測驗
[課後測驗](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/44?loc=zh_tw)
## 複習與自學
開發者在建立表單時需要發揮他們的創意,尤其是策畫資料驗證的規則。在 [CodePen](https://codepen.com) 上學習不同表單流程,你能發現什麼有趣且令人發想的表單嗎?
## 作業
[造型化你的銀行程式](assignment.zh-tw.md)

@ -0,0 +1,13 @@
# 造型化你的銀行程式
## 簡介
建立新的檔案 `styles.css`,匯入到你的 `index.html` 檔案中。藉由 CSS 檔,你能讓*登入*與*儀表板*頁面看起來更漂亮且整潔。試著為你的程式加入主題色彩,對應到你的品牌。
> 提示:你可以修改 HTML 檔,在必要時新增元素與 classes。
## 學習評量
| 作業內容 | 優良 | 普通 | 待改進 |
| -------- | -------------------------------------------------- | ------------------------------------ | ---------------------------------------- |
| | 所有的頁面整潔且易讀:統一的主題色彩且排版顯示正常 | 頁面有調整過,但缺乏主題且排版有瑕疵 | 頁面缺乏造型,排版凌亂且頁面資訊難以理解 |

@ -0,0 +1,336 @@
# 建立銀行網頁應用程式 Part 3取得並使用資料
## 課前測驗
[課前測驗](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/45?loc=zh_tw)
### 大綱
每一個網頁應用程式的核心為*資料*。資料有很多種格式,但它們的目的都是為了顯示使用者需要的資訊。網頁應用程式變得高互動性與複雜,使用者如何取得內容並與之進行互動變成網頁開發重要的一環。
在這堂課中,我們會了解伺服器是如何非同步地取得資料,並在不重新載入 HTML 的情況下,利用這些資料顯示在網頁上。
### 開始之前
你需要先完成系列課程 ── [登入與註冊表單](../../2-forms/translations/README.zh-tw.md)。你還需要安裝 [Node.js](https://nodejs.org) 並[執行伺服器 API](../../api/translations/README.zh-tw.md)。
你可以測試伺服器是否運作正常,在終端機中輸入指令:
```sh
curl http://localhost:5000/api
# -> 會回傳結果 "Bank API v1.0.0"
```
---
## AJAX 和取得資料
傳統的網頁在使用者點擊連結,或是提交表單資料時,重新載入全部的 HTML 頁面來更新網頁內容。每當資料要被更新時,伺服器就需要回傳全新的 HTML 頁面給瀏覽器處理,同時也干涉到使用者正在進行的動作,重新載入的機制也限制了許多互動功能。這種工作流程被稱為*多頁面應用程式(Multi-Page Application)*,簡稱 *MPA*
![多頁面應用程式的更新流程](../images/mpa.png)
網頁應用程式變得更加複雜,促使新的技術問世:[AJAX (Asynchronous JavaScript and XML)](https://zh.wikipedia.org/wiki/AJAX)。
這個技巧允許網頁應用程式使用 JavaScript 非同步地傳遞與接收伺服器的資料,不需要重新載入 HTML 頁面,也反映在更快速的更新速率與更流暢的使用者體驗。在接收伺服器的新資料時,目前的 HTML 頁面可以被 JavaScript 利用 [DOM](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model) API 更新。自此之後,這種流程演變成現今的[*單一頁面應用程式(Single-Page Application)**SPA*](https://zh.wikipedia.org/wiki/%E5%8D%95%E9%A1%B5%E5%BA%94%E7%94%A8)。
![單一頁面應用程式的更新流程](../images/spa.png)
在 AJAX 早期,唯一取得資料的 API 為 [`XMLHttpRequest`](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest)。但當代的瀏覽器已經建立出更方便且強大的 [`Fetch` API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API),它們使用 Promises 物件且更適合應用在 JSON 資料上。
> 許多當代瀏覽器支援 `Fetch API`,如果你想確認你的網頁應用程式是否運作在舊款的瀏覽器,檢查[caniuse.com 上的相容性測試](https://caniuse.com/fetch)是一個好方法。
### 課題
在[前一堂課程中](../../2-forms/translations/README.zh-tw.md),我們製作出註冊表單來建立新帳戶。現在我們來加入新程式,使用現有的帳戶登入,並取得其相關資料。開啟檔案 `app.js`並新增函式 `login`
```js
async function login() {
const loginForm = document.getElementById('loginForm')
const user = loginForm.user.value;
}
```
現在我們利用 `getElementById()` 接收表單元素,並藉由 `loginForm.user.value` 取得輸入框內的使用者名稱。每一個表單控制可以以各自名稱(即 HTML 內的 `name` 屬性)來存取。
就像我們為註冊帳戶作的事一樣,我們建立另一個函式來執行伺服器請求,但這次是為了取得帳戶資料:
```js
async function getAccount(user) {
try {
const response = await fetch('//localhost:5000/api/accounts/' + encodeURIComponent(user));
return await response.json();
} catch (error) {
return { error: error.message || 'Unknown error' };
}
}
```
我們使用 `fetch` API 來向伺服器做非同步資料請求。這次我們不需要添加額外的參數,如網址,我們只詢問資料內容。預設上,`fetch` 建立出 [`GET`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/GET) HTTP 請求,即我們想做的事情。
✅ 函式 `encodeURIComponent()` 可以轉換網址內的特殊字元。如果我們不呼叫這個函式,而是直接將 `user` 這項數值放入網址中,這會發生什麼問題?
讓我們來更新函式 `login`,使用 `getAccount`
```js
async function login() {
const loginForm = document.getElementById('loginForm')
const user = loginForm.user.value;
const data = await getAccount(user);
if (data.error) {
return console.log('loginError', data.error);
}
account = data;
navigate('/dashboard');
}
```
首先,`getAccount` 是一個非同步函式,它利用關鍵字 `await` 等待伺服器的回傳結果。就如其他伺服器請求一樣,我們也必須要處理錯誤的情況。現在我們只加錯誤訊息給這些情況,之後再回過頭解決這些問題。
接著,我們必須儲存資料,在之後可以輸出成儀表板的資訊。目前變數 `account` 還沒存在,我們建立它的全域變數在檔案最上方:
```js
let account = null;
```
在用戶資料存到變數中後,我們可以使用函式 `navigate()` 從*登入*頁面切換到*儀表板*頁面。
最後,在登入表單提交時,呼叫函式 `login`。修改 HTML 語法:
```html
<form id="loginForm" action="javascript:login()">
```
測試註冊功能,以及新註冊的帳戶的登入行為是否運作正常。
在進行下一步驟前,我們還必須完成函式 `register`。在此函式的最下方加入:
```js
account = result;
navigate('/dashboard');
```
✅ 你知道在預設上,你只能從*同一個網域(domain)與連接埠(port)*的網頁呼叫伺服器 APIs嗎這是瀏覽器強制性的安全機制。但我們的網頁應用程式在 `localhost:3000` 上執行,而伺服器 API 則在 `localhost:5000` 上執行。為什麼這樣能正常運作?利用[跨來源資源共用 (CORS)](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS),只要伺服器添加特殊的標頭檔到網頁回應中,我們就可以處理跨資源的 HTTP 請求,允許特殊的網域進行呼叫。
> 藉由前往[此課程](https://docs.microsoft.com/en-us/learn/modules/use-apis-discover-museum-art?WT.mc_id=academic-13441-cxa)學習更多有關 API 的資訊。
## 更新 HTML 顯示資料
現在取得完用戶資料,我們必須更新到現有的 HTML 上。我們已經知道如何接收 DOM 的元素,例子為 `document.getElementById()`。只要你有元素,這邊有一些 API 讓你修改,或是新增子元素上去:
- 使用 [`textContent`](https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent) 屬性,你可以改變元素的文字內容。注意改變此數值會刪除它的所有子元素(若存在的話),並以該文字內容來替換它。同時,這也是個有效的方法來刪去所有的子成員,只要賦予它空字串 `''`
- 使用 [`document.createElement()`](https://developer.mozilla.org/en-US/docs/Web/API/Document/createElement) 與 [`append()`](https://developer.mozilla.org/en-US/docs/Web/API/ParentNode/append) 這兩方法,你可以建立並接上一到多個子元素成員。
✅ 使用 [`innerHTML`](https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML) 元素屬性也能改變 HTML 的內容,但這方法要避免使用。這可能會遭遇有關[跨網站指令碼 (XSS)](https://developer.mozilla.org/en-US/docs/Glossary/Cross-site_scripting)的攻擊。
### 課題
在來到儀表板畫面之前,我們還需要幫*登入*頁面加一件事。目前,如果你試著使用不存在的帳戶進行登入,訊息只會出現在命令欄中,而使用者不會發覺到任何事情改變,也不清楚網頁發生了什麼事。
我們在登入表單中新增顯示錯誤訊息的地方。最好的地方為登入按鈕 `<button>` 之前:
```html
...
<div id="loginError"></div>
<button>Login</button>
...
```
這個 `<div>` 元素為空的,代表著畫面不會印出任何訊息,直到我們添加內容進去。我們還給了它 `id`,讓 JavaScript 可以容易地存取它。
回到檔案 `app.js`,建立新的補助函數 `updateElement`
```js
function updateElement(id, text) {
const element = document.getElementById(id);
element.textContent = text;
}
```
這條就很直觀:給定元素的 *id**text*,它會更新 DOM 元素內符合 `id` 條件的文字內容。我們也使用這個方法到前面 `login` 函式的錯誤訊息中:
```js
if (data.error) {
return updateElement('loginError', data.error);
}
```
現在,試著以不合法的帳戶進行登入,你應該能看到像這樣的畫面:
![登入出現錯誤訊息之截圖](../images/login-error.png)
現在我們印出錯誤訊息,但螢幕報讀器並沒有做任何報讀。為了讓被動態加入的文字能被螢幕報讀器閱讀出來,我們需要使用 [Live Region](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions)。這邊我們使用一種 Live Region 的類型 alert
```html
<div id="loginError" role="alert"></div>
```
建立同樣的行為到函式 `register` 的錯誤訊息當中,也別忘了更新你的 HTML。
## 在儀表板顯示資訊
使用同樣的技巧,我們需要將帳戶資訊印在儀表板頁面中。
這是從伺服器接收到的帳戶資料物件:
```json
{
"user": "test",
"currency": "$",
"description": "Test account",
"balance": 75,
"transactions": [
{ "id": "1", "date": "2020-10-01", "object": "Pocket money", "amount": 50 },
{ "id": "2", "date": "2020-10-03", "object": "Book", "amount": -10 },
{ "id": "3", "date": "2020-10-04", "object": "Sandwich", "amount": -5 }
],
}
```
> 筆記:為了讓開發更加的容易,你可以使用已經存在資料的帳戶 `test`
### 課題
我們開始置換掉 HTML 檔內的 "Balance" 區域,加入放置區:
```html
<section>
Balance: <span id="balance"></span><span id="currency"></span>
</section>
```
我們還需要在下方新增區域來顯示帳戶資訊:
```html
<h2 id="description"></h2>
```
✅ 表示帳戶資訊的函式剛好為在內容的標題處,我們可以將它作為語義化的標頭。學習更多關於[標頭結構](https://www.nomensa.com/blog/2017/how-structure-headings-web-accessibility),它對於網頁親和力格外重要,也明顯地表達出頁面的標頭位置。
接著,我們在 `app.js` 檔案中加入新的函式來為放置區新增內容:
```js
function updateDashboard() {
if (!account) {
return navigate('/login');
}
updateElement('description', account.description);
updateElement('balance', account.balance.toFixed(2));
updateElement('currency', account.currency);
}
```
首先,我們需要先檢查帳戶的資料。使用我們之前建立的函式 `updateElement()` 來更新 HTML 檔。
> 為了讓帳戶餘額漂亮地呈現,我們使用 [`toFixed(2)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toFixed) 這個方法,強迫數值只顯示到小數點第二位。
現在,每當儀表板被載入時,我們就需要呼叫函式 `updateDashboard()`。如果你已經完成[課程一的作業](../../1-template-route/translations/assignment.zh-tw.md),就不需要額外做處理,不然你可以使用接下來的設定。
加入這段程式碼到函式 `updateRoute()` 的下方:
```js
if (typeof route.init === 'function') {
route.init();
}
```
並更新路由定義:
```js
const routes = {
'/login': { templateId: 'login' },
'/dashboard': { templateId: 'dashboard', init: updateDashboard }
};
```
做完這些更動後,當儀表板要被呈現時,函式 `updateDashboard() 就會被呼叫。在你登入後就能看到帳戶的描述、餘額與交易狀況。
## 利用 HTML 模板動態建立表格列
在[第一堂課](../../1-template-route/translations/README.zh-tw.md)中,我們使用 HTML 模板與方法 [`appendChild()`](https://developer.mozilla.org/en-US/docs/Web/API/Node/appendChild) 來做出應用程式內的轉換。模板還能執行更小規模的行為,動態地改變一部份的頁面
我們使用類似的方式來顯示 HTML 表格中的交易清單。
### 課題
加入新的模板到 HTML 的 `<body>` 中:
```html
<template id="transaction">
<tr>
<td></td>
<td></td>
<td></td>
</tr>
</template>
```
這個模板表示單一條的表格列,其中包含了三格欄位:交易的*日期*、*物件* 與 *金額*
接著,加入 `id` 屬性到模板的表格 `<tbody>` 元素中,讓 JavaScript 能更容易地取得:
```html
<tbody id="transactions"></tbody>
```
當我們的 HTML 準備好時,我們切換到 JavaScript 檔案中,加入新函式 `createTransactionRow`
```js
function createTransactionRow(transaction) {
const template = document.getElementById('transaction');
const transactionRow = template.content.cloneNode(true);
const tr = transactionRow.querySelector('tr');
tr.children[0].textContent = transaction.date;
tr.children[1].textContent = transaction.object;
tr.children[2].textContent = transaction.amount.toFixed(2);
return transactionRow;
}
```
這個函式做就如它名字的功能:藉由剛建立的模板,建立出新的表格列並填入交易明細的資料。我們會在函式 `updateDashboard()` 中,利用它來更新表格:
```js
const transactionsRows = document.createDocumentFragment();
for (const transaction of account.transactions) {
const transactionRow = createTransactionRow(transaction);
transactionsRows.appendChild(transactionRow);
}
updateElement('transactions', transactionsRows);
```
這裡我們使用了方法 [`document.createDocumentFragment()`](https://developer.mozilla.org/en-US/docs/Web/API/Document/createDocumentFragment),建立新的 DOM 分段,再接到我們的 HTML 表格中。
我們還需要做一件事才能讓程式運作正常,目前函式 `updateElement()` 只能接受文字類型的內容。我們稍微修改一下程式碼:
```js
function updateElement(id, textOrNode) {
const element = document.getElementById(id);
element.textContent = ''; // Removes all children
element.append(textOrNode);
}
```
我們使用方法 [`append()`](https://developer.mozilla.org/en-US/docs/Web/API/ParentNode/append),它能連接文字或者是 [DOM 節點](https://developer.mozilla.org/en-US/docs/Web/API/Node)到父元素中,正好滿足我們的需求。
試著以 `test` 帳戶來登入,你應該能看到儀表板顯示出交易明細了 🎉。
---
## 🚀 挑戰
花功夫讓儀表板頁面看起來像是正規的銀行界面。如果你已經為你的應用程式做好造型,你可以試試 [media queries](https://developer.mozilla.org/en-US/docs/Web/CSS/Media_Queries) 來建立出[回應式網頁設計](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Responsive/responsive_design_building_blocks),它能完美地呈現在電腦或是行動裝置上。
這邊有造型過後的儀表板例子:
![造型化後的儀表板截圖](../../images/screen2.png)
## 課後測驗
[課後測驗](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/46?loc=zh_tw)
## 作業
[重構並註解你的程式碼](assignment.zh-tw.md)

@ -0,0 +1,15 @@
# 重構並註解你的程式碼
## 簡介
正當你的程式碼越來越多時,頻繁地重構你的程式,讓程式碼容易去閱讀與維護變得十分重要。加入一些註解並重構檔案 `app.js` 來增進檔案的品質:
- 取出常數,好比說伺服器 API 的根網址
- 重構相同的程式碼:舉例來說,你可以建立函式 `sendRequest()` 來合併 `createAccount()``getAccount()`
- 重新編排你的程式和加入註解,讓它更容易地閱讀。
## 學習評量
| 作業內容 | 優良 | 普通 | 待改進 |
| -------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------ | ---------------------------------------------- |
| | 程式碼有做註解,分塊地整理好。常數有被取出來且函式 `sendRequest()` 已設定完成 | 程式碼有做過處理,但仍可以藉由加入註解、排版與重構來增進品質 | 程式碼很雜亂,缺乏註解,常數與函式並沒有做規劃 |

@ -0,0 +1,284 @@
# 建立銀行網頁應用程式 Part 4 狀態控管的概念
## 課前測驗
[課前測驗](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/47?loc=zh_tw)
### 大綱
隨著網頁應用越來越龐大,追蹤資料流的動向也是一種挑戰。程式取得了何種資料、網頁如何處理它、何時何處被更新上去……這些很容易地導致程式碼凌亂而難以維護。尤其是當你需要在不同頁面上做資料共享時,好比說使用者的資料。*狀態控管(state management)* 的觀念已經存在於所有程式中,我們也開始需要在開發複雜的網頁應用程式時,注意這個關鍵點。
在這個最終章內,我們會總覽整個程式並重新思考該如何管理程式狀態,讓瀏覽器能在任何時刻做重新整理,在不同的使用者階段維持資料的狀態。
### 開始之前
你需要先完成[取得資料](../../3-data/translations/README.zh-tw.md)的網頁開發章節。你還需要安裝 [Node.js](https://nodejs.org) 並於本地端[執行伺服器 API](../../api/translations/README.zh-tw.md)以管理使用者資料。
你可以測試伺服器是否運作正常,在終端機中輸入指令:
```sh
curl http://localhost:5000/api
# -> should return "Bank API v1.0.0" as a result
```
---
## 思考狀態控管
在[前一堂課](../../3-data/translations/README.zh-tw.md)中,我們介紹了應用程式基本的狀態,全域變數 `account` 提供登入帳戶的相關銀行資料。然而,現在的專案存在著一些瑕疵。試著在儀表板介面中重新整理。發生了什麼事?
目前我們的程式碼有三個問題:
- 網頁狀態並沒有被儲存,當瀏覽器重新整理時,會被導回登入頁面。
- 有許多函式會修改網頁狀態。隨著應用程式變大,我們很難去追蹤之後的改變,時刻地去更新相關的網頁狀態。
- 網頁狀態並不完整,當你*登出*帳戶時,帳戶資訊仍然顯示在登入頁面上。
我們是可以逐一的解決這些問題,但這樣會創造出許多獨立的程式碼,讓應用程式更複雜而難以去管理。或者是我們停下來思考一下我們的策略。
> 我們究竟要解決什麼問題?
[狀態控管(State management)](https://en.wikipedia.org/wiki/State_management)可以為兩項問題提供良好的解決方案:
- 如何讓應用程式中的資料流容易理解?
- 如何讓網頁狀態一直與使用者介面,或是相關物件進行同步?
一旦你處理好這些問題,其他問題可以被簡化,甚至被一併解決。有許多可能的方法能解決這些問題,但我們使用一種常見的解法:**中心化資料與更新方式**。資料流會呈現下列模式:
![HTML、使用者行為與網頁狀態的架構圖](../images/data-flow.png)
> 我們不會處理如何讓資料同步觸發頁面的更新,這比較像是關於[回應式程式設計](https://zh.wikipedia.org/wiki/%E5%93%8D%E5%BA%94%E5%BC%8F%E7%BC%96%E7%A8%8B)的更進階知識。當你更深入網頁開發領域時,這是個很好的發展方向。
✅ 有許多函式庫提供狀態管理的方式,[Redux](https://redux.js.org) 就是常見的選擇。閱讀它的概念與運作模式,這是種有效的的學習方式,讓你在大型的網頁開發中預測潛在的風險,並預想解決方案。
### 課題
我們會先做一些程式重構。替換掉 `account` 的定義:
```js
let account = null;
```
變成:
```js
let state = {
account: null
};
```
這個構想是要*中心化*應用程式資料到一個狀態物件中。目前我們只有 `account` 在狀態中,但這能提供未來新增新功能的基礎。
我們還需要更新與它相關的函式。在函式 `register()``login()` ,將 `account = ...` 替換為 `state.account = ...`
在函式 `updateDashboard()` 的上方,加入此行:
```js
const account = state.account;
```
這個重構並不會帶來任何提升,但這是之後改變上的基礎。
This refactoring by itself did not bring much improvements, but the idea was to lay out the foundation for the next changes.
## 追蹤資料改變
現在我們有 `state` 物件儲存資料了,接下來要來中心化這些更新。目標是能輕易地追蹤任何被觸發的改變。
為了避免改動 `state` 物件,我們考慮使它[*不可變*](https://zh.wikipedia.org/wiki/%E4%B8%8D%E5%8F%AF%E8%AE%8A%E7%89%A9%E4%BB%B6),意味著它不能被做任何的修改。
這也代表你必須建立新的狀態物件來替換它。藉由這個方式,你就有一套保護措施阻絕潛在非預期[風險](https://zh.wikipedia.org/wiki/%E5%89%AF%E4%BD%9C%E7%94%A8_(%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6)),也開創出應用程式內還原與重做的功能,讓程式偵錯更加的容易。舉例來說,你可以紀錄狀態的改變,儲存狀態的歷史紀錄來了解錯誤的來源。
在 JavaScript 中,你可以使用 [`Object.freeze()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze) 來建立不可變物件。若你想在不可變物件上做更動,例外處理(exception)就會發生。
✅ 你知道*淺複製(shallow)*和*深複製(deep)*這兩種不可變物件的差別嗎?你可以從[這裡](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze#What_is_shallow_freeze)閱讀相關資訊。
### 課題
我們來建立新的函式 `updateState()`
```js
function updateState(property, newData) {
state = Object.freeze({
...state,
[property]: newData
});
}
```
在這個函式中,我們會建立新的狀態物件,並利用[*展開運算子(`...`)(Spread Operator)*](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax#Spread_in_object_literals)複製前一個資料狀態。接著,我們使用[括弧記法(Bracket Notation)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Working_with_Objects#Objects_and_properties) `[property]` 賦予並覆蓋特定的狀態物件。最後,我們為物件上鎖,`Object.freeze()` 避免任何的改動。目前我們只有 `account` 資料存在狀態中,利用此方法可以讓你新增任何你想要的資料。
我們會更新 `state` 初始化設定,確保初始狀態也被上鎖:
```js
let state = Object.freeze({
account: null
});
```
接著,更新函式 `register`,將 `state.account = result;` 替換為:
```js
updateState('account', result);
```
在函式 `login` 上做一樣的事,將 `state.account = data;` 替換為:
```js
updateState('account', data);
```
藉由這個機會,我們能解決帳戶資料在*登出*時,不會被清除的問題。
建立新的函式 `logout()`
```js
function logout() {
updateState('account', null);
navigate('/login');
}
```
`updateDashboard()` 中,替換重新導向 `return navigate('/login');``return logout()`
試著註冊新的帳戶,登入登出以確保功能都運作正常。
> 提示:你可以觀察所有的狀態改變,在 `updateState()` 裡的最下方加入 `console.log(state)`,開啟瀏覽器開發工具,命令欄就會顯示狀態的紀錄。
## 紀錄狀態
多數的網頁應用程式需要儲存資料以確保運作正常。所有重要的資料都會存在資料庫中,並藉由伺服器 API 來存取,就像我們專案中的帳戶資料。但有時候,瀏覽器用戶端的應用程式也需要儲存一些資料,提供更好的使用者體驗與增進負載效能。
當你想在瀏覽器內儲存資料,你必須思考幾項重要的問題:
- *這項資料很危險嗎?* 你應該要避免在用戶端儲存敏感的資料,例如帳戶密碼。
- *你需要儲存資料多久?* 你打算短時間內做存取,還是永久地保存?
網頁應用程式中有許多儲存資訊的方法,一切都取決於你想達成的目標。舉例來說,你可以利用網址來儲存搜尋資訊,讓使用者間能共享資訊。若資料需要與伺服器共享,好比說[認證](https://zh.wikipedia.org/wiki/%E8%BA%AB%E4%BB%BD%E9%AA%8C%E8%AF%81)資訊,你可以使用 [HTTP cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies)。
另一個選擇是使用其中一個廣大的瀏覽器 API 來儲存資料。下列這兩項就特別有趣:
- [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage)[Key/Value 儲存法](https://zh.wikipedia.org/wiki/%E9%94%AE-%E5%80%BC%E5%AD%98%E5%82%A8)可以保存不同時刻的網頁資料。這些資料不會有期限的限制。
- [`sessionStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage):它的運作模式與 `localStorage` 相同,只差在資料會在網頁段落結束時被清除,如瀏覽器關閉時。
紀錄一下這兩個 API 只能儲存[字串](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)格式。
如果你想儲存更複雜的物件,你需要利用 [`JSON.stringify()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify) 將資料整理成 [JSON](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON) 格式。
✅ 如果你想要建立不仰賴伺服器的網頁應用程式,你有辦法在用戶端建立資料庫。[`IndexedDB` API](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) 可以應用在更進階的案例上,儲存更大量的資料,當然使用上也相對複雜。
### 課題
我們想讓使用者在登出之前,保持登入狀態。所以我們使用 `localStorage` 來儲存帳戶資料。首先,定義一組 key 來紀錄我們的資料內容。
```js
const storageKey = 'savedAccount';
```
在函式 `updateState()` 末端加入此行:
```js
localStorage.setItem(storageKey, JSON.stringify(state.account));
```
藉由此方式,帳戶資料就能保存下來,並隨著之前中心化後的狀態而更新。我們開始從之前的重構獲取效益了 🙂。
當資料被儲存後,我們還需要在程式讀取時載入資料。在 `app.js` 下方編寫更多的初始化程式,建立新的函式 `init` 並收入之前的程式碼:
```js
function init() {
const savedAccount = localStorage.getItem(storageKey);
if (savedAccount) {
updateState('account', JSON.parse(savedAccount));
}
// 之前的初始化程式
window.onpopstate = () => updateRoute();
updateRoute();
}
init();
```
我們在此接收了儲存資料,並同步地更新狀態資訊。這必須在更新路由*之前*完成,否則有些程式碼會在頁面更新時,依據狀態來決定其行為。
當儲存完帳戶資料後,我們也定義了*儀表板*頁面為我們的預設首頁。若程式沒有找到資料,儀表板頁面也能重新導向回*登入*頁面。在 `updateRoute()` 中,替換回傳值 `return navigate('/login');``return navigate('/dashboard');`
登入應用程式並重新整理頁面。你應該能維持在儀表板那頁。這個改變也解決了我們最初面臨的問題......
## 重整資料
......但我們可能也產生了新問題。啊呀!
使用 `test` 帳戶進入儀表板頁面,在終端機內執行下列指令以建立新的交易項目:
```sh
curl --request POST \
--header "Content-Type: application/json" \
--data "{ \"date\": \"2020-07-24\", \"object\": \"Bought book\", \"amount\": -20 }" \
http://localhost:5000/api/accounts/test/transactions
```
試著重新整理瀏覽器內儀表板頁面。發生了什麼事?你有看到新的交易項目嗎?
感謝 `localStorage` 的幫助,狀態成功的儲存下來,但也代表我們在登出登入之前,不能再改變它的內容了!
一個可能的修復策略是在儀表板載入時,重新載入帳戶資訊以避免資料不同步。
### 課題
建立新的函式 `updateAccountData`
```js
async function updateAccountData() {
const account = state.account;
if (!account) {
return logout();
}
const data = await getAccount(account.user);
if (data.error) {
return logout();
}
updateState('account', data);
}
```
這個方法能檢查我們是否已經登入,重新從伺服器載入用戶資料。
建立另一個函式 `refresh`
```js
async function refresh() {
await updateAccountData();
updateDashboard();
}
```
這能更新帳戶資料,更新 HTML 中的儀表板頁面。這是在儀表板路由被載入時,我們所需要呼叫的函式。更新路由定義為:
```js
const routes = {
'/login': { templateId: 'login' },
'/dashboard': { templateId: 'dashboard', init: refresh }
};
```
試著重新載入儀表板,它現在應該能顯示更新後的帳戶資料。
---
## 🚀 挑戰
每一次儀表板載入時,我們都會重新載入帳戶資料,你認為我們還需要在用戶端儲存*所有的帳戶*資料嗎?
試著改變 `localStorage` 內的儲存內容,只包含我們能運行程式的必要資料。
## 課後測驗
[課後測驗](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/48?loc=zh_tw)
## 作業
[編寫"加入交易明細"視窗](assignment.zh-tw.md)
這邊有完成之後的結果:
!["加入交易明細"視窗的例子截圖](../images/dialog.png)

@ -0,0 +1,25 @@
# 編寫"加入交易明細"視窗
## 簡介
我們的銀行應用程式還缺乏一項重要的功能:輸入新的交易明細。
使用你在這四堂課中學到的知識,編寫"加入交易明細"視窗:
- 在儀表板頁面新增"加入交易明細"按鈕
- 加入新的 HTML 模板建立新頁面,或是在同一頁面中使用 JavaScript 顯示 HTML 窗格(可以使用 [`hidden`](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/hidden) 屬性,或是 CSS classes)
- 確保視窗能滿足[鍵盤與螢幕報讀器的相容性](https://developer.paciellogroup.com/blog/2018/06/the-current-state-of-modal-dialog-accessibility/)
- 編寫 HTML 表單來接收輸入資料
- 建立 JSON 表單資料並傳送到 API 上
- 使用新資料更新到儀表板頁面上
看看[伺服器 API 規格](../../api/README.zh-tw.md)來查詢你需要呼叫的 API 和所需的 JSON 格式。
這邊有完成作業後的成果:
!["加入交易明細"視窗的例子截圖](../images/dialog.png)
## 學習評量
| 作業內容 | 優良 | 普通 | 待改進 |
| -------- | ------------------------------------ | -------------------------------------------------- | ---------------------- |
| | 利用課程內容完美的製作出交易明細功能 | 有製作出交易明細功能,但有缺少部分要點且功能不完全 | 新的交易明細功能不正常 |

@ -0,0 +1,33 @@
# 銀行 API (Bank API)
> 由 [Node.js](https://nodejs.org) 與 [Express](https://expressjs.com/) 建立而成。
這套 API 已經建好而不在本次課程的範疇內。
然而,如果你想學習如何建立 API你可以追蹤這一系列的影片https://aka.ms/NodeBeginner (影片 17 到 21 為這套 API)。
你也可以看看這套互動式教學: https://aka.ms/learn/express-api
## 運行伺服器
確保你的 [Node.js](https://nodejs.org) 已經安裝完成。
1. Git clone 這個數據庫.
2. 在資料夾 `api` 中開啟終端機,執行 `npm install`
3. 執行 `npm start`
伺服器應該要在連接埠 `5000` 上監聽訊息。
> 筆記:所有儲存的資料不是永久保存的,伺服器終止時會遺失所有資料。
## API 項目
路由 | 描述
---------------------------------------------|------------------------------------
GET /api/ | 取得伺服器資訊
POST /api/accounts/ | 建立新的帳戶,範例: `{ user: 'Yohan', description: 'My budget', currency: 'EUR', balance: 100 }`
GET /api/accounts/:user | 取得特定帳戶的所有資料
DELETE /api/accounts/:user | 移除特定帳戶
POST /api/accounts/:user/transactions | 建立新的交易明細,範例: `{ date: '2020-07-23T18:25:43.511Z', object: 'Bought a book', amount: -20 }`
DELETE /api/accounts/:user/transactions/:id | 移除特定交易明細

@ -0,0 +1,21 @@
# :dollar: 建立銀行
在這個專案中,你會學習如何建立虛擬銀行。這些課程包含許多教程:設計網頁應用程式的格式、提供網頁路由、建立表單、管理狀態和利用 API 抓取銀行的資料。
| ![畫面1](../images/screen1.png) | ![畫面2](../images/screen2.png) |
|---------------------------------|-----------------------------------|
## 課程
1. [HTML 模板與網頁路由](../1-template-route/translations/README.md)
2. [建立登入與註冊表單](../2-forms/translations/README.md)
3. [取得並使用資料](../3-data/translations/README.md)
4. [狀態控管的概念](../4-state-management/translations/README.md)
### 參與人員
這些課程是由 [Yohan Lasorsa](https://twitter.com/sinedied) 用滿滿的 ♥️ 來編寫。
如果你有興趣建立本課程使用的[伺服器 API](../api/translations/README.zh-tw.md),你可以遵循[這一系列的影片](https://aka.ms/NodeBeginner),特別是影片 17 至 21。
你也可以造訪[這款互動式教學網站](https://aka.ms/learn/express-api)。

@ -12,6 +12,7 @@
<option>it</option>
<option>gr</option>
<option>ms</option>
<option>zh_tw</option>
</select>
</nav>
<div id="app">

@ -8,6 +8,7 @@ import ja from './ja.json';
import gr from './gr.json';
import ms from './ms.json';
import es from './es.json';
import zh_tw from './zh_tw.json';
//export const defaultLocale = 'en';
@ -21,6 +22,7 @@ const messages = {
gr: gr[0],
ms: ms[0],
es: es[0],
zh_tw: zh_tw[0],
};
export default messages;

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save