Code前端首页关于Code前端联系我们

Vue3项目里用Vue Router 4怎么才能少踩坑又顺手?

terry 1小时前 阅读数 28 #Vue

先搞懂Vue Router 4和Vue3的适配细节是前提

很多人刚上手Vue3就直接照搬Vue Router 3的代码,踩的第一个坑肯定是这里,先别着急写路由配置,得把两者的核心变化掰扯清楚,不然写一行错一行还找不到原因。 Vue Router 4是专门为Vue3定制的,完全抛弃了Vue2的Options API模式的核心依赖,比如new VueRouter这种写法,必须用createRouter工厂函数,因为Vue3的应用实例是createApp创建的,两者的注册方式是配套的组合式风格,不能混用旧语法里的全局注册钩子。 还有,Vue3核心引入了Composition API,路由里的useRoute、useRouter这些钩子函数只能在setup函数或者script setup标签里用,千万不要在普通的methods或者mounted里随便调用this.$route/$router,虽然Vue Router4为了兼容Options API保留了这两个属性,但现在主流都是用组合式了,而且混用容易在不同的作用域里拿到错误的路由对象——比如在一个组件内部的子组件封装函数里,this的指向可能变,但useRoute/useRouter会自动绑定当前组件所属的路由上下文,更稳定。 再提个细节,Vue Router4的history模式现在不是靠mode:'history'配置了,得用createWebHistory这个工厂函数传base路径,hash模式是createWebHashHistory,还有个Memory模式用createMemoryHistory,主要用于服务端渲染或者测试环境,日常开发用前两个就行,这点变化一定要记牢,不然配置完history模式刷新页面404的锅,还要怪后端没配置Nginx或者Apache? 哦对了,路由守卫也有变化,不过这个后面单独讲踩坑更合适,先别急。

配置文件别乱写,按项目规模分层才顺手

很多新手写路由配置,不管项目多大,都把所有路由塞在一个router/index.js文件里,几千行代码翻半天,加个新路由要找半天模块分组,删个旧路由怕删错别的模块,后期维护简直是噩梦。 那怎么分层才对?得分成基础配置层、业务模块层、权限控制层这三个部分,每个部分单独放一个文件或者文件夹,最后再在router/index.js里合并,这样逻辑清晰,维护起来也方便。 基础配置层就是刚才说的createRouter和历史模式、滚动行为这些公共配置,单独放在router/config.js里,滚动行为这里也有个Vue3/Vue Router4的小细节:Vue2里滚动行为的savedPosition参数是个对象,包含x和y,Vue Router4里还是一样的用法,但如果要用组合式API控制滚动的话,不能直接在这里用useRoute,得用路由元信息meta或者to参数里的query/params来判断,滚动行为函数现在支持返回Promise了,比如等页面加载完某个异步组件再滚动,这个体验会更好,特别是对于长列表详情页这种场景。 业务模块层就是把项目里的功能按模块拆成单独的路由文件,比如user模块的登录、注册、个人中心放在router/modules/user.js里,product模块的商品列表、详情、购物车放在router/modules/product.js里,每个模块的路由文件里可以定义自己的children和meta,最后统一用import.meta.glob(这是Vite的功能,如果是Webpack的话用require.context)自动导入合并,不用手动一个个import然后push到routes数组里,这样加新模块的时候直接新建一个modules里的文件就行,不用改index.js,减少了出错的概率。 权限控制层单独放在router/permission.js里,不管是前端路由守卫还是后端接口配合的动态路由,都在这里处理,这个后面单独讲踩坑的时候也会提到具体的实现方式。

别再乱用query和params传参了,搞懂它们的区别少出大问题

