背景

在 Vue 应用开发中,我们经常需要围绕 Vue Router 开发各种功能,比如页面导航方向、跨页面通信、滚动位置还原等。这些功能本可以作为 Vue Router 的扩展独立开发,但由于 Vue Router 官方并不支持插件机制,我们不得不将它们作为 Vue 插件来实现,这带来了以下问题:

插件的职责模糊不清

以页面缓存插件为例,它本应为 Vue Router 提供功能,却必须作为 Vue 插件开发,这让人感觉关注点有所偏离:

import type { ComputedRef, Plugin } from 'vue'

declare module 'vue-router' {
  interface Router {
    keepAlive: {
      pages: ComputedRef<string[]>
      add: (page: string) => void
      remove: (page: string) => void
    }
  }
}

export const KeepAlivePlugin: Plugin = (app) => {
  const router = app.config.globalProperties.$router
  if (!router) {
    throw new Error('[KeepAlivePlugin] 请先安装 Vue Router.')
  }

  const keepAlivePageSet = shallowReactive(new Set<string>())
  const keepAlivePages = computed(() => Array.from(keepAlivePageSet))

  router.keepAlive = {
    pages: keepAlivePages,
    add: (page: string) => keepAlivePageSet.add(page),
    remove: (page: string) => keepAlivePageSet.delete(page),
  }

  // 在路由变化时自动更新缓存列表
  router.afterEach((to, from) => {
    if (to.meta.keepAlive) {
      keepAlivePageSet.add(to.fullPath)
    }
  })
}
需要手动清理响应式副作用

仍以页面缓存插件为例,我们需要使用 effectScope 创建响应式副作用,并在应用卸载时手动停止:

import { effectScope } from 'vue'

// ...

export const KeepAlivePlugin: Plugin = (app) => {
  // ...

  const scope = effectScope(true)
  const keepAlivePageSet = scope.run(() => shallowReactive(new Set<string>()))!
  const keepAlivePages = scope.run(() =>
    computed(() => Array.from(keepAlivePageSet)),
  )!

  // ...

  app.onUnmount(() => {
    scope.stop()
    keepAlivePageSet.clear()
  })
}
插件初始化时机问题

Vue Router 的 createRouter()app.use(router) 是分离的,无法在创建 Router 时立即安装扩展插件,这可能导致插件功能在初始化之前就被调用:

// src/router/index.ts
export const router = createRouter({
  history: createWebHistory(),
  routes: [
    {
      path: '/home',
      component: HomeView,
    },
  ],
})

// KeepAlivePlugin 的类型扩展已生效,但插件可能尚未初始化
// 手动调用插件方法
router.keepAlive.add('/home')
// main.ts
app.use(router).use(KeepAlivePlugin)

解决方案

经过 Vue Router 4 插件集合与 Naive UI Pro 插件系统的设计实践,我开发了 vue-router-plugin-system,旨在为 Vue Router 提供标准化的插件系统与统一的安装机制,让路由扩展功能的开发和集成变得简单、高效、可复用。

仍以页面缓存插件为例,使用 vue-router-plugin-system 后的完整代码如下:

// src/router/plugins/keep-alive.ts
import type { ComputedRef, Plugin } from 'vue'
import type { RouterPlugin } from 'vue-router-plugin-system'
import { withInstall } from 'vue-router-plugin-system'

declare module 'vue-router' {
  interface Router {
    keepAlive: {
      pages: ComputedRef<string[]>
      add: (page: string) => void
      remove: (page: string) => void
    }
  }
}

// RouterPlugin 在插件安装时会通过 effectScope 自动收集响应式副作用,
// 在应用卸载时自动停止,且 router 实例会被显式地注入到插件上下文中,
// 无需再通过 app.config.globalProperties.$router 获取
export const KeepAlivePluginImpl: RouterPlugin = ({ router, onUninstall }) => {
  const keepAlivePageSet = shallowReactive(new Set<string>())
  const keepAlivePages = computed(() => Array.from(keepAlivePageSet))

  router.keepAlive = {
    pages: keepAlivePages,
    add: (page: string) => keepAlivePageSet.add(page),
    remove: (page: string) => keepAlivePageSet.delete(page),
  }

  // 在路由变化时自动更新缓存列表
  router.afterEach((to, from) => {
    if (to.meta.keepAlive) {
      keepAlivePageSet.add(to.fullPath)
    }
  })

  onUninstall(() => {
    keepAlivePageSet.clear()
  })
}
// src/router/index.ts
import { createRouter } from 'vue-router-plugin-system'
import { KeepAlivePluginImpl } from './plugins/keep-alive'

// 使用库提供的 createRouter 函数创建 Router 实例,
// 支持直接注册插件,也支持其他集成方式(详见下文)
export const router = createRouter({
  history: createWebHistory(),
  routes: [
    {
      path: '/home',
      component: HomeView,
    },
  ],

  // 通过 plugins 选项在创建 Router 时自动安装
  plugins: [KeepAlivePluginImpl],
})

// 插件功能已就绪,可以安全调用
router.keepAlive.add('/home')

插件开发

一个完整的插件示例:

import type { RouterPlugin } from 'vue-router-plugin-system'
import { inject, watch } from 'vue'

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

  // 需要 App 上下文时使用(如 inject、pinia store 等)
  runWithAppContext(() => {
    const theme = inject('theme', 'light')
    watch(router.currentRoute, (route) => {
      console.log('当前路由:', route.path, '主题:', theme)
    })
  })

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

更多实用插件示例可查阅 Vue Router 4 插件集合。


集成方式

方案一:插件库集成

插件库开发

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

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

export default MyRouterPlugin
// package.json
{
  "devDependencies": {
    "vue-router-plugin-system": "latest"
  }
}

应用侧安装

import MyRouterPlugin from 'some-plugin-package'

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

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

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

对于应用内部开发的路由插件,可以在应用侧统一注册和管理。

内部插件开发

// 只需导出 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

// 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

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],
})

核心特性

标准化插件接口

提供统一的 RouterPlugin 接口:

type RouterPlugin = (ctx: RouterPluginContext) => void

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

自动清理响应式副作用

插件中创建的响应式副作用(watchcomputed 等)会在卸载时自动清理,无需手动管理 effectScope


API 参考

核心 API

createRouter(options) - 扩展版路由创建函数,支持 plugins 选项

withInstall(plugin) - 包装插件,支持 app.use()Plugin.install(router) 两种安装方式

batchInstall(router, plugins) - 批量安装多个插件

插件上下文

interface RouterPluginContext {
  router: Router // Vue Router 实例
  runWithAppContext: (handler: (app: App) => void) => void // 在 App 上下文中执行
  onUninstall: (handler: () => void) => void // 注册清理回调
}
  • router - 用于添加路由守卫、访问路由信息、编程式导航
  • runWithAppContext - 当需要使用 inject()、pinia store 等 App 上下文 API 时使用
  • onUninstall - 注册清理回调,在应用卸载时按顺序执行

生命周期

  • 所有插件在共享的 effectScope 中运行,响应式副作用自动清理
  • 插件按注册顺序初始化和清理
  • 每个 Router 实例的 install 只会被包装一次

灵感与致谢

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


相关链接

  • Github 仓库
  • Vue Router 4 插件集合
  • Naive UI Pro 跟大家见面

结语

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

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

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

本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:[email protected]