13 KiB
建立銀行網頁應用程式 Part 1:HTML 模板與網頁路由
課前測驗
大綱
自從 JavaScript 出現在瀏覽器後,網頁開始變得更複雜、更多互動。網頁技術已經普遍地用於建立功能齊全的應用程式,執行在瀏覽器上,我們稱之為網頁應用程式。基於網頁應用程式的高互動性,使用者不會想在互動後做所有頁面載入所需的等待。這也是為什麼 JavaScript 使用 DOM 來更新 HTML,提供使用者更流暢的網頁體驗。
在這堂課程中,我們會譜出銀行網頁應用程式的基礎,使用 HTML 模板建立不同的畫面,各自顯示並更新內容,而不必每次都需要載入整個 HTML 頁面。
開始之前
你需要一個網頁伺服器來測試我們要建的專案。如果你還沒有,你可以安裝 Node.js 並在你的專案資料夾中使用指令 npx lite-server
。這會建立一個本地端的網頁伺服器,在瀏覽器上開啟你的網頁程式。
準備
在你的電腦上,建立資料夾 bank
,並在裡面建立檔案 index.html
。我們以這個 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 模板。
一個模板提供可重複利用的 HTML 區塊,它不會顯示在瀏覽器上,而在需要之時由 JavaScript 以呈現出來。
課題
我們會建立一個銀行網頁應用程式,其中包含兩個子畫面:登入頁面與儀表板頁面。首先,我們在網頁應用程式的 HTML body 上,建立放置區來放置模板的子頁面。
<div id="app">Loading...</div>
我們給它 id
,以利後續 JavaScript 對它的追蹤。
提示:因為它裡面元素的內容會被置換,我們可以建立載入中訊息或提示,在應用程式載入時顯示出來。
接下來,我們加入下列的 HTML 模板,給登入畫面使用。現在我們只加入一行標題與一個有連結的區塊,進行簡單的功能。
<template id="login">
<h1>Bank App</h1>
<section>
<a href="/dashboard">Login</a>
</section>
</template>
接著,加入另一個 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 模板。
展現模板通常需要三個步驟:
- 在 DOM 內接收模板元素,舉例來說,使用
document.getElementById
。 - 複製模板元素,使用
cloneNode
。 - 將複製元素接到 DOM 的顯示元素上,例如使用
appendChild
。
✅ 我們為什麼需要在接到 DOM 前,複製一份模板?你能想像如果我們省略了此步驟後,會發生什麼事嗎?
課題
在資料夾中建立新檔案 app.js
,並在你的 HTML 檔案的 <head>
區塊中中匯入這個新檔案:
<script src="app.js" defer></script>
在 app.js
中,我們建立新函式 updateRoute
:
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)
來複製整個模板的子樹。
現在我們呼叫這個函式,指定特定的模板並觀察結果。
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 網址與模板的關聯實體關係。加入這個物件到 app.js
檔案的最上方。
const routes = {
'/login': { templateId: 'login' },
'/dashboard': { templateId: 'dashboard' },
};
現在,我們對函式 updateRoute
做一些更動。我們不直接將 templateId
作為參數傳遞,而是接收現在的 URL 網址,在使用關聯表來取得相對應的模板 ID 數值。我們可以使用 window.location.pathname
來取得網址的部分路徑。
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);
}
這邊我們建立了模板的路由關係。你可以藉由修改網址,來測試你的網頁是否正確的轉移。
✅ 如果你輸入了不存在的網址,它會發生什麼事?我們該如何解決呢?
加入網頁訪問
下一個步驟為在不更改網址的情況下,新增網頁訪問的途徑。這會做出兩件事情:
- 更新現在的網址
- 更新要被顯示的模板到新的網址中
我們已經完成了第二點,藉由使用函式 updateRoute
來完成,所以我們需要釐清該如何更新現在的網址。
我們需要使用 JavaScript,詳細來看為 history.pushState
,更新網址位置並建立瀏覽紀錄,同時不更新整個 HTML 頁面。
筆記:網頁超連結元素
<a href>
可以建立不同網址的連接,但它預設上會讓瀏覽器重新載入 HTML 檔。我們需要手動新增 JavaScript 處理路由以避免此行為發生,在點擊事件中使用函式 preventDefault() 。
課題
我們來建立新的函式,讓應用程式得以做網頁的訪問:
function navigate(path) {
window.history.pushState({}, path, window.location.origin + path);
updateRoute();
}
這個方法根據導入的路徑位置,更新了現在的網址位置,再更新模板上去。window.location.origin
回傳了網址的根路徑,允許我們重新構築完整的網址。
現在,藉由上述的函式,我們可以解決找不到網頁路徑的問題。我們修改函式 updateRoute
,在找不到該網頁時強制轉移到一個存在的網頁。
function updateRoute() {
const path = window.location.pathname;
const route = routes[path];
if (!route) {
return navigate('/login');
}
...
如果找不到網頁路由時,我們會導往 login
的頁面。
現在,我們建立新的函式,在連結被點擊時取得網址位置,並避免瀏覽器進行預設上的重新載入:
function onLinkClick(event) {
event.preventDefault();
navigate(event.target.href);
}
現在我們完成應用程式的網頁訪問系統,在 HTML 檔的 Login 與 Logout 連結加入此函式。
<a href="/dashboard" onclick="onLinkClick()">Login</a>
...
<a href="/login" onclick="onLinkClick()">Logout</a>
使用 onclick
屬性會將 click
事件連接到 JavaScript 程式碼中,這邊會再呼叫函式 navigate()
。
試著點擊這些連結,你應該能造訪網頁中不同的的畫面了。
✅ history.pushState
這個方法是 HTML5 標準的一部份,支援在所有當代的瀏覽器上。如果你要為舊款的瀏覽器設計網頁應用程式的話,這邊有一個技巧來加在這個 API 上:在路徑前面加上 hash (#
),你可以完成網頁路由與不須重載網頁的功能,它的目的就是在同一個網頁中做內部連結的切換。
處理瀏覽器的「上一頁」與「下一頁」
使用 history.pushState
會建立瀏覽器的瀏覽紀錄。你可以使用瀏覽器的上一頁來確認,它應該要能呈現像這樣的畫面:
點擊上一頁數次,你會看到網址會改變且歷史紀錄也更新上去了,但同一個模板還是被顯示出來。
這是因為網頁不知道該如何依據歷史紀錄來呼叫 updateRoute()
。如果你閱讀了 history.pushState
技術文件,你會發現如果狀態改變 ── 同時代表著網址改變 ── popstate
事件就會被觸發。我們會利用此特徵來修復這個問題。
課題
為了在瀏覽器歷史改變時更新該被顯示的模板,我們會以新函式來呼叫 updateRoute()
。我們在 app.js
檔最下方加入:
window.onpopstate = () => updateRoute();
updateRoute();
筆記:我們在這裡使用箭頭函式,簡短地宣告
popstate
事件處理器。它與正規的函式的功能是一樣的。
這是關於箭頭函式的回想影片:
點擊上方圖片以觀看關於箭頭函式的影片。
現在,試著點擊瀏覽器上的上一頁與下一頁,檢查這次模板是否正確地更新出來。
🚀 挑戰
加入新的模板與對應的關聯表,顯示出本應用程式第三頁的功能 ── 帳戶餘額。
課後測驗
複習與自學
網頁路由是網頁開發中很棘手的部分,特別是將網頁切換轉變為單一頁面應用程式(Single Page Application)。閱讀關於Azure Static Web App 提供服務的方式以處理網頁路由。你能解釋為什麼文件上的某些決定會如此重要呢?