一、Vue Router 里咋监听路由变化?
做Vue单页应用时,路由切换是核心逻辑之一,比如从商品列表点进详情页、用户登录后跳转到首页、权限不够被拦截到403页面……这些场景都得“感知”路由变化,再执行对应的逻辑,那Vue Router里怎么处理路由变化?不同场景下有哪些实现方法?踩过哪些坑?今天咱就把这些问题拆开来聊聊。
想要处理路由变化,第一步得先“抓到”路由啥时候变了,Vue Router给了好几种方式,适用场景不一样,咱一个个看:用 watch 监听 $route
组件里可以直接 watch 实例上的 $route
对象,路由变化时触发回调,比如商品详情页,根据路由参数里的 id
请求数据:
export default { watch: { $route(to, from) { // to是目标路由,from是来源路由 const productId = to.params.id; this.fetchProductDetail(productId); } }, methods: { fetchProductDetail(id) { ... } } }
这种方式适合组件内响应路由变化,尤其是路由参数、查询参数(query)变了的时候,好处是写法简单,和Vue的响应式机制结合紧密;但如果是全局层面的监听,每个组件都写watch就太冗余了。
组件内的导航守卫:beforeRouteUpdate
当路由变化但组件被复用时(/user/1
跳到 /user/2
,用的同一个User组件),beforeRouteUpdate
会触发,它的参数和watch $route类似,也是to
和from
:
export default { beforeRouteUpdate(to, from, next) { // 处理路由变化逻辑,比如重新请求用户信息 this.fetchUserInfo(to.params.userId); next(); // 必须调用next()放行路由 } }
和watch $route比,beforeRouteUpdate
是导航守卫,执行时机更早(在路由更新流程中),适合需要在路由确认变化前做处理的场景,比如拦截不符合条件的参数,而watch是路由变化后触发,属于响应式更新。
全局导航守卫:router.beforeEach 等
如果是全局层面的路由拦截(比如权限校验、埋点统计),得用全局守卫,以 router.beforeEach
为例,它会在每次路由跳转前触发:
const router = createRouter({ ... }); router.beforeEach((to, from, next) => { // 比如判断是否需要登录 if (to.meta.requiresAuth && !isLoggedIn()) { next({ name: 'login' }); } else { next(); } });
除了beforeEach
,还有 beforeResolve
(导航确认前,所有组件内守卫和异步路由组件加载完后触发)、afterEach
(路由跳转完成后,主要用于埋点、页面标题修改)等,全局守卫适合统一处理所有路由的公共逻辑,减少组件内重复代码。
总结下这三种方式的区别:组件内用watch或beforeRouteUpdate处理局部逻辑,全局用router级别的守卫处理全局逻辑,选哪种得看需求是“局部响应”还是“全局管控”。
路由变化时,数据加载逻辑咋处理?
单页应用里,路由变化往往意味着要加载新数据,比如从首页跳转到文章详情,得根据路由参数请求文章内容;或者同个列表页,筛选条件变了(路由query变化),得重新请求列表数据,这里分享几种常见做法:
路由参数变化时,在watch $route里发请求
像前面举的商品详情页例子,路由参数id
变了,就重新调接口,这种方式的好处是“响应式”,路由变了自动触发,但要注意:如果路由变化时组件还没渲染(比如第一次进入页面),watch $route不会触发,这时候得在created
钩子也调一次请求:
export default { created() { this.fetchData(this.$route.params.id); }, watch: { $route(to) { this.fetchData(to.params.id); } }, methods: { fetchData(id) { ... } } }
这样不管是首次进入还是路由参数变化,都能拿到数据。
用beforeRouteUpdate提前处理数据
如果是组件复用的场景(比如用户点击不同商品,路由参数变了但组件没换),beforeRouteUpdate
可以在路由更新前就发起请求,等数据回来再渲染页面,避免页面闪烁:
export default { data() { return { product: null } }, beforeRouteUpdate(to, from, next) { // 先请求数据,拿到后更新data,再放行路由 this.fetchProductDetail(to.params.id).then(res => { this.product = res.data; next(); }); } }
这种方式能让数据加载和路由更新更“同步”,用户体验更流畅,但要注意错误处理,比如请求失败得跳错误页,这时候得用 next(error)
或者 next({ name: 'error' })
。
路由配置里的beforeEnter守卫预加载数据
如果多个组件都需要基于这个路由加载数据,或者数据加载逻辑和组件无关,可以把数据加载放到路由的beforeEnter
守卫里:
const routes = [ { path: '/product/:id', component: ProductDetail, beforeEnter: (to, from, next) => { fetchProduct(to.params.id).then(res => { // 可以把数据存在全局状态(比如Vuex)里,组件里直接取 store.commit('setProduct', res.data); next(); }).catch(err => { next({ name: 'error', params: { code: 404 } }); }); } } ]
这种方式适合数据和路由强绑定,且多个地方需要复用的情况,但要注意,如果数据加载失败,得妥善处理导航跳转,避免死循环。
结合异步组件和路由懒加载
如果页面组件很大,或者数据加载耗时久,可以用路由懒加载(component: () => import('./ProductDetail.vue')
)配合数据预加载,比如在路由进入前,先加载组件和数据,再渲染页面,不过Vue Router本身没提供“数据+组件”一起懒加载的API,得自己结合Promise.all
来实现:
const routes = [ { path: '/product/:id', component: () => Promise.all([ import('./ProductDetail.vue'), fetchProduct(to.params.id) // 假设fetchProduct返回Promise ]).then(([component, data]) => { // 把数据传给组件,比如通过props return { ...component.default, props: { data } }; }) } ]
这种做法能让组件和数据并行加载,减少首屏时间,但代码复杂度高,适合性能敏感的页面。
页面切换动画,路由变化咋控制?
很多项目里,页面切换要有过渡动画(比如淡入淡出、滑动切换),Vue的<transition>
组件和路由结合,可以实现路由变化时的动画效果,核心思路是给<router-view>
包一层<transition>
,利用路由进入/离开的时机触发动画。
基础的路由过渡动画
在App.vue
里,给<router-view>
加<transition>
:
<template> <transition name="fade"> <router-view></router-view> </transition> </template> <style> .fade-enter-active, .fade-leave-active { transition: opacity 0.5s; } .fade-enter-from, .fade-leave-to { opacity: 0; } </style>
这样每次路由变化,页面会有淡入淡出效果,但这种全局动画对所有路由生效,如果想给不同路由配不同动画,得用动态name
。
动态切换过渡动画
利用路由元信息(meta
)给不同路由标记动画类型,然后在<transition>
里动态绑定name
:
<template> <transition :name="transitionName"> <router-view></router-view> </transition> </template> <script> export default { computed: { transitionName() { // 从当前路由的meta里取动画名 return this.$route.meta.transition || 'fade'; } } } </script> <style> .slide-left-enter-active, .slide-left-leave-active { transition: all 0.3s; } .slide-left-enter-from { transform: translateX(100%); } .slide-left-leave-to { transform: translateX(-100%); } /* 其他动画类同理 */ </style>
然后在路由配置里给不同页面加meta
:
const routes = [ { path: '/', component: Home, meta: { transition: 'slide-left' } }, { path: '/about', component: About, meta: { transition: 'fade' } } ]
这样不同路由切换时,动画就不一样了,还可以结合路由的from
和to
,判断是前进还是后退,动态切换动画方向(比如返回时用slide-right
,前进时用slide-left
),这需要在watch $route或者全局守卫里判断路由变化方向,再设置transitionName
。
嵌套路由的动画处理
如果有嵌套路由(比如布局组件里包含<router-view>
),要注意动画的层级,通常父路由的过渡和子路由的过渡要分开处理,避免动画冲突,可以给每个<router-view>
都包一层<transition>
,分别控制动画。
路由变化时的权限校验咋做?
权限控制是路由变化时的高频需求——比如某些页面需要登录才能进,管理员页面只有管理员角色能进,Vue Router的导航守卫是实现权限校验的核心工具。
全局登录校验(路由元信息+beforeEach)
给需要登录的路由加meta.requiresAuth
,然后在全局beforeEach
里判断:
router.beforeEach((to, from, next) => { // 判断目标路由是否需要登录 if (to.meta.requiresAuth) { if (isLoggedIn()) { // 自定义的登录状态判断函数 next(); // 已登录,放行 } else { next({ name: 'login', query: { redirect: to.fullPath } }); // 跳登录页,记录要跳转的目标地址 } } else { next(); // 不需要登录,直接放行 } });
这样所有配置了requiresAuth
的路由,都会被拦截校验登录状态,登录成功后,再跳转到之前记录的redirect
地址。
角色权限校验(细化到角色)
如果某些页面只有管理员能进,得在meta
里加角色信息,比如meta.roles: ['admin']
,然后在beforeEach
里判断用户角色:
router.beforeEach((to, from, next) => { if (to.meta.roles) { const userRole = getUserRole(); // 从Vuex或localStorage取用户角色 if (to.meta.roles.includes(userRole)) { next(); } else { next({ name: '403' }); // 没有权限,跳403页面 } } else { next(); } });
这种方式适合多角色权限控制,但要注意用户角色的获取时机(比如异步获取角色信息时,得等角色加载完再判断,否则会出错),可以结合Promise,在beforeEach
里等待角色数据:
router.beforeEach(async (to, from, next) => { if (!userRoleLoaded) { // 标记角色是否已加载 await fetchUserRole(); // 异步获取角色 userRoleLoaded = true; } // 之后再判断角色... next(); });
组件内的权限拦截(beforeRouteEnter等)
除了全局守卫,组件内也能做权限拦截,比如用户进入某个表单页面,离开时如果表单没保存,提示是否离开:
export default { data() { return { formDirty: false } }, beforeRouteLeave(to, from, next) { if (this.formDirty) { const confirm = window.confirm('表单未保存,确定离开?'); if (confirm) { next(); } else { next(false); // 取消导航,留在当前页面 } } else { next(); } } }
beforeRouteEnter
是在组件进入前触发,这时候组件实例还没创建(this
是undefined
),所以适合做一些初始化的权限判断,比如从路由参数里取数据,判断是否有权限进入:
export default { beforeRouteEnter(to, from, next) { const hasPermission = checkPermission(to.params.id); // 自定义权限检查函数 if (hasPermission) { next(); } else { next({ name: '403' }); } } }
路由变化时,页面滚动行为咋控制?
单页应用路由切换后,页面滚动位置是个容易忽略的细节,比如从详情页回到列表页,希望列表页滚动到之前的位置;或者每次进入新页面,滚动到顶部,Vue Router的scrollBehavior
配置和组件内的滚动控制能解决这些问题。
全局配置scrollBehavior
在创建路由实例时,配置scrollBehavior
函数,控制路由切换后的滚动位置:
const router = createRouter({ history: createWebHistory(), routes, scrollBehavior(to, from, savedPosition) { // savedPosition是浏览器历史记录里的滚动位置(仅history模式下有效) if (savedPosition) { return savedPosition; // 回退时恢复滚动位置 } else { return { top: 0 }; // 新页面滚动到顶部 } } });
这样大部分场景能满足:前进到新页面滚动到顶部,回退时恢复之前的滚动位置,但如果是同一页面内的路由参数变化(比如列表页筛选条件变了,路由query变化),scrollBehavior
不会触发,这时候得在组件内手动控制。
组件内手动控制滚动
比如列表页有分页,路由query里的page
参数变化时,希望滚动到列表顶部,可以在watch $route里处理:
export default { watch: { $route() { // 滚动到列表容器顶部 this.$nextTick(() => { const listContainer = this.$refs.listContainer; listContainer.scrollTop = 0; }); } } }
$nextTick
是为了确保DOM更新后再操作滚动,避免找不到元素或滚动位置不对。
复杂场景:保存和恢复滚动位置
如果想更精细地控制(比如不同路由保存不同的滚动位置),可以用Vuex或sessionStorage
存储滚动位置,比如在beforeRouteLeave
时记录滚动位置,在beforeRouteEnter
时恢复:
export default { data() { return { scrollTop: 0 } }, beforeRouteLeave(to, from, next) { // 记录当前滚动位置 this.scrollTop = this.$refs.listContainer.scrollTop; localStorage.setItem('listScroll', this.scrollTop); next(); }, beforeRouteEnter(to, from, next) { next(vm => { // vm是组件实例,在next的回调里可以访问 const savedTop = localStorage.getItem('listScroll'); vm.$nextTick(() => { vm.$refs.listContainer.scrollTop = savedTop || 0; }); }); } }
这种方式适合需要记忆滚动位置的复杂列表页,但要注意存储和清理,避免不同页面之间的滚动位置冲突。
路由变化时常见的“坑”和解决办法
用Vue Router处理路由变化时,很容易遇到一些“反直觉”的问题,这里总结几个高频坑和解决思路:
路由参数变化,组件数据没更新
现象:/user/1
跳到 /user/2
,User组件里的数据还是用户1的。
原因:Vue Router会复用相同的组件实例(性能优化),所以组件的created
、mounted
等钩子不会重新执行,数据也不会自动更新。
解决:用watch $route
或者beforeRouteUpdate
监听路由变化,在回调里重新请求数据或更新状态。
导航守卫执行顺序搞混,逻辑出错
现象:多个守卫里的异步逻辑、权限判断互相干扰,导致页面跳转错误或数据加载不及时。
原因:导航守卫的执行顺序是:全局 beforeEach
→ 路由 beforeEnter
→
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。