Appearance
Pinia插件系统
Pinia的插件系统提供了一种强大的方式来扩展Pinia的功能,允许开发者自定义Store的行为、添加全局功能或集成第三方工具。本章将详细介绍Pinia插件的创建、使用和最佳实践。
插件基础概念
插件定义和注册
typescript
import { createPinia, defineStore, PiniaPluginContext } from 'pinia'
// 基础插件示例:添加创建时间戳
function createTimeTrackingPlugin() {
return (context: PiniaPluginContext) => {
// 为每个Store添加创建时间
context.store.$createTime = new Date()
// 返回要添加到Store的额外属性
return {
$created: true,
$creationTime: new Date()
}
}
}
// 在应用中使用插件
const pinia = createPinia()
pinia.use(createTimeTrackingPlugin())
// 定义一个Store来测试插件
const useUserStore = defineStore('user', {
state: () => ({
name: 'John Doe',
email: 'john@example.com'
}),
getters: {
displayName: (state) => state.name
},
actions: {
updateName(newName: string) {
this.name = newName
}
}
})
// 使用带有插件增强的Store
const userStore = useUserStore()
console.log(userStore.$createTime) // 插件添加的时间戳
console.log(userStore.$created) // true
插件上下文详解
typescript
function createDetailedPlugin() {
return (context: PiniaPluginContext) => {
const {
store, // Store实例
app, // Vue应用实例
storeDefinition, // Store定义
options // 传递给插件的选项
} = context
console.log('Store ID:', store.$id)
console.log('Store Definition:', storeDefinition)
// 可以为Store添加方法
store.$customMethod = () => {
console.log(`Custom method called on ${store.$id}`)
}
// 可以访问Store的原始状态定义
console.log('State definition:', storeDefinition.state?.())
return {
// 添加到Store的额外属性
$enhanced: true,
$meta: {
enhancedAt: new Date(),
originalId: store.$id
}
}
}
}
// 使用带选项的插件
function createConfigurablePlugin(options: { prefix: string } = { prefix: 'default' }) {
return (context: PiniaPluginContext) => {
return {
$prefixedId: `${options.prefix}_${context.store.$id}`,
$customLogger: (message: string) => {
console.log(`[${options.prefix}] ${context.store.$id}: ${message}`)
}
}
}
}
// 注册插件
const pinia = createPinia()
pinia.use(createDetailedPlugin())
pinia.use(createConfigurablePlugin({ prefix: 'APP' }))
常用插件类型
日志插件
typescript
// stores/plugins/logger.ts
interface LoggerPluginOptions {
enabled?: boolean
logMutations?: boolean
logActions?: boolean
format?: 'simple' | 'detailed'
}
export function createLoggerPlugin(options: LoggerPluginOptions = {}) {
const {
enabled = true,
logMutations = true,
logActions = true,
format = 'simple'
} = options
return (context: PiniaPluginContext) => {
if (!enabled) return {}
const store = context.store
const storeId = store.$id
// 记录action调用
const originalActions: Record<string, Function> = {}
// 拦截所有action
Object.keys(store).forEach(key => {
const prop = (store as any)[key]
if (typeof prop === 'function' && key !== '$reset' && key !== '$patch') {
originalActions[key] = prop
;(store as any)[key] = (...args: any[]) => {
if (logActions) {
console.group(`🔧 Action: ${storeId}/${key}`)
console.log('Arguments:', args)
const startTime = performance.now()
const result = prop.apply(store, args)
const endTime = performance.now()
console.log(`⏱️ Execution time: ${(endTime - startTime).toFixed(2)}ms`)
console.groupEnd()
return result
} else {
return prop.apply(store, args)
}
}
}
})
// 记录状态变化
const unwatch = store.$subscribe((mutation, state) => {
if (logMutations) {
console.group(`🔄 Mutation: ${mutation.storeId}`)
console.log('Type:', mutation.type)
console.log('Events:', mutation.events)
console.log('New State:', state)
console.groupEnd()
}
}, { detached: false })
// Store销毁时清理监听器
const originalDestroy = store.$onAction(() => {}).$dispose
store.$onAction(() => {
return () => {
unwatch()
originalDestroy()
}
})
return {
$logger: {
enabled,
logMutations,
logActions,
getLogHistory: () => {
// 这里可以实现日志历史记录
return []
}
}
}
}
}
// 使用日志插件
const pinia = createPinia()
pinia.use(createLoggerPlugin({
enabled: true,
logActions: true,
logMutations: true,
format: 'detailed'
}))
持久化插件
typescript
// stores/plugins/persistence.ts
interface PersistencePluginOptions {
key?: string
storage?: Storage
beforeRestore?: (ctx: PiniaPluginContext) => void
afterRestore?: (ctx: PiniaPluginContext) => void
serializer?: {
serialize: (state: any) => string
deserialize: (data: string) => any
}
paths?: string[]
}
export function createPersistencePlugin(options: PersistencePluginOptions = {}) {
const {
key = 'pinia',
storage = localStorage,
beforeRestore = () => {},
afterRestore = () => {},
serializer = {
serialize: JSON.stringify,
deserialize: JSON.parse
},
paths = undefined
} = options
return (context: PiniaPluginContext) => {
const { store } = context
const fullKey = `${key}:${store.$id}`
// 恢复状态
const hydrate = () => {
try {
beforeRestore(context)
const savedState = storage.getItem(fullKey)
if (savedState) {
const parsedState = serializer.deserialize(savedState)
if (paths) {
// 只恢复指定路径的状态
paths.forEach(path => {
const keys = path.split('.')
let current = parsedState
let target = store
for (let i = 0; i < keys.length - 1; i++) {
current = current[keys[i]]
target = target[keys[i]]
}
const lastKey = keys[keys.length - 1]
target[lastKey] = current[lastKey]
})
} else {
// 恢复整个状态
store.$patch(parsedState)
}
}
afterRestore(context)
} catch (error) {
console.error(`Failed to restore state for ${fullKey}:`, error)
}
}
// 保存状态
const persist = () => {
try {
let stateToSave = store.$state
if (paths) {
// 只保存指定路径的状态
stateToSave = {}
paths.forEach(path => {
const keys = path.split('.')
let source = store
let target = stateToSave
for (let i = 0; i < keys.length - 1; i++) {
if (!target[keys[i]]) target[keys[i]] = {}
target = target[keys[i]]
source = source[keys[i]]
}
const lastKey = keys[keys.length - 1]
target[lastKey] = source[lastKey]
})
}
storage.setItem(fullKey, serializer.serialize(stateToSave))
} catch (error) {
console.error(`Failed to persist state for ${fullKey}:`, error)
}
}
// 在Store初始化后恢复状态
setTimeout(hydrate, 0)
// 监听状态变化并保存
const unwatch = store.$subscribe(() => {
persist()
}, { detached: false })
// 添加持久化控制方法
return {
$persist: persist,
$hydrate: hydrate,
$unsubscribePersistence: unwatch,
$persistence: {
key: fullKey,
storage,
paths,
isEnabled: true
}
}
}
}
// 使用持久化插件
const pinia = createPinia()
pinia.use(createPersistencePlugin({
key: 'myapp',
storage: localStorage,
paths: ['user.profile', 'settings.theme'], // 只持久化特定路径
serializer: {
serialize: (state) => JSON.stringify(state),
deserialize: (data) => {
try {
return JSON.parse(data)
} catch {
return {}
}
}
}
}))
缓存插件
typescript
// stores/plugins/cache.ts
interface CachePluginOptions {
ttl?: number // Time to live in milliseconds
maxSize?: number
storage?: 'memory' | 'sessionStorage' | 'localStorage'
}
export function createCachePlugin(options: CachePluginOptions = {}) {
const {
ttl = 5 * 60 * 1000, // 5分钟
maxSize = 100,
storage = 'memory'
} = options
let cacheStore: Map<string, { data: any; timestamp: number; ttl: number }> | Storage
if (storage === 'memory') {
cacheStore = new Map()
} else {
cacheStore = storage === 'localStorage' ? localStorage : sessionStorage
}
const getCacheKey = (storeId: string, methodName: string, args: any[]) => {
return `${storeId}:${methodName}:${JSON.stringify(args)}`
}
const isExpired = (timestamp: number, ttl: number) => {
return Date.now() - timestamp > ttl
}
const cleanup = () => {
if (storage === 'memory') {
const cache = cacheStore as Map<string, any>
const now = Date.now()
for (const [key, value] of cache.entries()) {
if (isExpired(value.timestamp, value.ttl)) {
cache.delete(key)
}
}
// 控制缓存大小
if (cache.size > maxSize) {
const entries = Array.from(cache.entries())
const toRemove = entries.slice(0, cache.size - maxSize)
toRemove.forEach(([key]) => cache.delete(key))
}
}
}
return (context: PiniaPluginContext) => {
const store = context.store
const storeId = context.store.$id
// 包装所有方法以添加缓存功能
const originalMethods: Record<string, Function> = {}
Object.keys(store).forEach(key => {
const prop = (store as any)[key]
if (typeof prop === 'function' &&
!key.startsWith('$') &&
!['$reset', '$patch', '$subscribe'].includes(key)) {
originalMethods[key] = prop
;(store as any)[key] = new Proxy(prop, {
apply: function(target, thisArg, argumentsList) {
// 检查是否需要缓存此方法
const cacheKey = getCacheKey(storeId, key, argumentsList)
if (storage === 'memory') {
const cache = cacheStore as Map<string, any>
const cached = cache.get(cacheKey)
if (cached && !isExpired(cached.timestamp, cached.ttl)) {
return cached.data
}
const result = target.apply(thisArg, argumentsList)
cache.set(cacheKey, {
data: result,
timestamp: Date.now(),
ttl
})
cleanup()
return result
} else {
// 使用 Web Storage
const storageKey = `cache:${cacheKey}`
const cachedStr = cacheStore.getItem(storageKey)
if (cachedStr) {
try {
const cached = JSON.parse(cachedStr)
if (!isExpired(cached.timestamp, cached.ttl)) {
return cached.data
}
} catch (e) {
console.error('Cache deserialization error:', e)
}
}
const result = target.apply(thisArg, argumentsList)
cacheStore.setItem(storageKey, JSON.stringify({
data: result,
timestamp: Date.now(),
ttl
}))
return result
}
}
})
}
})
return {
$cache: {
clear: () => {
if (storage === 'memory') {
const cache = cacheStore as Map<string, any>
for (const key of cache.keys()) {
if (key.startsWith(storeId)) {
cache.delete(key)
}
}
} else {
const prefix = `cache:${storeId}:`
const keysToRemove: string[] = []
for (let i = 0; i < (cacheStore as Storage).length; i++) {
const key = (cacheStore as Storage).key(i)
if (key && key.startsWith(prefix)) {
keysToRemove.push(key)
}
}
keysToRemove.forEach(key => (cacheStore as Storage).removeItem(key))
}
},
getStats: () => {
if (storage === 'memory') {
const cache = cacheStore as Map<string, any>
const storeKeys = Array.from(cache.keys()).filter(k => k.startsWith(storeId))
return {
size: storeKeys.length,
maxSize,
keys: storeKeys
}
} else {
const prefix = `cache:${storeId}:`
const keys = []
for (let i = 0; i < (cacheStore as Storage).length; i++) {
const key = (cacheStore as Storage).key(i)
if (key && key.startsWith(prefix)) {
keys.push(key)
}
}
return {
size: keys.length,
maxSize,
keys
}
}
}
}
}
}
}
高级插件功能
Store元数据插件
typescript
// stores/plugins/metadata.ts
interface Metadata {
version?: string
author?: string
description?: string
tags?: string[]
dependencies?: string[]
createdAt?: Date
updatedAt?: Date
}
interface MetadataPluginOptions {
autoGenerate?: boolean
defaults?: Partial<Metadata>
}
export function createMetadataPlugin(options: MetadataPluginOptions = {}) {
const { autoGenerate = true, defaults = {} } = options
return (context: PiniaPluginContext) => {
const { storeDefinition, store } = context
// 从Store定义中提取元数据
const metadata: Metadata = {
...defaults,
...(storeDefinition as any)._metadata || {},
createdAt: new Date(),
updatedAt: new Date()
}
if (autoGenerate) {
// 自动生成一些元数据
metadata.version = metadata.version || '1.0.0'
metadata.tags = metadata.tags || []
metadata.dependencies = metadata.dependencies || []
}
// 添加元数据到Store
Object.defineProperties(store, {
$metadata: {
value: metadata,
writable: true,
enumerable: false,
configurable: true
},
$updateMetadata: {
value: (newMetadata: Partial<Metadata>) => {
Object.assign(metadata, newMetadata, { updatedAt: new Date() })
},
writable: true,
enumerable: false,
configurable: true
}
})
// 为actions添加执行跟踪
const actionMetadata = new Map<string, {
callCount: number
lastCalled: Date | null
totalTime: number
avgTime: number
}>()
Object.keys(store).forEach(key => {
const prop = (store as any)[key]
if (typeof prop === 'function' &&
!key.startsWith('$') &&
!['$reset', '$patch', '$subscribe'].includes(key)) {
actionMetadata.set(key, {
callCount: 0,
lastCalled: null,
totalTime: 0,
avgTime: 0
})
;(store as any)[key] = (...args: any[]) => {
const start = performance.now()
const result = prop.apply(store, args)
const end = performance.now()
const meta = actionMetadata.get(key)!
meta.callCount++
meta.lastCalled = new Date()
meta.totalTime += end - start
meta.avgTime = meta.totalTime / meta.callCount
return result
}
}
})
return {
$actionMetadata: actionMetadata,
$getStoreStats: () => {
const actions = Array.from(actionMetadata.entries()).map(([name, meta]) => ({
name,
...meta
}))
return {
metadata,
actions,
stateSize: JSON.stringify(store.$state).length
}
}
}
}
}
// 使用元数据插件
const pinia = createPinia()
pinia.use(createMetadataPlugin({
defaults: {
author: 'Development Team',
version: '1.0.0'
}
}))
// 定义带元数据的Store
const useProductStore = defineStore('products', {
state: () => ({
items: [] as Product[]
}),
actions: {
async loadProducts() {
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 100))
this.items = [{ id: '1', name: 'Product 1', price: 10 }]
}
}
} as any) // 添加元数据需要any类型
// 现在Store会有元数据
const productStore = useProductStore()
console.log(productStore.$metadata)
console.log(productStore.$getStoreStats())
权限控制插件
typescript
// stores/plugins/authorization.ts
interface Permission {
action: string
resource: string
condition?: (context: any) => boolean
}
interface AuthorizationPluginOptions {
permissions?: Permission[]
currentUser?: () => any
onUnauthorized?: (action: string) => void
}
export function createAuthorizationPlugin(options: AuthorizationPluginOptions = {}) {
const {
permissions = [],
currentUser = () => null,
onUnauthorized = (action) => console.warn(`Unauthorized access attempt: ${action}`)
} = options
const permissionMap = new Map<string, Permission[]>()
// 按资源分组权限
permissions.forEach(permission => {
const resourcePermissions = permissionMap.get(permission.resource) || []
resourcePermissions.push(permission)
permissionMap.set(permission.resource, resourcePermissions)
})
const checkPermission = (resource: string, action: string, context: any = {}): boolean => {
const resourcePermissions = permissionMap.get(resource) || []
// 检查是否有对应的权限
const matchingPermission = resourcePermissions.find(p =>
p.action === action &&
(!p.condition || p.condition(context))
)
return !!matchingPermission
}
return (context: PiniaPluginContext) => {
const store = context.store
const storeId = store.$id
// 包装需要权限控制的方法
const securedMethods: Record<string, Function> = {}
Object.keys(store).forEach(key => {
const prop = (store as any)[key]
if (typeof prop === 'function' &&
!key.startsWith('$') &&
!['$reset', '$patch', '$subscribe'].includes(key)) {
securedMethods[key] = prop
;(store as any)[key] = (...args: any[]) => {
// 检查权限
const user = currentUser()
if (!user) {
onUnauthorized(`${storeId}.${key}`)
throw new Error('Authentication required')
}
// 检查资源权限
if (!checkPermission(storeId, key, { user, args })) {
onUnauthorized(`${storeId}.${key}`)
throw new Error(`Insufficient permissions for ${key}`)
}
// 执行原方法
return prop.apply(store, args)
}
}
})
return {
$checkPermission: (action: string) => {
const user = currentUser()
return user && checkPermission(storeId, action, { user })
},
$currentUser: currentUser(),
$permissions: Array.from(permissionMap.get(storeId) || [])
}
}
}
// 使用授权插件
const mockCurrentUser = () => ({
id: 'user1',
roles: ['admin', 'editor'],
permissions: ['users:read', 'users:write', 'products:read']
})
const authorizationPlugin = createAuthorizationPlugin({
currentUser: mockCurrentUser,
permissions: [
{ action: 'updateUser', resource: 'users', condition: (ctx) => ctx.user.roles.includes('admin') },
{ action: 'deleteUser', resource: 'users', condition: (ctx) => ctx.user.roles.includes('admin') },
{ action: 'readUsers', resource: 'users', condition: (ctx) => ctx.user.permissions.includes('users:read') }
],
onUnauthorized: (action) => {
console.error(`Access denied to ${action}`)
// 可以重定向到登录页或显示错误信息
}
})
const pinia = createPinia()
pinia.use(authorizationPlugin)
插件组合和链式使用
插件组合器
typescript
// stores/plugins/composer.ts
export class PluginComposer {
private plugins: Array<(context: PiniaPluginContext) => any> = []
use(plugin: (context: PiniaPluginContext) => any) {
this.plugins.push(plugin)
return this
}
compose() {
return (context: PiniaPluginContext) => {
const results: any[] = []
for (const plugin of this.plugins) {
try {
const result = plugin(context)
if (result) {
results.push(result)
}
} catch (error) {
console.error('Plugin error:', error)
}
}
// 合并所有插件的结果
const mergedResult: any = {}
for (const result of results) {
if (typeof result === 'object' && result !== null) {
Object.assign(mergedResult, result)
}
}
return mergedResult
}
}
}
// 创建组合插件
const featurePlugin = PluginComposer
.use(createLoggerPlugin({ logActions: true }))
.use(createPersistencePlugin({ key: 'app' }))
.use(createCachePlugin({ maxSize: 50 }))
.compose()
const pinia = createPinia()
pinia.use(featurePlugin)
条件插件
typescript
// stores/plugins/conditional.ts
export function createConditionalPlugin(
condition: (context: PiniaPluginContext) => boolean,
plugin: (context: PiniaPluginContext) => any
) {
return (context: PiniaPluginContext) => {
if (condition(context)) {
return plugin(context)
}
return {}
}
}
// 环境特定插件
const developmentPlugin = createConditionalPlugin(
(context) => process.env.NODE_ENV === 'development',
createLoggerPlugin({ logActions: true, logMutations: true })
)
const productionPlugin = createConditionalPlugin(
(context) => process.env.NODE_ENV === 'production',
createPersistencePlugin({ storage: localStorage })
)
// Store特定插件
const userStorePlugin = createConditionalPlugin(
(context) => context.store.$id === 'user',
(context) => ({
$secureOperations: true,
$auditTrail: []
})
)
const pinia = createPinia()
pinia.use(developmentPlugin)
pinia.use(productionPlugin)
pinia.use(userStorePlugin)
插件最佳实践
插件开发最佳实践
typescript
// stores/plugins/best-practices.ts
export function createRobustPlugin() {
return (context: PiniaPluginContext) => {
const store = context.store
const storeId = store.$id
// 1. 错误处理和恢复
const safeWrapper = <T extends Function>(fn: T, operationName: string): T => {
return ((...args: any[]) => {
try {
return fn.apply(store, args)
} catch (error) {
console.error(`Plugin error in ${storeId}.${operationName}:`, error)
// 可以在这里添加错误恢复逻辑
throw error
}
}) as T
}
// 2. 性能监控
const trackedActions = new Map<string, {
totalCalls: number
totalTime: number
avgTime: number
maxTime: number
lastError?: Error
}>()
// 3. 资源清理
const cleanupFunctions: Array<() => void> = []
// 4. 类型安全的扩展
const extendedStore = {
// 插件特定的方法
$pluginInfo: {
name: 'robust-plugin',
version: '1.0.0',
storeId,
initializedAt: new Date()
},
// 性能监控
$getActionStats: () => {
return Object.fromEntries(trackedActions)
},
// 清理方法
$cleanupPlugin: () => {
cleanupFunctions.forEach(fn => {
try {
fn()
} catch (e) {
console.error('Cleanup error:', e)
}
})
cleanupFunctions.length = 0
},
// 安全的方法包装
$wrapAction: <T extends Function>(action: T, name: string): T => {
return safeWrapper(action, name)
}
}
// 5. 非侵入式扩展
Object.keys(extendedStore).forEach(key => {
if (!(key in store)) {
(store as any)[key] = (extendedStore as any)[key]
}
})
// 6. 监听Store销毁事件
const unsubscribe = store.$onAction(({ name, store, args, after, onError }) => {
const startTime = performance.now()
let callCount = trackedActions.get(name)?.totalCalls || 0
callCount++
after(() => {
const endTime = performance.now()
const duration = endTime - startTime
const stats = trackedActions.get(name) || {
totalCalls: 0,
totalTime: 0,
avgTime: 0,
maxTime: 0
}
stats.totalCalls = callCount
stats.totalTime += duration
stats.avgTime = stats.totalTime / stats.totalCalls
stats.maxTime = Math.max(stats.maxTime, duration)
trackedActions.set(name, stats)
})
onError((error) => {
const stats = trackedActions.get(name) || {} as any
stats.lastError = error
trackedActions.set(name, stats)
})
})
cleanupFunctions.push(() => unsubscribe())
return extendedStore
}
}
插件性能优化
typescript
// stores/plugins/performance.ts
export function createPerformancePlugin() {
return (context: PiniaPluginContext) => {
const store = context.store
const storeId = store.$id
// 使用WeakMap存储插件特定数据,避免内存泄漏
const pluginData = new WeakMap<object, any>()
// 批量操作优化
const batchOperations: Array<() => void> = []
let batchTimeout: NodeJS.Timeout | null = null
// 优化的状态订阅
let stateChangeCount = 0
const maxChangeThreshold = 100 // 每秒最大变化次数
const optimizedSubscribe = (callback: Function) => {
let lastExecution = 0
const minInterval = 16 // ~60fps
return store.$subscribe((mutation, state) => {
const now = Date.now()
if (now - lastExecution > minInterval) {
callback(mutation, state)
lastExecution = now
} else {
// 如果变化太频繁,限制执行
stateChangeCount++
if (stateChangeCount > maxChangeThreshold) {
console.warn(`High frequency state changes detected in ${storeId}`)
// 可以在这里添加节流逻辑
}
}
})
}
// 批量操作
const batch = (operation: () => void) => {
batchOperations.push(operation)
if (batchTimeout) {
clearTimeout(batchTimeout)
}
batchTimeout = setTimeout(() => {
// 执行批量操作
batchOperations.forEach(op => op())
batchOperations.length = 0
batchTimeout = null
}, 0) // 在下一个tick执行
}
return {
$batch: batch,
$optimizedSubscribe: optimizedSubscribe,
$pluginData: pluginData,
$performance: {
stateChangeCount,
maxChangeThreshold,
batchOperationsCount: () => batchOperations.length
}
}
}
}
第三方插件集成
集成现有插件
typescript
// 集成流行的pinia插件
import { createPersistedState } from 'pinia-plugin-persistedstate'
// 自定义配置的持久化插件
function createCustomPersistedState() {
return createPersistedState({
key: (id) => `myapp_${id}`,
storage: localStorage,
serializer: {
serialize: (value) => JSON.stringify(value),
deserialize: (value) => {
try {
return JSON.parse(value)
} catch {
return value
}
}
},
pick: ['user', 'settings'], // 只持久化特定的stores
overwrite: false
})
}
// 组合多个插件
const pinia = createPinia()
pinia.use(createLoggerPlugin())
pinia.use(createCustomPersistedState())
pinia.use(createCachePlugin())
pinia.use(createRobustPlugin())
插件测试
插件单元测试
typescript
// __tests__/plugins.spec.ts
import { createPinia, defineStore } from 'pinia'
import { createLoggerPlugin } from '@/stores/plugins/logger'
import { createPersistencePlugin } from '@/stores/plugins/persistence'
import { vi, describe, it, beforeEach, expect, afterEach } from 'vitest'
describe('Pinia Plugins', () => {
let pinia: any
let consoleSpy: any
beforeEach(() => {
pinia = createPinia()
consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
})
afterEach(() => {
consoleSpy.mockRestore()
vi.restoreAllMocks()
})
it('should enhance store with logging capabilities', () => {
pinia.use(createLoggerPlugin())
const useTestStore = defineStore('test', {
state: () => ({
count: 0
}),
actions: {
increment() {
this.count++
}
}
})
const store = useTestStore()
expect(store.$logger).toBeDefined()
expect(store.$logger.enabled).toBe(true)
store.increment()
expect(consoleSpy).toHaveBeenCalled()
})
it('should persist store state', () => {
// 使用内存存储进行测试
const mockStorage: Record<string, string> = {}
const storageMock = {
getItem: (key: string) => mockStorage[key] || null,
setItem: (key: string, value: string) => {
mockStorage[key] = value
},
removeItem: (key: string) => {
delete mockStorage[key]
},
clear: () => {
Object.keys(mockStorage).forEach(key => delete mockStorage[key])
}
}
pinia.use(createPersistencePlugin({
storage: storageMock as any,
key: 'test'
}))
const usePersistTestStore = defineStore('persistTest', {
state: () => ({
name: 'Test User',
preferences: { theme: 'dark' }
})
})
// 创建store并修改状态
let store = usePersistTestStore()
store.name = 'Updated User'
store.preferences.theme = 'light'
// 重新创建store,状态应该被恢复
store = usePersistTestStore() // 这会从存储中恢复状态
// 注意:由于我们的持久化插件是异步的,实际测试中可能需要等待
expect(store.name).toBe('Updated User')
})
it('should handle plugin errors gracefully', () => {
const erroringPlugin = () => {
return () => {
throw new Error('Plugin error')
}
}
const safePlugin = () => {
return (ctx: any) => ({
$safe: true
})
}
pinia.use(erroringPlugin())
pinia.use(safePlugin())
const useErrorTestStore = defineStore('errorTest', {
state: () => ({
value: 'test'
})
})
const store = useErrorTestStore()
// 即使一个插件出错,store仍然应该正常工作
expect(store.value).toBe('test')
// 安全插件仍然应该工作
expect(store.$safe).toBe(true)
})
})
总结
Pinia插件系统提供了强大的扩展能力,通过本章的学习,你应该掌握:
- 插件基础:插件的定义、注册和使用
- 常用插件:日志、持久化、缓存等实用插件
- 高级功能:元数据管理、权限控制等复杂插件
- 插件组合:如何组合和条件使用插件
- 最佳实践:开发高质量插件的原则
- 性能优化:编写高性能插件的技术
- 测试策略:如何测试插件功能
插件系统是Pinia强大灵活性的体现,合理使用插件可以大大增强应用的功能和可维护性。