Vue Router这8题:80%的人挂在"讲讲你的路由设计"

115 阅读14分钟

前言

"讲讲你项目的路由设计"——这是Vue二面最高频的问题,也是 80% 候选人的滑铁卢。

上周面试了个候选人,简历上写 "负责整个项目的路由架构"。我问他:"你们项目的路由权限是怎么控制的?"他愣了5秒,说:"就...配了个meta字段,然后在beforeEach里判断。"我追问:"具体怎么判断的?遇到过什么问题?"他说不出来。

这就是问题所在。不是你不会用router,是你不知道怎么把'写了几个路由配置'包装成'有架构思维的路由设计'。

路由管理藏着太多细节:权限控制怎么做、为什么用懒加载导航守卫用在哪。今天这8题会告诉你,面试官到底想从路由问题里看出什么。

欢迎阅读我的Vue专栏文章

Vue基础10题:答不上来的,简历别写"熟悉Vue"

组件化这12题答不好,面试官直接判定"项目经验水分大"

VUE响应式原理是分水岭:答对这8题的人,薪资直接高3K

Vuex面试7题:你以为的"会用",在面试官眼里都是"不懂原理"

2025还不会Vue3?这5题答不上来,直接失去竞争力

31. Vue Router的工作原理?hash模式和history模式的区别?

速记公式:监听URL,匹配路由,渲染组件,两种模式各有场景

标准答案

Vue Router是一个路由管理器,通过监听URL变化来渲染对应的组件,实现单页应用的页面切换。

核心工作流程:

  1. URL发生变化
  2. Router匹配预定义的路由规则
  3. 找到对应的组件
  4. 渲染到<router-view>

Hash模式(默认):

使用URL中的hash部分(#后面的内容)管理路由,如http://example.com/#/user/123特点是hash变化不会触发页面刷新,只会触发hashchange事件,Vue Router监听这个事件来切换组件。

优势:

  • 兼容性好,所有浏览器都支持
  • 不需要服务器配置,服务器会忽略hash部分
  • 部署简单,上传静态文件就能跑

History模式:

利用HTML5的History API(pushStatereplaceState)管理路由,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/aboutdomain.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:离开当前路由时执行,常用于未保存数据提示

完整执行顺序:

  1. 离开组件的beforeRouteLeave
  2. 全局beforeEach
  3. 路由独享beforeEnter
  4. 进入组件的beforeRouteEnter
  5. 全局beforeResolve
  6. 路由跳转确认
  7. 全局afterEach
  8. DOM更新
  9. 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
    })
  })
}

踩过的坑:

  1. 忘记调用next():导航守卫里不调next(),路由会卡住不跳转
  2. next()调用多次:一个守卫里调了多次next(),会报错
  3. 异步验证没处理:在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的场景:保留访问历史

  1. 商品列表→详情页
goToProduct(id) {
  this.$router.push({ name: 'productDetail', params: { id } })
}
// 用户看完详情可以后退回列表
  1. 搜索结果点击
search(keyword) {
  this.$router.push({
    path: '/search',
    query: { keyword }
  })
}
// 保留搜索历史,方便用户对比

用replace的场景:不保留历史

  1. 登录成功跳转
async login() {
  const res = await loginApi(this.form)
  if (res.success) {
    // 用replace,防止用户后退到登录页
    this.$router.replace('/dashboard')
  }
}
  1. 重定向场景
// 访问/old-page自动跳转到/new-page
router.beforeEach((to, from, next) => {
  if (to.path === '/old-page') {
    next({ path: '/new-page', replace: true })
  } else {
    next()
  }
})
  1. 表单提交成功
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('/')
  }
}

参数传递的注意事项:

  1. params只能用name
// 错误:params必须用name,不能用path
this.$router.push({ path: '/user', params: { id: 123 } })

// 正确
this.$router.push({ name: 'user', params: { id: 123 } })
  1. 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.2MB500KB500KB
