Vue 插件开发文档
1. Vue 插件体系
1.1 插件定义
Vue 插件是一种为 Vue 应用添加全局功能的代码组织方式。一个插件本质上是一个对象或函数,它通过暴露 install 方法在应用初始化时对 Vue 实例进行扩展。
install 方法
Vue 3 中,插件必须包含一个 install 方法,该方法接收 app 实例和用户传入的 options 参数:
typescript
import type { App } from 'vue'
interface PluginOptions {
prefix?: string
locale?: string
}
const MyPlugin = {
install(app: App, options?: PluginOptions) {
// 执行全局配置
console.log('Plugin installed with options:', options)
}
}
// 或作为函数形式
function MyPluginFunc(app: App, options?: PluginOptions) {
// install 逻辑
}
export { MyPlugin, MyPluginFunc }app.component
在 install 方法中注册全局组件:
typescript
import type { App } from 'vue'
import MyButton from './components/MyButton.vue'
import MyCard from './components/MyCard.vue'
import MyDialog from './components/MyDialog.vue'
export default {
install(app: App) {
app.component('MyButton', MyButton)
app.component('MyCard', MyCard)
app.component('MyDialog', MyDialog)
}
}directive
注册全局自定义指令:
typescript
import type { App } from 'vue'
export default {
install(app: App) {
app.directive('focus', {
mounted(el: HTMLElement) {
el.focus()
}
})
app.directive('permission', {
mounted(el: HTMLElement, binding) {
// 权限校验逻辑
}
})
}
}provider/inject
通过插件提供全局依赖注入:
typescript
import type { App, InjectionKey } from 'vue'
import { readonly, reactive } from 'vue'
export const ThemeKey: InjectionKey<ThemeContext> = Symbol('theme')
interface ThemeContext {
mode: 'light' | 'dark'
primaryColor: string
}
export default {
install(app: App) {
const theme = reactive<ThemeContext>({
mode: 'light',
primaryColor: '#1890ff'
})
app.provide(ThemeKey, readonly(theme))
}
}mixin
通过全局 mixin 为所有组件注入公共逻辑(谨慎使用):
typescript
import type { App } from 'vue'
export default {
install(app: App) {
app.mixin({
created() {
// 所有组件创建时执行
console.log(`${this.$options.name || 'Anonymous'} component created`)
},
methods: {
$formatDate(date: Date): string {
return date.toISOString().split('T')[0]
}
}
})
}
}config globalProperties
Vue 3 通过 app.config.globalProperties 替代了 Vue 2 的 Vue.prototype,用于添加全局属性:
typescript
import type { App } from 'vue'
export default {
install(app: App) {
app.config.globalProperties.$http = httpClient
app.config.globalProperties.$message = messageService
app.config.globalProperties.$confirm = confirmService
app.config.globalProperties.$toast = toastService
}
}1.2 插件使用
app.use
通过 app.use() 方法注册插件:
typescript
import { createApp } from 'vue'
import App from './App.vue'
import MyPlugin from './plugins/my-plugin'
const app = createApp(App)
// 无参数注册
app.use(MyPlugin)
// 带 options 参数注册
app.use(MyPlugin, {
prefix: 'App',
locale: 'zh-CN'
})
app.mount('#app')options 参数
插件应支持 options 参数实现可配置化:
typescript
import type { App } from 'vue'
interface ToastPluginOptions {
duration?: number
position?: 'top' | 'bottom' | 'center'
zIndex?: number
}
const ToastPlugin = {
install(app: App, options: ToastPluginOptions = {}) {
const defaultOptions: Required<ToastPluginOptions> = {
duration: 3000,
position: 'top',
zIndex: 1000
}
const mergedOptions = { ...defaultOptions, ...options }
app.config.globalProperties.$toast = {
show(message: string) {
// 使用 mergedOptions 中的配置
}
}
}
}插件注册时机
插件必须在 app.mount() 之前注册:
typescript
// 正确:mount 之前注册
const app = createApp(App)
app.use(RouterPlugin)
app.use(PiniaPlugin)
app.use(ToastPlugin)
app.mount('#app')
// 错误:mount 之后注册无效
app.mount('#app')
app.use(SomePlugin) // 无效插件与组件区分
| 维度 | 插件 | 组件 |
|---|---|---|
| 作用域 | 全局应用级别 | 局部页面或功能 |
| 功能 | 扩展 Vue 核心能力 | 封装 UI 和交互 |
| 注册方式 | app.use() | 引入后注册或直接使用 |
| 典型场景 | 全局指令、属性、注入 | 按钮、表单、表格 |
| 复用性 | 跨项目共享 | 项目内部复用 |
1.3 Vue 2 vs Vue 3 插件差异对比
| 特性 | Vue 2 | Vue 3 |
|---|---|---|
| 注册 API | Vue.use(plugin) | app.use(plugin) |
| 全局属性 | Vue.prototype.$http = x | app.config.globalProperties.$http = x |
| 全局组件 | Vue.component('name', comp) | app.component('name', comp) |
| 全局指令 | Vue.directive('name', dir) | app.directive('name', dir) |
| 全局 Mixin | Vue.mixin(mixin) | app.mixin(mixin) |
| 依赖注入 | Vue.prototype / 组件链 | app.provide(key, value) + inject |
| 插件 install | install(Vue, options) | install(app, options) |
| 移除内容 | $children、$listeners、$scopedSlots | 不再支持 |
| TypeScript 支持 | 需要 vue-class-component | 原生支持 |
| 多个应用实例 | 不支持(单例) | 支持多个 createApp() |
| 指令生命周期 | bind/inserted/update/componentUpdated/unbind | created/mounted/updated/unmounted |
| 插件类型声明 | Vue.use 扩展需要手动声明模块 | app.config.globalProperties 直接扩展 |
2. 插件开发实践
2.1 全局组件注册
install 中循环注册
当插件包含多个组件时,通过循环批量注册:
typescript
// plugins/components/index.ts
import type { App } from 'vue'
import MyButton from './MyButton.vue'
import MyInput from './MyInput.vue'
import MySelect from './MySelect.vue'
import MyTable from './MyTable.vue'
import MyDialog from './MyDialog.vue'
import MyForm from './MyForm.vue'
const components = {
MyButton,
MyInput,
MySelect,
MyTable,
MyDialog,
MyForm
}
export default {
install(app: App) {
Object.entries(components).forEach(([name, component]) => {
app.component(name, component)
})
}
}
// main.ts
import { createApp } from 'vue'
import App from './App.vue'
import UIComponents from './plugins/components'
const app = createApp(App)
app.use(UIComponents)
app.mount('#app')components 目录自动加载(vite glob)
使用 Vite 的 import.meta.glob 实现组件自动注册:
typescript
// plugins/auto-register.ts
import type { App, Component } from 'vue'
interface RegisterOptions {
prefix?: string
exclude?: string[]
}
export default {
install(app: App, options: RegisterOptions = {}) {
const { prefix = '', exclude = [] } = options
// Vite glob 导入所有 .vue 文件
const modules = import.meta.glob<{ default: Component }>(
'../components/**/*.vue',
{ eager: true }
)
Object.entries(modules).forEach(([path, module]) => {
const match = path.match(/\/(\w+)\.vue$/)
if (!match) return
const componentName = match[1]
// 排除不需要注册的组件
if (exclude.includes(componentName)) return
// 注册组件,支持自定义前缀
const registeredName = `${prefix}${componentName}`
app.component(registeredName, module.default)
})
}
}
// main.ts 使用
app.use(AutoRegisterPlugin, {
prefix: 'App',
exclude: ['BaseComponent']
})2.2 全局指令
v-focus
自动获取焦点的指令:
typescript
// plugins/directives/focus.ts
import type { Directive, App } from 'vue'
const vFocus: Directive<HTMLElement> = {
mounted(el: HTMLElement) {
el.focus()
},
updated(el: HTMLElement) {
el.focus()
}
}
export default {
install(app: App) {
app.directive('focus', vFocus)
}
}
// 使用
// <input v-focus />v-permission
基于权限控制的指令:
typescript
// plugins/directives/permission.ts
import type { Directive, App } from 'vue'
import { usePermissionStore } from '@/stores/permission'
type PermissionAction = 'create' | 'read' | 'update' | 'delete'
interface PermissionBinding {
action: PermissionAction
resource: string
}
const vPermission: Directive<HTMLElement, PermissionBinding> = {
mounted(el: HTMLElement, binding) {
const permissionStore = usePermissionStore()
const { action, resource } = binding.value
const hasPermission = permissionStore.checkPermission(action, resource)
if (!hasPermission) {
el.parentNode?.removeChild(el)
}
},
updated(el: HTMLElement, binding) {
const permissionStore = usePermissionStore()
const { action, resource } = binding.value
const hasPermission = permissionStore.checkPermission(action, resource)
if (!hasPermission) {
el.style.display = 'none'
} else {
el.style.display = ''
}
}
}
export default {
install(app: App) {
app.directive('permission', vPermission)
}
}
// 使用
// <button v-permission="{ action: 'create', resource: 'user' }">新增用户</button>v-debounce
防抖指令,用于输入框高频触发场景:
typescript
// plugins/directives/debounce.ts
import type { Directive, App } from 'vue'
interface DebounceBinding {
fn: (...args: unknown[]) => void
delay?: number
immediate?: boolean
}
const vDebounce: Directive<HTMLElement, DebounceBinding> = {
mounted(el: HTMLElement, binding) {
const { fn, delay = 300, immediate = false } = binding.value
let timer: ReturnType<typeof setTimeout> | null = null
let isImmediateCalled = false
el.addEventListener('input', (event: Event) => {
if (immediate && !isImmediateCalled) {
fn(event)
isImmediateCalled = true
return
}
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
fn(event)
isImmediateCalled = false
}, delay)
})
},
unmounted(el: HTMLElement) {
// 清理事件监听
}
}
export default {
install(app: App) {
app.directive('debounce', vDebounce)
}
}
// 使用
// <input v-debounce="{ fn: handleSearch, delay: 500 }" />v-throttle
节流指令,用于按钮点击等高频触发场景:
typescript
// plugins/directives/throttle.ts
import type { Directive, App } from 'vue'
interface ThrottleBinding {
fn: (...args: unknown[]) => void
delay?: number
}
const vThrottle: Directive<HTMLElement, ThrottleBinding> = {
mounted(el: HTMLElement, binding) {
const { fn, delay = 1000 } = binding.value
let lastTime = 0
el.addEventListener('click', (event: Event) => {
const now = Date.now()
if (now - lastTime >= delay) {
lastTime = now
fn(event)
}
})
}
}
export default {
install(app: App) {
app.directive('throttle', vThrottle)
}
}
// 使用
// <button v-throttle="{ fn: handleSubmit, delay: 2000 }">提交</button>v-lazy
图片懒加载指令:
typescript
// plugins/directives/lazy.ts
import type { Directive, App } from 'vue'
interface LazyBinding {
placeholder?: string
error?: string
}
const vLazy: Directive<HTMLImageElement, LazyBinding> = {
mounted(el: HTMLImageElement, binding) {
const placeholder = binding.value?.placeholder || 'data:image/svg+xml,...'
const errorImage = binding.value?.error || '/images/error.png'
// 保存原始 src
const originalSrc = el.getAttribute('src')
// 先显示占位图
el.src = placeholder
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const img = entry.target as HTMLImageElement
// 加载真实图片
if (originalSrc) {
img.src = originalSrc
img.onerror = () => {
img.src = errorImage
}
}
// 加载完成后停止观察
observer.unobserve(img)
}
})
},
{
rootMargin: '100px',
threshold: 0.01
}
)
observer.observe(el)
// 存储 observer 以便清理
;(el as any).__lazyObserver = observer
},
unmounted(el: HTMLImageElement) {
// 组件卸载时断开观察
;(el as any).__lazyObserver?.disconnect()
}
}
export default {
install(app: App) {
app.directive('lazy', vLazy)
}
}
// 使用
// <img v-lazy="{ placeholder: '/loading.png' }" data-src="https://example.com/image.jpg" />2.3 全局属性
通过 app.config.globalProperties 添加全局可用属性,配合 TypeScript 类型扩展提供完整类型支持。
typescript
// plugins/global-properties/index.ts
import type { App } from 'vue'
import axios from 'axios'
import { ElMessage, ElMessageBox, ElNotification } from 'element-plus'
interface HttpService {
get<T>(url: string, params?: Record<string, unknown>): Promise<T>
post<T>(url: string, data?: unknown): Promise<T>
put<T>(url: string, data?: unknown): Promise<T>
delete<T>(url: string, params?: Record<string, unknown>): Promise<T>
}
const httpService: HttpService = {
async get<T>(url: string, params?: Record<string, unknown>): Promise<T> {
const response = await axios.get<T>(url, { params })
return response.data
},
async post<T>(url: string, data?: unknown): Promise<T> {
const response = await axios.post<T>(url, data)
return response.data
},
async put<T>(url: string, data?: unknown): Promise<T> {
const response = await axios.put<T>(url, data)
return response.data
},
async delete<T>(url: string, params?: Record<string, unknown>): Promise<T> {
const response = await axios.delete<T>(url, { params })
return response.data
}
}
export default {
install(app: App) {
app.config.globalProperties.$http = httpService
app.config.globalProperties.$message = ElMessage
app.config.globalProperties.$confirm = ElMessageBox.confirm
app.config.globalProperties.$toast = (message: string, type: 'success' | 'error' | 'warning' | 'info' = 'info') => {
ElNotification({ title: '提示', message, type })
}
}
}类型扩展声明
typescript
// types/global.d.ts
import type { ElMessage, ElMessageBox } from 'element-plus'
interface HttpService {
get<T>(url: string, params?: Record<string, unknown>): Promise<T>
post<T>(url: string, data?: unknown): Promise<T>
put<T>(url: string, data?: unknown): Promise<T>
delete<T>(url: string, params?: Record<string, unknown>): Promise<T>
}
declare module 'vue' {
interface ComponentCustomProperties {
$http: HttpService
$message: typeof ElMessage
$confirm: typeof ElMessageBox.confirm
$toast: (message: string, type?: 'success' | 'error' | 'warning' | 'info') => void
}
}
export {}在组件中使用:
typescript
// 组件内直接使用
const users = await this.$http.get<User[]>('/api/users')
this.$message.success('操作成功')
// Composition API 中使用(通过 getCurrentInstance)
import { getCurrentInstance } from 'vue'
export function useGlobal() {
const { proxy } = getCurrentInstance()!
const fetchData = async () => {
const data = await proxy.$http.get('/api/data')
return data
}
return { fetchData }
}2.4 Provider/Inject 插件
Symbol 作为 key
typescript
// plugins/provider/app-context.ts
import type { App, InjectionKey } from 'vue'
import { reactive, computed, readonly } from 'vue'
// 定义 InjectionKey,确保类型安全
export interface AppContext {
user: UserInfo | null
theme: 'light' | 'dark'
locale: string
permissions: string[]
}
interface UserInfo {
id: number
name: string
role: string
avatar?: string
}
export const AppContextKey: InjectionKey<AppContext> = Symbol('app-context')
export default {
install(app: App) {
const state = reactive<AppContext>({
user: null,
theme: 'light',
locale: 'zh-CN',
permissions: []
})
app.provide(AppContextKey, readonly(state))
}
}响应式数据
提供可修改的响应式上下文:
typescript
// plugins/provider/theme-context.ts
import type { App, InjectionKey } from 'vue'
import { reactive, computed, readonly, toRefs } from 'vue'
interface ThemeState {
mode: 'light' | 'dark'
primaryColor: string
sidebarCollapsed: boolean
}
interface ThemeContext {
state: ThemeState
toggleMode: () => void
setPrimaryColor: (color: string) => void
toggleSidebar: () => void
isDark: import('vue').ComputedRef<boolean>
}
export const ThemeKey: InjectionKey<ThemeContext> = Symbol('theme')
export default {
install(app: App) {
const state = reactive<ThemeState>({
mode: 'light',
primaryColor: '#1890ff',
sidebarCollapsed: false
})
const isDark = computed(() => state.mode === 'dark')
const context: ThemeContext = {
state: readonly(state) as ThemeState,
toggleMode: () => {
state.mode = state.mode === 'light' ? 'dark' : 'light'
document.documentElement.setAttribute('data-theme', state.mode)
},
setPrimaryColor: (color: string) => {
state.primaryColor = color
document.documentElement.style.setProperty('--primary-color', color)
},
toggleSidebar: () => {
state.sidebarCollapsed = !state.sidebarCollapsed
},
isDark
}
app.provide(ThemeKey, context)
}
}深层嵌套传递
typescript
// 子组件中获取注入
import { inject } from 'vue'
import { ThemeKey } from '@/plugins/provider/theme-context'
export default {
setup() {
const theme = inject(ThemeKey)
if (!theme) {
throw new Error('ThemeContext not provided')
}
return {
isDark: theme.isDark,
toggleMode: theme.toggleMode,
primaryColor: theme.state.primaryColor
}
}
}
// 创建 inject 辅助函数
export function useTheme() {
const theme = inject(ThemeKey)
if (!theme) {
throw new Error('useTheme() must be used after ThemePlugin is installed')
}
return theme
}
// 使用辅助函数
import { useTheme } from '@/plugins/provider/theme-context'
export default {
setup() {
const { isDark, toggleMode, state } = useTheme()
return { isDark, toggleMode, sidebarCollapsed: state.sidebarCollapsed }
}
}3. Vue Router 插件
3.1 权限控制路由插件
完整的权限控制插件实现,包括登录拦截、角色判定、动态路由添加:
typescript
// plugins/router/permission-plugin.ts
import type { App } from 'vue'
import type { Router, RouteRecordRaw } from 'vue-router'
import { usePermissionStore } from '@/stores/permission'
import { useUserStore } from '@/stores/user'
interface PermissionPluginOptions {
// 路由白名单,不需要登录即可访问
whiteList: string[]
// 登录页面路径
loginPath?: string
// 无权限页面路径
forbiddenPath?: string
// 默认首页路径
defaultPath?: string
// 动态路由工厂函数
asyncRoutes?: () => Promise<RouteRecordRaw[]>
}
const defaultOptions: Required<Omit<PermissionPluginOptions, 'asyncRoutes'>> = {
whiteList: ['/login', '/register', '/forgot-password'],
loginPath: '/login',
forbiddenPath: '/403',
defaultPath: '/dashboard'
}
export default {
install(app: App, options: PermissionPluginOptions) {
const router: Router = app.config.globalProperties.$router
const mergedOptions = { ...defaultOptions, ...options }
// 获取 store 实例
const userStore = useUserStore()
const permissionStore = usePermissionStore()
let hasDynamicRoutes = false
router.beforeEach(async (to, from, next) => {
// 检查 token
const hasToken = !!userStore.token
// 1. 路由白名单直接放行
if (mergedOptions.whiteList.includes(to.path)) {
next()
return
}
// 2. 未登录,重定向到登录页
if (!hasToken) {
next({
path: mergedOptions.loginPath,
query: { redirect: to.fullPath }
})
return
}
// 3. 已登录,检查是否需要动态添加路由
if (!hasDynamicRoutes) {
try {
// 获取用户信息
await userStore.getUserInfo()
// 根据角色获取路由配置
const asyncRoutes = await permissionStore.getAsyncRoutes()
// 动态添加路由
asyncRoutes.forEach((route: RouteRecordRaw) => {
router.addRoute(route)
})
// 添加 404 页面通配路由
router.addRoute({
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import('@/views/error/404.vue')
})
hasDynamicRoutes = true
// 重新进入当前路由
next({ ...to, replace: true })
} catch (error) {
console.error('Failed to load async routes:', error)
// 清除用户信息,跳转到登录页
userStore.resetToken()
next({ path: mergedOptions.loginPath })
}
return
}
// 4. 检查页面权限
const hasPermission = permissionStore.checkRoutePermission(to)
if (!hasPermission) {
next({ path: mergedOptions.forbiddenPath })
return
}
next()
})
// 路由变更时重置组件滚动位置
router.afterEach((to) => {
// 更新页面标题
document.title = `${to.meta?.title || ''} - Admin System`
})
}
}3.2 路由守卫
beforeEach 全局前置守卫
typescript
// plugins/router/guards.ts
import type { Router, RouteLocationNormalized } from 'vue-router'
import { useUserStore } from '@/stores/user'
import { usePermissionStore } from '@/stores/permission'
import { useTabsStore } from '@/stores/tabs'
// 路由白名单
const WHITE_LIST = ['/login', '/register', '/forgot-password', '/404']
export function createGuards(router: Router) {
router.beforeEach(async (to: RouteLocationNormalized, from: RouteLocationNormalized, next) => {
// 启动进度条
startProgress()
// 更新页面标题
const title = to.meta?.title as string
if (title) {
document.title = `${title} - Admin System`
}
// 白名单直接放行
if (WHITE_LIST.includes(to.path)) {
next()
return
}
const userStore = useUserStore()
// 用户信息校验
if (!userStore.token) {
next({
path: '/login',
query: { redirect: to.fullPath }
})
return
}
// 尝试获取用户信息
if (!userStore.userInfo) {
try {
await userStore.getUserInfo()
} catch {
userStore.resetToken()
next({ path: '/login' })
return
}
}
// 菜单权限过滤与动态路由
const permissionStore = usePermissionStore()
if (!permissionStore.hasLoadedRoutes) {
try {
const routes = await permissionStore.getAsyncRoutes()
routes.forEach((route) => {
router.addRoute(route)
})
next({ ...to, replace: true })
return
} catch {
next({ path: '/403' })
return
}
}
// 菜单权限检查
if (to.meta?.roles) {
const userRoles = userStore.roles
const requiredRoles = to.meta.roles as string[]
const hasRole = requiredRoles.some((role) => userRoles.includes(role))
if (!hasRole) {
next({ path: '/403' })
return
}
}
next()
})
}路由白名单与动态添加路由
typescript
// plugins/router/dynamic-routes.ts
import type { RouteRecordRaw } from 'vue-router'
// 默认固定路由
export const constantRoutes: RouteRecordRaw[] = [
{
path: '/login',
component: () => import('@/views/login/Login.vue'),
meta: { title: '登录', hidden: true }
},
{
path: '/403',
component: () => import('@/views/error/Forbidden.vue'),
meta: { title: '无权限', hidden: true }
},
{
path: '/404',
component: () => import('@/views/error/NotFound.vue'),
meta: { title: '页面不存在', hidden: true }
}
]
// 根据角色获取动态路由
export function filterAsyncRoutes(routes: RouteRecordRaw[], roles: string[]): RouteRecordRaw[] {
return routes.filter((route) => {
if (route.meta?.roles) {
return (route.meta.roles as string[]).some((role) => roles.includes(role))
}
return true
}).map((route) => {
const filteredRoute = { ...route }
if (filteredRoute.children) {
filteredRoute.children = filterAsyncRoutes(filteredRoute.children, roles)
}
return filteredRoute
})
}
// 异步路由配置(按模块拆分)
export const asyncRoutes: RouteRecordRaw[] = [
{
path: '/dashboard',
component: () => import('@/layout/Layout.vue'),
meta: { title: '仪表盘', icon: 'dashboard' },
children: [
{
path: '',
name: 'Dashboard',
component: () => import('@/views/dashboard/Index.vue'),
meta: { title: '工作台', roles: ['admin', 'editor'] }
}
]
},
{
path: '/system',
component: () => import('@/layout/Layout.vue'),
meta: { title: '系统管理', icon: 'setting' },
children: [
{
path: 'user',
name: 'UserManagement',
component: () => import('@/views/system/user/Index.vue'),
meta: { title: '用户管理', roles: ['admin'] }
},
{
path: 'role',
name: 'RoleManagement',
component: () => import('@/views/system/role/Index.vue'),
meta: { title: '角色管理', roles: ['admin'] }
}
]
}
]路由变更重置
typescript
// plugins/router/reset-router.ts
import type { Router, RouteRecordRaw } from 'vue-router'
export function resetRouter(router: Router, constantRoutes: RouteRecordRaw[]) {
// 获取当前所有路由
const records = router.getRoutes()
// 移除动态添加的路由(保留固定路由)
records.forEach((record) => {
if (record.name && !constantRoutes.some((route) => route.name === record.name)) {
router.removeRoute(record.name)
}
})
}
// 在用户登出时使用
import { resetRouter } from '@/plugins/router/reset-router'
import { constantRoutes } from '@/plugins/router/dynamic-routes'
function handleLogout() {
resetRouter(router, constantRoutes)
userStore.resetToken()
router.push('/login')
}4. Pinia 插件
4.1 Pinia 插件机制
Pinia 插件通过 pinia.use() 注册,每个插件是一个函数,接收 context 对象:
typescript
import { createPinia } from 'pinia'
import type { PiniaPluginContext } from 'pinia'
const pinia = createPinia()
pinia.use((context: PiniaPluginContext) => {
// context 包含:
// - store: 当前 store 实例
// - app: Vue 应用实例
// - pinia: Pinia 实例
// - options: 创建 store 时传入的选项
console.log('Plugin context:', context)
// 可以在所有 store 上添加公共属性或方法
context.store.$reset = () => {
// 自定义重置逻辑
}
})
app.use(pinia)context 对象详解
typescript
import type { PiniaPluginContext } from 'pinia'
import { createApp } from 'vue'
pinia.use((context: PiniaPluginContext) => {
const { store, app, pinia, options } = context
// store: 当前正在创建的 store
store.sharedData = { timestamp: Date.now() }
// app: Vue 应用实例
console.log(app._context)
// pinia: Pinia 实例
console.log(pinia.state.value)
// options: store 定义时的选项
// 例如,对于 useUserStore(options),options 就是传入的参数
if (options.persist) {
// 执行持久化逻辑
}
})4.2 持久化插件实现
完整的 Pinia 持久化插件,支持选项式配置:
typescript
// plugins/pinia/persist-plugin.ts
import type { PiniaPluginContext } from 'pinia'
import { watch } from 'vue'
interface PersistOptions {
// localStorage key 前缀
key?: string
// 需要持久化的 path 列表,空数组表示持久化全部
paths?: string[]
// 存储方式,默认 localStorage
storage?: Storage
// 序列化函数
serializer?: {
serialize: (value: unknown) => string
deserialize: (value: string) => unknown
}
// 持久化前的数据转换
beforeRestore?: (context: PiniaPluginContext) => void
afterRestore?: (context: PiniaPluginContext) => void
}
declare module 'pinia' {
export interface DefineStoreOptionsBase<S, Store> {
persist?: boolean | PersistOptions
}
}
export function createPersistPlugin(defaultStorage: Storage = localStorage) {
return (context: PiniaPluginContext) => {
const { store, options } = context
// 检查是否启用了持久化
if (!options.persist) return
const persistOptions: PersistOptions = typeof options.persist === 'boolean'
? { storage: defaultStorage }
: { storage: defaultStorage, ...options.persist }
const {
key = store.$id,
paths = [],
storage = defaultStorage,
serializer = {
serialize: JSON.stringify,
deserialize: JSON.parse
},
beforeRestore,
afterRestore
} = persistOptions
// 初始化时从 localStorage 还原 state
try {
beforeRestore?.(context)
const savedState = storage.getItem(key)
if (savedState) {
const parsed = serializer.deserialize(savedState)
if (paths.length > 0) {
// 只恢复指定路径
paths.forEach((path) => {
if (parsed[path] !== undefined) {
store.$patch({ [path]: parsed[path] })
}
})
} else {
// 恢复全部
store.$patch(parsed)
}
}
afterRestore?.(context)
} catch (error) {
console.error(`[PersistPlugin] Failed to restore state for store "${key}":`, error)
}
// 订阅 state 变化,同步到 storage
store.$subscribe(
(_mutation, state) => {
try {
if (paths.length > 0) {
// 只持久化指定路径
const toPersist: Record<string, unknown> = {}
paths.forEach((path) => {
toPersist[path] = state[path]
})
storage.setItem(key, serializer.serialize(toPersist))
} else {
storage.setItem(key, serializer.serialize(state))
}
} catch (error) {
console.error(`[PersistPlugin] Failed to persist state for store "${key}":`, error)
}
},
{ detached: true }
)
}
}
// 使用示例
// import { defineStore } from 'pinia'
//
// export const useUserStore = defineStore('user', {
// state: () => ({
// token: '',
// userInfo: null
// }),
// persist: {
// key: 'user-store',
// paths: ['token'],
// storage: localStorage
// }
// })4.3 日志插件
typescript
// plugins/pinia/logger-plugin.ts
import type { PiniaPluginContext } from 'pinia'
import { stringify } from './utils'
interface LogEntry {
storeId: string
action: string
timestamp: string
before: Record<string, unknown>
after: Record<string, unknown>
duration: number
}
export function createLoggerPlugin(options?: {
enabled?: boolean
maxLogs?: number
filter?: (context: PiniaPluginContext) => boolean
}) {
const { enabled = true, maxLogs = 100, filter } = options || {}
// 存储最近的日志记录
const logs: LogEntry[] = []
const addLog = (entry: LogEntry) => {
logs.unshift(entry)
if (logs.length > maxLogs) {
logs.pop()
}
}
return (context: PiniaPluginContext) => {
const { store, options: storeOptions } = context
// 检查是否启用
if (!enabled) return
// 过滤某些 store
if (filter && !filter(context)) return
// 记录 store 初始化
const storeName = store.$id
// 拦截 $patch
const originalPatch = store.$patch.bind(store)
store.$patch = (partialStateOrMutation: unknown) => {
const before = { ...store.$state }
const startTime = performance.now()
originalPatch(partialStateOrMutation)
const after = { ...store.$state }
const duration = performance.now() - startTime
addLog({
storeId: storeName,
action: '$patch',
timestamp: new Date().toISOString(),
before,
after,
duration
})
// 开发环境下格式化输出
if (import.meta.env.DEV) {
console.group(`[Pinia Logger] ${storeName} - $patch`)
console.log('%c之前的状态', 'color: #9E9E9E', before)
console.log('%c之后的状态', 'color: #4CAF50', after)
console.log(`%c耗时: ${duration.toFixed(2)}ms`, 'color: #2196F3')
console.groupEnd()
}
}
// 订阅 action 追踪
store.$onAction(({
name,
store,
args,
after,
onError
}) => {
const startTime = performance.now()
console.groupCollapsed(
`%c[Pinia Logger] Action: ${store.$id}.${name}`,
'color: #FF9800'
)
console.log('参数:', args)
after((result) => {
const duration = performance.now() - startTime
console.log('返回结果:', result)
console.log(`耗时: ${duration.toFixed(2)}ms`)
console.groupEnd()
})
onError((error) => {
console.error('Action 错误:', error)
console.groupEnd()
})
})
// 提供获取日志的方法
store.$logs = () => logs.filter((log) => log.storeId === storeName)
// 状态快照对比方法
store.$snapshot = () => {
return {
state: { ...store.$state },
timestamp: new Date().toISOString()
}
}
}
}
// 类型扩展
declare module 'pinia' {
export interface PiniaCustomProperties {
$logs: () => LogEntry[]
$snapshot: () => { state: Record<string, unknown>; timestamp: string }
}
}
// main.ts 使用
import { createLoggerPlugin } from '@/plugins/pinia/logger-plugin'
pinia.use(createLoggerPlugin({
enabled: import.meta.env.DEV,
filter: (context) => context.store.$id !== 'loading'
}))5. VitePress 插件
5.1 VitePress 自定义主题
typescript
// .vitepress/theme/index.ts
import type { App } from 'vue'
import type { EnhanceAppContext } from 'vitepress'
import DefaultTheme from 'vitepress/theme'
import MyLayout from './components/MyLayout.vue'
import MyHomePage from './components/MyHomePage.vue'
import { registerComponents } from './register-components'
export default {
// 继承默认主题
extends: DefaultTheme,
// 自定义 Layout 组件
Layout: MyLayout,
// 自定义 404 页面
NotFound: () => '404 - Page Not Found',
enhanceApp({ app, router, siteData }: EnhanceAppContext) {
// 注册全局组件
app.component('MyHomePage', MyHomePage)
registerComponents(app)
}
}Layout 插槽
vue
<!-- .vitepress/theme/components/MyLayout.vue -->
<template>
<DefaultTheme.Layout>
<template #nav-bar-title-before>
<div class="custom-logo">My Docs</div>
</template>
<template #nav-bar-content-after>
<div class="nav-extra">
<a href="https://github.com">GitHub</a>
</div>
</template>
<template #sidebar-top>
<div class="sidebar-custom">Custom Sidebar Content</div>
</template>
<template #content-top>
<div class="content-banner" v-if="$frontmatter.banner">
{{ $frontmatter.banner }}
</div>
</template>
<template #doc-footer-before>
<div class="edit-link">
<a :href="editUrl">编辑此页面</a>
</div>
</template>
<template #layout-bottom>
<footer class="site-footer">
Copyright 2024 My Project
</footer>
</template>
</DefaultTheme.Layout>
</template>
<script setup>
import DefaultTheme from 'vitepress/theme'
</script>主题配置与全局组件注册
typescript
// .vitepress/theme/register-components.ts
import type { App } from 'vue'
import CodeGroup from './components/CodeGroup.vue'
import CodeGroupItem from './components/CodeGroupItem.vue'
import ImageZoom from './components/ImageZoom.vue'
import TableOfContents from './components/TableOfContents.vue'
export function registerComponents(app: App) {
app.component('CodeGroup', CodeGroup)
app.component('CodeGroupItem', CodeGroupItem)
app.component('ImageZoom', ImageZoom)
app.component('TableOfContents', TableOfContents)
}5.2 VitePress 增强插件
markdown-it 扩展
typescript
// .vitepress/config.ts
import { defineConfig } from 'vitepress'
export default defineConfig({
markdown: {
config: (md) => {
// 添加 markdown-it 自定义插件
md.use(require('markdown-it-task-lists'), { enabled: true })
md.use(require('markdown-it-footnote'))
// 自定义容器
md.use(require('markdown-it-container'), 'tip', {
validate(params: string) {
return params.trim().match(/^tip\s*(.*)$/)
},
render(tokens: any, idx: number) {
const m = tokens[idx].info.trim().match(/^tip\s*(.*)$/)
if (tokens[idx].nesting === 1) {
return `<div class="custom-block tip">${m ? `<p class="custom-block-title">${m[1]}</p>` : ''}\n`
} else {
return '</div>\n'
}
}
})
// 自定义警告容器
md.use(require('markdown-it-container'), 'warning', {
validate(params: string) {
return params.trim().match(/^warning\s*(.*)$/)
},
render(tokens: any, idx: number) {
const m = tokens[idx].info.trim().match(/^warning\s*(.*)$/)
if (tokens[idx].nesting === 1) {
return `<div class="custom-block warning">${m ? `<p class="custom-block-title">${m[1]}</p>` : ''}\n`
} else {
return '</div>\n'
}
}
})
}
}
})图片缩放组件
vue
<!-- .vitepress/theme/components/ImageZoom.vue -->
<template>
<div class="image-zoom-container" @click="toggleZoom">
<img :src="src" :alt="alt" :class="{ zoomed: isZoomed }" />
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
defineProps<{
src: string
alt?: string
}>()
const isZoomed = ref(false)
const toggleZoom = () => {
isZoomed.value = !isZoomed.value
}
</script>
<style scoped>
.image-zoom-container {
cursor: zoom-in;
display: inline-block;
}
.image-zoom-container img {
max-width: 100%;
transition: transform 0.3s ease;
}
.image-zoom-container img.zoomed {
transform: scale(1.5);
cursor: zoom-out;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
}
</style>目录生成组件
vue
<!-- .vitepress/theme/components/TableOfContents.vue -->
<template>
<nav class="toc" v-if="headings.length > 0">
<h3 class="toc-title">目录</h3>
<ul class="toc-list">
<li
v-for="heading in headings"
:key="heading.id"
:style="{ paddingLeft: `${(heading.level - 2) * 16}px` }"
:class="{ active: activeId === heading.id }"
>
<a :href="`#${heading.id}`" @click.prevent="scrollToHeading(heading.id)">
{{ heading.text }}
</a>
</li>
</ul>
</nav>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { useRoute } from 'vitepress'
interface Heading {
id: string
text: string
level: number
}
const route = useRoute()
const headings = ref<Heading[]>([])
const activeId = ref<string>('')
const extractHeadings = () => {
const elements = document.querySelectorAll('.content h2, .content h3, .content h4')
headings.value = Array.from(elements)
.filter((el) => el.id)
.map((el) => ({
id: el.id,
text: el.textContent || '',
level: parseInt(el.tagName[1], 10)
}))
}
const scrollToHeading = (id: string) => {
const element = document.getElementById(id)
if (element) {
element.scrollIntoView({ behavior: 'smooth' })
}
}
const handleScroll = () => {
const scrollY = window.scrollY + 100
for (const heading of headings.value) {
const element = document.getElementById(heading.id)
if (element && element.offsetTop <= scrollY) {
activeId.value = heading.id
}
}
}
onMounted(() => {
extractHeadings()
window.addEventListener('scroll', handleScroll)
})
onUnmounted(() => {
window.removeEventListener('scroll', handleScroll)
})
</script>文章元数据插件
typescript
// .vitepress/plugins/article-meta.ts
import type { Plugin } from 'vite'
import type { PageData } from 'vitepress'
interface ArticleMeta {
title: string
description?: string
date?: string
author?: string
tags?: string[]
category?: string
readingTime?: number
}
export function articleMetaPlugin(): Plugin {
return {
name: 'vitepress-article-meta',
transform(code: string, id: string) {
// 只在 .md 文件上处理
if (!id.endsWith('.md')) return
return {
code: `${code}\n\nexport const __ARTICLE_META__ = extractMeta(${JSON.stringify(id)})`,
map: null
}
}
}
}
// 在主题中使用
// .vitepress/theme/index.ts
export function getArticleMeta(pageData: PageData): ArticleMeta | null {
const frontmatter = pageData.frontmatter
if (!frontmatter) return null
// 估算阅读时间
const content = pageData.contentRaw || ''
const wordsPerMinute = 300
const wordCount = content.split(/\s+/).length
const readingTime = Math.ceil(wordCount / wordsPerMinute)
return {
title: frontmatter.title || pageData.title,
description: frontmatter.description,
date: frontmatter.date,
author: frontmatter.author,
tags: frontmatter.tags,
category: frontmatter.category,
readingTime
}
}6. 插件发布
6.1 发布到 npm
package.json 配置
json
{
"name": "my-vue-plugin",
"version": "1.0.0",
"description": "A comprehensive Vue 3 plugin for enterprise applications",
"keywords": ["vue", "vue3", "plugin", "components", "utils"],
"license": "MIT",
"author": {
"name": "Your Name",
"email": "your@email.com",
"url": "https://github.com/yourname"
},
"repository": {
"type": "git",
"url": "git+https://github.com/yourname/my-vue-plugin.git"
},
"bugs": {
"url": "https://github.com/yourname/my-vue-plugin/issues"
},
"homepage": "https://github.com/yourname/my-vue-plugin#readme",
"files": [
"dist",
"src",
"types",
"README.md",
"LICENSE"
],
"main": "dist/index.cjs.js",
"module": "dist/index.esm.js",
"types": "dist/types/index.d.ts",
"exports": {
".": {
"import": "./dist/index.esm.js",
"require": "./dist/index.cjs.js",
"types": "./dist/types/index.d.ts"
},
"./dist/style.css": "./dist/style.css",
"./*": "./*"
},
"sideEffects": [
"**/*.css"
],
"scripts": {
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
"build:types": "vue-tsc --declaration --emitDeclarationOnly",
"lint": "eslint src --ext .vue,.ts,.tsx",
"format": "prettier --write src",
"test": "vitest run",
"release": "standard-version",
"prepublishOnly": "npm run build"
},
"peerDependencies": {
"vue": "^3.3.0",
"pinia": "^2.1.0",
"vue-router": "^4.2.0"
},
"devDependencies": {
"@vue/tsconfig": "^0.4.0",
"typescript": "^5.3.0",
"vite": "^5.0.0",
"vue-tsc": "^1.8.0",
"vitest": "^1.0.0",
"standard-version": "^9.5.0"
},
"engines": {
"node": ">=18.0.0"
},
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
}
}src 目录结构
my-vue-plugin/
├── src/
│ ├── components/
│ │ ├── MyButton.vue
│ │ ├── MyInput.vue
│ │ └── index.ts
│ ├── directives/
│ │ ├── focus.ts
│ │ ├── permission.ts
│ │ ├── debounce.ts
│ │ ├── throttle.ts
│ │ ├── lazy.ts
│ │ └── index.ts
│ ├── composables/
│ │ ├── useTheme.ts
│ │ ├── usePermission.ts
│ │ └── index.ts
│ ├── plugins/
│ │ ├── persist.ts
│ │ ├── logger.ts
│ │ └── router-guard.ts
│ ├── utils/
│ │ ├── storage.ts
│ │ └── format.ts
│ ├── index.ts
│ └── index.d.ts
├── types/
│ ├── global.d.ts
│ └── components.d.ts
├── dist/
├── README.md
├── LICENSE
├── tsconfig.json
├── vite.config.ts
└── package.json6.2 TypeScript 类型声明
声明文件与类型导出
typescript
// src/index.ts - 插件主入口
import type { App } from 'vue'
import type { Router } from 'vue-router'
import ComponentsPlugin from './components'
import DirectivesPlugin from './directives'
import { createPersistPlugin } from './plugins/persist'
import { createLoggerPlugin } from './plugins/logger'
import { createGuardPlugin } from './plugins/router-guard'
export interface PluginOptions {
components?: boolean | { prefix?: string }
directives?: boolean | string[]
persist?: boolean | { key?: string; paths?: string[] }
logger?: boolean | { enabled?: boolean }
router?: Router
routerGuard?: {
whiteList?: string[]
loginPath?: string
asyncRoutes?: () => Promise<any[]>
}
}
export default {
install(app: App, options: PluginOptions = {}) {
// 注册组件
if (options.components !== false) {
app.use(ComponentsPlugin, typeof options.components === 'object' ? options.components : undefined)
}
// 注册指令
if (options.directives !== false) {
app.use(DirectivesPlugin, typeof options.directives === 'object' ? options.directives : undefined)
}
// 注册 Pinia 插件
const pinia = app._context.config.globalProperties.$pinia
if (pinia) {
if (options.persist !== false) {
pinia.use(createPersistPlugin())
}
if (options.logger !== false) {
pinia.use(createLoggerPlugin(typeof options.logger === 'object' ? options.logger : undefined))
}
}
// 注册路由守卫
if (options.router && options.routerGuard !== false) {
createGuardPlugin(options.router, options.routerGuard)
}
}
}
// 导出子模块
export { ComponentsPlugin, DirectivesPlugin }
export { createPersistPlugin, createLoggerPlugin, createGuardPlugin }
export * from './composables'
export * from './directives'd.ts 生成
typescript
// tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"declaration": true,
"declarationDir": "dist/types",
"declarationMap": true,
"emitDeclarationOnly": true,
"outDir": "dist",
"rootDir": "src",
"strict": true,
"jsx": "preserve",
"skipLibCheck": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true,
"types": ["vue"]
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.vue"],
"exclude": ["node_modules", "dist", "tests"]
}typescript
// src/index.d.ts - 模块类型声明
import type { App } from 'vue'
declare const MyVuePlugin: {
install(app: App, options?: PluginOptions): void
}
export default MyVuePlugintypescript
// types/global.d.ts - 全局类型扩展声明
import type {
Router,
RouteRecordRaw,
NavigationGuardNext,
RouteLocationNormalized
} from 'vue-router'
import type { PiniaPluginContext } from 'pinia'
// 组件类型声明
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
// globalProperties 类型声明
declare module 'vue' {
interface ComponentCustomProperties {
$http: {
get<T>(url: string, params?: Record<string, unknown>): Promise<T>
post<T>(url: string, data?: unknown): Promise<T>
put<T>(url: string, data?: unknown): Promise<T>
delete<T>(url: string, params?: Record<string, unknown>): Promise<T>
}
$message: {
success(message: string): void
error(message: string): void
warning(message: string): void
info(message: string): void
}
$confirm: (message: string, title?: string) => Promise<void>
$toast: (message: string, type?: 'success' | 'error' | 'warning' | 'info') => void
}
}
// 路由元信息扩展
declare module 'vue-router' {
interface RouteMeta {
title?: string
icon?: string
hidden?: boolean
roles?: string[]
keepAlive?: boolean
permission?: string[]
activeMenu?: string
}
}
// Pinia 扩展
declare module 'pinia' {
export interface DefineStoreOptionsBase<S, Store> {
persist?: boolean | PersistOptions
}
}
export {}6.3 README 编写
markdown
# My Vue Plugin
A comprehensive Vue 3 plugin suite for enterprise applications, providing global components, directives, state persistence, logging, and route guard functionality.
## 安装
```bash
npm install my-vue-plugin
# 或
yarn add my-vue-plugin
# 或
pnpm add my-vue-plugin快速开始
typescript
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import { createRouter, createWebHistory } from 'vue-router'
import MyVuePlugin from 'my-vue-plugin'
import App from './App.vue'
const app = createApp(App)
const pinia = createPinia()
const router = createRouter({
history: createWebHistory(),
routes: []
})
app.use(pinia)
app.use(router)
app.use(MyVuePlugin, {
components: { prefix: 'App' },
directives: true,
persist: {
key: 'app-v1',
paths: ['token']
},
logger: import.meta.env.DEV,
router,
routerGuard: {
whiteList: ['/login'],
loginPath: '/login'
}
})
app.mount('#app')使用
全局组件
vue
<template>
<div>
<AppButton type="primary">提交</AppButton>
<AppInput v-model="search" placeholder="搜索..." />
<AppTable :columns="columns" :data="tableData" />
</div>
</template>全局指令
vue
<template>
<input v-focus />
<button v-permission="{ action: 'create', resource: 'user' }">新增</button>
<input v-debounce="{ fn: handleSearch, delay: 500 }" />
<button v-throttle="{ fn: handleSubmit, delay: 1000 }">提交</button>
<img v-lazy="{ placeholder: '/loading.png' }" data-src="/real-image.jpg" />
</template>全局属性
typescript
// Options API
export default {
mounted() {
this.$http.get('/api/data').then((data) => {
console.log(data)
})
this.$message.success('操作成功')
this.$confirm('确认删除?', '提示').then(() => {
this.$toast('已删除', 'success')
})
}
}
// Composition API
import { getCurrentInstance } from 'vue'
const { proxy } = getCurrentInstance()!
proxy.$http.get('/api/data')Store 持久化
typescript
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
token: '',
userInfo: {
name: '',
avatar: '',
role: ''
}
}),
// 只需配置 persist 即可自动持久化
persist: {
key: 'user-store',
paths: ['token', 'userInfo.name', 'userInfo.role'],
storage: localStorage
},
actions: {
async login(credentials: { username: string; password: string }) {
const data = await this.$http.post('/auth/login', credentials)
this.token = data.token
this.userInfo = data.userInfo
}
}
})API
PluginOptions
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| components | boolean | { prefix?: string } | true | 是否注册全局组件 |
| directives | boolean | string[] | true | 是否注册全局指令 |
| persist | boolean | PersistConfig | true | 是否启用状态持久化 |
| logger | boolean | LoggerConfig | true | 是否启用状态日志 |
| router | Router | - | Vue Router 实例 |
| routerGuard | GuardConfig | - | 路由守卫配置 |
Directives
| 指令 | 绑定值类型 | 说明 |
|---|---|---|
v-focus | - | 自动获取焦点 |
v-permission | { action, resource } | 权限控制 |
v-debounce | { fn, delay? } | 输入防抖 |
v-throttle | { fn, delay? } | 点击节流 |
v-lazy | { placeholder?, error? } | 图片懒加载 |
Composables
| Hook | 返回值 | 说明 |
|---|---|---|
useTheme() | { isDark, toggleMode, state } | 主题管理 |
usePermission() | { checkPermission, hasRole } | 权限校验 |
开发
bash
# 安装依赖
pnpm install
# 启动开发服务器
pnpm dev
# 运行测试
pnpm test
# 构建
pnpm build
# 生成类型声明
pnpm build:types更新日志
1.0.0 (2024-01-01)
- 初始发布
- 支持全局组件注册(Button、Input、Table 等 10+ 组件)
- 支持全局指令(focus、permission、debounce、throttle、lazy)
- 集成 Pinia 持久化插件
- 集成 Pinia 日志插件
- 集成路由权限控制
- 完整的 TypeScript 类型支持
许可证
MIT
---
## 总结
本文档全面覆盖了 Vue 3 插件开发的完整知识体系,从基础的插件定义和使用,到全局组件、指令、属性的具体实现,再到 Vue Router、Pinia、VitePress 等生态工具的插件集成,最后包含了插件发布和文档编写的完整流程。所有示例均使用 Vue 3 + TypeScript 编写,可直接应用于生产项目。