前言

在我们开发 Vue 应用时,路由守卫(router.beforeEach)是无法避开的核心环节。我们需要在这里判断用户是否登录、是否完成实名认证、是否有权限访问当前页面……随着业务逻辑的不断增加,我们写出的路由守卫往往会变成下面这种难以维护的“面条代码”:

router.beforeEach((to, from, next) => {
  // 从状态管理库获取用户的当前状态
  const isLogin = store.isLogin; // 是否已登录
  const isRealName = store.isRealNameVerified; // 是否已实名认证

  // 判断目标页面是否必须登录才能访问
  if (to.meta.requiresAuth) {
    if (!isLogin) {
      // 情况1:目标页面需要登录,但用户未登录 -> 拦截并跳转到登录页
      next('/login');
    } else {
      // 判断目标页面是否不仅需要登录,还需要“实名认证”才能访问
      if (to.meta.requiresRealName && !isRealName) {
        // 情况2:已登录但未实名 -> 拦截并跳转到实名认证页
        next('/realname');
      } else {
        // 情况3:已登录,且(不需要实名 或 已经完成了实名) -> 正常放行
        next();
      }
    }
  } else if (to.meta.isGuest) {
    // 目标页面是“访客专属”页面(例如:登录页、注册页)
    if (isLogin) {
      // 情况4:用户已经登录了,就不该再访问登录页了 -> 强制送回首页
      next('/');
    } else {
      // 情况5:用户确实未登录,正常访问登录页 -> 放行
      next();
    }
  } else {
    // 情况6:无需任何权限的完全公开页面 -> 直接放行
    next();
  }
});

可以看到,在这段代码中出现了一层套一层的 if-else。随着条件越来越复杂,这段代码将变得非常难以阅读,只要稍微改错一个条件判断,或者漏写一个 next(),页面死循环崩溃是常有的事。

那么,有没有一种方法能让路由鉴权变得清晰、优雅且易于扩展呢?答案就是使用策略模式。

第一步:扩展路由 Meta 属性

首先,我们需要在配置路由的时候,明确告诉路由守卫某个页面需要使用哪些策略。因此我们需要扩展 vue-routerRouteMeta 接口 的类型定义,增加一个 auth 数组,用于存放该页面对应的所有策略标识。

在你的全局类型文件(如 types.d.tsrouter.d.ts )中添加如下代码:

import 'vue-router'
import type { AuthType } from "@/router/router-auth";

declare module "vue-router" {
  // 路由配置项
  interface RouteMeta {
    // 路由鉴权类型数组,后续策略会按照数组中填写的顺序依次查验
    auth?: AuthType[];
  }
}

export {};

这里的作用就是告诉 TypeScript,RouteMeta 中新增了一个 auth 属性,它是一个可选的数组,数组中的元素类型为 AuthType。这样在配置路由时,我们就可以在 meta 中使用 auth 来指定该路由需要的鉴权策略。如果你没有这个文件,请自行创建一个,并确保它被 tsconfig.jsoninclude 包含。

设置完毕后,我们在定义路由时就可以这样用了:meta: { auth: ["required", "real-name-verified"] }

第二步:定义类型和策略接口

为了让所有的策略能统一规范,我们需要定义一个通用接口。只要符合这个接口的类,就是一个标准的“策略”。

新建 src/router/router-auth/types.ts

import { RouterAuthContext } from "./context";

// 策略模式接口规范
export interface AuthStrategy {
  // 每个策略都必须实现 execute 方法,返回一个布尔值表示:当前策略是否通过验证
  execute(context: RouterAuthContext): boolean;
}

// 支持的鉴权类型(后续如果新增了权限规则,只需要在这里追加即可)
export type AuthType =
  | "public" // 公开路由
  | "guest" // 访客路由(已登录不能访问)
  | "required" // 必须登录
  | "real-name-verified" // 必须实名认证
  | "real-name-pending"; // 必须是未实名认证状态才能访问

第三步:打包公共的上下文数据(Context)

在执行各项鉴权逻辑时,几乎每个策略都会用到当前用户的 isLogin(是否登录)状态,以及 Vue Router 本身提供的 to, from, next 函数。如果每次都当做参数繁琐地传来传去很不方便,所以我们把它们打包放进了一个“上下文(Context)”对象中集中管理。

新建 src/router/router-auth/context.ts

import { useUserStore } from "@/stores";  // pinia 状态管理库
import type { NavigationGuardNext, RouteLocationNormalized } from "vue-router";

// 路由鉴权数据上下文
export class RouterAuthContext {
  // 存放当前用户的状态数据
  isLogin: boolean;
  isRealNameVerified: boolean;

  constructor(
    public to: RouteLocationNormalized,
    public form: RouteLocationNormalized,
    public next: NavigationGuardNext
  ) {
    // 实例化 Context 对象时自动去 Pinia 取出用户状态,供后面的所有策略使用
    const userStore = useUserStore();
    this.isLogin = userStore.isLogin;
    this.isRealNameVerified = userStore.isRealNameVerified;
  }
}

第四步:编写具体的策略

