背景
在 Vue 应用开发中,我们经常需要开发一些与路由强相关的功能,如权限控制、页面缓存等。但是目前 Vue Router 官方并不支持插件机制,导致必须将插件作为 Vue 插件来开发,这会存在以下问题:
-
插件的职责模糊不清,要为 Vue Router 提供功能,又无法确保插件安装在 Vue Router 之后
ts// 无法确保 MyPlugin 在 Vue Router 之后安装 app.use(MyPlugin).use(router) -
Vue Router 的
createRouter和app.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 回调中创建的响应式副作用(watch、computed 等)会自动在插件卸载时清理,无需手动处理。
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这样的全局属性时很有用。tsrunWithAppContext(() => { 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()调用时:- 首先停止
effectScope - 然后按注册顺序调用所有
onUninstall处理器
- 首先停止
- 确保插件创建的响应式副作用(
watch、computed等)得到可靠清理
安装顺序和幂等性
- 插件按注册顺序初始化
runWithAppContext处理器在app.use(router)后按注册顺序执行- 每个 Router 实例的
install只会被包装一次 - 本库不会对同一插件的多次注册进行去重,如需去重请在调用方实现
灵感与致谢
该插件化模式由 Naive UI Pro 项目启发而来,并在其基础上进行了扩展和完善,感谢 Zheng-Changfu 提供的宝贵思路和参考实现。
相关链接
结语
如果这套路由插件化方案对你有帮助,欢迎:
- 为 Github 仓库 点个 Star,关注后续更新
- 在你的项目中试用,并通过 Issues 提交使用反馈、Bug 或功能建议
- 基于本体系开发你的路由插件,欢迎通过 PR/Issue 分享给社区
你的每一次 Star、反馈与分享,都是我们持续完善的动力。感谢支持!