vue-router的两种路由模式hash和history,区别以及它们的底层实现原理
Vue Router 常用 hash 和 HTML5 history 两种 Web 历史模式。下面保留原文的 <router-link>、<router-view>、原生实验和版本对照,同时区分 Router 3 历史 API 与当前 Router 4 API。
路由入口与出口
<template>
<router-link to="/user">用户页</router-link>
<router-view />
</template>
<router-link> 默认渲染为带正确 href 的 <a>,点击时由 Router 调用客户端导航(通常相当于 router.push());普通 <a href="/user"> 默认执行文档导航,可能重新加载页面。<router-view> 是显示当前匹配路由组件的出口,并不是“预渲染”。
Router 3 历史写法与 Router 4 当前写法
Vue Router 3 通过 mode 选择模式,浏览器环境默认 hash:
// Router 3 历史 API
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
Vue Router 4 不使用 mode 字符串,必须显式传入 history 工厂:
import {
createMemoryHistory,
createRouter,
createWebHashHistory,
createWebHistory
} from 'vue-router'
const router = createRouter({
history: createWebHistory(),
// 可替换为 createWebHashHistory() 或 createMemoryHistory()
routes
})
hash 和 history 的区别
hash 模式 URL 形如 /#/user,fragment 不发送给服务器,因此无法配置服务器 fallback 时仍容易部署。history 模式 URL 形如 /user,更接近普通站点地址;但直接打开或刷新时服务器必须配合。
“hash 很丑、项目中很少用”是主观且绝对的判断。更合理的选择是:能配置服务器并重视规范 URL、SEO 时通常使用 history;静态托管无法配置 fallback、file:// 或无 host 场景可以选择 hash。
原生 hash 路由实验
手写 hash 路由可监听全小写的 hashchange:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>hashchange 实验</title>
</head>
<body>
<button id="btn">修改 hash</button>
<script>
const btn = document.querySelector('#btn')
btn.addEventListener('click', () => {
location.hash = '#/user'
})
window.addEventListener('hashchange', (event) => {
console.log(event.oldURL)
console.log(event.newURL)
console.log(location.hash)
})
</script>
</body>
</html>
这个实验适合解释浏览器原生机制和自制路由,也可作为 Router 3 历史背景;不能断言当前 Router 4 的 createWebHashHistory() 全部由 onhashchange 实现。Router 4 的公开契约是 history 工厂,内部实现不应作为稳定 API。
原生 History API 实验
popstate 在浏览器前进、后退或 history.go() 激活另一历史条目时触发。原生 pushState()、replaceState() 本身不会自动触发它:
window.addEventListener('popstate', (event) => {
console.log('历史条目发生切换', event.state)
})
history.pushState({ page: 'b' }, '', '/b')
history.replaceState({ page: 'c' }, '', '/c')
原文通过重写方法观察调用的实验可以保留,但它只是自制监听实验,不是 Vue Router 4 的使用方式:
const rawPushState = window.history.pushState
window.history.pushState = function (...args) {
const result = rawPushState.apply(this, args)
console.log('观察到 pushState')
return result
}
const rawReplaceState = window.history.replaceState
window.history.replaceState = function (...args) {
const result = rawReplaceState.apply(this, args)
console.log('观察到 replaceState')
return result
}
正确对应关系是:
router.push()对应新增历史条目,底层语义类似history.pushState()。router.replace()对应替换当前条目,底层语义类似history.replaceState()。router.go()、router.back()、router.forward()用于遍历历史。
Vue Router 会主动维护当前路由状态,不能把所有程序化导航都归因于 popstate;重写原生方法也不会自动产生 popstate。
history 模式的服务器配置
直接访问或刷新 /user 会向服务器请求该路径。服务器要把未命中真实后端接口与静态文件的前端路由请求 rewrite/fallback 到 index.html,保持浏览器 URL 不变。例如 nginx 的核心思路是:
# 真实部署中应先为静态资源和 API 配置更高优先级的 location。
location ^~ /assets/ {
try_files $uri =404;
}
# /api/ 通常应 proxy_pass 到后端;这里的 backend 只是示意 upstream 名称。
location ^~ /api/ {
proxy_pass http://backend;
}
# 只有前端路由请求才回退到应用入口。
location / {
try_files $uri $uri/ /index.html;
}
不能让静态资源和 /api 请求也回退成 HTML,否则会出现资源 MIME 错误或掩盖接口 404。fallback 后还应在客户端配置 catch-all 404 路由:
{
path: '/:pathMatch(.*)*',
name: 'not-found',
component: () => import('../views/NotFound.vue')
}