跳到主要内容

Vue Router 原理

问题

Vue Router 是如何实现的?Hash 模式和 History 模式有什么区别?导航守卫是如何工作的?

答案

Vue Router 是 Vue 官方的路由管理器,核心功能是监听 URL 变化,然后匹配路由规则,最后渲染对应组件

核心架构

Hash 模式 vs History 模式

特性Hash 模式History 模式
URL 格式example.com/#/pathexample.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 实现
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 模式的区别?

答案

维度HashHistory
URL 美观/#/path(不美观)/path(美观)
服务器配置无需需要 fallback 配置
原理hashchangepopstate + 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, metapush, 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)

相关链接