🚀 Vue Router 插件系统:让路由扩展变得简单优雅

背景

在 Vue 应用开发中,我们经常需要开发一些与路由强相关的功能,如权限控制、页面缓存等。但是目前 Vue Router 官方并不支持插件机制,导致必须将插件作为 Vue 插件来开发,这会存在以下问题:

  • 插件的职责模糊不清,要为 Vue Router 提供功能,又无法确保插件安装在 Vue Router 之后

    ts 复制代码
    // 无法确保 MyPlugin 在 Vue Router 之后安装
    app.use(MyPlugin).use(router)
  • Vue Router 的 createRouterapp.use(router) 分离,无法在创建 Router 时立即安装扩展插件,这会导致插件功能调用可能早于插件初始化的问题

    ts 复制代码
    // router.ts
    const router = createRouter({
      history: createWebHistory(),
      routes: [],
    })
    
    // 在 router.ts 或其他文件顶级作用域中使用时,
    // 插件的类型扩展已存在,但插件可能尚未初始化
    router.myPlugin.fn()
    
    // main.ts
    Promise.resolve().then(() => {
      app.use(router).use(MyPlugin)
    })

解决方案

vue-router-plugin-system 提供了标准化的插件接口和多种安装策略,让路由扩展功能的开发和集成变得简单、高效、可复用。


核心特性

标准化插件接口

提供统一的 RouterPlugin 接口,让所有路由扩展功能都有标准的实现方式:

ts 复制代码
type RouterPlugin = (ctx: RouterPluginContext) => void

interface RouterPluginContext {
  router: Router // Vue Router 实例
  runWithAppContext: (handler: (app: App) => void) => void // 在 App 上下文中执行
  onUninstall: (handler: () => void) => void // 注册清理回调
}

响应式副作用管理

在插件函数作用域及 runWithAppContext 回调中创建的响应式副作用(watchcomputed 等)会自动在插件卸载时清理,无需手动处理。

ts 复制代码
import { watch } from 'vue'

const MyPlugin: RouterPlugin = ({ router }) => {
  router.myPlugin = {
    data: ref([]),
  }

  watch(router.currentRoute, (route) => {
    router.myPlugin.data.value = route.matched.map(match => match.meta.title)
  })
}

集成方式

方案一:插件库集成

插件库开发

ts 复制代码
// 将此包作为开发依赖,用 withInstall 包装插件并打包到 dist 中
import { withInstall } from 'vue-router-plugin-system'

const MyRouterPlugin = withInstall(
  ({ router, runWithAppContext, onUninstall }) => {
    // 插件实现
  },
)

export default MyRouterPlugin
json 复制代码
// package.json
{
  "devDependencies": {
    "vue-router-plugin-system": "^1.0.0"
  }
}

应用侧安装

ts 复制代码
import MyRouterPlugin from 'some-plugin-package'

// 选项 A:直接安装到路由实例,推荐紧跟在 createRouter 之后调用
MyRouterPlugin.install(router)

// 选项 B:作为 Vue 插件注册,必须在 Vue Router 之后,否则会抛出异常
app.use(router)
app.use(MyRouterPlugin)

方案二:应用内部插件集成

应用内部开发的路由插件,希望在应用侧统一注册和管理。

内部插件开发

ts 复制代码
// 只需导出 RouterPlugin 实现
import type { RouterPlugin } from 'vue-router-plugin-system'

// src/router/plugins/auth.ts
export const AuthPlugin: RouterPlugin = ({
  router,
  runWithAppContext,
  onUninstall,
}) => {
  // 插件实现
  router.beforeEach((to, from, next) => {
    // 权限检查逻辑
    next()
  })
}

// src/router/plugins/cache.ts
export const CachePlugin: RouterPlugin = ({
  router,
  runWithAppContext,
  onUninstall,
}) => {
  // 缓存管理逻辑
}

应用侧安装

使用 batchInstall

ts 复制代码
// router.ts
import { batchInstall } from 'vue-router-plugin-system'
import { AuthPlugin, CachePlugin } from './plugins'

const router = createRouter({
  history: createWebHistory(),
  routes: [],
})

// 紧跟在 createRouter 之后调用
batchInstall(router, [AuthPlugin, CachePlugin])

使用 createRouter

ts 复制代码
import { createWebHistory } from 'vue-router'
import { createRouter } from 'vue-router-plugin-system'
import { AuthPlugin, CachePlugin } from './plugins'

const router = createRouter({
  history: createWebHistory(),
  routes: [],
  // 新增插件选项
  plugins: [AuthPlugin, CachePlugin],
})

插件开发指南

ts 复制代码
import type { RouterPlugin } from 'vue-router-plugin-system'
import { inject, watch } from 'vue'

const LoggerPlugin: RouterPlugin = ({
  router,
  runWithAppContext,
  onUninstall,
}) => {
  console.log('插件初始化:', router)

  // 添加路由守卫
  router.beforeEach((to, from, next) => {
    console.log(`路由跳转: ${from.path} → ${to.path}`)
    next()
  })

  // 需要 Vue App 上下文时使用
  runWithAppContext((app) => {
    console.log('Vue 应用已就绪:', app)

    // 可以使用 inject、provide 等 Vue 上下文 API
    const theme = inject('theme', 'light')
    console.log('当前主题:', theme)

    // 创建响应式效果(会自动清理)
    watch(router.currentRoute, (route) => {
      console.log('当前路由:', route.path)
    })
  })

  // 注册清理逻辑
  onUninstall(() => {
    console.log('插件正在清理')
  })
}

