浅谈前端路由原理:Hash 和 History
原文从 SPA、hash、History API 和 Vue Router 两种模式展开。本文保留原有讲解顺序,修正
location属性拼写、popstate触发条件、History 模式与 SEO 的关系,并补充 Vue Router 3/4/5 和 Nuxt 4 的当前用法。
一、什么是前端路由
传统多页面应用中,浏览器访问一个 URL,服务器根据路径返回一份新的 HTML 文档。SPA(Single Page Application)通常先加载一个 HTML 和 JavaScript 应用,之后由客户端根据 URL 决定显示哪个组件,不必每次导航都重新加载整份文档。
前端路由主要负责三件事:
- 读取当前 URL 并匹配路由记录;
- 在客户端导航时更新 URL 和当前页面状态;
- 监听浏览器前进、后退,把历史记录变化同步回应用。
Vue Router 是 Vue 生态中的路由库;React 项目可以使用 React Router 等其他方案。Hash 和 History 是浏览器 URL/历史记录的两种实现方式,不是 Vue 专属概念。
二、URL 的组成
以这个 URL 为例:
http://127.0.0.1:8001/01-hash.html?a=100&b=20#/aaa/bbb
| 属性 | 正确写法 | 示例 | 含义 |
|---|---|---|---|
| 协议 | location.protocol | http: | 通信协议 |
| 主机名 | location.hostname | 127.0.0.1 | 不包含端口的主机名 |
| 主机 | location.host | 127.0.0.1:8001 | 主机名和端口 |
| 端口 | location.port | 8001 | 端口号 |
| 路径 | location.pathname | /01-hash.html | 当前文档路径 |
| 查询字符串 | location.search | ?a=100&b=20 | ? 后的查询部分 |
| 哈希片段 | location.hash | #/aaa/bbb | # 及其后的片段 |
示例:
const url = new URL(
'http://127.0.0.1:8001/01-hash.html?a=100&b=20#/aaa/bbb',
)
console.log(url.protocol) // 'http:'
console.log(url.hostname) // '127.0.0.1'
console.log(url.host) // '127.0.0.1:8001'
console.log(url.port) // '8001'
console.log(url.pathname) // '/01-hash.html'
console.log(url.search) // '?a=100&b=20'
console.log(url.hash) // '#/aaa/bbb'
原文中的 protocal、patchname、serach 和 localtion 都是拼写错误;浏览器标准属性分别是 protocol、pathname、search 和 location。
三、Hash 模式
1. 工作原理
Hash 模式把前端路由放在 URL 的 # 后面:
https://example.com/index.html#/users/42
修改 fragment 通常不会请求新的 HTML 文档,而会触发 hashchange。应用可以读取 location.hash,去掉开头的 # 后匹配路由:
function getHashPath() {
return window.location.hash.slice(1) || '/'
}
function renderByHash() {
const path = getHashPath()
console.log('渲染路由:', path)
}
window.addEventListener('hashchange', renderByHash)
renderByHash()
浏览器会为 hash 导航记录历史,因此用户可以使用前进、后退;但 hash 片段不会作为 HTTP 请求路径发送给服务器。
2. Hash 模式的特点
- URL 中有
#,路由片段通常位于当前文档之后; - hash 改变通常不触发新的文档请求;
- 浏览器会触发
hashchange,应用据此更新视图; - 服务器收到请求时看不到
#后面的片段; - 通常不需要服务器为每个前端路径配置 fallback;
- URL 美观度和 SEO 通常不如 History 模式。
“hash 不能用于前后端分离”是不准确的。Hash 可以用于前后端分离 SPA,只是服务器无法直接根据 hash 访问后端资源,且如果需要搜索引擎理解可索引路径,hash 通常不是理想选择。
Hash 也不是“改变 URL 但没有历史记录”:改变 hash 通常会产生可前进/后退的 session history entry;是否新增记录取决于具体赋值和浏览器历史行为。
四、History 模式
1. History API
HTML5 History API 可以在同一源下改变地址栏中的路径,而不重新加载当前文档:
history.pushState({ page: 2 }, '', '/users/2')
history.replaceState({ page: 2 }, '', '/users/2')
两个方法的区别:
pushState(state, unused, url):新增一条历史记录;replaceState(state, unused, url):替换当前历史记录;state必须是可序列化的数据,浏览器可能对序列化大小有限制;url通常必须与当前文档同源,不能借此跳转到任意外站;title参数目前大多数浏览器仍会忽略,通常传空字符串。
2. popstate 的正确触发时机
原文说 window.onpopstate “响应 pushState 或 replaceState 的调用”,这不准确。单独调用 pushState() 或 replaceState() 不会自动触发 popstate。路由库通常在调用它们之后主动渲染一次;当用户点击前进/后退,或调用 history.back()、history.forward()、history.go() 激活历史记录时,浏览器才会触发 popstate:
function render(path) {
console.log('渲染:', path)
}
function navigate(path, { replace = false } = {}) {
const method = replace ? 'replaceState' : 'pushState'
history[method]({ path }, '', path)
render(path) // pushState 本身不会触发 popstate,所以要主动更新
}
window.addEventListener('popstate', event => {
render(event.state?.path || location.pathname)
})
render(location.pathname)
navigate('/about')
popstate 发生时,location 通常已经反映新 URL;浏览器是否在初次加载时发送 popstate 也存在环境差异,路由库一般会主动执行初始导航,不依赖这个事件。
3. History 模式的特点
- URL 可以是正常的路径,例如
/users/42; - 客户端导航可以不重新请求文档;
- push/replace 可以携带可序列化的
history.state; - 前进、后退通过
popstate通知应用; - 直接刷新或从外部打开深层路径时,浏览器会向服务器请求这个路径;
- 服务器必须把未知前端路径回退到 SPA 的
index.html,否则可能返回 404。
五、Hash 和 History 对比
| 对比项 | Hash | History |
|---|---|---|
| URL | /#/users | /users |
| 监听 | hashchange | popstate,push/replace 后需主动渲染 |
| hash 是否发给服务器 | 不会 | pathname/search 会发送 |
| 深层链接刷新 | 通常不需要额外 fallback | 需要 server fallback |
| URL 美观度 | 较弱 | 更自然 |
| SEO | 通常不利 | URL 更适合,但 SPA 本身仍需 SSR/SSG 才更利于 SEO |
| 部署成本 | 低 | 需要服务器或托管平台配置 |
| 适合场景 | 静态托管、无法改服务器、内部工具 | 可配置服务器、需要正常路径的应用 |
History 模式并不会自动让 SPA 变成 SEO 友好的服务端渲染站点;如果 SEO 是核心需求,还应考虑 SSR、SSG、预渲染、正确的 meta 和可抓取内容。Nuxt 4 默认使用文件路由和 SSR/预渲染能力,通常不需要手动在页面中实现 hash/history 路由。
六、Vue Router 的当前用法
Vue Router 3(Vue 2 项目)
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
const router = new Router({
mode: 'history', // 默认是 hash
routes: [
{ path: '/', component: Home },
{ path: '/users/:id', component: User },
],
})
Vue Router 3 的 mode 仍然是 hash、history 或 abstract 等历史写法。Vue 2.7 项目升级时需要结合现有 Vue Router 3 版本确认 API。
Vue Router 4/5(Vue 3 项目)
Vue Router 4/5 使用 history 实例:
import {
createRouter,
createWebHashHistory,
createWebHistory,
} from 'vue-router'
const router = createRouter({
// 正常路径,需要服务器 fallback
history: createWebHistory(),
// 如果无法配置服务器,可改为:
// history: createWebHashHistory(),
routes: [
{ path: '/', component: Home },
{ path: '/users/:id', component: User },
],
})
Vue Router 文档推荐 HTML5 History 模式,但明确要求服务器在找不到静态文件时返回 index.html。例如 nginx:
location / {
try_files $uri $uri/ /index.html;
}
生产环境还要根据部署子目录设置 router base,并让静态资源路径与 base 一致。应用内部应配置兜底路由展示自己的 404 页面,避免服务器把所有不存在路径都当成成功的 index.html。
七、项目中如何选择
选择 Hash
- 使用 GitHub Pages、对象存储等难以配置 fallback 的静态托管;
- 项目是内部工具,对 URL 美观度和 SEO 要求不高;
- 希望直接刷新深层路径也能回到同一个 HTML。
选择 History
- 可以控制 nginx、Node、CDN 或托管平台的 rewrite;
- 希望使用正常的
/users/42路径; - 需要与后端路由、分析平台或 SEO 体系配合。
无论选择哪种模式,都要测试:首次打开、刷新、前进、后退、复制深层链接、404、部署子路径、资源加载和服务端权限。
八、手写一个最小路由器
下面的代码只演示浏览器 API,不包含参数匹配、守卫、异步组件、滚动恢复和 SSR:
const routes = {
'/': '<h1>首页</h1>',
'/about': '<h1>关于</h1>',
}
const app = document.querySelector('#app')
function render(path = location.pathname) {
app.textContent = ''
app.insertAdjacentHTML('afterbegin', routes[path] || '<h1>404</h1>')
}
function go(path) {
if (path === location.pathname) return
history.pushState({ path }, '', path)
render(path)
}
window.addEventListener('popstate', () => {
render(location.pathname)
})
render()
如果把用户可控内容通过 innerHTML 写入页面,必须先进行安全处理;示例中的路由内容是固定常量,实际应用应使用模板/组件渲染,避免 XSS。
总结
- Hash 和 History 都可以用于前后端分离 SPA;
- hash 变化通常触发
hashchange,片段不会发送给服务器; pushState/replaceState不会自动触发popstate,路由器需要主动更新视图;- History 模式刷新深层路径会请求服务器,需要 fallback 到
index.html; - History URL 更自然,但不会自动解决 SPA 的 SEO,SSR/SSG 仍然重要;
- Vue Router 3 使用
mode,Vue Router 4/5 使用createWebHistory或createWebHashHistory; - Nuxt 4 通常由文件路由和 Nuxt 的服务端/静态生成流程管理路由,不要把本文的手写代码直接放进 Nuxt 页面。
参考资料
- MDN:History.pushState
- MDN:History.replaceState
- MDN:popstate 事件
- MDN:hashchange 事件
- Vue Router:不同 History 模式
- Nuxt 4 路由
原文作者:周一同学 Zelina。原文关于 SPA、hash、History API、刷新 404 和模式选择的主线予以保留;无关表情/推广、拼写错误、popstate 触发条件错误、History 与 SEO 的绝对化结论已整理或修正。原文图片为表情素材,未作为技术图片保留。