一、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前端网发表,如需转载,请注明页面地址。
code前端网



