跳至主要內容
Skip to content

路由的演進:從後端路由到前端 Hash 與 History

在我們輸入 npm install vue-router 之前,你有想過「路由 (Routing)」原本是什麼嗎?

為什麼在 2010 年以前的網頁切換頁面會「白屏」一下?而現代的單頁應用 (SPA) 卻能像原生 App 一樣流暢切換?

這一切的秘密,都在於我們欺騙了瀏覽器。


一、 史前時代:後端路由 (Server-Side Routing)

在 Web 剛誕生的年代,每一個 URL 都對應到伺服器上的一個真實檔案(或動態生成的 HTML)。

當你點擊 <a href="/about">About</a> 時:

  1. 瀏覽器向伺服器發送 GET /about 請求。
  2. 伺服器回傳一整份 HTML。
  3. 瀏覽器卸載當前頁面,重新繪製新頁面

這個「卸載重繪」的過程,就是造成畫面閃爍與白屏的元兇。每一次跳轉,所有的 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),動態更新頁面的特定部分。

實際範例:

javascript
// 使用現代的 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 方式的差異:

傳統方式(會重新載入整頁):

  1. 用戶點擊連結
  2. 瀏覽器發送請求
  3. 伺服器回傳完整的新 HTML 頁面
  4. 整個頁面重新載入,畫面閃爍,狀態丟失

AJAX 方式(不重新載入):

  1. 用戶觸發操作(如點擊按鈕)
  2. JavaScript 在背景發送請求
  3. 伺服器只回傳需要的資料(JSON/XML)
  4. 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,但不能讓瀏覽器向伺服器發送請求。」

這聽起來很矛盾,但瀏覽器給了我們兩個後門:HashHistory API


四、 第一代解法:Hash 模式 (#)

你有注意過有些舊網站的網址長這樣嗎? http://example.com/#/user/profile

URL 中 # 後面的部分稱為 Hash。它原本是用來做頁面內的錨點跳轉 (Anchor) 的。

Hash 的兩大特性:

  1. 改变 Hash 不會 觸發頁面刷新 (不會發送 HTTP 請求)。
  2. Hash 改變時,會觸發 hashchange 事件。

利用這兩點,我們可以寫出第一代的前端 Router:

javascript
// 監聽 Hash 改變
window.addEventListener("hashchange", () => {
  const path = window.location.hash.slice(1); // 去掉 #

  if (path === "/home") {
    renderHome();
  } else if (path === "/about") {
    renderAbout();
  }
});

這就是 vue-routercreateWebHashHistory 背後的原理。雖然有效,但網址裡多個 # 總覺得不夠優雅。


五、 現代標準:History API

HTML5 推出後,瀏覽器給了我們一組更強大的 API:history.pushStatehistory.replaceState

它們允許我們直接修改 URL 的路徑部分,而且完全不觸發頁面刷新

javascript
// 把網址改成 /about,但不發送請求
history.pushState({ page: "about" }, "", "/about");

配合 popstate 事件(當使用者按上一頁/下一頁時觸發),我們就擁有了一套完美的路由系統。這就是 createWebHistory 的原理。


六、 手寫一個 50 行的 Vanilla JS Router

要在 Vue 使用 Router 之前,最好的學習方式就是自己造輪子。讓我們用 History API 實作一個最簡功能的 Router。

請在你的電腦上建立一個 index.html 試試看:

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 的三個核心工作:

  1. RouterTable: routes 物件定義了路徑與元件的對應。
  2. RouterView: render() 函數負責把對應的內容塞進 DOM。
  3. 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/#/aboutwindow.location.hash簡單,無需伺服器配置網址醜,對 SEO 較差 (部分爬蟲不讀 Hash)
History/abouthistory.pushState網址漂亮,符合標準刷新會 404,需伺服器設定支援

這篇文章解釋了前端路由的「物理定律」。下一篇,我們將正式引入 Vue Router 4,看它如何幫我們優雅地封裝這一切。

下一章:Vue Router 4 快速上手