跳至主要內容
Skip to content

Vue 路由實現:Hash 路由與 History 路由原理

Vue Router 的面試題,通常不是問你會不會寫 createRouter,而是問你能不能說清楚:為什麼 SPA 可以在不刷新頁面的情況下切換 URL 和畫面?Hash 模式和 History 模式到底差在哪?

一句話回答:前端路由的核心是「攔截導航行為,只改變瀏覽器 URL,不讓瀏覽器重新向伺服器請求整頁 HTML,然後根據目前 URL 匹配對應組件並渲染到 <router-view>」。


一、 必考觀念

1. 什麼是前端路由?

傳統後端路由是:

text
GET /about
-> server 回傳 about.html
-> 瀏覽器重新載入整頁

前端路由是:

text
使用者點擊 /about
-> JavaScript 攔截點擊
-> 修改 URL
-> 根據 URL 找到 About 組件
-> 局部更新畫面,不重新載入整頁

前端路由要完成三件事:

  1. 監聽 URL 變化。
  2. 根據 URL 匹配路由表。
  3. 渲染對應組件。

2. Vue Router 的核心角色

一個最簡化的 Vue Router 配置:

typescript
import { createRouter, createWebHistory } from 'vue-router'

const routes = [
  { path: '/', component: () => import('../views/Home.vue') },
  { path: '/about', component: () => import('../views/About.vue') },
]

export const router = createRouter({
  history: createWebHistory(),
  routes,
})

核心概念可以拆成:

角色職責
routes路徑和組件的對照表
history決定 URL 如何變化與如何監聽變化
router.push()主動導航
<router-link>產生連結並攔截點擊
<router-view>根據目前 route 渲染匹配到的組件

二、 Hash 路由原理

1. Hash 是什麼?

URL 中 # 後面的部分叫 hash:

text
https://example.com/#/user/1

其中:

text
pathname: /
hash: #/user/1

Hash 原本是用來做頁面內錨點定位,例如:

text
https://example.com/docs#install

瀏覽器會跳到 id="install" 的位置。

2. Hash 路由為什麼不會刷新頁面?

Hash 有一個重要特性:改變 hash 不會讓瀏覽器重新向伺服器請求頁面。

例如:

javascript
location.hash = '#/about'

URL 會變,但瀏覽器不會發送 GET /about。這讓前端可以把 #/about 當成一個「假路徑」,再由 JavaScript 解析並渲染對應畫面。

3. 如何監聽 hash 變化?

瀏覽器提供 hashchange 事件:

javascript
window.addEventListener('hashchange', () => {
  console.log(location.hash)
})

一個極簡 Hash Router 可以這樣寫:

html
<nav>
  <a href="#/">Home</a>
  <a href="#/about">About</a>
</nav>

<main id="app"></main>
javascript
const routes = {
  '/': '<h1>Home</h1>',
  '/about': '<h1>About</h1>',
}

function render() {
  const path = location.hash.slice(1) || '/'
  document.querySelector('#app').innerHTML = routes[path] || '<h1>404</h1>'
}

window.addEventListener('hashchange', render)
window.addEventListener('DOMContentLoaded', render)

這就是 Hash 路由的最小原理:

  • location.hash 保存目前路由。
  • hashchange 監聽路由變化。
  • 根據 hash 匹配組件或畫面。

4. Hash 模式的優缺點

優點缺點
不需要伺服器特殊配置URL 有 #,不夠乾淨
重新整理不會 404SEO 和語義較差
相容性好# 後內容不會送到伺服器

Hash 模式很適合靜態部署、後端無法配置 fallback、內部工具或管理後台。


三、 History 路由原理

1. History API 是什麼?

History 模式的 URL 更乾淨:

text
https://example.com/user/1

它依賴瀏覽器的 History API:

javascript
history.pushState(state, '', '/about')
history.replaceState(state, '', '/profile')

pushState 可以改變網址,並新增一筆瀏覽歷史,但不會重新載入頁面。

2. 如何監聽瀏覽器前進後退?

使用者點擊上一頁或下一頁時,瀏覽器會觸發 popstate

javascript
window.addEventListener('popstate', () => {
  console.log(location.pathname)
})

但要注意:pushState 本身不會觸發 popstate。所以前端路由在主動導航時,通常會自己更新內部狀態並觸發渲染。