这绝对是Vue Router(不管3还是4)里最高频的踩坑点之一,很多人传参的时候想都不想就用params,结果刷新页面参数就没了,还以为是路由配置错了,其实根本不是。 先明确一下两者的本质区别:query是拼在URL路径后面的,用?和&连接,product/detail?id=123&name=apple,刷新页面之后参数还在,适合传一些公开的、需要分享或者收藏的参数,比如商品ID、搜索关键词、页码这些;params是动态路由的一部分,product/detail/:id,必须在路由配置里提前定义好路径参数,传参的时候不能用path属性,得用name属性,不然参数会自动被忽略,params参数不会显示在URL的query部分,但会显示在路径里(除非是动态路由的可选参数,在参数名后面加个?,product/detail/:id?),刷新页面之后如果是必填参数没了就会跳404,可选参数的话会正常跳转但参数会变成undefined,适合传一些短的、不需要分享但又需要在路由里体现的参数,比如用户ID的加密值(如果不想让用户直接看到明文ID的话,可选加密但其实还是会在URL里,真要隐藏的话建议用Pinia或者Vuex存)。 还有个Vue Router4的小变化:在script setup里用useRouter.push的时候,传参的格式可以用字符串、对象、甚至带state的对象(这个state是HTML5 History API里的,刷新页面之后不会丢,但只能在同一个会话里用,关闭浏览器就没了),带state的对象用法是这样的:router.push({ path: '/product/detail', query: { id: 123 }, state: { from: 'home' } }),然后在目标页面里用useRoute().state就能拿到from的值,这个功能特别适合做页面来源的判断,比如从首页进来的商品详情页显示“返回首页”,从搜索结果页进来的显示“返回搜索结果”,体验会比用query传from更好,因为URL里不会多出来一个from参数,看起来更干净。

路由守卫的坑最多,按类型逐个拆解

Vue Router4的路由守卫有全局前置守卫、全局解析守卫、全局后置钩子、路由独享守卫、组件内守卫这五种,每种的触发时机不一样,用法也不一样,踩的坑也不一样,一个一个说。 首先是全局前置守卫router.beforeEach,这个是最常用的,用于权限控制、登录验证这些,很多人踩的第一个坑就是在这个守卫里无限重定向,比如判断用户没登录就跳/login,结果跳/login的时候又触发了beforeEach,又判断没登录又跳/login,死循环了,解决方法很简单,就是在判断的时候加个白名单,把/login、/register这些不需要登录就能访问的页面加进去,只要to.path在白名单里,就直接next()放行,不用再判断登录状态了。 还有个全局前置守卫的坑,就是动态路由加载的时机问题,如果是后端接口返回动态路由的话,不能在每次触发beforeEach的时候都去请求一次接口,不然会很慢,而且容易造成请求堆积,解决方法是在Pinia或者Vuex里存一个hasAddRoutes的状态,第一次请求接口拿到动态路由之后,用router.addRoute添加进去,然后把hasAddRoutes设为true,再用next(to.fullPath)重新触发一次路由跳转,这样路由才能正确匹配到动态添加的页面,第二次及以后的beforeEach就直接判断hasAddRoutes是true,直接走正常的权限判断就行,不用再请求接口了。 然后是全局后置钩子router.afterEach,这个钩子没有next()函数,不能拦截路由跳转,只能做一些跳转后的操作,比如修改页面标题、埋点统计这些,很多人踩的坑就是在这里修改页面标题,结果发现标题变了但是浏览器的前进后退按钮对应的历史记录标题还是旧的,解决方法是在meta里定义每个页面的title,然后在afterEach里用document.title = to.meta.title || '默认标题'来修改,这样前进后退的时候历史记录的标题也会跟着变,因为HTML5 History API会自动记录document.title的变化。 接下来是组件内守卫,在Vue3的组合式API里,组件内守卫不能用Options API里的beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave这些钩子函数了,得用onBeforeRouteEnter、onBeforeRouteUpdate、onBeforeRouteLeave这三个组合式API钩子,而且onBeforeRouteEnter钩子不能直接访问组件实例,因为这时候组件还没创建,得用next(vm => {})回调函数来访问,这点和Vue Router3是一样的,不过现在主流都是用Pinia或者Vuex存数据,很少会在onBeforeRouteEnter里访问组件实例了。 还有个组件内守卫的小坑,就是onBeforeRouteLeave钩子如果在弹窗组件或者tab切换组件里用的话,可能会出现多次触发的情况,比如关闭弹窗的时候触发一次,切换tab的时候又触发一次,解决方法是在离开的时候判断一下是不是真的要离开当前的主路由页面,而不是子组件或者弹窗的显示隐藏,可以用to.matched.length和from.matched.length来判断,或者用to.meta和from.meta里的自定义属性来判断。

