Skip to content
On this page

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插件系统提供了强大的扩展能力,通过本章的学习,你应该掌握:

  1. 插件基础:插件的定义、注册和使用
  2. 常用插件:日志、持久化、缓存等实用插件
  3. 高级功能:元数据管理、权限控制等复杂插件
  4. 插件组合:如何组合和条件使用插件
  5. 最佳实践:开发高质量插件的原则
  6. 性能优化:编写高性能插件的技术
  7. 测试策略:如何测试插件功能

插件系统是Pinia强大灵活性的体现,合理使用插件可以大大增强应用的功能和可维护性。