第五章:生产级插件系统与扩展
在前几章中,我们深入探讨了 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 的插件系统:
pinia.use()插件机制允许扩展 Pinia 功能- 插件本质上是接收上下文并返回扩展对象的函数
PiniaPluginContext提供了丰富的上下文信息- 常见插件场景包括持久化、日志记录和开发工具集成
- 实现了完整的持久化插件,支持路径过滤和自定义序列化
- 插件可以条件性应用,也可以相互通信
在下一章中,我们将探讨 Pinia 在 SSR(服务端渲染)环境中的应用,包括状态脱水与注水机制。
思考题:
- 你在项目中使用过哪些 Pinia 插件?是如何解决具体问题的?
- 如果要实现一个加密持久化插件,你会如何设计其安全机制?