路由的演進:從後端路由到前端 Hash 與 History
在我們輸入 npm install vue-router 之前,你有想過「路由 (Routing)」原本是什麼嗎?
為什麼在 2010 年以前的網頁切換頁面會「白屏」一下?而現代的單頁應用 (SPA) 卻能像原生 App 一樣流暢切換?
這一切的秘密,都在於我們欺騙了瀏覽器。
一、 史前時代:後端路由 (Server-Side Routing)
在 Web 剛誕生的年代,每一個 URL 都對應到伺服器上的一個真實檔案(或動態生成的 HTML)。
當你點擊 <a href="/about">About</a> 時:
- 瀏覽器向伺服器發送 GET
/about請求。 - 伺服器回傳一整份 HTML。
- 瀏覽器卸載當前頁面,重新繪製新頁面。
這個「卸載重繪」的過程,就是造成畫面閃爍與白屏的元兇。每一次跳轉,所有的 CSS、JS 都要重新載入執行。
💡 延伸閱讀:想了解 HTTP 請求的完整流程?參考 網路請求的一生、HTTP Methods: GET 與 POST、以及後端的 路由設計與參數處理。
二、 關鍵技術:AJAX 革命
在理解前端路由之前,我們必須先認識一個改變 Web 歷史的技術:AJAX。
AJAX 是什麼?
AJAX 的全名是 Asynchronous JavaScript and XML(非同步 JavaScript 與 XML)。
它是一種網頁開發技術,允許網頁在不重新載入整個頁面的情況下,與伺服器進行非同步的資料交換和更新部分頁面內容。雖然名稱中包含 XML,但現代開發中更常使用 JSON 作為資料交換格式。
AJAX 如何做到「不刷新頁面」?
這個機制主要依賴幾個核心技術:
1. XMLHttpRequest / Fetch API
瀏覽器提供的 API,允許 JavaScript 在背景發送 HTTP 請求到伺服器,而不影響當前頁面的顯示。
💡 延伸閱讀:想深入了解這些 API?參考 XMLHttpRequest 完整解析 和 Fetch API 詳解。
2. 非同步執行
請求在背景執行,不會阻塞(block)頁面的其他操作,用戶可以繼續與頁面互動。
3. 回調函數 / Promise
當伺服器回應到達時,執行預先定義的函數來處理資料。
4. DOM 操作
使用 JavaScript 操作 DOM(Document Object Model),動態更新頁面的特定部分。
實際範例:
// 使用現代的 Fetch API
fetch("/api/user/123")
.then((response) => response.json()) // 發送請求到伺服器
.then((data) => {
// 接收回應資料
// 只更新頁面特定部分(不重新載入整頁)
document.getElementById("username").textContent = data.name;
document.getElementById("email").textContent = data.email;
})
.catch((error) => console.error("錯誤:", error));
// 頁面其他部分不受影響,用戶可以繼續操作傳統方式 vs AJAX
讓我們比較一下傳統方式和 AJAX 方式的差異:
傳統方式(會重新載入整頁):
- 用戶點擊連結
- 瀏覽器發送請求
- 伺服器回傳完整的新 HTML 頁面
- 整個頁面重新載入,畫面閃爍,狀態丟失
AJAX 方式(不重新載入):
- 用戶觸發操作(如點擊按鈕)
- JavaScript 在背景發送請求
- 伺服器只回傳需要的資料(JSON/XML)
- JavaScript 只更新頁面需要變動的部分,無需重新載入
這就是為什麼使用 AJAX 的網頁(如 Gmail、Google Maps)可以提供更流暢的使用體驗!
為什麼以前做不到?技術演進的秘密
你可能會疑惑:「這聽起來很簡單啊,為什麼以前做不到?」
答案是:早期的瀏覽器根本沒有提供這個能力。
網頁的初始設計理念
早期的網頁(1990 年代)被設計成靜態文檔瀏覽系統,就像閱讀一本書:
- 點擊連結 → 跳到新頁面
- 提交表單 → 伺服器回傳新頁面
- 沒有「在背景與伺服器溝通」的概念
缺少關鍵 API
早期瀏覽器根本沒有提供讓 JavaScript 發送 HTTP 請求的功能:
- 沒有
XMLHttpRequest物件 - 沒有
fetch()API - JavaScript 只能操作當前頁面的 DOM,無法主動與伺服器通訊
技術演進時間線
1995 年 - JavaScript 誕生
↓ 但只能做一些簡單的頁面互動(如驗證表單)
1999 年 - 微軟在 IE5 中引入 XMLHttpRequest(最初為了 Outlook Web Access)
↓ 但這是專屬功能,其他瀏覽器沒有
2005 年 - "AJAX" 這個術語被提出
↓ 各大瀏覽器開始標準化支援 XMLHttpRequest
2006年後 - AJAX 技術開始普及(Gmail、Google Maps 展示了它的威力)
2015 年 - Fetch API 成為新標準💡 延伸閱讀:想了解 HTTP 協定的演進歷史?參考 HTTP 版本演進史。
類比說明:
想像一下:
- 早期網頁 = 傳統郵寄信件
- 寄出信 → 等待 → 收到整封回信 → 閱讀
- AJAX 之後 = 有了電話
- 可以隨時撥打電話問問題
- 對方只回答你需要的資訊
- 你可以邊講電話邊做其他事
但問題是:早期瀏覽器就像「沒有電話這個設備」,所以即使你想打電話也做不到!必須等到瀏覽器廠商實作並提供 API 之後,開發者才有能力使用這些功能。
三、 前端路由的誕生 (Client-Side Routing)
隨著 AJAX 技術普及,開發者發現我們可以在不刷新頁面的情況下,跟伺服器要資料並更新局部畫面。
但這產生了一個嚴重問題:URL 沒有變。 使用者沒辦法透過「上一頁」回到剛才的狀態,也沒辦法把特定畫面分享給朋友。
於是,「前端路由」的概念誕生了: 「我們要改變 URL,但不能讓瀏覽器向伺服器發送請求。」
這聽起來很矛盾,但瀏覽器給了我們兩個後門:Hash 與 History API。
四、 第一代解法:Hash 模式 (#)
你有注意過有些舊網站的網址長這樣嗎? http://example.com/#/user/profile
URL 中 # 後面的部分稱為 Hash。它原本是用來做頁面內的錨點跳轉 (Anchor) 的。
Hash 的兩大特性:
- 改变 Hash 不會 觸發頁面刷新 (不會發送 HTTP 請求)。
- Hash 改變時,會觸發
hashchange事件。
利用這兩點,我們可以寫出第一代的前端 Router:
// 監聽 Hash 改變
window.addEventListener("hashchange", () => {
const path = window.location.hash.slice(1); // 去掉 #
if (path === "/home") {
renderHome();
} else if (path === "/about") {
renderAbout();
}
});這就是 vue-router 的 createWebHashHistory 背後的原理。雖然有效,但網址裡多個 # 總覺得不夠優雅。
五、 現代標準:History API
HTML5 推出後,瀏覽器給了我們一組更強大的 API:history.pushState 與 history.replaceState。
它們允許我們直接修改 URL 的路徑部分,而且完全不觸發頁面刷新。
// 把網址改成 /about,但不發送請求
history.pushState({ page: "about" }, "", "/about");配合 popstate 事件(當使用者按上一頁/下一頁時觸發),我們就擁有了一套完美的路由系統。這就是 createWebHistory 的原理。
六、 手寫一個 50 行的 Vanilla JS Router
要在 Vue 使用 Router 之前,最好的學習方式就是自己造輪子。讓我們用 History API 實作一個最簡功能的 Router。
請在你的電腦上建立一個 index.html 試試看:
<!DOCTYPE html>
<html>
<head>
<title>Vanilla Router</title>
</head>
<body>
<!-- 導航連結 -->
<nav>
<!-- 注意:這裡不能用 href="/home",否則會觸發頁面刷新 -->
<a href="#" onclick="navigate('/home'); return false;">Home</a>
<a href="#" onclick="navigate('/about'); return false;">About</a>
</nav>
<!-- 渲染區域 -->
<div id="app"></div>
<script>
// 1. 定義路由表
const routes = {
"/home": "<h1>Home Page</h1><p>Welcome!</p>",
"/about": "<h1>About Page</h1><p>We are cool.</p>",
};
// 2. 渲染函數:根據當前路徑顯示內容
function render() {
const path = window.location.pathname;
const content = routes[path] || "<h1>404 Not Found</h1>";
document.getElementById("app").innerHTML = content;
}
// 3. 導航函數:改變 URL 並手動渲染
function navigate(path) {
window.history.pushState({}, "", path);
render();
}
// 4. 監聽上一頁/下一頁 (PopState)
window.addEventListener("popstate", render);
// 初始化
render();
</script>
</body>
</html>為什麼這段程式碼很重要?
這個簡單的範例揭示了 Vue Router 的三個核心工作:
- RouterTable:
routes物件定義了路徑與元件的對應。 - RouterView:
render()函數負責把對應的內容塞進 DOM。 - RouterLink:
navigate()函數攔截了預設的點擊行為,改用pushState。
七、 致命陷阱:History 模式的 404 問題
使用 History 模式 (無 # 號) 雖然漂亮,但有一個致命缺點:如果在 /about 頁面按重整,你會得到 404 錯誤。
為什麼? 因為按重整時,瀏覽器會真的向伺服器請求 http://example.com/about 這個檔案。但伺服器上根本沒有 about.html,只有一個 index.html。
解決方案: 你必須配置你的 Web Server (Nginx, Apache, Vercel),讓不管請求什麼路徑,全部都回傳 index.html。
這就是為什麼在部署 Vue 專案時,需要設定 try_files $uri $uri/ /index.html; 的原因。
總結
| 模式 | URL 範例 | 原理 | 優點 | 缺點 |
|---|---|---|---|---|
| Hash | /#/about | window.location.hash | 簡單,無需伺服器配置 | 網址醜,對 SEO 較差 (部分爬蟲不讀 Hash) |
| History | /about | history.pushState | 網址漂亮,符合標準 | 刷新會 404,需伺服器設定支援 |
這篇文章解釋了前端路由的「物理定律」。下一篇,我們將正式引入 Vue Router 4,看它如何幫我們優雅地封裝這一切。