【面试速答版】
Q1: “Vue Router 4 相比 3 有哪些主要变化?”
创建从 new VueRouter() 变为 createRouter({ history: createWebHistory() })。全面拥抱 Composition API——useRouter 和 useRoute。导航守卫 next() 变为可选,不 return 即放行,不再有”忘记调用 next 页面卡死”的 bug。移除了 <transition> + <keep-alive> 的旧嵌套用法,改用 <router-view v-slot> 灵活控制。TypeScript 类型全面增强。动态路由 API 更完善(addRoute/removeRoute)。
Q2: “导航守卫的执行顺序是怎样的?”
1 2 3 4 5 6 7 8
| 1. 路由失活组件的 onBeforeRouteLeave 2. 全局 beforeEach 3. 路由配置的 beforeEnter(如适用) 4. 组件内 onBeforeRouteUpdate(如复用) 5. 解析异步路由组件 6. 全局 beforeResolve 7. 导航确认 8. 全局 afterEach
|
Vue Router 4 的守卫通过 return 控制导航方向。return false 中止;return 路径对象进行重定向;不 return 则放行。
Q3: “Hash 模式和 History 模式有什么区别?”
Hash 模式通过 hashchange 事件监听 URL # 后的变化,兼容性最好(无需服务器配置)。History 模式通过 History API(pushState/replaceState + popstate),URL 美观不带 #,但需要服务器配置 fallback(否则刷新 404)。History 模式还能利用 state 传递数据。Vue Router 4 源码层面两者只差一个监听函数和一个状态读取方式。
Q4: “路由组件如何传递 props?”
1 2 3
| { path: '/user/:id', component: User, props: true } { path: '/user/:id', component: User, props: { role: 'admin' } } { path: '/user/:id', component: User, props: route => ({ id: route.params.id }) }
|
推荐用 props: true,组件通过 defineProps(['id']) 接收,不再依赖 useRoute()。
Q5: “如何实现路由鉴权?”
1 2 3 4 5 6 7 8 9
| router.beforeEach((to, from) => { const token = localStorage.getItem('token'); if (to.meta.requiresAuth && !token) { return { name: 'login', query: { redirect: to.fullPath } }; } if (to.meta.role && !hasRole(to.meta.role)) { return '/403'; } });
|
注意加上 to.name !== 'login' 判断,避免循环重定向。
【深入理解版】
1. 这个知识点要解决什么问题?
SPA 路由需要解决三个核心问题:① URL 与 UI 的映射——/user/123 展示用户详情页;② 浏览器前进/后退——利用 History API 或 hash 实现;③ 导航过程控制——鉴权、权限、数据预加载、离开确认。
Vue Router 4 是为 Vue 3 重新设计的版本,不兼容 Vue 2。主要变化原因:Vue 3 不再支持 Vue.use 插件机制(改为 app.use),全面拥抱 Composition API,TypeScript 成为一等公民。
2. 核心原理
2.1 创建流程
1 2 3 4 5 6 7 8 9 10 11 12
| import { createRouter, createWebHistory } from 'vue-router';
const router = createRouter({ history: createWebHistory(), routes: [ { path: '/', name: 'home', component: () => import('./views/Home.vue') }, { path: '/user/:id', name: 'user', component: () => import('./views/User.vue') }, ], });
const app = createApp(App); app.use(router);
|
app.use(router) 内部做了三件事:
1 2 3 4 5
| 1. router.install(app) ├── 调用 app.component('RouterLink', RouterLink) ├── 调用 app.component('RouterView', RouterView) ├── 通过 provide 注入 router 和 route 到全局 └── 调用 router.push(router.currentRoute) 初始化路由
|
2.2 Hash 模式 vs History 模式
1 2 3 4 5 6 7 8 9 10 11
| import { createWebHashHistory } from 'vue-router';
import { createWebHistory } from 'vue-router';
|
Hash 模式的兼容性好(无需服务器配置),但 URL 不美观,且 hash 部分不会被搜索引擎收录。History 模式需要服务器配置 fallback,否则用户直接在地址栏输入 /user/123 会返回 404:
1 2 3 4
| location / { try_files $uri $uri/ /index.html; }
|
2.3 路由匹配原理
Vue Router 内部将用户配置的 routes 编译为路由记录(RouteRecord) 的扁平列表,再匹配时基于路径模式进行评分(类似正则匹配):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| const records = [ { path: '/', regex: /^\/(\?|$)/, components: Home }, { path: '/user/:id', regex: /^\/user\/([^/]+)$/, components: User }, { path: '/user/:id/post/:postId', regex: /^\/user\/([^/]+)\/post\/([^/]+)$/, components: Post }, ];
function match(path) { for (const record of records) { const match = path.match(record.regex); if (match) { return { ...record, params: extractParams(record, match) }; } } return null; }
|
路径参数 :id 会被编译为捕获组 ([^/]+)。/user/123 匹配后 params.id 就是 '123'。
常用匹配模式:
1 2 3 4 5
| { path: '/user/:id' } { path: '/user/:id(\\d+)' } { path: '/article/:slug+' } { path: '/file/:path(.*)' } { path: '/:optional?' }
|
2.4 嵌套路由
1 2 3 4 5 6 7 8 9 10 11
| const routes = [ { path: '/dashboard', component: DashboardLayout, children: [ { path: '', name: 'dashboard', component: DashboardHome }, { path: 'settings', name: 'settings', component: Settings }, { path: 'users/:id', name: 'user-detail', component: UserDetail }, ], }, ];
|
1 2 3 4 5 6 7 8 9
| <!-- DashboardLayout.vue --> <template> <div class="layout"> <aside>侧边栏</aside> <main> <router-view /> <!-- 渲染子路由组件 --> </main> </div> </template>
|
子路由的 path 会自动拼接父路由路径:/dashboard/settings。空字符串 '' 表示默认子路由,匹配 /dashboard。
Vue Router 4 中**一个 <router-view> 组件内部还可以有子 <router-view>**,由 children 配置决定渲染哪一层。
2.5 编程式导航
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| const router = useRouter();
router.push('/user/123');
router.push({ path: '/user/123' });
router.push({ name: 'user', params: { id: '123' } });
router.push({ path: '/search', query: { q: 'vue' } });
router.push({ path: '/settings', hash: '#profile' });
router.replace('/user/123');
router.go(1); router.go(-1);
|
注意:如果提供了 path,params 会被忽略。推荐使用 name + params 的组合,更稳定。
2.6 RouterLink
1 2 3 4 5 6 7 8 9 10 11 12
| <router-link to="/user/123">用户</router-link> <router-link :to="{ name: 'user', params: { id: user.id } }">用户</router-link> <router-link to="/user/123" replace>替换</router-link> <router-link to="/user/123" active-class="active">当前激活</router-link> <router-link to="/user/123" exact-active-class="exact">精确匹配</router-link>
<!-- v-slot 自定义渲染 --> <router-link to="/home" v-slot="{ href, route, navigate, isActive, isExactActive }"> <a :href="href" @click="navigate" :class="{ active: isExactActive }"> <slot /> </a> </router-link>
|
2.7 RouterView 高级用法
1 2 3 4 5 6 7 8 9 10 11 12 13
| <!-- 配合 transition 动画 --> <router-view v-slot="{ Component, route }"> <transition :name="route.meta.transition || 'fade'" mode="out-in"> <component :is="Component" :key="route.path" /> </transition> </router-view>
<!-- 配合 keep-alive --> <router-view v-slot="{ Component }"> <keep-alive :include="['Home', 'User']"> <component :is="Component" /> </keep-alive> </router-view>
|
<router-view v-slot> 是 Vue Router 4 提供的灵活模式,开发者可以自己控制组件的渲染、过渡动画和缓存策略。
2.8 导航守卫详解
守卫按作用范围分三类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| const router = createRouter({ ... });
router.beforeEach((to, from) => { }); router.beforeResolve((to, from) => { }); router.afterEach((to, from, failure) => { });
{ path: '/admin', component: Admin, beforeEnter: (to, from) => { if (!isAdmin()) return '/403'; }, }
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router';
onBeforeRouteLeave((to, from) => { if (hasUnsavedChanges) return false; });
onBeforeRouteUpdate((to, from) => { fetchUser(to.params.id); });
|
守卫的返回值控制导航行为:
1 2 3 4
| return undefined / 不 return → 放行 return false → 中止导航 return { name: 'login' } → 重定向到登录页 return '/login' → 同上的字符串写法
|
完整执行顺序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 导航被触发 ↓ 1. 失活组件的 onBeforeRouteLeave ↓ 依次执行 2. 全局 beforeEach ↓ 依次执行 3. 路由配置的 beforeEnter(只在参数变化时触发) ↓ 4. 解析异步路由组件 ↓ 5. 复用组件的 onBeforeRouteUpdate ↓ 6. 全局 beforeResolve ↓ 7. 导航确认 ↓ 8. 全局 afterEach ↓ 9. DOM 更新 ↓ 10. 触发路由完成回调
|
2.9 滚动行为
1 2 3 4 5 6 7 8 9 10 11
| const router = createRouter({ scrollBehavior(to, from, savedPosition) { if (savedPosition) { return savedPosition; } if (to.hash) { return { el: to.hash, behavior: 'smooth' }; } return { top: 0, behavior: 'smooth' }; }, });
|
3. 实际应用场景
场景 1:权限管理系统
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| const routes = [ { path: '/login', name: 'login', component: () => import('@/views/Login.vue') }, { path: '/admin', component: AdminLayout, meta: { requiresAuth: true, role: 'admin' }, children: [ { path: '', name: 'admin-dashboard', component: () => import('@/views/AdminDashboard.vue') }, { path: 'users', name: 'admin-users', component: () => import('@/views/AdminUsers.vue') }, ], }, ];
const router = createRouter({ history: createWebHistory(), routes });
router.beforeEach(async (to, from) => { const token = localStorage.getItem('token');
if (to.meta.requiresAuth && !token) { return { name: 'login', query: { redirect: to.fullPath } }; }
if (to.name === 'login' && token) { return '/'; }
if (to.meta.role) { const user = await fetchUserInfo(); if (!user.roles.includes(to.meta.role)) { return '/403'; } } });
export default router;
|
场景 2:动态路由——后端返回权限配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| async function initRouter(user) { router.removeRoute('dashboard');
const dynamicRoutes = await fetchUserRoutes(user.role);
dynamicRoutes.forEach(route => { router.addRoute({ path: route.path, name: route.name, component: () => import(`@/views/${route.component}.vue`), meta: { ...route.meta }, }); }); }
|
场景 3:表单离开确认
1 2 3 4 5 6 7 8 9 10 11 12
| <script setup> import { onBeforeRouteLeave } from 'vue-router';
const hasUnsavedChanges = ref(false);
onBeforeRouteLeave((to, from) => { if (hasUnsavedChanges.value) { const leave = window.confirm('有未保存的内容,确定离开吗?'); if (!leave) return false; } }); </script>
|
场景 4:路由过渡动画
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <template> <router-view v-slot="{ Component, route }"> <transition :name="route.meta.transition || 'fade'" mode="out-in" > <component :is="Component" :key="route.path" /> </transition> </router-view> </template>
<style> .fade-enter-active, .fade-leave-active { transition: opacity 0.2s ease; } .fade-enter-from, .fade-leave-to { opacity: 0; } </style>
|
4. 常见误区 & 实际项目中的坑
误区 1:在 <script setup> 中用 this.$router
1 2 3 4
| <script setup> this.$router.push('/home'); // ❌ 没有 this const router = useRouter(); // ✅ </script>
|
误区 2:History 模式未配置服务器 fallback
部署到 Nginx 时未配置 try_files,用户刷新子页面返回 404。解决方案见上文。
误区 3:导航守卫循环重定向
1 2 3 4 5
| router.beforeEach((to) => { if (!isLogin) { return '/login'; } });
|
正确的写法是加条件判断:if (!isLogin && to.name !== 'login')。
误区 4:watch route 不及时
1 2 3 4
| <script setup> const route = useRoute(); watch(() => route.params.id, (id) => { fetch(id); }); </script>
|
在组件复用时(如从 /user/1 到 /user/2),watch 可以监听到参数变化。但如果组件不复用(如 /user/1 到 /article/2),组件会重新挂载,应使用 onMounted 或路由守卫。
误区 5:path 和 params 混用
1 2
| router.push({ path: '/user', params: { id: '123' } }); router.push({ name: 'user', params: { id: '123' } });
|
使用 path 时 params 会被忽略。用 name + params 替代。
5. 与相关知识的关联 & 对比
| 对比维度 | Vue Router 3 | Vue Router 4 |
|---|
| 创建方式 | new VueRouter({ ... }) | createRouter({ ... }) |
| 路由模式 | mode: 'history' | history: createWebHistory() |
| 组合式 API | ❌ 不支持 | ✅ useRouter / useRoute |
| 导航守卫 next | 必须调用(否则卡住) | 可选(return 替代) |
| TypeScript | 弱类型 | 强类型(RouteRecordRaw 等) |
| 动态路由 | 限制多 | addRoute / removeRoute 完善 |
| router-view | 不支持 v-slot | v-slot + component 灵活控制 |
| 滚动行为 | 支持 | 支持,且返回 Promise |
| 编码大小 | ~20KB | 更小 |
6. 现代最佳实践
- 始终使用 History 模式(
createWebHistory),除非无法配置服务器 fallback - 路由配置按模块拆分,通过
addRoute 按需加载 - 用
props: true 解耦路由组件,组件不依赖 $route - 导航守卫统一管理鉴权,避免分散在各组件中
- 配合
defineAsyncComponent 做路由级代码分割 - 路由 meta 字段规范:统一在 meta 中定义
requiresAuth、role、title、transition、keepAlive 等字段 - 使用命名路由(
name)替代路径字符串,方便重构和维护 - 用 navigation failure 判断导航结果:
1 2 3 4 5 6 7 8
| const result = await router.push('/admin'); if (result === undefined) { } else { if (result.type === 'aborted') { } if (result.type === 'duplicated') { } }
|
7. 常见疑问解答
Q:useRoute() 返回的是响应式对象吗?
是的。它通过 Vue 的 shallowRef 实现响应式。当导航发生时,Vue Router 内部更新 currentRoute,所有使用了 useRoute() 的组件都会自动更新。
Q:app.use(router) 时内部做了什么?
1 2 3 4
| 1. 注册全局组件 RouterLink + RouterView 2. 通过 provide 向全应用注入 router 实例和当前路由 3. 调用 router.push(router.currentRoute) 初始化 4. 在根组件上注册 `app.config.globalProperties.$router` 等
|
Q:router.addRoute 添加的路由什么时候生效?
立即生效。addRoute 将新路由记录添加到路由表中,下一次导航匹配就会包含新路由。无须重新调用 createRouter。
Q:嵌套路由可以无限嵌套吗?
理论上可以,但实际建议嵌套不超过 3 层。每层嵌套都会在 RouterView 中再套一个 RouterView,层级过深会影响渲染性能。
Q:路由懒加载的原理是什么?
1
| { component: () => import('./views/Home.vue') }
|
() => import() 返回一个 Promise,Vue Router 在首次导航到该路由时才执行 import,Webpack/Vite 会将动态 import 的模块打包为独立的 chunk。导航到该路由时,路由守卫中的 beforeResolve 会等待异步组件解析完成,然后才渲染。
关联知识点索引
Vue3 核心变化与 Composition API.md — Composition API 基础响应式原理.md — shallowRef 与 provide/injectVite 详解.md — 动态 import 与代码分割