【重构笔记】Vue Router 路由守卫:拒绝“隐式依赖”,拥抱“集中编排”

24 阅读3分钟

前言

在中大型 Vue 项目中,全局路由守卫(Router Guards)往往是逻辑堆积的重灾区:权限校验、进度条控制(NProgress)、动态 Title 修改、埋点上报等逻辑杂糅在一起。

许多项目的做法是将这些逻辑分散在不同的文件中,然后在 main.js 中直接引入。这种做法虽然实现了物理文件的分离,却带来了严重的隐式依赖问题。

本文将介绍如何通过集中编排模式,重构路由拦截机制,提升系统的可维护性与稳定性。

一、痛点:隐式依赖带来的“竞态风险”

看看你的 main.js 是否长这样:

// main.js
import router from './router'

// 这里的引入顺序决定了执行顺序
import './permission'  // 权限控制
import './nprogress'   // 进度条
import './analytics'   // 埋点

而在 permission.js 内部,代码通常是直接操作 router 实例:

// permission.js
import router from './router'
router.beforeEach((to, from, next) => {
  // ... 业务逻辑
})

这种模式存在的问题:

  1. 执行顺序不可见:守卫的执行顺序完全依赖于 import 语句的物理顺序。如果新人不小心调整了引入顺序(例如 IDE 自动格式化),可能导致“先渲染页面后校验权限”的严重 Bug。
  2. 耦合度高:每个模块都强依赖全局 router 实例,难以进行单元测试。
  3. 调试困难:由于逻辑分散,想要理清一个完整的路由跳转链路(Flow),需要在多个文件间反复横跳。

二、解决方案:集中式编排设计

重构的核心思想是:将“业务逻辑实现”与“执行流程注册”解耦。

我们需要建立一个路由守卫编排中心,显式地管理所有守卫的生命周期。

1. 拆分逻辑为纯函数

将具体的业务逻辑剥离为独立的函数,不再直接依赖全局 router 实例。

// src/guards/nprogress.ts
import NProgress from 'nprogress'
import { NavigationGuardNext, RouteLocationNormalized } from 'vue-router'

export const startProgress = () => {
  NProgress.start()
}

export const stopProgress = () => {
  NProgress.done()
}
// src/guards/permission.ts
import store from '@/store'

// 这是一个纯粹的逻辑函数,只负责判断
export const checkPermission = async (
  to: RouteLocationNormalized,
  from: RouteLocationNormalized,
  next: NavigationGuardNext
) => {
  const hasToken = store.getters.token
  if (hasToken) {
    // ... 校验逻辑
    next()
  } else {
    next('/login')
  }
}

2. 建立统一编排入口

创建一个 index.ts 作为“总管”,负责按既定顺序注册这些守卫。

// src/guards/index.ts
import { Router } from 'vue-router'
import { startProgress, stopProgress } from './nprogress'
import { checkPermission } from './permission'
import { updateTitle } from './title'

export function setupGuards(router: Router) {
  // 1. HTTP 拦截与进度条开始(最先执行)
  router.beforeEach(startProgress)

  // 2. 核心权限校验(中间执行,决定去留)
  router.beforeEach(checkPermission)

  // 3. UI 状态更新(最后执行)
  router.beforeEach(updateTitle)

  // 4. 后置钩子
  router.afterEach(stopProgress)
}

3. 在入口文件中调用

main.ts 中,我们只需要调用这个 setup 函数。

// main.ts
import { createApp } from 'vue'
import router from './router'
import { setupGuards } from './guards'

const app = createApp(App)

// 显式初始化守卫
setupGuards(router)

app.use(router).mount('#app')

三、重构收益

通过这次简单的架构调整,我们获得了三个关键收益:

  1. 显式编排(Explicit Orchestration) 打开 src/guards/index.ts,所有人都能一眼看清路由跳转的完整生命周期。执行顺序由代码逻辑决定,不再依赖脆弱的 import 顺序。

  2. 关注点分离(Separation of Concerns)

    • permission.ts 只关注有没有权限,不需要知道进度条什么时候关。
    • setupGuards 只关注流程顺序,不需要知道具体的校验细节。
    • 代码结构符合单一职责原则(SRP)。
  3. 热插拔能力(Pluggable) 如果需要在某个特殊版本去掉“埋点功能”,只需在 setupGuards 中注释掉一行代码即可,无需侵入修改具体的业务文件,极大降低了回归测试的风险。

总结

架构不仅仅是分层,更是对依赖关系的治理。

将分散的 router.beforeEach 这种“隐式依赖”重构为集中管理的“显式编排”,虽然代码量没有减少,但显著提升了工程的可预测性可维护性。这在多人协作的中大型前端项目中,是一项投入产出比极高的优化。