Skip to content

第五章:生产级插件系统与扩展

在前几章中,我们深入探讨了 Pinia 的核心机制,包括状态管理、actions 和 getters。现在,让我们进入更高级的主题——Pinia 的插件系统。我们将学习 pinia.use() 插件机制、持久化插件的实现原理以及如何构建自定义插件。

pinia.use() 插件机制

Pinia 的插件系统是其强大扩展性的重要体现。通过 pinia.use() 方法,我们可以向 Pinia 实例添加插件,这些插件可以扩展 store 的功能:

typescript
import { createPinia } from 'pinia'

// 创建 Pinia 实例
const pinia = createPinia()

// 添加插件
pinia.use((context) => {
  // 插件逻辑
  console.log('Store created:', context.store.$id)
})

// 插件可以返回对象来扩展 store
pinia.use((context) => {
  return {
    // 添加新的属性
    createdAt: new Date(),
    
    // 添加新的方法
    $log() {
      console.log('Store state:', context.store.$state)
    }
  }
})

插件的本质:(context: PiniaPluginContext) => Partial<Store>

Pinia 插件本质上是一个函数,接收插件上下文作为参数,并可以返回一个对象来扩展 store:

typescript
// 简化的插件类型定义
interface PiniaPluginContext {
  pinia: Pinia
  store: Store
  app: App // Vue 应用实例
  options: DefineStoreOptionsInPlugin
}

type PiniaPlugin = (context: PiniaPluginContext) => Partial<Store> | void

// 插件系统实现原理
function createPinia() {
  const pinia = {
    install(app) {
      // 设置应用实例
      pinia.app = app
      // 提供 pinia 实例
      app.provide(piniaSymbol, pinia)
    },
    
    use(plugin) {
      pinia._p.push(plugin)
      return pinia
    },
    
    _p: [] as PiniaPlugin[] // 插件数组
  }
  
  return pinia
}

// 在创建 store 时应用插件
function createSetupStore(id, setup, options, pinia) {
  // 执行 setup 函数创建 store
  const setupResult = setup()
  
  const store = reactive({
    $id: id,
    ...setupResult
  })
  
  // 应用插件
  pinia._p.forEach(plugin => {
    const pluginResult = plugin({
      pinia,
      store,
      app: pinia.app,
      options
    })
    
    if (pluginResult) {
      Object.assign(store, pluginResult)
    }
  })
  
  return store
}

PiniaPluginContext 包含的详细信息

插件上下文提供了丰富的信息,让插件能够根据不同的条件进行扩展:

typescript
interface PiniaPluginContext {
  // Pinia 实例
  pinia: Pinia
  
  // 当前创建的 store
  store: Store
  
  // Vue 应用实例
  app: App
  
  // store 的选项配置
  options: DefineStoreOptionsInPlugin
}

interface DefineStoreOptionsInPlugin<Id, S, G, A> {
  id: Id
  state?: () => S
  getters?: G & ThisType<Readonly<S> & StoreGetters<G> & PiniaCustomProperties>
  actions?: A & ThisType<A & S & StoreGetters<G> & PiniaCustomProperties>
  
  // 其他自定义选项
  [key: string]: any
}

常见插件场景

1. 持久化插件

typescript
// 持久化插件示例
function createPersistedStatePlugin(options = {}) {
  return (context) => {
    const { store, pinia } = context
    const key = `pinia_${store.$id}`
    
    // 从存储中恢复状态
    try {
      const fromStorage = localStorage.getItem(key)
      if (fromStorage) {
        store.$patch(JSON.parse(fromStorage))
      }
    } catch (error) {
      console.warn(`Failed to restore store ${store.$id} from localStorage`, error)
    }
    
    // 监听状态变化并保存到存储
    store.$subscribe((mutation, state) => {
      try {
        localStorage.setItem(key, JSON.stringify(state))
      } catch (error) {
        console.warn(`Failed to save store ${store.$id} to localStorage`, error)
      }
    })
  }
}

// 使用插件
const pinia = createPinia()
pinia.use(createPersistedStatePlugin())

2. 日志插件

typescript
// 日志插件示例
function createLoggerPlugin(options = {}) {
  return (context) => {
    const { store } = context
    
    // 记录 store 创建
    console.log(`[Pinia] Store "${store.$id}" created`)
    
    // 记录状态变化
    store.$subscribe((mutation, state) => {
      console.log(`[Pinia] Store "${store.$id}" mutated:`, {
        type: mutation.type,
        storeId: mutation.storeId,
        payload: mutation.payload,
        state
      })
    })
    
    // 记录 actions 执行
    store.$onAction((context) => {
      const startTime = Date.now()
      
      console.log(`[Pinia] Action "${context.name}" started on store "${store.$id}"`, {
        args: context.args
      })
      
      context.after((result) => {
        console.log(`[Pinia] Action "${context.name}" finished`, {
          duration: Date.now() - startTime,
          result
        })
      })
      
      context.onError((error) => {
        console.error(`[Pinia] Action "${context.name}" failed`, {
          duration: Date.now() - startTime,
          error
        })
      })
    })
  }
}

// 使用插件
const pinia = createPinia()
pinia.use(createLoggerPlugin())

3. 开发工具插件

typescript
// 开发工具插件示例
function createDevtoolsPlugin(options = {}) {
  return (context) => {
    const { store, app } = context
    
    // 仅在开发环境启用
    if (process.env.NODE_ENV !== 'production') {
      // 与 Vue Devtools 集成
      setupDevtools(app, store)
    }
  }
}

