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/3-data/translations/README.zh-tw.md

337 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 3取得並使用資料
## 課前測驗
[課前測驗](https://ashy-river-0debb7803.1.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/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/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest)。但當代的瀏覽器已經建立出更方便且強大的 [`Fetch` API](https://developer.mozilla.org/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/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/docs/Web/HTTP/CORS),只要伺服器添加特殊的標頭檔到網頁回應中,我們就可以處理跨資源的 HTTP 請求,允許特殊的網域進行呼叫。
> 藉由前往[此課程](https://docs.microsoft.com/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/docs/Web/API/Node/textContent) 屬性,你可以改變元素的文字內容。注意改變此數值會刪除它的所有子元素(若存在的話),並以該文字內容來替換它。同時,這也是個有效的方法來刪去所有的子成員,只要賦予它空字串 `''`
- 使用 [`document.createElement()`](https://developer.mozilla.org/docs/Web/API/Document/createElement) 與 [`append()`](https://developer.mozilla.org/docs/Web/API/ParentNode/append) 這兩方法,你可以建立並接上一到多個子元素成員。
✅ 使用 [`innerHTML`](https://developer.mozilla.org/docs/Web/API/Element/innerHTML) 元素屬性也能改變 HTML 的內容,但這方法要避免使用。這可能會遭遇有關[跨網站指令碼 (XSS)](https://developer.mozilla.org/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/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/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/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/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/docs/Web/API/ParentNode/append),它能連接文字或者是 [DOM 節點](https://developer.mozilla.org/docs/Web/API/Node)到父元素中,正好滿足我們的需求。
試著以 `test` 帳戶來登入,你應該能看到儀表板顯示出交易明細了 🎉。
---
## 🚀 挑戰
花功夫讓儀表板頁面看起來像是正規的銀行界面。如果你已經為你的應用程式做好造型,你可以試試 [media queries](https://developer.mozilla.org/docs/Web/CSS/Media_Queries) 來建立出[回應式網頁設計](https://developer.mozilla.org/docs/Web/Progressive_web_apps/Responsive/responsive_design_building_blocks),它能完美地呈現在電腦或是行動裝置上。
這邊有造型過後的儀表板例子:
![造型化後的儀表板截圖](../../images/screen2.png)
## 課後測驗
[課後測驗](https://ashy-river-0debb7803.1.azurestaticapps.net/quiz/46?loc=zh_tw)
## 作業
[重構並註解你的程式碼](assignment.zh-tw.md)