# Vue Router实战指南,路由守卫用对了能省很多事

1 阅读2分钟

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');   // 没有白名单判断
});

总结

路由守卫的核心价值是把"横切逻辑"统一处理——权限、标题、进度、埋点,不用在每个页面里重复写。把守卫写好了,代码能干净不少。