function setupDevtools(app, store) {
  // 这里是与 Vue Devtools 集成的具体实现
  // 通常涉及自定义事件和时间旅行功能
  app.config.globalProperties.$piniaStores = app.config.globalProperties.$piniaStores || {}
  app.config.globalProperties.$piniaStores[store.$id] = store
}

持久化插件手写实现

让我们深入实现一个完整的持久化插件:

typescript
// 完整的持久化插件实现
interface PersistedStateOptions {
  key?: string
  storage?: Storage
  paths?: string[]
  serializer?: {
    serialize: (value: any) => string
    deserialize: (value: string) => any
  }
}

function createPersistedStatePlugin(globalOptions: PersistedStateOptions = {}) {
  return (context) => {
    const { store, options } = context
    
    // 获取持久化配置
    const persistOptions = options.persist || globalOptions
    if (!persistOptions) return
    
    // 默认配置
    const defaultOptions: Required<PersistedStateOptions> = {
      key: `pinia_${store.$id}`,
      storage: localStorage,
      paths: [],
      serializer: {
        serialize: JSON.stringify,
        deserialize: JSON.parse
      }
    }
    
    // 合并配置
    const mergedOptions = {
      ...defaultOptions,
      ...(typeof persistOptions === 'object' ? persistOptions : {})
    }
    
    const { key, storage, paths, serializer } = mergedOptions
    
    try {
      // 从存储中恢复状态
      const fromStorage = storage.getItem(key)
      if (fromStorage) {
        const state = serializer.deserialize(fromStorage)
        
        if (paths.length) {
          // 只恢复指定路径的状态
          paths.forEach(path => {
            const value = getNestedProperty(state, path)
            if (value !== undefined) {
              store.$patch((state) => {
                setNestedProperty(state, path, value)
              })
            }
          })
        } else {
          // 恢复全部状态
          store.$patch(state)
        }
      }
    } catch (error) {
      console.warn(`[Pinia] Failed to restore store "${store.$id}" from storage`, error)
    }
    
    // 监听状态变化并保存到存储
    store.$subscribe((mutation, state) => {
      try {
        let toSave = state
        
        if (paths.length) {
          // 只保存指定路径的状态
          toSave = {}
          paths.forEach(path => {
            const value = getNestedProperty(state, path)
            if (value !== undefined) {
              setNestedProperty(toSave, path, value)
            }
          })
        }
        
        storage.setItem(key, serializer.serialize(toSave))
      } catch (error) {
        console.warn(`[Pinia] Failed to save store "${store.$id}" to storage`, error)
      }
    })
  }
}

// 工具函数:获取嵌套属性
function getNestedProperty(obj, path) {
  return path.split('.').reduce((current, key) => {
    return current && current[key] !== undefined ? current[key] : undefined
  }, obj)
}

// 工具函数:设置嵌套属性
function setNestedProperty(obj, path, value) {
  const keys = path.split('.')
  const lastKey = keys.pop()
  
  let current = obj
  for (const key of keys) {
    if (!(key in current)) {
      current[key] = {}
    }
    current = current[key]
  }
  
  if (lastKey) {
    current[lastKey] = value
  }
}

// 使用示例
const pinia = createPinia()
pinia.use(createPersistedStatePlugin())

// 在 store 中配置持久化
export const useUserStore = defineStore('user', () => {
  const user = ref(null)
  const preferences = ref({})
  
  return { user, preferences }
}, {
  // 配置持久化选项
  persist: {
    key: 'user-store',
    storage: localStorage,
    paths: ['user'] // 只持久化 user 状态
  }
})

插件的高级用法

条件性插件应用

typescript
// 根据条件应用插件
function conditionalPlugin(context) {
  const { store, options } = context
  
  // 只对特定 store 应用插件
  if (options.myCustomOption) {
    return {
      customMethod() {
        console.log('Custom method for', store.$id)
      }
    }
  }
}

// 在 store 中启用自定义选项
export const useSpecialStore = defineStore('special', () => {
  const data = ref([])
  
  return { data }
}, {
  myCustomOption: true // 启用插件
})

插件间的通信

typescript
// 插件间通信示例
const pluginRegistry = new Map()

function pluginA(context) {
  const { store } = context
  
  // 注册插件功能
  pluginRegistry.set('pluginA', {
    doSomething() {
      console.log('Plugin A doing something')
    }
  })
  
  return {
    $pluginA: pluginRegistry.get('pluginA')
  }
}

function pluginB(context) {
  const { store } = context
  
  // 使用其他插件的功能
  const pluginA = pluginRegistry.get('pluginA')
  if (pluginA) {
    pluginA.doSomething()
  }
}

小结

在本章中,我们深入探讨了 Pinia 的插件系统:

  1. pinia.use() 插件机制允许扩展 Pinia 功能
  2. 插件本质上是接收上下文并返回扩展对象的函数
  3. PiniaPluginContext 提供了丰富的上下文信息
  4. 常见插件场景包括持久化、日志记录和开发工具集成
  5. 实现了完整的持久化插件,支持路径过滤和自定义序列化
  6. 插件可以条件性应用,也可以相互通信

在下一章中,我们将探讨 Pinia 在 SSR(服务端渲染)环境中的应用,包括状态脱水与注水机制。


思考题

  1. 你在项目中使用过哪些 Pinia 插件?是如何解决具体问题的?
  2. 如果要实现一个加密持久化插件,你会如何设计其安全机制?