3. 極簡 History Router

html
<nav>
  <a href="/" data-link>Home</a>
  <a href="/about" data-link>About</a>
</nav>

<main id="app"></main>
javascript
const routes = {
  '/': '<h1>Home</h1>',
  '/about': '<h1>About</h1>',
}

function render() {
  const path = location.pathname
  document.querySelector('#app').innerHTML = routes[path] || '<h1>404</h1>'
}

function navigate(path) {
  history.pushState(null, '', path)
  render()
}

document.addEventListener('click', (event) => {
  const link = event.target.closest('[data-link]')

  if (!link) {
    return
  }

  event.preventDefault()
  navigate(link.getAttribute('href'))
})

window.addEventListener('popstate', render)
window.addEventListener('DOMContentLoaded', render)

這段做了幾件事:

  1. 攔截 <a> 點擊,避免瀏覽器整頁跳轉。
  2. history.pushState 改變 URL。
  3. 根據 location.pathname 渲染內容。
  4. popstate 支援上一頁 / 下一頁。

4. History 模式為什麼重新整理可能 404?

這是 History 模式最常見面試題。

假設你的 SPA 部署在:

text
https://example.com

前端路由有:

text
/user/1

當你在頁面內點擊連到 /user/1,前端用 pushState 改 URL,沒有問題。

但如果你直接在瀏覽器輸入:

text
https://example.com/user/1

或在這個頁面按重新整理,瀏覽器會真的向伺服器請求:

text
GET /user/1

如果伺服器沒有這個路徑,就會回 404。

所以 History 模式需要伺服器 fallback:

text
所有非靜態資源請求
-> 回傳 index.html
-> 前端 Router 接手解析 /user/1

Nginx 常見配置:

nginx
location / {
  try_files $uri $uri/ /index.html;
}

5. History 模式的優缺點

優點缺點
URL 乾淨,接近正常網站路徑需要伺服器 fallback
語義好,分享連結自然配置錯誤會重新整理 404
更適合正式產品部署在子目錄時要處理 base path

四、 Vue Router 如何接起來?

1. createWebHashHistory

Hash 模式:

typescript
import { createRouter, createWebHashHistory } from 'vue-router'

const router = createRouter({
  history: createWebHashHistory(),
  routes,
})

URL 會像:

text
https://example.com/#/about

內部主要依賴 hash 變化。

2. createWebHistory

History 模式:

typescript
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(),
  routes,
})

URL 會像:

text
https://example.com/about

內部依賴 History API,部署時需要伺服器 fallback。

<router-link> 不是單純輸出 <a> 而已。它會:

  • 產生正確的 href
  • 攔截可由前端處理的點擊。
  • 呼叫 router.push()
  • 避免瀏覽器重新載入整頁。
  • 處理 active class。

如果使用者按下 Cmd/Ctrl + Click、右鍵開新分頁、外部連結等情況,通常仍應保留瀏覽器原生行為。

4. <router-view> 的原理

<router-view> 是路由出口。它會根據目前的 currentRoute,找到匹配到的 route record,然後渲染對應組件。

簡化理解:

vue
<template>
  <component :is="matchedComponent" />
</template>

currentRoute 改變時,matchedComponent 改變,Vue 會重新 patch 對應的組件樹。


五、 Hash vs History 對比

面向Hash 模式History 模式
URL/#/about/about
核心 APIlocation.hashhashchangehistory.pushStatepopstate
是否刷新改 hash 不刷新pushState 不刷新
重新整理通常不會 404需要伺服器 fallback
伺服器配置幾乎不需要必須配置回退到 index.html
URL 美觀較差較好
適合場景靜態站、後台、無法控伺服器正式產品、SEO/分享需求較高

六、 追問題庫

Q1:前端路由和後端路由差在哪?

後端路由是伺服器根據 URL 回傳不同 HTML。前端路由是瀏覽器先載入同一份 index.html,之後由 JavaScript 根據 URL 決定要顯示哪個組件。

Q2:Hash 模式為什麼不用伺服器配置?

因為 # 後面的 hash 不會被送到伺服器。伺服器通常只收到 GET /,回傳 index.html 後,前端再讀取 location.hash 來渲染畫面。

Q3:History 模式為什麼需要 fallback?

