Vue Router 原理
问题
Vue Router 是如何实现的?Hash 模式和 History 模式有什么区别?导航守卫是如何工作的?
面试速答版
Vue Router 是如何实现的? 核心做的事就三步:监听 URL 变化 → 匹配路由规则 → 渲染对应组件。
createRouter创建路由实例,传入路由表和一个 history 实现(createWebHistory/createWebHashHistory/createMemoryHistory)。- 内部维护一个响应式的当前路由对象(
currentRoute),URL 变化时更新它。 <RouterView>是个内置组件,订阅currentRoute的变化,根据匹配结果渲染对应组件,支持嵌套路由(多层<RouterView>)。<RouterLink>拦截点击,调用router.push而不是真实跳转。
Hash 模式和 History 模式有什么区别?
- Hash 模式:URL 形如
example.com/#/path,靠监听hashchange事件触发更新。优点是不需要后端配合,刷新永远拿到首页;缺点是 URL 不美观、SEO 一般。 - History 模式:URL 形如
example.com/path,靠history.pushState/replaceState修改 URL,监听popstate处理前进后退。URL 干净、利于 SEO,但需要后端把所有未匹配的路径回退到index.html,否则刷新就 404。
导航守卫是如何工作的? 守卫是路由切换过程中按顺序执行的一系列异步钩子,构成一个 Promise 链:
- 触发顺序大致是:失活组件
beforeRouteLeave→ 全局beforeEach→ 复用组件beforeRouteUpdate→ 路由配置beforeEnter→ 新组件beforeRouteEnter→ 全局beforeResolve→ 导航确认 → 全局afterEach。 - 每个守卫可以
return false/return '/login'/throw来取消或重定向,否则导航继续。 - 典型用法:登录鉴权(
beforeEach里查 token)、权限路由、页面进入埋点、离开页面提示未保存。
答案
Vue Router 是 Vue 官方的路由管理器,核心功能是监听 URL 变化,然后匹配路由规则,最后渲染对应组件。
核心架构
Hash 模式 vs History 模式
| 特性 | Hash 模式 | History 模式 |
|---|---|---|
| URL 格式 | example.com/#/path | example.com/path |
| 服务器配置 | 不需要 | 需要配置 |
| 兼容性 | 更好(IE9+) | 较好(IE10+) |
| SEO | 不友好 | 友好 |
| 原理 | hashchange 事件 | popstate + pushState |
Hash 模式实现
// Hash 模式核心原理
function createWebHashHistory(): RouterHistory {
// 获取当前 hash
function getCurrentLocation(): string {
return window.location.hash.slice(1) || '/';
}
// 监听 hash 变化
window.addEventListener('hashchange', () => {
const newPath = getCurrentLocation();
// 触发路由更新
handleRouteChange(newPath);
});
// 导航
function push(path: string) {
window.location.hash = path;
}
function replace(path: string) {
const url = new URL(window.location.href);
url.hash = path;
window.location.replace(url.toString());
}
return {
getCurrentLocation,
push,
replace
};
}
History 模式实现
// History 模式核心原理
function createWebHistory(): RouterHistory {
// 获取当前路径
function getCurrentLocation(): string {
return window.location.pathname + window.location.search;
}
// 监听 popstate(前进/后退按钮)
window.addEventListener('popstate', () => {
const newPath = getCurrentLocation();
handleRouteChange(newPath);
});
// 导航
function push(path: string, state?: any) {
// pushState 不会触发 popstate
window.history.pushState(state, '', path);
// 手动触发路由更新
handleRouteChange(path);
}
function replace(path: string, state?: any) {
window.history.replaceState(state, '', path);
handleRouteChange(path);
}
return {
getCurrentLocation,
push,
replace
};
}
History 模式需要服务器配置
History 模式下,直接访问 /user/123 会导致 404,需要服务器将所有路由重定向到 index.html:
# Nginx 配置
location / {
try_files $uri $uri/ /index.html;
}
路由匹配
Vue Router 将路由配置转换为正则表达式进行匹配:
// 路由配置
const routes = [
{ path: '/', component: Home },
{ path: '/user/:id', component: User },
{ path: '/user/:id/posts/:postId', component: Post },
{ path: '/:pathMatch(.*)*', component: NotFound } // 通配符
];
// 路径转正则
// '/user/:id' => /^\/user\/([^/]+)$/
// '/user/:id/posts/:postId' => /^\/user\/([^/]+)\/posts\/([^/]+)$/
function pathToRegex(path: string): RegExp {
// 转换动态参数 :id => ([^/]+)
const pattern = path
.replace(/:\w+/g, '([^/]+)')
.replace(/\//g, '\\/');
return new RegExp(`^${pattern}$`);
}
function matchRoute(path: string, routes: Route[]): RouteMatch | null {
for (const route of routes) {
const regex = pathToRegex(route.path);
const match = path.match(regex);
if (match) {
// 提取参数
const paramNames = route.path.match(/:\w+/g) || [];
const params: Record<string, string> = {};
paramNames.forEach((name, i) => {
params[name.slice(1)] = match[i + 1];
});
return { route, params };
}
}
return null;
}
导航守卫
Vue Router 提供多个导航守卫钩子,形成守卫管道:
全局守卫
import { createRouter } from 'vue-router';
const router = createRouter({ /* ... */ });
// 全局前置守卫
router.beforeEach(async (to, from) => {
// to: 目标路由
// from: 当前路由
// 检查登录状态
if (to.meta.requiresAuth && !isAuthenticated()) {
// 重定向到登录页
return { path: '/login', query: { redirect: to.fullPath } };
}
// 返回 true 或 undefined 继续导航
// 返回 false 取消导航
// 返回路由对象重定向
});
// 全局解析守卫(在组件守卫之后、导航确认之前)
router.beforeResolve(async (to) => {
// 用于获取数据或其他异步操作
if (to.meta.requiresData) {
try {
await fetchData(to.params.id);
} catch {
return false; // 取消导航
}
}
});
// 全局后置守卫(不接受 next,不能改变导航)
router.afterEach((to, from, failure) => {
// 用于分析、修改页面标题等
document.title = to.meta.title || 'My App';
if (failure) {
console.log('导航失败:', failure);
}
});
路由独享守卫
const routes = [
{
path: '/admin',
component: Admin,
beforeEnter: (to, from) => {
// 只在进入该路由时触发
if (!isAdmin()) {
return { path: '/403' };
}
}
}
];
组件内守卫
<!-- Options API -->
<script>
export default {
// 进入前(不能访问 this)
beforeRouteEnter(to, from, next) {
// 通过 next 回调访问组件实例
next(vm => {
vm.fetchData();
});
},
// 路由更新时(复用组件)
beforeRouteUpdate(to, from) {
this.fetchData(to.params.id);
},
// 离开前
beforeRouteLeave(to, from) {
// 例如:提示用户保存
if (this.hasUnsavedChanges) {
return window.confirm('确定离开?未保存的更改将丢失');
}
}
};
</script>
<!-- Composition API -->
<script setup>
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router';
// 注意:没有 onBeforeRouteEnter,因为 setup 时组件还没创建
onBeforeRouteUpdate((to, from) => {
// 路由参数变化时
});
onBeforeRouteLeave((to, from) => {
// 离开前
return confirm('确定离开?');
});
</script>
RouterView 实现原理
// 简化的 RouterView 实现
import { inject, h, computed } from 'vue';
const RouterView = {
name: 'RouterView',
setup() {
// 注入当前路由
const route = inject('currentRoute');
// 获取匹配的组件
const component = computed(() => {
return route.value.matched[0]?.component;
});
return () => {
if (component.value) {
return h(component.value);
}
return null;
};
}
};
RouterLink 实现原理
// 简化的 RouterLink 实现
import { inject, h } from 'vue';
const RouterLink = {
name: 'RouterLink',
props: {
to: { type: [String, Object], required: true }
},
setup(props, { slots }) {
const router = inject('router');
const currentRoute = inject('currentRoute');
const isActive = computed(() => {
return currentRoute.value.path === props.to;
});
function navigate(e: Event) {
e.preventDefault();
router.push(props.to);
}
return () => h(
'a',
{
href: props.to,
class: { 'router-link-active': isActive.value },
onClick: navigate
},
slots.default?.()
);
}
};
路由懒加载
const routes = [
{
path: '/about',
// 动态 import 实现代码分割
component: () => import('./views/About.vue')
},
{
path: '/user/:id',
// 带 webpackChunkName 的命名分块
component: () => import(/* webpackChunkName: "user" */ './views/User.vue')
}
];
常见面试问题
Q1: Hash 和 History 模式的区别?
答案:
| 维度 | Hash | History |
|---|---|---|
| URL 美观 | /#/path(不美观) | /path(美观) |
| 服务器配置 | 无需 | 需要 fallback 配置 |
| 原理 | hashchange | popstate + pushState |
| 兼容性 | IE8+ | IE10+ |
| SEO | # 后内容不被索引 | 友好 |
| 请求服务器 | # 后内容不发送到服务器 | 完整路径发送 |
Q2: 路由懒加载是如何实现的?
答案:
// 路由懒加载利用动态 import + Webpack/Vite 代码分割
// 编译前
const routes = [
{ path: '/about', component: () => import('./About.vue') }
];
// Webpack 编译后,About.vue 被打包成单独的 chunk
// 访问 /about 时才下载 about.chunk.js
// Vite 编译后生成
// /assets/About.abc123.js
// 原理:
// 1. () => import() 返回 Promise
// 2. 路由激活时,Vue Router 调用该函数
// 3. 浏览器动态加载对应的 chunk
// 4. 加载完成后渲染组件
配合 Suspense 显示加载状态:
<template>
<RouterView v-slot="{ Component }">
<Suspense>
<component :is="Component" />
<template #fallback>
<LoadingSpinner />
</template>
</Suspense>
</RouterView>
</template>
Q3: 导航守卫的完整执行顺序是什么?
答案:
1. 导航被触发
2. 失活组件的 beforeRouteLeave
3. 全局 beforeEach
4. 重用组件的 beforeRouteUpdate
5. 路由配置的 beforeEnter
6. 解析异步路由组件
7. 激活组件的 beforeRouteEnter
8. 全局 beforeResolve
9. 导航被确认
10. 全局 afterEach
11. DOM 更新
12. beforeRouteEnter 的 next 回调
Q4: 如何实现路由权限控制?
答案:
// 路由配置
const routes = [
{
path: '/admin',
component: Admin,
meta: {
requiresAuth: true,
roles: ['admin']
}
}
];
// 全局守卫
router.beforeEach(async (to) => {
// 不需要认证的路由
if (!to.meta.requiresAuth) return true;
// 检查登录状态
const user = await getUser();
if (!user) {
return { path: '/login', query: { redirect: to.fullPath } };
}
// 检查角色权限
if (to.meta.roles && !to.meta.roles.includes(user.role)) {
return { path: '/403' };
}
return true;
});
Q5: $route 和 $router 的区别?
答案:
| $route | $router | |
|---|---|---|
| 类型 | 当前路由对象 | 路由实例 |
| 作用 | 获取路由信息 | 操作路由 |
| 访问方式 | useRoute() | useRouter() |
| 常用属性/方法 | path, params, query, meta | push, replace, go, back |
import { useRoute, useRouter } from 'vue-router';
const route = useRoute();
console.log(route.path); // '/user/123'
console.log(route.params.id); // '123'
console.log(route.query); // { tab: 'profile' }
console.log(route.meta); // { requiresAuth: true }
const router = useRouter();
router.push('/home');
router.replace('/login');
router.go(-1); // 后退
router.back(); // 等同于 go(-1)