嵌套路由的children数组别乱配,要注意路径的拼接规则

嵌套路由也是Vue Router里常用的功能,比如管理后台的布局,顶部导航和侧边栏是公共的,只有中间的内容区域是变化的,这时候就可以用嵌套路由,公共布局组件是父路由,内容区域组件是子路由。 很多人踩的第一个坑就是子路由的path开头加了/,以为是绝对路径,其实在Vue Router的嵌套路由里,子路由的path如果开头加了/,就会被当成根路径,不会和父路由的path拼接,比如父路由的path是'/admin',子路由的path是'/dashboard',结果完整的路径是'/dashboard',而不是预期的'/admin/dashboard',解决方法就是子路由的path开头不要加/,直接写'dashboard',这样完整的路径就是'/admin/dashboard'。 还有个嵌套路由的坑,就是父路由没有设置,结果子路由匹配到了但是内容区域不显示,这个是新手最容易犯的错误,一定要记住,嵌套路由的父组件里必须要有一个标签来渲染子组件,不然子路由是不会生效的。 嵌套路由也可以有自己的meta,子路由的meta会继承父路由的meta,如果有相同的属性,子路由的属性会覆盖父路由的属性,比如父路由的meta里有requiresAuth: true,子路由的meta里有requiresAuth: false,那访问子路由的时候就不需要登录,这点可以灵活运用,比如管理后台的大部分页面都需要登录,但登录后的某个子页面不需要,就可以这样配置。

命名视图和命名路由别搞混,按需使用效率更高

很多人容易把命名视图和命名路由搞混,其实两者完全是不同的东西,命名视图是用来在同一个页面里渲染多个标签的,每个标签对应一个组件,比如一个博客的首页,左边是文章列表,右边是热门标签和作者介绍,这时候就可以用命名视图,分别渲染ArticleList、HotTags、AuthorInfo这三个组件;命名路由是用来给路由起个名字的,传参的时候用name属性比用path属性更方便,因为path属性如果改了的话,所有用path传参的地方都要改,用name的话只要改路由配置里的path就行,不用改其他地方。 先讲命名视图的用法,在路由配置里,把component属性改成components属性,值是一个对象,key是的name属性,value是对应的组件,如果key是default的话,就是默认的,比如这样配置: { path: '/blog', components: { default: ArticleList, sidebar: HotTags, header: AuthorInfo } } 然后在父组件里这样写:

// 默认的,对应default 这样三个组件就会分别渲染在对应的位置了。 命名视图的坑不多,主要就是别把components和component搞混,还有的name属性要和配置里的key对应上。 再讲命名路由的用法,在路由配置里加个name属性就行,比如这样配置: { path: '/product/detail/:id', name: 'ProductDetail', component: () => import('@/views/product/Detail.vue') } 然后传参的时候用name属性: router.push({ name: 'ProductDetail', params: { id: 123 } }) 这样比用path属性更方便,而且如果path改成'/goods/detail/:id'的话,只要改配置里的path和name,不用改传参的地方。

路由懒加载的正确打开方式,提升首屏加载速度

路由懒加载是Vue项目里提升首屏加载速度的重要手段,Vue Router4的路由懒加载和Vue Router3的差不多,都是用动态import()函数,不过Vite里的动态import函数有个小细节,就是不能用变量拼接完整的路径,只能用字符串字面量,比如这样写是对的: () => import('@/views/product/Detail.vue') 这样写是错的: const path = '@/views/product/Detail.vue' () => import(path) 不过如果是要自动导入业务模块的话,可以用import.meta.glob配合字符串模板,比如这样: const modules = import.meta.glob('./modules/.js') 然后遍历modules对象合并路由就行,这个在之前的配置分层里已经提过了。 还有个路由懒加载的小技巧,就是可以用webpackChunkName或者vite的chunkName来自定义打包后的chunk名称,方便调试和性能分析,在动态import函数里加个注释就行,比如这样: // Vite写法 () => import(/ @vite-ignore webpackChunkName: "product-detail" / '@/views/product/Detail.vue') // 或者只写Vite的注释也可以,Vite会自动识别 () => import(/ @vite-ignore vite-chunk-name: "product-detail" / '@/views/product/Detail.vue') // Webpack写法 () => import(/ webpackChunkName: "product-detail" */ '@/views/product/Detail.vue') 这样打包后,product/Detail.vue就会被单独打包成一个叫product-detail.[hash].js的文件,首屏加载的时候不会加载这个文件,只有访问/product/detail/:id的时候才会加载,大大提升了首屏加载速度。