首屏加载8s1.8s1.8s
二次页面即时300ms100ms

注意事项:

  1. 不要过度拆分:每个chunk都有加载开销,太多小chunk反而慢
  2. 公共依赖提取:配置webpack的splitChunks,避免重复打包
  3. 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
      }
    ]
  }
]

关键点:

  1. 子路由path不需要斜杠开头(相对路径),Vue Router会自动拼接父路径
  2. 父组件必须包含<router-view>标签作为子路由的渲染出口
  3. 空字符串path表示默认子路由,访问父路径时自动显示
  4. 支持无限层级嵌套,满足复杂应用的路由组织需求

父组件示例:

<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>

嵌套路由的优势:

  1. 代码组织清晰:功能模块按层级划分,维护方便
  2. 布局复用:公共布局定义一次,所有子页面共享
  3. 权限控制方便:可以在父路由统一控制整个模块的权限

注意事项:

  1. 子路由path是相对路径'profile'会被拼接成'/user/profile'
  2. 空path作为默认子路由:访问父路径时显示默认内容
  3. 每层都需要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
  }
}

核心区别:

特性paramsquery
URL显示路径一部分 /user/123查询字符串 ?id=123
刷新后丢失(除非在路径中)保留
跳转方式必须用namepath或name都可以
使用场景资源标识、内部传递筛选条件、公开参数

面试官真正想听什么

这题考察你对不同传参方式的理解和实际应用场景的判断。选错传参方式,要么影响用户体验,要么影响SEO。

加分回答

"我在项目中根据不同场景选择不同的传参方式:

用params的场景:资源ID、内部传递

  1. 商品详情页
// 路由配置
{ 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更规范。

  1. 表单编辑传递完整对象
// 列表页跳转到编辑页,传递整个用户对象
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怎么用,而是:

为什么这样设计路由(架构思维)

遇到过什么路由相关的问题、怎么解决的(问题解决能力)

如何平衡用户体验和技术实现(产品思维)

高频挂科点:

  1. 说不出hash和history的部署区别,不知道history需要服务器配置
  2. 不知道动态路由会复用组件,数据不更新找不到原因
  3. 守卫执行顺序搞不清,权限控制逻辑混乱
  4. 不会做路由懒加载,首屏加载慢
  5. 权限控制只检查登录,不检查角色和按钮权限

面试加分技巧:

  1. 讲路由设计时带上业务场景:不要只说技术,说清楚为什么这么设计
  2. 提到遇到的坑和解决方案:证明你有实战经验
  3. 谈性能优化:懒加载、预加载、分组打包
  4. 说出权限控制的完整方案:不只是登录拦截,还有角色、按钮、数据权限

接下来该做什么:

  1. 检查项目的路由配置:有没有做懒加载、权限控制是否完善
  2. 梳理路由设计思路:能清晰说出为什么这样设计
  3. 优化首屏性能:用Chrome DevTools看看能不能再优化

下一篇我会讲Vuex/Pinia状态管理的7道题:核心概念、模块化、异步处理...

Vuex这7题,是面试官用来判断你'是真懂状态管理,还是只会复制粘贴代码'的试金石。

最近好多同学挂在 HR 面而不知道为什么,这些问题你都会吗:

  1. 对于互联网公司的快节奏工作,你是怎么看的?
  2. 能说说你印象最深的一次团队合作经历吗?
  3. 你觉得工作和生活应该怎么平衡?
  4. 你觉得什么样的公司文化最吸引你?

没有答题思路? 快来牛面题库看看吧,这是我们共同打造的面试学习一站式平台,拥有丰富的免费题库资源,AI模拟面试等等功能,加入我们,早日斩获Offer吧。

留言区互动:

你项目里的路由权限是怎么做的?遇到过什么路由相关的难题?

评论区说说,点赞最高的问题我会专门分析解决方案。