跳到主要内容

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/#/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)

相关链接