Vue项目里路由是标配,但大部分人对Vue Router的使用停留在"配置routes然后router-link跳转"。
其实路由守卫能做的事远超想象——权限控制、页面标题、加载进度、埋点统计,都能在守卫里优雅解决。
基础配置
// router/index.js
import { createRouter, createWebHistory } from 'vue-router';
const routes = [
{
path: '/',
name: 'Home',
component: () => import('@/views/Home.vue'),
meta: { title: '首页', requiresAuth: false }
},
{
path: '/user/:id',
name: 'UserDetail',
component: () => import('@/views/UserDetail.vue'),
props: true, // 把params作为props传给组件
meta: { title: '用户详情', requiresAuth: true }
},
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import('@/views/NotFound.vue')
}
];
const router = createRouter({
history: createWebHistory(),
routes,
scrollBehavior(to, from, savedPosition) {
if (savedPosition) return savedPosition; // 浏览器前进后退恢复位置
return { top: 0 }; // 新页面滚到顶部
}
});
export default router;
() => import()是路由懒加载,首屏不用加载所有页面的代码。
三种守卫
全局守卫
// 前置:跳转前
router.beforeEach((to, from, next) => {
// ...
next();
});
// 解析:所有组件内守卫和异步路由解析完之后
router.beforeResolve((to, from, next) => {
next();
});
// 后置:跳转完成后(没有next)
router.afterEach((to, from) => {
// ...
});
路由独享守卫
{
path: '/admin',
component: Admin,
beforeEnter: (to, from, next) => {
if (!isAdmin()) return next('/');
next();
}
}
组件内守卫
export default {
beforeRouteEnter(to, from, next) {
// 组件实例还没创建,不能访问this
next(vm => {
// 通过回调访问实例
vm.loadData();
});
},
beforeRouteUpdate(to, from, next) {
// 路由参数变化但组件复用
this.fetchData(to.params.id);
next();
},
beforeRouteLeave(to, from, next) {
// 离开前,可以做未保存提示
if (this.hasUnsavedChanges) {
const answer = window.confirm('有未保存的内容,确定离开吗?');
if (!answer) return next(false);
}
next();
}
};
实战一:权限控制
router.beforeEach(async (to, from, next) => {
const token = localStorage.getItem('token');
// 白名单直接放行
const whiteList = ['/login', '/register', '/404'];
if (whiteList.includes(to.path)) {
return next();
}
// 没有token跳登录
if (!token) {
return next(`/login?redirect=${encodeURIComponent(to.fullPath)}`);
}
// 已有用户信息
const userStore = useUserStore();
if (userStore.roles.length > 0) {
return checkPermission(to, userStore.roles, next);
}
// 首次进入,拉取用户信息
try {
await userStore.fetchUserInfo();
return checkPermission(to, userStore.roles, next);
} catch (e) {
await userStore.logout();
return next('/login');
}
});
function checkPermission(to, roles, next) {
const requiredRoles = to.meta.roles;
if (!requiredRoles) return next();
const hasPermission = roles.some(r => requiredRoles.includes(r));
if (hasPermission) return next();
return next('/403');
}
实战二:动态标题
router.afterEach((to) => {
const baseTitle = '我的应用';
document.title = to.meta.title
? `${to.meta.title} - ${baseTitle}`
: baseTitle;
});
实战三:加载进度条
配合NProgress:
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
NProgress.configure({ showSpinner: false });
router.beforeEach(() => {
NProgress.start();
});
router.afterEach(() => {
NProgress.done();
});
实战四:埋点统计
router.afterEach((to, from) => {
// 上报PV
analytics.track('page_view', {
path: to.fullPath,
from: from.fullPath,
title: to.meta.title
});
// 停留时长(在离开时上报)
const startTime = Date.now();
const leaveHandler = () => {
analytics.track('page_leave', {
path: to.fullPath,
duration: Date.now() - startTime
});
};
window.addEventListener('beforeunload', leaveHandler, { once: true });
});
next()的几种用法
next() // 放行
next(false) // 中断,停留在当前页
next('/login') // 跳转到指定路径
next({ path: '/' }) // 对象形式
next(new Error()) // 中断并触发router.onError
注意:每个守卫里next()只能调用一次,调用两次会导致奇怪的问题。
常见坑
坑1:守卫里忘记next()
页面直接卡住不跳转,很难排查。
坑2:在beforeEach里做异步请求但没await
// 错误
router.beforeEach((to, from, next) => {
fetchUser().then(user => {
next(); // 可能在别的跳转之后才执行
});
});
// 正确
router.beforeEach(async (to, from, next) => {
const user = await fetchUser();
next();
});
坑3:死循环重定向
// 错误:跳/login时又触发守卫,又跳/login
router.beforeEach((to, from, next) => {
if (!token) next('/login'); // 没有白名单判断
});
总结
路由守卫的核心价值是把"横切逻辑"统一处理——权限、标题、进度、埋点,不用在每个页面里重复写。把守卫写好了,代码能干净不少。