路由元信息meta是个好东西,别浪费了

路由元信息meta是个非常灵活的配置项,可以在里面存任何你想存的东西,比如页面标题、是否需要登录、是否需要缓存、页面权限等级、面包屑导航的文本、埋点的eventId等等,之前讲的很多功能都用到了meta,比如全局后置钩子修改页面标题、全局前置守卫做权限控制。 这里再讲一个用meta做缓存的例子,Vue3里的缓存组件是,但Vue Router4里的不能直接包裹了,得用的v-slot属性拿到Component组件,然后再包裹,比如这样:

然后在路由配置里的meta里加个keepAlive属性, { path: '/product/list', name: 'ProductList', component: () => import('@/views/product/List.vue'), meta: { '商品列表', keepAlive: true } } 这样访问商品列表页的时候,滚动位置和输入框的内容都会被缓存,从商品详情页返回的时候,不需要重新请求接口,也不需要重新滚动到之前的位置,体验会非常好。 还有个用meta做面包屑导航的例子,在meta里加个breadcrumbs数组,数组里的每个对象是面包屑的文本和路径, { path: '/admin/product/list', name: 'AdminProductList', component: () => import('@/views/admin/product/List.vue'), meta: { '商品列表', breadcrumbs: [ { text: '首页', path: '/admin' }, { text: '商品管理', path: '/admin/product' }, { text: '商品列表', path: '/admin/product/list' } ] } } 然后在公共布局组件里用useRoute().meta.breadcrumbs来渲染面包屑导航就行,非常方便。

最后再提几个容易忽略的小细节

第一个是的active-class和exact-active-class属性,active-class是当路由部分匹配的时候添加的类名,exact-active-class是当路由完全匹配的时候添加的类名,比如父路由是'/admin',子路由是'/admin/product',访问'/admin/product'的时候,会添加active-class,不会添加exact-active-class,会添加两个类名,这点在做导航栏高亮的时候非常有用,可以根据需要灵活配置。 第二个是路由的重定向redirect和别名alias,redirect是跳转到另一个路由,URL会变,alias是给当前路由起个别的名字,URL不会变,内容显示的是当前路由的组件, { path: '/home', name: 'Home', component: () => import('@/views/Home.vue'), alias: '/' } 这样访问'/'的时候,URL还是'/',但内容显示的是Home.vue的组件,访问'/home'的时候,URL是'/home',内容也是Home.vue的组件,这点和redirect不一样,redirect会把URL变成'/home'。 第三个是路由的 props 传参,之前讲的query和params传参都是通过useRoute()拿到的,现在可以用props传参,把路由参数直接传给组件的props,这样组件和路由的耦合度更低,更方便复用,比如这样配置: { path: '/product/detail/:id', name: 'ProductDetail', component: () => import('@/views/product/Detail.vue'), props: true // 把params参数直接传给组件的props } 或者这样配置: { path: '/product/detail', name: 'ProductDetail', component: () => import('@/views/product/Detail.vue'), props: route => ({ id: route.query.id }) // 把query参数直接传给组件的props } 然后在组件里直接用defineProps接收就行: defineProps(['id']) 这样组件就不需要引入useRoute了,更干净,更方便复用。

就是Vue3项目里用Vue Router4的所有少踩坑又顺手的技巧了,希望能对大家有所帮助,如果有什么遗漏的或者不懂的地方,欢迎在评论区留言讨论。

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

热门