Vue 路由實現:Hash 路由與 History 路由原理
Vue Router 的面試題,通常不是問你會不會寫 createRouter,而是問你能不能說清楚:為什麼 SPA 可以在不刷新頁面的情況下切換 URL 和畫面?Hash 模式和 History 模式到底差在哪?
一句話回答:前端路由的核心是「攔截導航行為,只改變瀏覽器 URL,不讓瀏覽器重新向伺服器請求整頁 HTML,然後根據目前 URL 匹配對應組件並渲染到 <router-view>」。
一、 必考觀念
1. 什麼是前端路由?
傳統後端路由是:
GET /about
-> server 回傳 about.html
-> 瀏覽器重新載入整頁前端路由是:
使用者點擊 /about
-> JavaScript 攔截點擊
-> 修改 URL
-> 根據 URL 找到 About 組件
-> 局部更新畫面,不重新載入整頁前端路由要完成三件事:
- 監聽 URL 變化。
- 根據 URL 匹配路由表。
- 渲染對應組件。
2. Vue Router 的核心角色
一個最簡化的 Vue Router 配置:
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:
https://example.com/#/user/1其中:
pathname: /
hash: #/user/1Hash 原本是用來做頁面內錨點定位,例如:
https://example.com/docs#install瀏覽器會跳到 id="install" 的位置。
2. Hash 路由為什麼不會刷新頁面?
Hash 有一個重要特性:改變 hash 不會讓瀏覽器重新向伺服器請求頁面。
例如:
location.hash = '#/about'URL 會變,但瀏覽器不會發送 GET /about。這讓前端可以把 #/about 當成一個「假路徑」,再由 JavaScript 解析並渲染對應畫面。
3. 如何監聽 hash 變化?
瀏覽器提供 hashchange 事件:
window.addEventListener('hashchange', () => {
console.log(location.hash)
})一個極簡 Hash Router 可以這樣寫:
<nav>
<a href="#/">Home</a>
<a href="#/about">About</a>
</nav>
<main id="app"></main>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 有 #,不夠乾淨 |
| 重新整理不會 404 | SEO 和語義較差 |
| 相容性好 | # 後內容不會送到伺服器 |
Hash 模式很適合靜態部署、後端無法配置 fallback、內部工具或管理後台。
三、 History 路由原理
1. History API 是什麼?
History 模式的 URL 更乾淨:
https://example.com/user/1它依賴瀏覽器的 History API:
history.pushState(state, '', '/about')
history.replaceState(state, '', '/profile')pushState 可以改變網址,並新增一筆瀏覽歷史,但不會重新載入頁面。
2. 如何監聽瀏覽器前進後退?
使用者點擊上一頁或下一頁時,瀏覽器會觸發 popstate:
window.addEventListener('popstate', () => {
console.log(location.pathname)
})但要注意:pushState 本身不會觸發 popstate。所以前端路由在主動導航時,通常會自己更新內部狀態並觸發渲染。
3. 極簡 History Router
<nav>
<a href="/" data-link>Home</a>
<a href="/about" data-link>About</a>
</nav>
<main id="app"></main>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)這段做了幾件事:
- 攔截
<a>點擊,避免瀏覽器整頁跳轉。 - 用
history.pushState改變 URL。 - 根據
location.pathname渲染內容。 - 用
popstate支援上一頁 / 下一頁。
4. History 模式為什麼重新整理可能 404?
這是 History 模式最常見面試題。
假設你的 SPA 部署在:
https://example.com前端路由有:
/user/1當你在頁面內點擊連到 /user/1,前端用 pushState 改 URL,沒有問題。
但如果你直接在瀏覽器輸入:
https://example.com/user/1或在這個頁面按重新整理,瀏覽器會真的向伺服器請求:
GET /user/1如果伺服器沒有這個路徑,就會回 404。
所以 History 模式需要伺服器 fallback:
所有非靜態資源請求
-> 回傳 index.html
-> 前端 Router 接手解析 /user/1Nginx 常見配置:
location / {
try_files $uri $uri/ /index.html;
}5. History 模式的優缺點
| 優點 | 缺點 |
|---|---|
| URL 乾淨,接近正常網站路徑 | 需要伺服器 fallback |
| 語義好,分享連結自然 | 配置錯誤會重新整理 404 |
| 更適合正式產品 | 部署在子目錄時要處理 base path |
四、 Vue Router 如何接起來?
1. createWebHashHistory
Hash 模式:
import { createRouter, createWebHashHistory } from 'vue-router'
const router = createRouter({
history: createWebHashHistory(),
routes,
})URL 會像:
https://example.com/#/about內部主要依賴 hash 變化。
2. createWebHistory
History 模式:
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(),
routes,
})URL 會像:
https://example.com/about內部依賴 History API,部署時需要伺服器 fallback。
3. <router-link> 的原理
<router-link> 不是單純輸出 <a> 而已。它會:
- 產生正確的
href。 - 攔截可由前端處理的點擊。
- 呼叫
router.push()。 - 避免瀏覽器重新載入整頁。
- 處理 active class。
如果使用者按下 Cmd/Ctrl + Click、右鍵開新分頁、外部連結等情況,通常仍應保留瀏覽器原生行為。
4. <router-view> 的原理
<router-view> 是路由出口。它會根據目前的 currentRoute,找到匹配到的 route record,然後渲染對應組件。
簡化理解:
<template>
<component :is="matchedComponent" />
</template>當 currentRoute 改變時,matchedComponent 改變,Vue 會重新 patch 對應的組件樹。
五、 Hash vs History 對比
| 面向 | Hash 模式 | History 模式 |
|---|---|---|
| URL | /#/about | /about |
| 核心 API | location.hash、hashchange | history.pushState、popstate |
| 是否刷新 | 改 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。
<nav>
<a href="#/">Home</a>
<a href="#/about">About</a>
</nav>
<main id="app"></main>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.hash 與 hashchange,不需伺服器配置 |
| History 模式 | 使用 pushState、replaceState、popstate,URL 乾淨但要 fallback |
router-link | 產生連結並攔截點擊,呼叫前端導航 |
router-view | 根據目前 route 渲染匹配組件 |
| 部署陷阱 | History 模式刷新 404,需要回退到 index.html |
一句完整的面試回答可以是:
Vue 前端路由的核心是攔截瀏覽器導航,在不重新載入整頁的情況下更新 URL,然後根據目前 URL 匹配路由表並渲染到
<router-view>。Hash 模式利用location.hash和hashchange,因為 hash 不會送到伺服器,所以部署簡單;History 模式利用history.pushState和popstate,URL 比較乾淨,但直接刷新時伺服器會收到真實路徑請求,因此必須配置 fallback 到index.html。
延伸閱讀
- 前置補充:Vue 元件通訊方式有哪些及原理
- 前置補充:Vue 狀態管理:Pinia、Vuex 與狀態歸屬
- 前置補充:Vue 插槽設計:Slot、Scoped Slot 與 Headless Component
- 前置補充:Vue 表單、非同步狀態與資料流設計
- 前置補充:Vue 與 TypeScript 實戰
- 前置補充:Vue 復用手段選型:Composable、Directive、Plugin、Mixin
- 前置補充:watch 與 computed 的區別
- 前置補充:接口請求一般放在哪個生命週期?為什麼?
- 進階補充:Vue SPA 如何優化首屏載入速度
- 進階補充:Vue 權限與路由守衛設計
- 進階補充:深入 Vue KeepAlive:組件緩存與狀態保留
- 進階補充:Vue SSR 的實現原理
- 專題延伸:路由的演進:從後端路由到前端 Hash 與 History
- 專題延伸:Vue Router 4 快速上手