前言
"讲讲你项目的路由设计"——这是Vue二面最高频的问题,也是 80% 候选人的滑铁卢。
上周面试了个候选人,简历上写 "负责整个项目的路由架构"。我问他:"你们项目的路由权限是怎么控制的?"他愣了5秒,说:"就...配了个meta字段,然后在beforeEach里判断。"我追问:"具体怎么判断的?遇到过什么问题?"他说不出来。
这就是问题所在。不是你不会用router,是你不知道怎么把'写了几个路由配置'包装成'有架构思维的路由设计'。
路由管理藏着太多细节:权限控制怎么做、为什么用懒加载、导航守卫用在哪。今天这8题会告诉你,面试官到底想从路由问题里看出什么。
欢迎阅读我的Vue专栏文章
Vuex面试7题:你以为的"会用",在面试官眼里都是"不懂原理"
31. Vue Router的工作原理?hash模式和history模式的区别?
速记公式:监听URL,匹配路由,渲染组件,两种模式各有场景
标准答案
Vue Router是一个路由管理器,通过监听URL变化来渲染对应的组件,实现单页应用的页面切换。
核心工作流程:
- URL发生变化
- Router匹配预定义的路由规则
- 找到对应的组件
- 渲染到
<router-view>中
Hash模式(默认):
使用URL中的hash部分(#后面的内容)管理路由,如http://example.com/#/user/123。特点是hash变化不会触发页面刷新,只会触发hashchange事件,Vue Router监听这个事件来切换组件。
优势:
- 兼容性好,所有浏览器都支持
- 不需要服务器配置,服务器会忽略hash部分
- 部署简单,上传静态文件就能跑
History模式:
利用HTML5的History API(pushState、replaceState)管理路由,URL更美观,如http://example.com/user/123。用户点击浏览器前进后退时触发popstate事件,Router监听此事件切换组件。
优势:
- URL美观,没有
#号 - SEO友好(配合SSR)
- 更符合传统URL习惯
关键区别:
History模式需要服务器配合。用户直接访问/user/123或刷新页面时,浏览器会向服务器请求这个路径。如果服务器没有配置将所有路由都返回index.html,会出现404错误。
服务器配置示例(nginx):
location / {
try_files $uri $uri/ /index.html;
}
面试官真正想听什么
这题考察你对前端路由原理的理解和工程化部署经验。只说"一个有#一个没#"是远远不够的。
加分回答
"我在项目中两种模式都用过,各有场景:
Hash模式的实战:
做过一个内嵌在APP里的H5页面,用Hash模式因为:
- 不需要和客户端协商路由:APP的WebView直接加载index.html,所有路由在前端处理
- 部署简单:放到CDN上就能访问,不用配置服务器
- 兼容性好:老版本Android的WebView对History API支持不好
History模式的实战:
官网和管理后台都用History模式,因为:
- URL更正规:对外展示的官网,
domain.com/about比domain.com/#/about专业 - 利于SEO:虽然单页应用SEO不好,但至少URL结构清晰
- 用户体验好:用户分享链接时,没有#号的URL更可信
服务器配置踩过的坑:
最开始用History模式部署到nginx,没配置try_files,结果:
- 首页能访问(/)
- 点击跳转正常(前端路由)
- 刷新页面404(服务器找不到/user/123这个文件)
配置了try_files $uri $uri/ /index.html后,所有路径都返回index.html,Vue Router接管路由,问题解决。
**但又遇到新问题:**真正的404页面也被index.html接管了,静态资源路径错误也返回index.html。最后加了判断:
location / {
try_files $uri $uri/ /index.html;
}
location ~* \.(js|css|png|jpg)$ {
expires 7d;
}
选择标准:
- 内嵌页面、演示项目用Hash模式
- 官网、后台管理用History模式
- 混合APP根据客户端能力选择
这让我理解:技术选型要考虑部署环境、用户场景、团队能力,不是哪个'更好'的问题。"
减分回答
❌ "History模式更好,没有#号"(不考虑部署成本)
❌ 不知道History模式需要服务器配置(缺少部署经验)
❌ 说不出实际项目中的选择依据(理论派)
32. Vue Router如何定义路由?动态路由参数如何获取?
速记公式:path配置,冒号参数,params获取,变化需监听
标准答案
路由定义通过createRouter函数配置,在routes数组中定义路径与组件的映射关系。
基础路由:
const routes = [
{ path: '/user', component: UserComponent },
{ path: '/about', component: AboutComponent }
]
动态路由使用冒号语法定义参数:
{
path: '/user/:id',
component: UserDetail
}
这里的:id就是动态参数,可以匹配/user/123、/user/456等路径。
获取路由参数有两种方式:
选项式API:
export default {
mounted() {
const userId = this.$route.params.id
}
}
组合式API:
import { useRoute } from 'vue-router'
const route = useRoute()
const userId = route.params.id
关键点:路由参数变化时组件会被复用。比如从/user/1跳到/user/2,组件不会重新创建,只是参数变了。如果你的组件依赖参数,需要通过watch监听$route的变化:
watch: {
'$route'(to, from) {
// 响应路由参数变化
this.fetchUserData(to.params.id)
}
}
支持多个参数和正则约束:
// 多个参数
{ path: '/category/:type/product/:id' }
// 正则约束(只匹配数字)
{ path: '/user/:id(\\d+)' }
面试官真正想听什么
这题考察你对路由复用机制的理解。很多人不知道组件会复用,导致数据不更新。
加分回答
"我在项目中踩过动态路由复用的坑:
问题场景:
做商品详情页,路由是/product/:id。用户从商品A点击"相关推荐"跳到商品B,URL变了但页面内容没更新,还是商品A的数据。
原因:
Vue Router复用了同一个组件实例,只改了$route.params.id,但组件的mounted钩子不会再次执行,之前在mounted里调用的接口也不会重新请求。
错误写法:
mounted() {
this.fetchProduct(this.$route.params.id) // 只执行一次
}
解决方案1:监听路由变化
watch: {
'$route.params.id': {
handler(newId) {
this.fetchProduct(newId)
},
immediate: true // 初始化时也执行
}
}
解决方案2:用key强制重新创建
<router-view :key="$route.fullPath" />
这样每次路由变化都会销毁重建组件,但性能差一些,谨慎使用。
方案3:路由守卫(Vue3推荐)
import { onBeforeRouteUpdate } from 'vue-router'
onBeforeRouteUpdate((to, from) => {
fetchProduct(to.params.id)
})
多参数的实际应用:
做过一个分类商品列表页,路由是/category/:catId/brand/:brandId,可以同时筛选分类和品牌:
// 访问 /category/1/brand/5
route.params.catId // '1'
route.params.brandId // '5'
正则约束避免错误参数:
// 只接受数字ID
{ path: '/user/:id(\\d+)', component: UserDetail }
// 访问 /user/abc 会匹配失败,走404
这让我养成习惯:动态路由的组件一定要处理参数变化,不能只在mounted里请求数据。"
减分回答
❌ 不知道组件会被复用(基础不扎实)
❌ 数据不更新但说不出原因(没踩过坑)
❌ 所有路由都用key强制重建(性能差,过度优化)
33. Vue Router导航守卫有哪些?执行顺序是怎样的?
速记公式:全局-路由-组件,进入-解析-离开,层层拦截
标准答案
Vue Router导航守卫分三类,用于在路由跳转的不同阶段执行逻辑。
全局守卫:
- beforeEach:每次导航前触发,常用于权限验证、登录检查
- beforeResolve:所有守卫和异步组件解析完成后触发
- afterEach:导航完成后触发,常用于页面统计、修改标题
路由独享守卫:
- beforeEnter:直接定义在路由配置中,只对特定路由生效,适合单个路由的特殊权限控制
组件内守卫:
- beforeRouteEnter:组件实例创建前执行,无法访问this
- beforeRouteUpdate:路由参数变化但组件复用时触发
- beforeRouteLeave:离开当前路由时执行,常用于未保存数据提示
完整执行顺序:
- 离开组件的
beforeRouteLeave - 全局
beforeEach - 路由独享
beforeEnter - 进入组件的
beforeRouteEnter - 全局
beforeResolve - 路由跳转确认
- 全局
afterEach - DOM更新
beforeRouteEnter的next回调
这个流程确保了从权限验证到组件渲染的完整控制。
面试官真正想听什么
这题考察你对路由生命周期的理解和权限控制的实现能力。导航守卫是实现复杂路由逻辑的核心。
加分回答
"我在项目中用导航守卫实现了完整的权限控制系统:
全局beforeEach:统一权限验证
router.beforeEach((to, from, next) => {
const token = localStorage.getItem('token')
// 白名单:登录页、注册页不需要token
const whiteList = ['/login', '/register']
if (whiteList.includes(to.path)) {
next()
return
}
// 需要登录但没有token,跳转登录页
if (!token) {
next(`/login?redirect=${to.fullPath}`)
return
}
// 检查路由权限
const userRole = store.state.user.role
const requiredRole = to.meta.role
if (requiredRole && !hasPermission(userRole, requiredRole)) {
next('/403')
return
}
next()
})
路由独享beforeEnter:特殊页面验证
{
path: '/admin',
component: AdminPanel,
beforeEnter: (to, from, next) => {
// 管理员才能访问
if (store.state.user.role === 'admin') {
next()
} else {
Message.error('需要管理员权限')
next('/dashboard')
}
}
}
beforeRouteLeave:防止数据丢失
export default {
data() {
return {
formChanged: false
}
},
beforeRouteLeave(to, from, next) {
if (this.formChanged) {
const answer = window.confirm('有未保存的内容,确定离开吗?')
if (answer) {
next()
} else {
next(false) // 取消导航
}
} else {
next()
}
}
}
afterEach:页面统计和标题
router.afterEach((to, from) => {
// 修改页面标题
document.title = to.meta.title || '默认标题'
// 发送页面浏览统计
analytics.track('pageview', {
path: to.path,
from: from.path
})
// 滚动到顶部
window.scrollTo(0, 0)
})
beforeRouteEnter的next回调
beforeRouteEnter(to, from, next) {
// 此时this还不可用
fetchUserData(to.params.id).then(data => {
next(vm => {
// 通过vm访问组件实例
vm.userData = data
})
})
}
踩过的坑:
- 忘记调用next():导航守卫里不调next(),路由会卡住不跳转
- next()调用多次:一个守卫里调了多次next(),会报错
- 异步验证没处理:在beforeEach里调接口验证权限,但没等接口返回就next()了
这套守卫体系让权限控制变得层次分明:全局处理通用逻辑,路由独享处理特殊页面,组件内处理个性化需求。"
减分回答
❌ 只知道beforeEach(其他守卫不了解)
❌ 说不出执行顺序(不理解流程)
❌ 没用过beforeRouteLeave(缺少用户体验意识)
34. Vue Router编程式导航如何实现?push和replace的区别?
速记公式:$router跳转,push加历史,replace替换,go前进后退
标准答案
编程式导航通过$router实例的方法在JavaScript代码中控制路由跳转。
主要方法:
$router.push():跳转到新路由,在浏览器历史栈中新增一条记录,用户可以通过后退按钮返回上一页。
// 字符串路径
this.$router.push('/user/123')
// 路径对象
this.$router.push({ path: '/user/profile' })
// 命名路由+参数
this.$router.push({ name: 'user', params: { id: 123 } })
// 带查询参数
this.$router.push({ path: '/search', query: { keyword: 'vue' } })
$router.replace(): 跳转到新路由,替换当前历史记录,不会增加历史栈长度,用户无法通过后退按钮回到被替换的页面。
this.$router.replace('/login')
$router.go(n): 在历史栈中前进或后退n步。
this.$router.go(-1) // 后退一页
this.$router.go(1) // 前进一页
this.$router.back() // 后退,等于go(-1)
this.$router.forward() // 前进,等于go(1)
核心区别:
push会留下历史记录,replace不会。选择哪个取决于业务逻辑:登录成功后跳转首页用replace(用户不应该通过后退返回登录页),商品列表跳详情页用push(用户可以返回列表继续浏览)。
面试官真正想听什么
这题考察你对用户体验的理解和路由设计的思考。会用API是基础,知道什么场景用什么方法才是关键。
加分回答
"我在项目中根据不同场景选择不同的导航方法:
用push的场景:保留访问历史
- 商品列表→详情页
goToProduct(id) {
this.$router.push({ name: 'productDetail', params: { id } })
}
// 用户看完详情可以后退回列表
- 搜索结果点击
search(keyword) {
this.$router.push({
path: '/search',
query: { keyword }
})
}
// 保留搜索历史,方便用户对比
用replace的场景:不保留历史
- 登录成功跳转
async login() {
const res = await loginApi(this.form)
if (res.success) {
// 用replace,防止用户后退到登录页
this.$router.replace('/dashboard')
}
}
- 重定向场景
// 访问/old-page自动跳转到/new-page
router.beforeEach((to, from, next) => {
if (to.path === '/old-page') {
next({ path: '/new-page', replace: true })
} else {
next()
}
})
- 表单提交成功
async submitForm() {
await saveData(this.form)
// 提交成功后跳转,不保留表单页历史
// 防止用户后退重复提交
this.$router.replace('/success')
}
go的实际应用:
// 面包屑导航的后退
handleBack() {
// 判断是否有历史记录
if (window.history.length > 1) {
this.$router.go(-1)
} else {
// 没有历史就跳首页
this.$router.push('/')
}
}
参数传递的注意事项:
- params只能用name
// 错误:params必须用name,不能用path
this.$router.push({ path: '/user', params: { id: 123 } })
// 正确
this.$router.push({ name: 'user', params: { id: 123 } })
- query适合公开参数,params适合内部参数
// query在URL里可见:/search?keyword=vue
this.$router.push({ path: '/search', query: { keyword: 'vue' } })
// params在URL里按路由规则显示:/user/123
this.$router.push({ name: 'user', params: { id: 123 } })
这让我形成习惯:跳转前先想清楚用户是否需要后退,再决定用push还是replace。"
减分回答
❌ 所有跳转都用push(不考虑用户体验)
❌ 不知道params和query的区别(基础不扎实)
❌ 不知道什么时候该用replace(缺少场景思考)
35. Vue Router路由懒加载如何实现?代码分割的原理?
速记公式:动态import,webpack分chunk,按需加载,首屏提速
标准答案
路由懒加载通过动态import()语法实现,将组件按需加载而非一次性全部打包。
基础实现:
const routes = [
{
path: '/home',
component: () => import('./views/Home.vue')
},
{
path: '/about',
component: () => import('./views/About.vue')
}
]
把原来的静态导入import Home from './Home.vue'改为动态导入函数() => import('./Home.vue')即可。
代码分割原理:
Webpack识别import()语法,会将对应的组件及其依赖打包成独立的chunk文件。浏览器只有在用户访问特定路由时才会下载对应的chunk,而不是在初始加载时下载整个应用包。
命名chunk优化打包:
component: () => import(
/* webpackChunkName: "user" */
'./views/User.vue'
)
使用魔法注释给chunk命名,便于调试和缓存管理。相同chunkName的组件会被打包到同一个文件中。
分组加载策略:
// 把相关的页面打包到同一个chunk
const UserProfile = () => import(/* webpackChunkName: "user" */ './UserProfile.vue')
const UserPosts = () => import(/* webpackChunkName: "user" */ './UserPosts.vue')
const UserSettings = () => import(/* webpackChunkName: "user" */ './UserSettings.vue')
性能优势:
- 减少首屏加载时间:只加载当前页面需要的代码
- 按需加载:用户访问哪个页面才下载哪个页面的代码
- 提升缓存命中率:页面代码独立,修改一个不影响其他
面试官真正想听什么
这题考察你对性能优化的理解和工程化能力。不做懒加载的应用,首屏加载慢,用户体验差。
加分回答
"我在项目中用路由懒加载做了显著的性能优化:
优化前的问题:
管理后台有20个页面,全部直接import,打包后:
- main.js体积3.2MB
- 首屏加载时间8秒(4G网络)
- 用户体验极差,进入系统要等很久
优化方案1:基础懒加载
const routes = [
{
path: '/dashboard',
component: () => import('./views/Dashboard.vue')
},
{
path: '/user-list',
component: () => import('./views/UserList.vue')
},
{
path: '/product-list',
component: () => import('./views/ProductList.vue')
}
// ... 其他20个页面
]
优化结果:
- main.js降到500KB
- 首屏加载1.8秒
- 每个页面首次访问时加载自己的chunk
优化方案2:分组加载
发现有些页面经常一起访问,比如用户管理的增删改查页面,合并打包:
// 用户管理模块
const UserList = () => import(/* webpackChunkName: "user" */ './views/user/List.vue')
const UserAdd = () => import(/* webpackChunkName: "user" */ './views/user/Add.vue')
const UserEdit = () => import(/* webpackChunkName: "user" */ './views/user/Edit.vue')
// 商品管理模块
const ProductList = () => import(/* webpackChunkName: "product" */ './views/product/List.vue')
const ProductAdd = () => import(/* webpackChunkName: "product" */ './views/product/Add.vue')
优化方案3:预加载优化
首页加载后,用户很可能访问某些高频页面,提前预加载:
// 首页组件mounted后预加载高频页面
mounted() {
// 2秒后开始预加载
setTimeout(() => {
import(/* webpackPrefetch: true */ './views/UserList.vue')
import(/* webpackPrefetch: true */ './views/ProductList.vue')
}, 2000)
}
实测数据对比:
| 指标 | 优化前 | 基础懒加载 | 分组+预加载 |
|---|---|---|---|
| main.js大小 | 3.2MB | 500KB | 500KB |
| 首屏加载 | 8s | 1.8s | 1.8s |
| 二次页面 | 即时 | 300ms | 100ms |
注意事项:
- 不要过度拆分:每个chunk都有加载开销,太多小chunk反而慢
- 公共依赖提取:配置webpack的splitChunks,避免重复打包
- loading状态:懒加载时显示loading,提升体验
const AsyncComponent = defineAsyncComponent({
loader: () => import('./Heavy.vue'),
loadingComponent: LoadingSpinner,
delay: 200
})
这让我明白:性能优化不是一刀切,要根据实际情况平衡加载策略。"
减分回答
❌ 不知道懒加载能提升首屏性能(没有优化意识)
❌ 所有组件都懒加载包括很小的组件(过度优化)
❌ 不知道怎么命名chunk(工程化能力弱)
36. Vue Router嵌套路由如何配置?children属性的使用?
速记公式:children嵌套,相对路径,router-view出口,层级清晰
标准答案
嵌套路由通过children属性实现路由层级结构,允许在父路由组件内渲染子路由组件。
基础配置:
const routes = [
{
path: '/user',
component: User,
children: [
{
path: '', // 默认子路由,匹配/user
component: UserHome
},
{
path: 'profile', // 匹配/user/profile
component: UserProfile
},
{
path: 'posts', // 匹配/user/posts
component: UserPosts
}
]
}
]
关键点:
- 子路由path不需要斜杠开头(相对路径),Vue Router会自动拼接父路径
- 父组件必须包含
<router-view>标签作为子路由的渲染出口 - 空字符串path表示默认子路由,访问父路径时自动显示
- 支持无限层级嵌套,满足复杂应用的路由组织需求
父组件示例:
<template>
<div class="user">
<h2>用户中心</h2>
<nav>
<router-link to="/user">首页</router-link>
<router-link to="/user/profile">个人资料</router-link>
<router-link to="/user/posts">我的文章</router-link>
</nav>
<!-- 子路由渲染出口 -->
<router-view></router-view>
</div>
</template>
面试官真正想听什么
这题考察你对路由层级结构的理解和复杂应用的组织能力。嵌套路由是大型应用必备,不会用说明没做过复杂项目。
加分回答
"我在管理后台项目中用嵌套路由实现了清晰的页面结构:
场景:管理后台的布局
所有管理页面共享同一个布局(顶部导航+侧边栏+内容区),内容区根据路由动态切换:
const routes = [
{
path: '/admin',
component: AdminLayout, // 通用布局组件
children: [
{
path: '',
redirect: 'dashboard' // 访问/admin重定向到dashboard
},
{
path: 'dashboard',
component: Dashboard,
meta: { title: '仪表盘' }
},
{
path: 'users',
component: UserManage,
children: [
{
path: '',
component: UserList
},
{
path: 'add',
component: UserAdd
},
{
path: 'edit/:id',
component: UserEdit
}
]
},
{
path: 'products',
component: ProductManage,
children: [
{
path: '',
component: ProductList
},
{
path: 'add',
component: ProductAdd
}
]
}
]
}
]
三层嵌套的结构:
- 第一层:
/admin- AdminLayout(整体布局) - 第二层:
/admin/users- UserManage(用户管理模块布局) - 第三层:
/admin/users/edit/123- UserEdit(具体编辑页面)
AdminLayout.vue:
<template>
<div class="admin-layout">
<Header />
<div class="main">
<Sidebar />
<div class="content">
<!-- 二级路由出口 -->
<router-view></router-view>
</div>
</div>
</div>
</template>
UserManage.vue:
<template>
<div class="user-manage">
<div class="toolbar">
<router-link to="/admin/users">用户列表</router-link>
<router-link to="/admin/users/add">新增用户</router-link>
</div>
<!-- 三级路由出口 -->
<router-view></router-view>
</div>
</template>
嵌套路由的优势:
- 代码组织清晰:功能模块按层级划分,维护方便
- 布局复用:公共布局定义一次,所有子页面共享
- 权限控制方便:可以在父路由统一控制整个模块的权限
注意事项:
- 子路由path是相对路径:
'profile'会被拼接成'/user/profile' - 空path作为默认子路由:访问父路径时显示默认内容
- 每层都需要router-view:忘记加router-view,子路由不显示
实际踩过的坑:
最开始子路由写成了path: '/profile'(绝对路径),结果匹配不到父路由,单独成了一个顶级路由。后来才知道子路由不能以/开头,要用相对路径。
这种层级结构让大型应用的路由管理变得井然有序。"
减分回答
❌ 不知道children的作用(基础不扎实)
❌ 子路由path写绝对路径(概念错误)
❌ 忘记在父组件加router-view(粗心)
37. Vue Router路由传参的方式有哪些?params和query的区别?
速记公式:params路径参数,query查询字符串,props解耦,各有场景
标准答案
Vue Router有三种主要的路由传参方式:
1. params传参(路径参数):
需要在路由配置中预定义参数占位符,参数成为URL路径的一部分。
// 路由配置
{ path: '/user/:id', component: User }
// 跳转
this.$router.push({ name: 'user', params: { id: 123 } })
// URL: /user/123
// 获取
this.$route.params.id // 123
特点:
- 参数不在URL查询字符串中显示
- 页面刷新后params会丢失(除非参数是路径的一部分)
- 必须使用name跳转,不能用path
2. query传参(查询参数):
参数以查询字符串形式附加在URL后面。
// 跳转
this.$router.push({ path: '/search', query: { keyword: 'vue', page: 1 } })
// URL: /search?keyword=vue&page=1
// 获取
this.$route.query.keyword // 'vue'
this.$route.query.page // '1'
特点:
- 参数在URL中可见且持久化
- 页面刷新后参数依然存在
- 可以用path或name跳转
3. props传参(解耦方式):
在路由配置中设置props: true,将路由参数作为组件props传入。
// 路由配置
{
path: '/user/:id',
component: User,
props: true // 或者函数: props: route => ({ id: route.params.id })
}
// 组件中
export default {
props: ['id'],
mounted() {
console.log(this.id) // 直接用props,不用$route.params
}
}
核心区别:
| 特性 | params | query |
|---|---|---|
| URL显示 | 路径一部分 /user/123 | 查询字符串 ?id=123 |
| 刷新后 | 丢失(除非在路径中) | 保留 |
| 跳转方式 | 必须用name | path或name都可以 |
| 使用场景 | 资源标识、内部传递 | 筛选条件、公开参数 |
面试官真正想听什么
这题考察你对不同传参方式的理解和实际应用场景的判断。选错传参方式,要么影响用户体验,要么影响SEO。
加分回答
"我在项目中根据不同场景选择不同的传参方式:
用params的场景:资源ID、内部传递
- 商品详情页
// 路由配置
{ path: '/product/:id', component: ProductDetail, props: true }
// 跳转
goToDetail(id) {
this.$router.push({ name: 'product', params: { id } })
}
// URL: /product/123
优势: URL简洁,符合RESTful风格,/product/123比/product?id=123更规范。
- 表单编辑传递完整对象
// 列表页跳转到编辑页,传递整个用户对象
editUser(user) {
this.$router.push({
name: 'userEdit',
params: { userData: user }
})
}
// 编辑页获取
mounted() {
const user = this.$route.params.userData
}
注意: 这种方式刷新后数据会丢失,适合临时传递,不适合需要持久化的场景。
用query的场景:筛选条件、需要持久化
1.搜索页面
search(keyword) {
this.$router.push({
path: '/search',
query: { keyword, category: 'all', page: 1 }
})
}
// URL: /search?keyword=vue&category=all&page=1
优势:
- 用户可以收藏/分享链接,打开后保持相同的搜索状态
- 刷新页面保留筛选条件,用户体验好
- 利于SEO,搜索引擎能索引不同的筛选结果
2.分页列表
changePage(page) {
this.$router.push({
path: '/list',
query: {
...this.$route.query,
page
}
})
}
用props的场景:组件解耦
// 路由配置
{
path: '/article/:id',
component: ArticleDetail,
props: route => ({
id: Number(route.params.id), // 类型转换
category: route.query.category
})
}
// 组件中
export default {
props: {
id: {
type: Number,
required: true
},
category: String
},
mounted() {
// 直接用props,不依赖$route
// 方便组件测试和复用
this.fetchArticle(this.id)
}
}
踩过的坑:
1.params用path跳转,参数丢失
// 错误
this.$router.push({ path: '/user', params: { id: 123 } })
// params会被忽略
// 正确
this.$router.push({ name: 'user', params: { id: 123 } })
2.query参数类型问题
// URL中的query都是字符串
this.$route.query.page // '1' 不是数字1
// 需要手动转换
const page = Number(this.$route.query.page) || 1
选择标准:
- 资源标识用params(商品ID、文章ID)
- 筛选条件用query(搜索关键词、分类、页码)
- 临时数据用params(表单对象)
- 需要分享的用query(让URL能完整还原状态)
这让我明白:传参方式的选择不仅影响代码实现,更影响用户体验和SEO。"
减分回答
❌ 不知道params和query的区别(基础不扎实)
❌ 用params传需要持久化的数据(场景判断错误)
❌ 不知道props传参方式(缺少解耦意识)
38. Vue Router如何实现路由权限控制?登录拦截的实现?
速记公式:beforeEach拦截,meta存权限,token验证,重定向登录
标准答案
路由权限控制主要通过导航守卫实现,核心是在beforeEach全局前置守卫中检查用户权限。
基础实现步骤:
1. 在路由配置中添加meta字段标识权限:
const routes = [
{
path: '/login',
component: Login,
meta: { requiresAuth: false }
},
{
path: '/dashboard',
component: Dashboard,
meta: { requiresAuth: true, role: 'user' }
},
{
path: '/admin',
component: Admin,
meta: { requiresAuth: true, role: 'admin' }
}
]
2. 在beforeEach中编写拦截逻辑:
router.beforeEach((to, from, next) => {
const token = localStorage.getItem('token')
// 检查目标路由是否需要认证
if (to.meta.requiresAuth) {
if (!token) {
// 未登录,跳转登录页,记录目标路径用于登录后跳转
next({ path: '/login', query: { redirect: to.fullPath } })
} else {
// 已登录,检查角色权限
const userRole = store.state.user.role
if (to.meta.role && to.meta.role !== userRole) {
// 权限不足
next('/403')
} else {
next()
}
}
} else {
next() // 不需要认证的路由直接通过
}
})
3. 登录成功后跳转到目标页面:
async login() {
const res = await loginApi(this.form)
if (res.success) {
localStorage.setItem('token', res.token)
// 跳转到登录前想访问的页面
const redirect = this.$route.query.redirect || '/dashboard'
this.$router.replace(redirect)
}
}
面试官真正想听什么
这题是判断你会不会做权限系统的核心题。权限控制是所有管理后台的必备功能,实现不好会有安全漏洞。
加分回答
"我在管理后台项目中实现了完整的权限控制系统:
方案1:基于角色的权限控制(RBAC)
// 路由配置
const routes = [
{
path: '/admin',
component: AdminLayout,
meta: { roles: ['admin', 'super_admin'] },
children: [
{
path: 'users',
component: UserManage,
meta: { roles: ['admin', 'super_admin'], permission: 'user:view' }
},
{
path: 'settings',
component: Settings,
meta: { roles: ['super_admin'], permission: 'system:config' }
}
]
}
]
// 权限检查函数
function hasPermission(userRoles, routeRoles) {
if (!routeRoles || routeRoles.length === 0) return true
return routeRoles.some(role => userRoles.includes(role))
}
// beforeEach守卫
router.beforeEach(async (to, from, next) => {
const token = getToken()
if (token) {
if (to.path === '/login') {
next({ path: '/' })
} else {
// 检查是否已获取用户信息
if (!store.state.user.roles || store.state.user.roles.length === 0) {
try {
// 获取用户信息和权限
const { roles, permissions } = await store.dispatch('user/getInfo')
// 根据权限动态生成路由
const accessRoutes = await store.dispatch('permission/generateRoutes', roles)
// 动态添加路由
accessRoutes.forEach(route => {
router.addRoute(route)
})
// 重新跳转,确保addRoute生效
next({ ...to, replace: true })
} catch (error) {
// token过期或获取用户信息失败
await store.dispatch('user/logout')
next(`/login?redirect=${to.path}`)
}
} else {
// 已有用户信息,检查权限
if (hasPermission(store.state.user.roles, to.meta.roles)) {
next()
} else {
next({ path: '/403' })
}
}
}
} else {
// 白名单:不需要登录的页面
const whiteList = ['/login', '/register', '/forgot-password']
if (whiteList.includes(to.path)) {
next()
} else {
next(`/login?redirect=${to.path}`)
}
}
})
方案2:动态路由(根据权限生成路由表)
// permission.js - Vuex模块
const state = {
routes: [],
addRoutes: []
}
const mutations = {
SET_ROUTES: (state, routes) => {
state.addRoutes = routes
state.routes = constantRoutes.concat(routes)
}
}
const actions = {
generateRoutes({ commit }, roles) {
return new Promise(resolve => {
let accessedRoutes
// admin能访问所有路由
if (roles.includes('admin')) {
accessedRoutes = asyncRoutes
} else {
// 根据角色过滤路由
accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
}
commit('SET_ROUTES', accessedRoutes)
resolve(accessedRoutes)
})
}
}
// 递归过滤路由
function filterAsyncRoutes(routes, roles) {
const res = []
routes.forEach(route => {
const tmp = { ...route }
if (hasPermission(roles, tmp)) {
if (tmp.children) {
tmp.children = filterAsyncRoutes(tmp.children, roles)
}
res.push(tmp)
}
})
return res
}
方案3:按钮级权限控制
// 自定义指令
app.directive('permission', {
mounted(el, binding) {
const { value } = binding
const permissions = store.state.user.permissions
if (value && value.length > 0) {
const hasPermission = permissions.some(permission => {
return value.includes(permission)
})
if (!hasPermission) {
el.parentNode && el.parentNode.removeChild(el)
}
}
}
})
// 使用
<button v-permission="['user:delete']">删除用户</button>
实际踩过的坑:
1.token过期处理:最开始只在登录时存token,接口401时才发现过期。后来改成接口拦截器统一处理,401自动跳登录。
2.刷新页面权限丢失:动态添加的路由刷新后丢失,要在beforeEach里重新获取权限并添加路由。
3.路由无限循环:在beforeEach里跳转时,如果条件写错,会无限循环调用守卫。要确保每个分支都有明确的next()。
完整的权限控制系统应该包括:
- 路由级权限(能不能访问页面)
- 按钮级权限(能不能执行操作)
- 数据级权限(能看到哪些数据)
- 接口级权限(后端验证)
这让我理解:前端权限控制是用户体验的保障,但真正的安全还要靠后端。"
减分回答
❌ 只在前端判断权限(没有安全意识)
❌ 不知道怎么处理token过期(实战经验少)
❌ 没有考虑动态路由(只会写死路由表)
总结
这8道Vue Router题,是面试的实战考场。路由不只是配置,更是应用架构的体现。
每道题的核心不是API怎么用,而是:
为什么这样设计路由(架构思维)
遇到过什么路由相关的问题、怎么解决的(问题解决能力)
如何平衡用户体验和技术实现(产品思维)
高频挂科点:
- 说不出hash和history的部署区别,不知道history需要服务器配置
- 不知道动态路由会复用组件,数据不更新找不到原因
- 守卫执行顺序搞不清,权限控制逻辑混乱
- 不会做路由懒加载,首屏加载慢
- 权限控制只检查登录,不检查角色和按钮权限
面试加分技巧:
- 讲路由设计时带上业务场景:不要只说技术,说清楚为什么这么设计
- 提到遇到的坑和解决方案:证明你有实战经验
- 谈性能优化:懒加载、预加载、分组打包
- 说出权限控制的完整方案:不只是登录拦截,还有角色、按钮、数据权限
接下来该做什么:
- 检查项目的路由配置:有没有做懒加载、权限控制是否完善
- 梳理路由设计思路:能清晰说出为什么这样设计
- 优化首屏性能:用Chrome DevTools看看能不能再优化
下一篇我会讲Vuex/Pinia状态管理的7道题:核心概念、模块化、异步处理...
Vuex这7题,是面试官用来判断你'是真懂状态管理,还是只会复制粘贴代码'的试金石。
最近好多同学挂在 HR 面而不知道为什么,这些问题你都会吗:
- 对于互联网公司的快节奏工作,你是怎么看的?
- 能说说你印象最深的一次团队合作经历吗?
- 你觉得工作和生活应该怎么平衡?
- 你觉得什么样的公司文化最吸引你?
没有答题思路? 快来牛面题库看看吧,这是我们共同打造的面试学习一站式平台,拥有丰富的免费题库资源,AI模拟面试等等功能,加入我们,早日斩获Offer吧。
留言区互动:
你项目里的路由权限是怎么做的?遇到过什么路由相关的难题?
评论区说说,点赞最高的问题我会专门分析解决方案。