You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Web-Dev-For-Beginners/7-bank-project/2-forms/translations/README.zh-tw.md

298 lines
15 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 建立銀行網頁應用程式 Part 2登入與註冊表單
## 課前測驗
[課前測驗](https://ashy-river-0debb7803.1.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/docs/Web/HTML/Element/input),舉例來說,若要建立使用者輸入使用者名稱的地方,你可以:
```html
<input id="username" name="username" type="text">
```
`name` 屬性同時亦是表單傳輸資料的名稱。`id` 屬性是用來與 `<label>` 做表單控制(form control)的連接。
> 花點時間看看 [`<input>` 種類](https://developer.mozilla.org/docs/Web/HTML/Element/input)的清單與[其他表單控制](https://developer.mozilla.org/docs/Learn/Forms/Other_form_controls),讓你在建立使用者介面時,有全部供你使用的原生 UI 元素可以參考。
✅ 紀錄一下 `<input>` 是種[空元素](https://developer.mozilla.org/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/docs/Learn/Accessibility/What_is_accessibility)是非常重要但常被忽視的主題。感謝[語義化 HTML 元素](https://developer.mozilla.org/docs/Learn/Accessibility/HTML)的幫助,建立無障礙的網頁內容變得更加容易。你可以[閱讀更多有關網頁親和力的文章](https://developer.mozilla.org/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/docs/Web/API/FormData) 協助從表單控制中取出 key/value 的數據對。
之後,利用 [`Object.fromEntries()`](https://developer.mozilla.org/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/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/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/docs/Web/HTTP/Status/400#:~:text=The%20HyperText%20Transfer%20Protocol%20(HTTP,%2C%20or%20deceptive%20request%20routing).)。
在傳輸資料給伺服器之前,最好先[驗證表單資料](https://developer.mozilla.org/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/docs/Web/HTML/Element/input)。這個屬性可能會改變表單控制的表現方法。
- `pattern` 允許定義[正規表示法](https://developer.mozilla.org/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://ashy-river-0debb7803.1.azurestaticapps.net/quiz/44?loc=zh_tw)
## 複習與自學
開發者在建立表單時需要發揮他們的創意,尤其是策畫資料驗證的規則。在 [CodePen](https://codepen.com) 上學習不同表單流程,你能發現什麼有趣且令人發想的表單嗎?
## 作業
[造型化你的銀行程式](assignment.zh-tw.md)