ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Vue 3路由实战:Vue Router 4核心配置与守卫权限详解

Vue 3路由实战:Vue Router 4核心配置与守卫权限详解 刚把 Vue 3 的组合式 API 和组件通信过了一遍紧接着就碰到路由这一大块。不管是写后台管理系统还是做移动端 H5路由都是绕不开的骨架。我之前在真实项目里栽过不少跟头——比如刷新页面 404、路由守卫把用户卡在登录页出不来、动态路由加完白屏……这次趁着整理《Vue 3 入门与实践复习手册》系列的第三篇把路由这部分重新梳理了一遍顺便把踩过的坑和排查思路一并记录下来。这篇东西适合刚学完 Vue 3 基础语法、想往实战项目迈一步的读者也适合准备面试前快速把路由相关知识过一遍的同学。Vue Router 4 是 Vue 3 配套的路由方案底层和 Vue Router 3 有几次关键调整比如废弃了旧的new Router()写法、全面拥抱组合式 API。这篇复习手册不会讲太多源码层面的设计重点放在“路由表怎么搭”“页面之间怎么跳转”“权限拦截怎么实现”“嵌套路由和动态路由怎么处理”这些实际开发天天要碰的点上。1. 先把路由的核心逻辑理清楚1.1 路由到底解决什么问题很多人一开始学路由会觉得它就是“URL 变了页面变了”这么简单但在真实项目里路由承担的事情远比这个多。我在实际开发里对路由的理解是这样的它是页面状态和 URL 之间的一座桥。用户点了一个菜单URL 变了对应的页面组件加载出来用户刷新浏览器URL 还在应用能根据地扯把页面状态还原出来。如果没有路由Vue 应用就只是一个永远停留在 index.html 里的单页应用所有页面切换只能靠组件 v-if 来控制刷新一下就回到初始状态浏览器前进后退按钮完全失效。Vue Router 的核心由三块拼起来路由表routes、路由视图router-view、路由跳转router-link 或 useRouter。路由表定义了 URL 路径和组件之间的对应关系router-view 是组件挂载的出口位置跳转则负责改变 URL 并触发视图更新。这个三角结构看着简单却是后面理解懒加载、嵌套路由、路由守卫这些东西的基础。1.2 Vue Router 4 与 Vue 3 的配合方式Vue Router 4 和以前相比有个最直观的变化写法从“new 一个全局实例”变成了“通过 createRouter 创建实例”。这个变化的背后逻辑是让路由实例和 Vue 应用实例的生命周期更紧密地关联在一起也更契合 Vue 3 的按需引入和 tree-shaking 思路。// router/index.js import { createRouter, createWebHistory } from vue-router import Home from ../views/Home.vue const router createRouter({ history: createWebHistory(), routes: [ { path: /, name: home, component: Home } ] }) export default router然后在 main.js 里通过app.use(router)把路由实例注册进应用。这里有个细节history 模式的选择。createWebHistory 对应的 History 模式 URL 是http://localhost:8080/home这种简洁路径而 createWebHashHistory 对应的是http://localhost:8080/#/home。开发阶段两者都无所谓但上线部署到 Nginx 或其他静态服务器时History 模式要额外配置 try_files 把所有路径回退到 index.html否则用户直接访问/home这个地址会 404。我个人的建议是开发 API 由后端控制的内部项目可以优先用 History 模式注意部署期配置纯静态演示项目或者无法控制服务器配置的场景直接用 Hash 模式少惹麻烦。2. 路由表搭建时的几个实际选择2.1 普通加载与懒加载的取舍路由表里component这一项可以直接写() import(...)实现组件懒加载也可以直接import一个组件进来。这个选择直接决定打包出来的 JS 文件体积和首屏加载速度。{ path: /about, name: about, // 懒加载访问到 /about 时才下载这段代码 component: () import(../views/About.vue) }我在真实项目里的经验是所有页面级组件都用懒加载初期就把这个习惯养成后面项目大了不用返工。但要注意一个坑如果你的路由是通过动态方式从接口获取后addRoute加进来的动态加路由时传入的 component 也要用懒加载写法否则接口返回之前那段代码可能已经全部打进主包里了。2.2 命名路由与路径跳转并存vue-router 跳转页面有两种常见姿势用path直接跳或者用name跳。刚上手时我习惯写 path后来发现项目里到处硬编码路径一旦路由结构调整所有引用全得跟着改。用 name 跳转的好处是路径变化时不需要全局搜索替换。// 模板里 router-link :to{ name: productDetail, params: { id: 123 } }查看详情/router-link // 组合式 API 里 const router useRouter() router.push({ name: productDetail, params: { id: 123 } })不过在代码里传参数时还得注意一个细节params 只在 name 跳转时有效。如果写router.push({ path: /product, params: { id: 123 } })params 会被直接忽略掉不会出现在 URL 上目标页面拿不到参数。很多人第一次踩这个坑时一脸懵查了半天发现传参根本没生效就是这个原因。2.3 重定向、别名与 404 兜底路由表里除了正常匹配还有几个冷门但实用的配置项。redirect用于把某个路径直接重定向到另一个路径适合做“旧链接跳新地址”和“根路径进首页”这类需求。alias则是给同一个组件挂多个路径比如移动端有时候同一个页面既想用/user又想用/member访问alias 就能解决。404 兜底路由这个配置几乎每个项目都该有不然用户乱敲一个地址会看到白屏{ path: /:pathMatch(.*)*, name: not-found, component: () import(../views/NotFound.vue) }这里要特别注意通配符的写法。Vue Router 4 不再支持*这种简单通配写法必须要写成/:pathMatch(.*)*否则启动时直接报错说缺参数。我刚从 Vue Router 3 迁到 4 时就踩过这个坑看起来是小改动但文档里不细看真发现不了。3. 动态路由、路由传参与页面衔接3.1 动态路由参数的声明与接收商品详情页、文章页这类场景路由路径里往往带一个动态变化的 ID比如/article/42和/article/18。配置路由表时用冒号声明动态段{ path: /article/:id, name: article, component: () import(../views/Article.vue }在页面组件里拿这个参数有两种方式。Vue Router 4 里推荐用useRoute获取整个路由对象然后取route.params.id。如果页面是用script setup写的直接这样import { useRoute } from vue-router const route useRoute() console.log(route.params.id)另一种方式是开 props 传参。在路由配置里加上props: true动态段就会被当作组件的 props 传进去页面组件里用defineProps声明接收。这种方式对组件复用更友好测试和以后改造都方便。我实际项目里看团队习惯有的老项目喜欢在组件里读route.params新一点的团队普遍改成 props 解耦。3.2 页面复用时参数响应的坑动态路由有个很隐蔽的坑从/article/42直接跳转到/article/18组件实例不会重新创建onMounted不会再次触发。如果你只在 onMounted 里根据参数拉详情数据切换文章时会发现页面内容没变。解决思路有两种。最简单的办法是监听route.params.id的变化import { watch } from vue import { useRoute } from vue-router const route useRoute() watch(() route.params.id, (newId, oldId) { // 根据新 id 重新拉数据 })另一个办法是在路由跳转的目标组件里加:keyroute.params.id但实际操作中不太推荐——强制重建整个组件代价较大远不如在组件内部 watch 一下参数来得轻量。我在详情页这种场景基本都采用 watch 方案。3.3 query 传参与业务状态恢复除了 params业务里大量使用 query 传参比如列表页点击筛选后把筛选条件挂到 URL 上。这种方式的优势是刷新页面后状态还在可以分享链接让别人看到同样的筛选结果。router.push({ path: /product-list, query: { category: digital, page: 2, keyword: 键盘 } })接收端用route.query.category读取。真实项目里我会把 query 里的值都当作字符串处理——因为 URL 上的参数天然就是字符串传数字类型page: 2取出来会变成2。如果不注意类型转换直接拿来做条件判断或者传给后端很容易出现隐藏 bug。我在开发中习惯在拿到 query 参数后马上统一处理const page Number(route.query.page ?? 1) const category String(route.query.category ?? all)4. 嵌套路由与布局结构设计4.1 嵌套路由适合什么场景后台管理系统几乎都会遇到这种布局顶部有导航左侧有菜单右侧是内容区。切换菜单时顶栏和侧边栏不变只有内容区变化。这种结构用嵌套路由表达非常清晰父路由对应整个布局组件子路由对应具体的页面。{ path: /admin, component: () import(../layouts/AdminLayout.vue), children: [ { path: dashboard, name: admin-dashboard, component: () import(../views/admin/Dashboard.vue) }, { path: users, name: admin-users, component: () import(../views/admin/UserList.vue) } ] }这里有个容易混淆的点子路由的 path 不要以/开头除非想写绝对路径。以/开头的子路径会被当成根路径处理嵌套结构就失效了。我之前就因为多写了个斜杠怎么点菜单都不渲染到子路由查了半天才发现是路径拼写的问题。4.2 默认子路由与 child 渲染出口嵌套路由对应到页面组件里必须在父组件中放一个router-view子路由渲染的内容才会显示出来。这个 router-view 就是布局组件的内容出口!-- AdminLayout.vue -- template div classadmin-layout Sidebar / div classadmin-content router-view / /div /div /template如果访问父路径/admin时想默认展示某个子页面可以把子路由的 path 写成空字符串{ path: dashboard, name: admin-dashboard, component: () import(../views/admin/Dashboard.vue) }也就是path: 那个子路由会被当作默认渲染内容。大多数后台系统的做法是父路径直接redirect到某个默认子路由这样地址栏显示得更明确。两种方案都能用redirect 对用户更友好一些因为地址栏能看到具体页面路径。4.3 嵌套路由的权限联动嵌套路由和权限控制结合得比较紧。后台管理系统的菜单权限往往按模块划分比如“用户管理”“订单管理”它们对应嵌套路由的子路由。我在项目里一般把权限标识放到路由的meta字段上{ path: users, name: admin-users, component: () import(../views/admin/UserList.vue), meta: { title: 用户管理, permission: admin:user:list } }然后在全局守卫里检查to.meta.permission是否在用户的权限列表里。这种方式的好处是权限和路由紧密绑定菜单渲染和路由拦截共用同一个数据源。5. 路由跳转的多种方式与页面过渡5.1 声明式跳转与编程式跳转router-link是模板里最常用的声明式跳转方式。它最终会渲染成a标签同时自带激活样式控制默认会给当前激活的路由加上router-link-active类。router-link to/ exact首页/router-link router-link :to{ name: about }关于/router-link遇到需要判断条件、登录后才能跳转、或者按钮点击时跳转的场景就得使用编程式跳转。在script setup里调用useRouterconst router useRouter() function handleGoDetail(id) { if (!id) return router.push({ name: productDetail, params: { id } }) }router.push相当于往历史栈里加一条记录用户点浏览器返回键能回到上一个页面。如果某个操作不想留下历史记录比如登录成功后跳首页不希望用户点返回又回到登录页就用router.replace。我在登录跳转这个场景几乎都是用的 replace。5.2 路由过渡动画与滚动位置页面切换时加一点淡入淡出效果会显得应用更精致。Vue 3 过渡动画配合 router-view 的使用方式是router-view v-slot{ Component } transition namefade modeout-in component :isComponent / /transition /router-viewmodeout-in的意思是先让旧页面完全退出再让新页面进入避免两个页面同时渲染导致的高度跳动。很多新手不知道这个配置结果动画切换时两个页面叠在一起样式乱糟糟的。滚动位置处理是另一个容易忽略的细节。页面滚动到一半点击跳转到另一个路由新页面默认可能停留在旧页面的滚动位置。可以在路由配置里加 scrollBehaviorconst router createRouter({ history: createWebHistory(), routes, scrollBehavior(to, from, savedPosition) { if (savedPosition) { return savedPosition } return { top: 0 } } })这样每次切换路由都回到顶部而浏览器前进后退时会恢复到之前保存的位置体验更接近原生多页应用。6. 路由守卫与权限控制6.1 全局前置守卫的核心逻辑路由守卫是权限控制的最主要阵地。全局前置守卫通过beforeEach注册每次路由跳转前都会执行router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next({ name: login, query: { redirect: to.fullPath } }) return } next() })这个例子展示了一个最常见的场景目标路由要求登录态但本地没有 token就把用户撵到登录页并且带上回跳地址。登录成功之后能根据redirect参数跳回原来想去的页面这个交互在很多后台系统里都能看到。6.2 不要在守卫里瞎用 next这里重点说一个 Vue Router 4 里被强调过很多次的坑next函数虽然还没被移除但官方已经不那么推荐使用了。现在的写法是在守卫里直接返回目标路由或 true/falserouter.beforeEach((to) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { return { name: login, query: { redirect: to.fullPath } } } return true })两种写法在功能上等价。但我在团队 code review 时会更倾向新的返回式写法因为next一旦被调用多次容易出问题——比如写了个条件分支忘了 return 又执行了 next导致守卫被重复调用。直接返回结果的写法更直观逻辑分支一目了然。6.3 权限控制与动态路由的配合真正带权限的动态路由实现起来要复杂一些。常见做法是登录成功后后端返回当前用户的角色和权限点列表前端根据这个列表筛选出用户能访问的路由表然后用router.addRoute动态注册进去。// 用户登录后拉取的权限路由 const asyncRoutes [ { path: /admin, component: () import(../layouts/AdminLayout.vue), children: [ { path: users, component: () import(../views/admin/UserList.vue) } ] } ] asyncRoutes.forEach(route { router.addRoute(route) })这里有两个容易被忽略的点。一是菜单配置和路由注册最好共用同一份数据避免“路由已经加进去了但菜单没显示”这种不一致问题。二是动态路由注册之后别忘了用next({ ...to, replace: true })或类似方案重新触发一次导航否则用户刷新页面后可能因为路由表还是空白的而直接落到 404。我在项目里就是先在全局守卫里判断“当前路由是否已注册、用户是否有动态权限”如果没注册就先注册再重新跳转这套逻辑一开始很容易漏。6.4 路由 meta 的扩展用法除了权限控制meta 还可以存页面标题、图标、缓存标记、面包屑层级信息等。比如页面标题这块配合全局守卫就能自动设置浏览器标签页标题router.afterEach((to) { const baseTitle 后台管理系统 const pageTitle to.meta.title document.title pageTitle ? ${pageTitle} - ${baseTitle} : baseTitle })keep-alive 缓存页面的场景也有妙用。列表页到详情页返回时希望列表页的筛选条件还在就可以用 meta.keepAlive 标记需要缓存的页面配合keep-alive判断。这个部分和路由本身结合得很紧密后面复习手册里如果讲到页面缓存我会单独展开说。7. 常见问题与排查技巧实录7.1 页面刷新后 404白屏不知所措这个问题真的高频出现。排查思路分两步先看项目用的什么模式。如果是createWebHistory 静态服务器部署多半是服务器没做 fallback。Nginx 配置加上这句就能解决location / { try_files $uri $uri/ /index.html; }如果是开发环境下刷新 404一般不是服务器问题而是路由表配置有误——比如某个路径没写对或者动态路由没注册成功。开发环境出现 404 时我都先打开浏览器 Network 面板看接口请求判断是静态资源请求 404 还是路由匹配不上的问题。7.2 路由跳转 url 变了但页面没变这个问题常见于嵌套路由场景。URL 变了router-view 却没更新第一反应检查子路由 path 是不是多了个斜杠。比如在 children 里写path: /users它会被当成根路由处理和/admin并列。在嵌套路由里子路由 path 如果不以/开头则会自动继承父路由的前缀这是最容易被忽略的一点。另一个原因是 router-view 放错了位置。多个命名视图时不带 name 的 router-view 只渲染默认出口。如果父组件里放了两个 router-view内容渲染到了另一个视图出口看起来就像“跳转没生效”。7.3 路由跳转后组件内的状态丢失页面切走再切回来组件内的搜索条件、表单内容、列表滚动位置全没了。这个严格来说不是 bug而是 Vue 组件默认行为——路由切换会卸载旧组件。如果业务上确实需要保留状态方案是 keep-aliverouter-view v-slot{ Component } keep-alive component :isComponent / /keep-alive /router-view但要注意 keep-alive 是全局缓存所有页面都会被缓存。想精准控制哪些页面缓存、哪些不缓存就得配合 meta.keepAlive 标记做动态判断或者设置include数组。缓存名字用的是组件的 name所以用script setup写组件时如果想配合 keep-alive 使用需要额外用 defineOptions 显式声明组件名。7.4 路由守卫循环跳转页面疯狂重定向写守卫时脑一抽可能写出无限重定向。典型的错误是“用户没有 token 就跳到登录页但登录页也被全局守卫拦截又因为没有 token 再次跳登录页”。这种问题表现是页面不断刷新或者提示重定向过多。排查方法很直接在守卫里加console.log(to.path)跑一下看路径是不是在反复横跳。防范办法就是在跳转之前判断目标是不是已经在登录页if (to.meta.requiresAuth !token) { if (to.name login) { return true } return { name: login } }7.5 动态路由刷新后失效动态路由接口返回的数据存在内存里刷新页面后接口重新请求之前路由表一直是空白的。这时候访问动态路由对应的路径vue-router 匹配不到就会直接落到 404。我的做法是在全局守卫里加一个“是否已初始化动态路由”的标记存到内存或 localStorage 都行。每次导航前检查这个标记如果没初始化就重新拉权限接口、重新 addRoute然后手动跳转一次。这个流程一旦跑通后面刷新、登录、权限变更都不会再出问题。8. 写在后面的一些经验路由这部分内容不算难但知识点很琐碎每个小点单独拿出来都不复杂凑到一起就很容易乱。我梳理这套复习手册时的思路是先看路由表怎么配置再看页面之间怎么跳转然后看守卫和权限怎么拦最后用嵌套路由把整个布局串起来。把这个顺序想明白路由这块基本就通了。我个人在实际项目里感受最深的一句话是路由表就是应用的地图组件是地图上的地点而守卫是路口的检查站。平时写页面时可以不用太关注路由的底层实现但一旦遇到刷新 404、权限失效、动态路由加载异常这些问题还是要回到路由配置本身来找原因。最后分享一个小技巧排查路由问题时在 main.js 里加一行router.afterEach(() {})无论何时都能通过 vue-router 的调试工具在 Vue Devtools 里看到当前路由的状态和参数比瞎猜快得多。而且路由表和 meta 信息都会展示出来定位问题非常方便。下一篇复习手册我会继续整理状态管理和跨页面通信的部分那也是一块实战中绕不开的硬骨头。
返回列表