因為 /about/user/1 看起來像真實伺服器路徑。直接刷新時,瀏覽器會向伺服器請求該路徑。如果伺服器沒有配置 fallback 到 index.html,就會 404。

Q4:pushState 會觸發 popstate 嗎?

不會。

popstate 主要在使用者按上一頁、下一頁,或程式呼叫 history.back()history.forward() 時觸發。主動 pushState 後,路由器通常會自己更新內部狀態。

Q5:為什麼不能直接用 <a href="/about">

可以,但如果不攔截點擊,瀏覽器會走傳統跳轉,向伺服器請求 /about 並重新載入整頁。

Vue Router 的 <router-link> 會保留 <a> 的語義,但攔截普通左鍵點擊,改用前端路由導航。

Q6:前端路由切換時,組件會重新建立嗎?

看 route 對應的組件是否改變、key 如何設定,以及是否搭配 KeepAlive

例如同一個路由組件從 /user/1 切到 /user/2,可能是同一個組件實例更新 route params,而不是整個重新建立。這時要 watch route params 或使用路由守衛處理資料更新。

如果希望保留切換狀態,可以延伸讀:深入 Vue KeepAlive:組件緩存與狀態保留


七、 實作題

題目:手寫一個極簡 Hash Router

需求:

  • 支援 #/#/about
  • URL 改變時更新畫面。
  • 未匹配路由顯示 404。
html
<nav>
  <a href="#/">Home</a>
  <a href="#/about">About</a>
</nav>

<main id="app"></main>
javascript
const routes = {
  '/': () => '<h1>Home</h1>',
  '/about': () => '<h1>About</h1>',
}

function getCurrentPath() {
  return location.hash.slice(1) || '/'
}

function render() {
  const path = getCurrentPath()
  const view = routes[path]

  document.querySelector('#app').innerHTML = view
    ? view()
    : '<h1>404</h1>'
}

window.addEventListener('hashchange', render)
window.addEventListener('DOMContentLoaded', render)

面試時要能補上:真實 Vue Router 還會處理動態路由、巢狀路由、導航守衛、非同步組件、滾動行為、路由參數解析等問題。


八、 資深視角

1. 路由不只是 URL 到組件

大型應用裡,路由還會承載:

  • 權限判斷。
  • Layout 切換。
  • 頁面資料載入。
  • 搜尋條件同步到 query。
  • 返回時的捲軸恢復。
  • 分包與懶載入策略。

所以 Router 設計不是只看「能不能跳頁」,還要看 URL 是否能表達頁面狀態。

2. URL 狀態和全域狀態要分清楚

適合放 URL 的狀態:

  • 搜尋關鍵字。
  • 分頁頁碼。
  • 篩選條件。
  • 排序方式。
  • 可以分享或重新整理後恢復的頁面狀態。

不適合放 URL 的狀態:

  • access token。
  • 大型表單草稿。
  • 只屬於當前元件的 UI 展開狀態。
  • 敏感資訊。

如果一個狀態需要分享、收藏、返回復原,通常值得考慮放進 path 或 query。

3. History 模式的問題常常出在部署

前端工程師不能只會寫 createWebHistory()。如果部署環境沒有配置 fallback,正式環境刷新 404 是非常常見的事故。

資深回答應該補一句:History 模式需要 Nginx、CDN、靜態託管平台或後端服務把未知路徑回退到 index.html,再交給前端 Router 接手。


總結

問題面試回答重點
前端路由是什麼URL 改變但不刷新整頁,由 JS 匹配組件並更新畫面
Hash 模式使用 location.hashhashchange,不需伺服器配置
History 模式使用 pushStatereplaceStatepopstate,URL 乾淨但要 fallback
router-link產生連結並攔截點擊,呼叫前端導航
router-view根據目前 route 渲染匹配組件
部署陷阱History 模式刷新 404,需要回退到 index.html

一句完整的面試回答可以是:

Vue 前端路由的核心是攔截瀏覽器導航,在不重新載入整頁的情況下更新 URL,然後根據目前 URL 匹配路由表並渲染到 <router-view>。Hash 模式利用 location.hashhashchange,因為 hash 不會送到伺服器,所以部署簡單;History 模式利用 history.pushStatepopstate,URL 比較乾淨,但直接刷新時伺服器會收到真實路徑請求,因此必須配置 fallback 到 index.html

延伸閱讀