各位观众老爷,晚上好!我是今晚的讲师,咱们今天聊聊 Pinia 源码里那些有点意思的插件机制。放心,保证不让你听得想睡觉。
Pinia 插件机制:让你的 Store 飞起来
想象一下,你的 Pinia store 就像一辆汽车。它能跑,能载人,基本功能没问题。但如果你想让它更牛逼,比如加个涡轮增压,或者装个自动驾驶系统,那就得靠插件了。Pinia 的插件机制,就是让你给 Store 加各种“外挂”的魔法。
插件的定义:一个简单的函数
Pinia 插件本质上就是一个函数。这个函数接收一个 PiniaPluginContext
对象作为参数,你可以在这个函数里对 Store 进行各种操作。
import { PiniaPluginContext } from 'pinia';
function myPlugin(context: PiniaPluginContext) {
// 在这里对 Store 进行操作
}
这个 PiniaPluginContext
对象里都有些啥呢?咱们来细瞅瞅:
属性 | 类型 | 描述 |
---|---|---|
pinia |
Pinia |
Pinia 实例。你可以用它来访问和操作所有 Store。 |
app |
VueApp (Vue 3) / any (Vue 2) |
Vue 应用实例。如果你在 Vue 应用中使用 Pinia,就可以访问 Vue 的各种 API。 |
store |
PiniaStore |
当前正在使用的 Store 实例。这是你操作 Store 的主要入口。 |
options |
DefineStoreOptions |
定义 Store 时的选项对象。你可以根据这些选项来定制插件的行为。 |
插件的注册:让 Pinia 知道你的存在
要让你的插件生效,你需要把它注册到 Pinia 实例上。有两种方式:
-
全局注册: 对所有 Store 生效。
import { createPinia } from 'pinia'; import myPlugin from './myPlugin'; const pinia = createPinia(); pinia.use(myPlugin); // 注册插件
-
Store 特定的注册: 只对特定的 Store 生效。这需要在定义 Store 的时候,通过
defineStore
的options
对象来指定。import { defineStore } from 'pinia'; import myPlugin from './myPlugin'; export const useMyStore = defineStore('myStore', { state: () => ({ count: 0, }), actions: { increment() { this.count++; }, }, pinia: { plugins: [myPlugin], // Store 特定的插件 }, });
注意: 在
options
对象中指定插件,需要在pinia
属性下创建一个plugins
数组,并将你的插件函数添加到这个数组中。
插件的功能:无限可能
有了插件,你就可以对 Store 做各种各样的事情:
-
添加全局属性: 比如,给所有 Store 都加上一个
$reset
方法,方便重置状态。import { PiniaPluginContext } from 'pinia'; function resetPlugin({ store }: PiniaPluginContext) { store.$reset = () => { const initialState = store.$state; // 保存初始状态 store.$patch(initialState); // 使用 $patch 来更新状态 }; } export default resetPlugin;
使用:
import { defineStore } from 'pinia'; export const useMyStore = defineStore('myStore', { state: () => ({ count: 0, }), actions: { increment() { this.count++; }, }, }); const myStore = useMyStore(); myStore.$reset(); // 重置状态
-
修改 Store 的行为: 比如,在每次状态更新的时候,自动把状态保存到 localStorage。
import { PiniaPluginContext } from 'pinia'; function persistPlugin({ store }: PiniaPluginContext) { const STORAGE_KEY = `pinia-store-${store.$id}`; // 初始化时从 localStorage 加载状态 const storedState = localStorage.getItem(STORAGE_KEY); if (storedState) { store.$patch(JSON.parse(storedState)); } // 监听状态变化,保存到 localStorage store.$subscribe((mutation, state) => { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); }); } export default persistPlugin;
使用:
import { defineStore } from 'pinia'; export const useMyStore = defineStore('myStore', { state: () => ({ count: 0, }), actions: { increment() { this.count++; }, }, }); // 不需要手动调用,状态会自动保存和加载
-
扩展 Store 的功能: 比如,添加一些常用的工具函数,方便操作数据。
import { PiniaPluginContext } from 'pinia'; function utilsPlugin({ store }: PiniaPluginContext) { store.$double = () => { // 假设 state 中有 count 属性 if (typeof store.$state.count === 'number') { return store.$state.count * 2; } else { return 0; // 或者抛出错误,取决于你的需求 } }; } export default utilsPlugin;
使用:
import { defineStore } from 'pinia'; export const useMyStore = defineStore('myStore', { state: () => ({ count: 0, }), actions: { increment() { this.count++; }, }, }); const myStore = useMyStore(); const doubledCount = myStore.$double(); // 使用 $double 函数 console.log(doubledCount);
访问和修改 Store 实例:核心技巧
在插件里,最重要的就是访问和修改 Store 实例。PiniaPluginContext
提供的 store
属性,就是你操作 Store 的入口。
-
访问 Store 的状态: 直接通过
store.$state
访问。const count = store.$state.count;
-
修改 Store 的状态: 有三种方式:
-
直接修改: 最简单粗暴的方式,但不推荐。因为这样修改的状态不会被 Pinia 追踪,导致一些插件(比如持久化插件)失效。
store.$state.count = 10; // 不推荐
-
$patch
方法: 推荐使用的方式。$patch
可以批量更新状态,而且会被 Pinia 追踪。store.$patch({ count: 10, name: 'John', }); // 也可以传入一个函数,进行更复杂的状态更新 store.$patch((state) => { state.count++; state.name = state.name + ' Doe'; });
-
调用 actions: 如果你想通过 actions 来修改状态,也是可以的。不过要注意,actions 必须是同步的。
store.increment(); // 调用 actions
-
-
访问 Store 的 actions: 直接通过
store.actionName()
访问。store.increment();
-
访问 Store 的 getters: 直接通过
store.getterName
访问。const doubleCount = store.doubleCount;
源码解析:看看 Pinia 是怎么实现插件机制的
Pinia 的插件机制其实并不复杂,核心代码主要在 pinia.ts
和 store.ts
文件里。
-
createPinia
函数: 这个函数负责创建 Pinia 实例,并且提供了use
方法来注册插件。export function createPinia(): Pinia { const scope = effectScope(true) const state = scope.run(() => reactive({}))! let _p: Pinia['_p'] = [] const pinia: Pinia = markRaw({ install(app: App) { setActivePinia(pinia) app.provide(piniaSymbol, pinia) app.config.globalProperties.$pinia = pinia // 避免在 ssr 中出现警告,如果未找到 defineStore,则使用一个假函数 if (__DEV__ && !app.config.globalProperties.$defineStore) { Object.defineProperty(app.config.globalProperties, '$defineStore', { get() { return () => { throw new Error( '`defineStore` is not defined. Did you forget to install pinia as a plugin?n' + 'ex: `app.use(pinia)`.' ) } }, }) } }, use(plugin: PiniaPlugin) { _p.push(plugin) return this }, _p, _a: null, _e: scope, _s: new Set<SubscriptionCallback<Pinia>>(), state, }) return pinia }
pinia.use(plugin)
实际上就是把插件函数添加到一个数组_p
里。 -
defineStore
函数: 这个函数负责定义 Store,并且在 Store 创建之后,会遍历_p
数组,执行所有的插件函数。export function defineStore< Id extends string, S extends StateTree, G extends _GettersTree<S>, A extends _ActionsTree >( id: Id, options: | DefineStoreOptions<Id, S, G, A> | ((this: Pinia, pinia: Pinia) => DefineStoreOptions<Id, S, G, A>), pinia?: Pinia | null ): StoreDefinition<Id, S, G, A> { const isSetupStore = typeof options === 'function' function useStore(pinia?: Pinia | null, hot?: StoreGeneric): Store<Id, S, G, A> { const currentPinia = pinia ?? currentActivePinia ?? activePinia if (!currentPinia && typeof window !== 'undefined') { throw new Error( `[🍍]: getActivePinia was called with no active Pinia. Did you forget to install pinia as a plugin?n` + `See https://pinia.vuejs.org/core-concepts/plugins.html#using-a-plugin` ) } pinia = currentPinia! if (__DEV__ && hot) { hot._hmrType = 'defineStore' hot._hmrId = id } const idToUse = isSetupStore ? id : options.id if (pinia._s.has(idToUse)) { return pinia._s.get(idToUse)! as Store<Id, S, G, A> } let optionsForStore: DefineStoreOptions<Id, S, G, A> if (isSetupStore) { optionsForStore = options(pinia) } else { optionsForStore = options } const { state, actions, getters } = optionsForStore let initialState: S | undefined if (!isSetupStore) { initialState = pinia.scope.run(() => state ? state() : {} )! } // 将 store 作为一个可观察的对象,以便触发依赖项 const store: StoreGeneric = pinia.scope.run(() => reactive( extend( { $id: idToUse, $onAction: addSubscription.bind(null, actionSubscriptions), $onAction: addSubscription.bind(null, actionSubscriptions), $patch, $reset, $subscribe, $dispose, $state: initialState, $options: optionsForStore, }, state ? defineSetupStore(pinia, idToUse, { state, actions, getters }, initialState, isOptionsAPI) : defineSetupStore(pinia, idToUse, optionsForStore as any, initialState, isOptionsAPI) ) ) )! // devtools custom properties if (__DEV__) { devtoolsPlugin && devtoolsPlugin({ app: pinia._a, store, id: idToUse, acceptHMR: hot ? acceptHMR : null, pinia, }) } // apply all plugins before anything pinia._p.forEach((plugin) => { plugin({ pinia, app: pinia._a, store, options: optionsForStore, }) }) // 将 store 添加到 pinia 的集合中 pinia._s.add(idToUse) return store as Store<Id, S, G, A> } useStore.$id = id useStore.$reset = function $reset() { const store = this() store.$reset() } useStore.$patch = function $patch(state: ((storeState: S) => void) | Partial<S>) { const store = this() store.$patch(state) } useStore.$subscribe = function $subscribe( callback: SubscriptionCallback<S>, options?: SubscriptionOptions ) { const store = this() return store.$subscribe(callback, options) } return useStore }
在
defineStore
函数内部,可以看到这段代码:pinia._p.forEach((plugin) => { plugin({ pinia, app: pinia._a, store, options: optionsForStore, }); });
这行代码就是遍历
_p
数组,执行所有的插件函数。插件函数接收一个PiniaPluginContext
对象,包含了pinia
实例,Vue 应用实例,当前 Store 实例,以及定义 Store 时的选项对象。
实际案例:一个完整的插件示例
咱们来写一个完整的插件示例,实现一个简单的状态同步功能。这个插件会将 Store 的状态同步到另一个 Store。
import { PiniaPluginContext, Store } from 'pinia';
interface SyncPluginOptions {
targetStore: Store;
keys: string[]; // 需要同步的 state keys
}
function syncPlugin(options: SyncPluginOptions) {
return function ({ store }: PiniaPluginContext) {
const { targetStore, keys } = options;
store.$subscribe((mutation, state) => {
keys.forEach((key) => {
if (state.hasOwnProperty(key)) {
targetStore.$patch({ [key]: state[key] });
}
});
});
targetStore.$subscribe((mutation, state) => {
keys.forEach((key) => {
if (state.hasOwnProperty(key)) {
store.$patch({ [key]: state[key] });
}
});
});
};
}
export default syncPlugin;
使用:
import { defineStore } from 'pinia';
import syncPlugin from './syncPlugin';
export const useStoreA = defineStore('storeA', {
state: () => ({
name: 'Alice',
age: 20,
}),
actions: {
setName(name: string) {
this.name = name;
},
},
});
export const useStoreB = defineStore('storeB', {
state: () => ({
name: 'Bob',
age: 30,
}),
actions: {
setAge(age: number) {
this.age = age;
},
},
pinia: {
plugins: [
syncPlugin({
targetStore: useStoreA(),
keys: ['name', 'age'],
}),
],
},
});
const storeA = useStoreA();
const storeB = useStoreB();
// 现在,修改 storeA 的 name 或 age,storeB 也会同步更新
storeA.setName('Charlie');
console.log(storeA.name); // Charlie
console.log(storeB.name); // Charlie
// 修改 storeB 的 age,storeA 也会同步更新
storeB.setAge(40);
console.log(storeA.age); // 40
console.log(storeB.age); // 40
注意事项:
- 插件的执行顺序: 插件的执行顺序和注册顺序有关。先注册的插件先执行。
- 插件的副作用: 插件可能会产生副作用,比如修改全局变量,或者操作 DOM。要注意控制副作用,避免影响其他模块。
- 插件的性能: 插件可能会影响性能,比如在每次状态更新的时候执行一些复杂的计算。要注意优化插件的性能,避免影响用户体验。
- 避免循环依赖: 如果两个 Store 互相依赖,并且都使用了插件,可能会导致循环依赖的问题。要注意避免循环依赖,或者使用一些技巧来解决循环依赖的问题。
- 类型安全: 在使用 TypeScript 的时候,要注意插件的类型安全。要确保插件函数接收的参数类型正确,并且返回值的类型也正确。
总结:
Pinia 的插件机制是一个强大的工具,可以让你扩展 Store 的功能,定制 Store 的行为,提高开发效率。只要掌握了插件的定义,注册,访问和修改 Store 实例的技巧,就可以玩转 Pinia 的插件机制,让你的 Store 飞起来。
希望今天的讲座对大家有所帮助。如果有什么问题,欢迎随时提问。下次再见!