插件开发
本指南将教你如何为 Pinia 开发自定义插件,从基础概念到高级模式。
什么是 Pinia 插件?
Pinia 插件是扩展 store 功能的函数。它们可以:
- 向 store 添加属性
- 向 store 添加新方法
- 包装现有方法
- 更改或扩展 store 选项
- 添加全局功能
基础插件结构
插件函数签名
ts
type PiniaPlugin = (context: PiniaPluginContext) =>
Partial<PiniaCustomProperties & PiniaCustomStateProperties> | void
插件上下文
ts
interface PiniaPluginContext {
pinia: Pinia // Pinia 实例
app: App // Vue 应用实例
store: Store // 当前 store 实例
options: StoreOptions // Store 定义选项
}
基础插件示例
js
function myFirstPlugin({ store }) {
// 向每个 store 添加属性
return {
hello: 'world'
}
}
// 注册插件
const pinia = createPinia()
pinia.use(myFirstPlugin)
// 现在每个 store 都有 'hello' 属性
const store = useMyStore()
console.log(store.hello) // 'world'
开发环境设置
项目结构
my-pinia-plugin/
├── src/
│ ├── index.ts # 主插件文件
│ ├── types.ts # TypeScript 定义
│ └── utils.ts # 工具函数
├── tests/
│ ├── plugin.test.ts # 插件测试
│ └── setup.ts # 测试设置
├── examples/
│ └── basic-usage.ts # 使用示例
├── package.json
├── tsconfig.json
└── README.md
Package.json 设置
json
{
"name": "pinia-my-plugin",
"version": "1.0.0",
"description": "一个自定义 Pinia 插件",
"main": "dist/index.js",
"module": "dist/index.esm.js",
"types": "dist/index.d.ts",
"files": ["dist"],
"scripts": {
"build": "rollup -c",
"test": "vitest",
"dev": "rollup -c -w"
},
"peerDependencies": {
"pinia": "^2.0.0",
"vue": "^3.0.0"
},
"devDependencies": {
"@rollup/plugin-typescript": "^11.0.0",
"pinia": "^2.0.0",
"rollup": "^3.0.0",
"typescript": "^4.9.0",
"vitest": "^0.28.0",
"vue": "^3.2.0"
}
}
插件开发模式
1. 添加属性
js
function addPropertiesPlugin() {
return {
// 添加响应式属性
timestamp: ref(Date.now()),
// 添加计算属性
formattedTime: computed(() => {
return new Date(timestamp.value).toLocaleString()
}),
// 添加方法
updateTimestamp() {
timestamp.value = Date.now()
}
}
}
2. 包装 Actions
js
function actionWrapperPlugin({ store }) {
// 存储原始 actions
const originalActions = {}
Object.keys(store).forEach(key => {
if (typeof store[key] === 'function') {
originalActions[key] = store[key]
// 包装 action
store[key] = function(...args) {
console.log(`Action ${key} 被调用,参数:`, args)
const result = originalActions[key].apply(this, args)
console.log(`Action ${key} 完成,结果:`, result)
return result
}
}
})
}
3. 状态持久化
js
function createPersistencePlugin(options = {}) {
return function persistencePlugin({ store, options: storeOptions }) {
// 检查此 store 是否启用持久化
if (!storeOptions.persist) return
const {
key = store.$id,
storage = localStorage,
serializer = JSON,
beforeRestore = (state) => state,
afterRestore = (state) => state
} = typeof storeOptions.persist === 'object'
? storeOptions.persist
: {}
// 从存储恢复状态
try {
const savedState = storage.getItem(key)
if (savedState) {
const parsed = serializer.parse(savedState)
const restored = beforeRestore(parsed)
store.$patch(restored)
afterRestore(store.$state)
}
} catch (error) {
console.error('恢复状态失败:', error)
}
// 状态变化时保存
store.$subscribe((mutation, state) => {
try {
const serialized = serializer.stringify(state)
storage.setItem(key, serialized)
} catch (error) {
console.error('持久化状态失败:', error)
}
})
}
}
4. 验证插件
js
function createValidationPlugin() {
return function validationPlugin({ store, options }) {
// 如果定义了验证模式则添加
if (!options.validation) return
const { schema, onError = console.error } = options.validation
// 验证初始状态
validateState(store.$state, schema, onError)
// 状态变化时验证
store.$subscribe((mutation, state) => {
validateState(state, schema, onError)
})
return {
$validate() {
return validateState(store.$state, schema, onError)
}
}
}
}
function validateState(state, schema, onError) {
try {
// 简单验证示例
for (const [key, validator] of Object.entries(schema)) {
if (!validator(state[key])) {
throw new Error(`${key} 验证失败`)
}
}
return true
} catch (error) {
onError(error)
return false
}
}
5. 异步 Actions 插件
js
function createAsyncActionsPlugin() {
return function asyncActionsPlugin({ store }) {
const loadingStates = reactive({})
const errors = reactive({})
// 包装异步 actions
Object.keys(store).forEach(key => {
const action = store[key]
if (typeof action === 'function') {
store[key] = async function(...args) {
loadingStates[key] = true
errors[key] = null
try {
const result = await action.apply(this, args)
return result
} catch (error) {
errors[key] = error
throw error
} finally {
loadingStates[key] = false
}
}
}
})
return {
$loading: readonly(loadingStates),
$errors: readonly(errors),
$isLoading: (actionName) => !!loadingStates[actionName],
$getError: (actionName) => errors[actionName],
$clearError: (actionName) => {
errors[actionName] = null
}
}
}
}
高级插件技术
插件组合
js
function composePlugins(...plugins) {
return function composedPlugin(context) {
const results = plugins.map(plugin => plugin(context)).filter(Boolean)
return Object.assign({}, ...results)
}
}
// 使用
const myPlugin = composePlugins(
persistencePlugin,
validationPlugin,
asyncActionsPlugin
)
pinia.use(myPlugin)
条件插件
js
function createConditionalPlugin(condition, plugin) {
return function conditionalPlugin(context) {
if (condition(context)) {
return plugin(context)
}
}
}
// 只应用于特定 stores
const userStorePlugin = createConditionalPlugin(
({ store }) => store.$id === 'user',
userSpecificPlugin
)
// 只在开发环境应用
const devPlugin = createConditionalPlugin(
() => process.env.NODE_ENV === 'development',
debugPlugin
)
带选项的插件
js
function createConfigurablePlugin(defaultOptions = {}) {
return function configurablePlugin(userOptions = {}) {
const options = { ...defaultOptions, ...userOptions }
return function plugin({ store }) {
// 使用合并的选项
if (options.enableLogging) {
store.$onAction(({ name, args }) => {
console.log(`Action ${name} 被调用:`, args)
})
}
if (options.enablePersistence) {
// 添加持久化逻辑
}
return {
$options: options
}
}
}
}
// 使用
const myPlugin = createConfigurablePlugin({
enableLogging: true,
enablePersistence: false
})
pinia.use(myPlugin({
enablePersistence: true // 覆盖默认值
}))
TypeScript 支持
插件类型定义
ts
// types.ts
import { PiniaPluginContext } from 'pinia'
export interface MyPluginOptions {
enabled?: boolean
prefix?: string
storage?: Storage
}
export interface MyPluginProperties {
$myMethod: () => void
$myProperty: string
}
export type MyPlugin = (options?: MyPluginOptions) =>
(context: PiniaPluginContext) => MyPluginProperties
模块声明
ts
// 扩展 Pinia 类型
declare module 'pinia' {
export interface PiniaCustomProperties {
$myMethod: () => void
$myProperty: string
}
}
插件实现
ts
// index.ts
import { PiniaPluginContext } from 'pinia'
import { MyPlugin, MyPluginOptions, MyPluginProperties } from './types'
export const createMyPlugin: MyPlugin = (options = {}) => {
return ({ store }: PiniaPluginContext): MyPluginProperties => {
return {
$myMethod() {
console.log('方法被调用')
},
$myProperty: options.prefix || 'default'
}
}
}
export * from './types'
测试插件
测试设置
ts
// tests/setup.ts
import { createApp } from 'vue'
import { createPinia, setActivePinia } from 'pinia'
import { beforeEach } from 'vitest'
beforeEach(() => {
const app = createApp({})
const pinia = createPinia()
app.use(pinia)
setActivePinia(pinia)
})
插件测试
ts
// tests/plugin.test.ts
import { describe, it, expect } from 'vitest'
import { defineStore } from 'pinia'
import { createMyPlugin } from '../src'
describe('MyPlugin', () => {
it('应该向 store 添加属性', () => {
const pinia = createPinia()
pinia.use(createMyPlugin())
const useTestStore = defineStore('test', {
state: () => ({ count: 0 })
})
const store = useTestStore()
expect(store.$myMethod).toBeDefined()
expect(store.$myProperty).toBe('default')
})
it('应该支持自定义选项', () => {
const pinia = createPinia()
pinia.use(createMyPlugin({ prefix: 'custom' }))
const useTestStore = defineStore('test', {
state: () => ({ count: 0 })
})
const store = useTestStore()
expect(store.$myProperty).toBe('custom')
})
})
集成测试
ts
// tests/integration.test.ts
import { mount } from '@vue/test-utils'
import { createPinia } from 'pinia'
import { createMyPlugin } from '../src'
import TestComponent from './TestComponent.vue'
it('应该在 Vue 组件中工作', () => {
const pinia = createPinia()
pinia.use(createMyPlugin())
const wrapper = mount(TestComponent, {
global: {
plugins: [pinia]
}
})
// 测试组件与插件的行为
expect(wrapper.text()).toContain('插件已加载')
})
发布你的插件
构建配置
js
// rollup.config.js
import typescript from '@rollup/plugin-typescript'
export default {
input: 'src/index.ts',
output: [
{
file: 'dist/index.js',
format: 'cjs',
exports: 'named'
},
{
file: 'dist/index.esm.js',
format: 'es'
}
],
plugins: [typescript()],
external: ['pinia', 'vue']
}
文档
markdown
# My Pinia Plugin
## 安装
```bash
npm install pinia-my-plugin
使用
js
import { createPinia } from 'pinia'
import { createMyPlugin } from 'pinia-my-plugin'
const pinia = createPinia()
pinia.use(createMyPlugin({
// 选项
}))
API
选项
enabled
(boolean): 启用/禁用插件prefix
(string): 属性前缀
添加的属性
$myMethod()
: 自定义方法$myProperty
: 自定义属性
### 发布检查清单
- [ ] 测试通过
- [ ] 包含 TypeScript 定义
- [ ] 文档完整
- [ ] 提供示例
- [ ] 指定对等依赖
- [ ] 生成构建产物
- [ ] 版本号更新
- [ ] 更新日志
## 最佳实践
### 1. 命名约定
```js
// 插件属性使用 $ 前缀
return {
$myMethod() {}, // ✅ 好的
myMethod() {}, // ❌ 可能与用户代码冲突
}
2. 错误处理
js
function safePlugin({ store }) {
try {
// 插件逻辑
return {
$safeMethod() {
try {
// 方法实现
} catch (error) {
console.error('插件方法失败:', error)
}
}
}
} catch (error) {
console.error('插件初始化失败:', error)
return {}
}
}
3. 性能考虑
js
function performantPlugin({ store }) {
// 防抖昂贵操作
const debouncedSave = debounce(() => {
// 昂贵操作
}, 1000)
store.$subscribe(() => {
debouncedSave()
})
// 使用 WeakMap 存储特定于 store 的数据
const storeData = new WeakMap()
storeData.set(store, { /* 数据 */ })
}
4. 内存管理
js
function memoryAwarePlugin({ store }) {
const cleanup = []
// 设置订阅
const unsubscribe = store.$subscribe(() => {})
cleanup.push(unsubscribe)
const interval = setInterval(() => {}, 1000)
cleanup.push(() => clearInterval(interval))
// store 销毁时清理
store.$dispose(() => {
cleanup.forEach(fn => fn())
})
}
5. 向后兼容性
js
function compatiblePlugin({ store, options }) {
// 检查 Pinia 版本
const piniaVersion = store.$pinia.version || '2.0.0'
if (semver.gte(piniaVersion, '2.1.0')) {
// 使用新功能
} else {
// 旧版本回退
}
}
常见模式
插件注册表
js
class PluginRegistry {
constructor() {
this.plugins = new Map()
}
register(name, plugin) {
this.plugins.set(name, plugin)
}
get(name) {
return this.plugins.get(name)
}
createComposed(...names) {
const plugins = names.map(name => this.get(name))
return composePlugins(...plugins)
}
}
const registry = new PluginRegistry()
registry.register('persistence', persistencePlugin)
registry.register('validation', validationPlugin)
const myPlugin = registry.createComposed('persistence', 'validation')
插件中间件
js
function createMiddleware() {
const middlewares = []
return {
use(middleware) {
middlewares.push(middleware)
},
createPlugin() {
return function middlewarePlugin(context) {
let result = {}
for (const middleware of middlewares) {
const middlewareResult = middleware(context, result)
if (middlewareResult) {
result = { ...result, ...middlewareResult }
}
}
return result
}
}
}
}
故障排除
常见问题
- 插件不工作: 检查插件是否在 store 创建前注册
- TypeScript 错误: 确保包含模块声明
- 性能问题: 使用防抖并避免在订阅中进行昂贵操作
- 内存泄漏: 始终清理订阅和定时器
调试
js
function debugPlugin({ store }) {
console.log('插件应用于 store:', store.$id)
console.log('Store 状态:', store.$state)
console.log('Store 选项:', store.$options)
return {
$debug: {
store,
state: store.$state,
options: store.$options
}
}
}