现在,我们可以把曾经那些乱糟糟的 if-else 分离出来,变成独立且专注的策略文件。我们来看两个最常用的策略例子:

  1. 必须登录的策略 (router/strategy/required-strategy.ts)

    import { RouterAuthContext } from "../context";
    import type { AuthStrategy } from "../types";
    
    export class RequiredStrategy implements AuthStrategy {
      execute(context: RouterAuthContext): boolean {
        // 1. 从上下文中取出我们需要的数据
        const { isLogin, to, next } = context;
    
        if (!isLogin) {
          // 2. 发现未登录:直接拦截,跳转登录页,并带上原本想要去的页面路径以备将来跳转回来
          next({ name: "Login", query: { redirect: to.fullPath } });
          return false; // 返回 false,告诉系统当前策略未通过,让系统停止校验后续其他策略
        }
    
        // 3. 发现已登录:验证通过,允许放行
        return true;
      }
    }
  2. 访客专属的策略 (router/strategy/guest-strategy.ts) (比如:登录页面,只允许未曾登录过的纯访客访问)
import { RouterAuthContext } from "../context";
import type { AuthStrategy } from "../types";

export class GuestStrategy implements AuthStrategy {
  execute(context: RouterAuthContext): boolean {
    const { isLogin, next } = context;

    if (isLogin) {
      // 已经登录的用户访问访客专属页面(如企图再次访问 /login ),强制拦截并送回首页
      next({ name: "Dashboard" });
      return false;
    }

    // 未登录的访客,允许通过
    return true;
  }
}

以此类推,无论未来你的系统里蹦出什么样的奇葩权限要求,你只需要新建一个策略类实现它的逻辑,所有的老代码都可以做到一行不碰,互不干扰!

第五步:组装策略执行引擎

所有的零配件都有了,我们需要一个调度中心,根据每个路由 meta.auth 中配置的字符串,把对应的各种具体策略串联起来运行。

新建 src/router/router-auth/index.ts

import { RouterAuthContext } from "./context";
import type { AuthType, AuthStrategy } from "./types";
import { RequiredStrategy } from "./strategy/required-strategy";
import { GuestStrategy } from "./strategy/guest-strategy";
// 根据需要继续引入其他的鉴权策略...

export { RouterAuthContext };
export type * from "./types";

// 1. 将策略的字符串名称,与对应的策略实例做成映射表(字典)
const StrategyMap: Record<AuthType, AuthStrategy> = {
  // 当路由中写了 "guest" 权限时,就执行 GuestStrategy 的实例逻辑
  "guest": new GuestStrategy(),
  // 当路由写了 "required" 权限时,就执行 RequiredStrategy 的实例逻辑
  "required": new RequiredStrategy(),
  // ... 其他你实例化的策略
};

// 2. 策略执行引擎 (批量运行策略)
export function executeStrategies(strategies: AuthType[], context: RouterAuthContext): boolean {
  for (const strategyType of strategies) {
    const strategy = StrategyMap[strategyType];

    if (!strategy) {
      console.warn(`未知的策略类型: ${strategyType}`);
      continue;
    }

    // 执行到具体的策略校验逻辑中
    const passed = strategy.execute(context);

    if (!passed) {
      // 只要有一个策略返回了 false,说明在这个策略内部已经做出了 next() 拦截跳转。
      // 这个时候必须 return false 以立刻中止掉整个循环,防止后面别的策略再去调用一次 next() 引发错误。
      return false;
    }
  }

  // 所以策略执行完毕没有触发半路拦截,代表全部平稳通过
  return true;
}

第六步:简化全局的路由守卫

现在我们回到负责注册路由的 index.ts 或主体代码。看看使用策略模式后,最核心的 router.beforeEach 变得有多么清爽和简单:

import { createRouter, createWebHistory } from "vue-router";
import { executeStrategies, RouterAuthContext } from "./router-auth";

const router = createRouter({
  history: createWebHistory(),
  routes: [
    // ...这里是你的路由配置
  ]
});

// 全局前置路由守卫
router.beforeEach((to, from, next) => {

  // 1. 获取当前路由配置的鉴权策略数组。如果没有配置 auth,默认按 ["required"](必须登录)处理
  const strategies = to.meta.auth ?? ["required"];

  // 2. 实例化鉴权上下文,将常用数据统一打包
  const context = new RouterAuthContext(to, from, next);

  // 3. 执行策略链!将复杂的 if-else 判断工作全权委托给策略执行引擎
  const passed = executeStrategies(strategies, context);

  // 4. 判断结果
  if (passed) {
      // 如果 passed 为 true,说明所有的策略验证均已通过
      next();
  }

  // 注意重点:如果 passed 是 false,不用写 else 语句!
  // 因为在某一个未通过的策略内部(例如 RequiredStrategy 内部),它早已主动调用了 next('/login') 处理了重定向。
  // 所以这个时候我们什么都不用管,策略内部自己完美闭环!
});

export default router;

总结

使用了策略模式后,我们彻底改变了对项目的掌控力:

  1. if-else 说再见: 无论逻辑增长有多复杂,主路由守卫里面的代码永远只有上面那清爽的几行,看着十分舒心。
  2. 极佳的可扩展性: 某天产品经理要求临时加一个“仅限老用户的特权页面”?没问题,我们只要新建一个 OldUserStrategy 专属策略文件映射进去就好了,原有的代码你一行都不需要改!
  3. 分工作业互不干扰: 每个鉴权规则独立成一个文件,非常有利于前端多人协作开发,再也不用担心你动了一个小逻辑影响到所有人的整个系统路由结构。
分类: vue 项目实战 标签: vue-router鉴权策略模式守卫

评论

暂无评论数据

暂无评论数据

目录