实用插件示例

更多实用插件可以查阅 Vue Router 4 插件集合

页面标题插件

ts 复制代码
export interface TitlePluginOptions {
  titleTemplate?: (title: string) => string
}

// 当插件需要配置项时,可以通过工厂函数创建插件
export function TitlePlugin({
  titleTemplate = t => t,
}: TitlePluginOptions): RouterPlugin {
  return ({ router, runWithAppContext }) => {
    // 在 App 安装就绪后监听路由变化并更新标题
    runWithAppContext(() => {
      watchEffect(() => {
        const title = router.currentRoute.value.meta.title
        if (!title) {
          return
        }

        document.title = titleTemplate(title)
      })
    })
  }
}

API 参考

RouterPlugin

插件函数类型定义:

ts 复制代码
type RouterPlugin = (ctx: RouterPluginContext) => void

RouterPluginContext

插件上下文对象:

ts 复制代码
interface RouterPluginContext {
  router: Router
  runWithAppContext: (handler: (app: App) => void) => void
  onUninstall: (handler: () => void) => void
}

router: Router

Vue Router 实例,可以用来:

  • 添加路由守卫
  • 访问当前路由信息
  • 进行编程式导航

runWithAppContext(handler)

在 Vue App 上下文中执行代码:

  • 何时使用 :当需要使用 inject() 等依赖注入 API 时。这在注入像 pinia stores 这样的全局属性时很有用。

    ts 复制代码
    runWithAppContext(() => {
      const global = inject('global') // 'hello injections'
      // 获取 pinia store
      const userStore = useAuthStore()
      // ...
    })
  • 自动清理 :回调函数在独立的 effectScope 中执行,其中创建的响应式副作用会自动清理。

onUninstall(handler)

注册清理回调:

  • 执行时机:应用卸载时调用
  • 执行顺序:按注册顺序执行

createRouter(options)

扩展版的路由创建函数:

ts 复制代码
interface RouterOptions extends VueRouter.RouterOptions {
  // 新增插件选项
  plugins?: RouterPlugin[]
}

function createRouter(options: RouterOptions): VueRouter.Router

withInstall(plugin)

RouterPlugin 包装为支持两种安装模式的插件:

  • 支持 Vue 插件系统:app.use(Plugin)
  • 支持直接安装到路由:Plugin.install(router)
ts 复制代码
interface RouterPluginInstall {
  install: (instance: App | Router) => void
}

function withInstall(plugin: RouterPlugin): RouterPlugin & RouterPluginInstall

batchInstall(router, plugins)

批量安装多个插件到路由实例上,等价于对每个插件调用 plugin.install(router)

ts 复制代码
function batchInstall(router: Router, plugins: RouterPlugin[]): void

生命周期和清理机制

EffectScope 管理

  • 同一 Router 实例上的所有插件在共享的 effectScope 中运行
  • app.unmount() 调用时:
    1. 首先停止 effectScope
    2. 然后按注册顺序调用所有 onUninstall 处理器
  • 确保插件创建的响应式副作用(watchcomputed 等)得到可靠清理

安装顺序和幂等性

  • 插件按注册顺序初始化
  • runWithAppContext 处理器在 app.use(router) 后按注册顺序执行
  • 每个 Router 实例的 install 只会被包装一次
  • 本库不会对同一插件的多次注册进行去重,如需去重请在调用方实现

灵感与致谢

该插件化模式由 Naive UI Pro 项目启发而来,并在其基础上进行了扩展和完善,感谢 Zheng-Changfu 提供的宝贵思路和参考实现。

相关链接

结语

如果这套路由插件化方案对你有帮助,欢迎:

  • Github 仓库 点个 Star,关注后续更新
  • 在你的项目中试用,并通过 Issues 提交使用反馈、Bug 或功能建议
  • 基于本体系开发你的路由插件,欢迎通过 PR/Issue 分享给社区

你的每一次 Star、反馈与分享,都是我们持续完善的动力。感谢支持!

相关推荐
辣辣y6 小时前
Tailwind CSS 使用指南
前端·css
wgb04096 小时前
vxe table 升级之后页面数据不显示解决方法
java·前端·javascript
集成显卡6 小时前
Bun.js + Elysia 框架实现基于 SQLITE3 的简单 CURD 后端服务
开发语言·javascript·sqlite·bun.js
不如摸鱼去7 小时前
从 Wot UI 出发谈 VSCode 插件的自动化发布
前端·vscode·开源·自动化
im_AMBER7 小时前
JavaScript 03 【基础语法学习】
javascript·笔记·学习
IT_陈寒8 小时前
Python开发者必看:这5个鲜为人知的Pandas技巧让你的数据处理效率提升50%
前端·人工智能·后端
豆苗学前端8 小时前
写给女朋友的第一封信,测试方法概论
前端·后端·设计模式
半桶水专家8 小时前
Vue 3 插槽(Slot)详解
前端·javascript·vue.js