Airi 中的 Pinia 插件机制:为所有 Store 注入自定义属性、状态与行为的完整实践
Airi 中的 Pinia 插件机制为所有 Store 注入自定义属性、状态与行为的完整实践【免费下载链接】airi Self hosted, you-owned Grok Companion, a container of souls of waifu, cyber livings to bring them into our worlds, wishing to achieve Neuro-samas altitude. Capable of realtime voice chat, Minecraft, Factorio playing. Web / macOS / Windows supported.项目地址: https://gitcode.com/GitHub_Trending/ai/airiPinia 的插件Plugin是跨 Store 统一增强能力的关键扩展点通过一个在pinia.use()注册的函数即可为应用中的每一个 store 注入自定义属性、响应式状态或全新行为。本文以 Pinia 官方插件文档骨架为主线完整覆盖插件上下文、属性/状态注入、markRaw外部对象、自定义 Store 选项、TypeScript 模块增强与 Nuxt 集成等全部实操内容并结合 airi 仓库中真实落地的piniaPluginTracing与跨渲染器同步插件源码展示这些模式在 Web / Electron 多窗口场景下的工程化运用。插件的基本形态一个返回对象的函数Pinia 插件最简单的形态是一个函数接收插件上下文可以完全忽略返回一个对象该对象的所有键值对被展开到每一个store 实例上。以官方文档中的示例为基础import { createPinia } from pinia function SecretPiniaPlugin() { return { secret: the cake is a lie } } const pinia createPinia() pinia.use(SecretPiniaPlugin) // In any store const store useStore() store.secret // the cake is a lie这里的关键点是注册时机pinia.use()既对新创建的 store 生效也会立即应用于已经存在的 store——即使用先调用useStore()再注册插件插件依然会补齐属性。airi 的 Web 端入口 apps/stage-web/src/main.ts 就是这个模式的标准用法const pinia createPinia() const synced setupSynced() pinia.use(synced.pinia) if (import.meta.env.DEV) pinia.use(piniaPluginTracing)可以看出两个工程细节其一多个插件按注册顺序依次执行synced.pinia是运行时对象的plugin属性下文展开其二调试插件通过import.meta.env.DEV门控仅在开发构建中安装避免生产环境残留跟踪逻辑。插件上下文PiniaPluginContext四大字段插件函数接收一个上下文对象类型为PiniaPluginContext这是编写任何非平凡插件的起点import { PiniaPluginContext } from pinia export function myPiniaPlugin(context: PiniaPluginContext) { context.pinia // pinia instance context.app // Vue app instance context.store // store being augmented context.options // store definition options }各字段的作用边界如下context.pinia当前 pinia 实例可访问$id、已注册插件列表_p等context.app宿主 Vue 应用实例可用于app.provide()向组件注入资源context.store正在被扩展的 store 本身这是绝大多数插件操作的主体直接在其上赋值即可挂载属性context.optionsOptions Store 的原始定义对象setup store 场景下见下文的第三个参数是读取自定义选项的唯一入口。注意context对每个 store 调用一次因此插件函数体内不能假设只执行一次如需全局单例资源如 channel 连接应使用模块级变量缓存。添加自定义属性返回对象 vs 直接赋值官方文档给出两种等价路径方式一返回对象。返回值会被展开到 store 上并且默认即被 devtools 识别pinia.use(() ({ hello: world }))方式二直接赋值给store。这种方式下开发工具不会自动感知新属性需要手动登记pinia.use(({ store }) { store.hello world // For devtools visibility in dev mode if (process.env.NODE_ENV development) { store._customProperties.add(hello) } })store._customProperties是一个Setstringdevtools 通过它枚举非 state 来源的自定义属性。仅登记属性名即可让 DevTools 在面板中展示该字段避免“运行时存在但面板里看不到”的排查成本。添加自定义 State同时写入 store 与 store.$state与直接挂属性不同自定义state需要同时操作两个位置才能兼顾响应式、$patch/$state重置语义以及 SSR 与 devtools 的正确性import { toRef, ref } from vue pinia.use(({ store }) { if (!store.$state.hasOwnProperty(hasError)) { const hasError ref(false) store.$state.hasError hasError } store.hasError toRef(store.$state, hasError) })这段代码体现了三条规则以store.$state为事实源先创建ref(false)写入store.$state.hasError这样store.$state的整体替换如$patch或 hydration会自然覆盖新字段用toRef建立代理store.hasError toRef(store.$state, hasError)使 store 顶层出现一个只读视图读写都透传到$state组件模板中store.hasError与普通 state 字段无差别hasOwnProperty守卫防止同一插件被重复安装或测试中多次createPinia复用 store时重复初始化导致覆盖。注入非响应式外部对象markRawVue Router 实例、WebGL 上下文、第三方 SDK 这类大型且不应被代理化的对象注入前必须用markRaw()包裹否则 Vue 的 reactive 系统会递归代理它们造成性能损耗与不可预期的副作用import { markRaw } from vue import { router } from ./router pinia.use(({ store }) { store.router markRaw(router) })配合下文的PiniaCustomProperties模块增强后store.router即获得完整的Router类型推导。自定义 Store 选项让插件读取 store 的私有配置这是插件机制最灵活的用法store 定义中声明自定义选项插件统一消费。文档以「按 action 名做防抖」为例展示了 Options Store 的完整链路。Store 定义侧defineStore(search, { actions: { searchContacts() { /* ... */ }, }, debounce: { searchContacts: 300, }, })插件侧读取options并用 lodashdebounce包装原 actionimport debounce from lodash/debounce pinia.use(({ options, store }) { if (options.debounce) { return Object.keys(options.debounce).reduce((acc, action) { acc[action] debounce(store[action], options.debounce[action]) return acc }, {}) } })注意这里同时使用了两种增强手段直接改写了store.searchContacts原 action 被防抖版本替换又通过返回值再次挂载——插件返回值会覆盖同名属性最终store.searchContacts就是防抖后的函数。对于Setup Store由于没有 options 对象自定义选项作为defineStore的第三个参数传入插件中仍通过context.options读到同一份数据defineStore( search, () { /* ... */ }, { debounce: { searchContacts: 300 }, } )两种写法对插件是透明的这让「同一套插件代码兼容两种 store 风格」成为可能。TypeScript 模块增强类型层面的三块拼图仅靠运行时赋值store.hello、store.hasError与debounce选项在 TS 中都报类型错误。Pinia 预留了三个可合并的接口分别对应三种增强场景自定义属性挂到 store 实例上import pinia import type { Router } from vue-router declare module pinia { export interface PiniaCustomProperties { router: Router hello: string } }自定义 State$state上的字段declare module pinia { export interface PiniaCustomStatePropertiesS { hasError: boolean } }自定义 OptionsdefineStore的 options 字段declare module pinia { export interface DefineStoreOptionsBaseS, Store { debounce?: PartialRecordkeyof StoreActionsStore, number } }其中keyof StoreActionsStore的写法值得注意它把debounce的键约束为该 store 真实存在的 action 名拼错 action 名时编译器直接报错。实际落地时这三段增强声明应集中放在每个应用或共享包的全局类型文件中保证所有引用方统一生效。在插件中订阅$subscribe 与 $onAction插件内部可以注册store.$subscribestate 变更回调与store.$onActionaction 生命周期钩子实现日志、埋点、跨窗口通知等横切逻辑pinia.use(({ store }) { store.$subscribe(() { // React to state changes }) store.$onAction(() { // React to actions }) })airi 的piniaPluginTracing正是该模式的完整工程化实现位于 packages/stage-ui/src/libs/pinia/pinia-plugin-tracing.tsexport const piniaPluginTracing: PiniaPlugin ({ store }) { const tracedWindow startRateTracing() if (tracedWindow) { store.$subscribe((mutation) { incrementRateTraceCount(tracedWindow.mutations, mutation.storeId) incrementRateTraceCount(tracedWindow.mutationTypes, mutation.type) }, { detached: true, flush: sync }) } store.$onAction(({ name, after, onError }) { if (tracedWindow) incrementRateTraceCount(tracedWindow.actions, ${store.$id}.${name}) const event { invocationId: nanoid(), storeId: store.$id, actionName: name, ...(typeof location undefined ? {} : { sourceUrl: location.href }), } emitActionEvent(event, started) after(() emitActionEvent(event, completed)) onError((error) { if (tracedWindow) tracedWindow.actionFailures 1 emitActionEvent(event, failed, error) }) }) }对照文档中的模式这份实现补充了几个细节$onAction的三段式钩子回调参数里的after与onError分别注册 action 成功完成与抛错时的回调配合started事件即可完整追踪started → completed | failed生命周期。事件通过BroadcastChannel见 emitActionEvent发出供独立渲染的调试浮层消费——注意插件刻意不保留action 参数、结果或 state 快照只记录调用元信息$subscribe的第二参数{ detached: true, flush: sync }使订阅不随 store 销毁、且以同步 flush 触发保证统计窗口内计数不丢帧限频统计窗开发环境下在localStorage置airi:debug:pinia-tracing true后刷新插件每 5 秒rateTraceIntervalMs 5_000输出一次 action/mutation 速率摘要并对 top 10 热点按名聚合见 startRateTracing 与 reportRateTraceWindow。其行为有测试用例 pinia-plugin-tracing.test.ts 覆盖安装位置该插件随proj-airi/stage-ui/libs/pinia从 index.ts 统一导出除 apps/stage-web/src/main.ts 外Electron 桌面端 apps/stage-tamagotchi/src/renderer/main.ts 与移动端 apps/stage-pocket/src/main.ts 均以同样的pinia.use(...)方式安装保证了三个 stage 应用插件行为一致。跨渲染器同步插件插件返回值的另一种打开方式airi 的 stage 应用常存在「同一浏览器多个渲染器 / 多个窗口」运行同一套 UI 的形态此时 state 需要在渲染器之间选举主从并同步。仓库将这一能力封装为setupSynced()位于 packages/stage-ui/src/libs/pinia/setup-synced.tsexport function setupSynced(options: PickSyncedOptions, leadership {}): { pinia: PiniaPlugin, vue: Plugin } { const runtime createSyncedPiniaPlugin({ namespace: airi:stage:pinia, // Chat and image-generation actions can outlive the plugins 30-second // default. Keep the timeout aligned with the previous Electron coordinator. callTimeout: 5 * 60 * 1000, ...options, onError(error) { console.error([stage-synced-pinia] Synchronization failed:, error) }, }) // ... 封装 Vue 侧 provide/生命周期清理 return { pinia: runtime.plugin, vue, } }这里有几个值得留意的点runtime.plugin是一个标准PiniaPlugin因此同步能力完全复用pinia.use()通道与文档描述的基本插件形态无缝衔接callTimeout从插件默认 30 秒调整为 5 分钟注释明确说明原因是「聊天与图像生成类 action 可能超时」——这类长任务 action 正是上文$onAction追踪要关注的对象返回的vue插件则通过app.provide(injectKeyPiniaSynced, runtime)暴露运行时并提供pagehide/onUnmount双重清理保证同步通道随应用卸载释放。组件侧可通过usePiniaSynced()获取运行时未安装时会抛出明确错误提示先安装 Vue 插件。Nuxt 环境在 Nuxt 插件中注册 Pinia 插件文档最后一节覆盖 Nuxt 场景由于 Nuxt 中 pinia 实例通过$pinia注入官方建议将pinia.use()放进一个 Nuxt 插件文件// plugins/myPiniaPlugin.ts import { PiniaPluginContext } from pinia function MyPiniaPlugin({ store }: PiniaPluginContext) { store.$subscribe((mutation) { console.log([ ${mutation.storeId}]: ${mutation.type}) }) return { creationTime: new Date() } } export default defineNuxtPlugin(({ $pinia }) { $pinia.use(MyPiniaPlugin) })这里同时演示了「直接改 store」与「返回新增属性」的混合$subscribe挂在 store 上做变更日志返回的creationTime自动成为每个 store 的新属性。airi 目前基于 Vite Vue 直接createApp().use(pinia)装配见 apps/stage-web/src/main.ts未采用 Nuxt该小节主要面向同样基于 Pinia 的 Nuxt 项目读者模式本身与框架无关。小结插件机制的能力矩阵需求官方文档模式airi 仓库对应实践全 store 注入只读属性返回对象如secret、creationTimeN/A通用模式开发期调试/埋点$subscribe$onActionpiniaPluginTracingDEV门控安装跨渲染器 state 同步第三方 PiniaPlugin 经pinia.use()安装setupSyncedcallTimeout调至 5 分钟类型安全PiniaCustomProperties/PiniaCustomStatePropertiesS/DefineStoreOptionsBase三段模块增强类型声明随包内类型文件组织掌握这条链路后读者可以在任何 Pinia 应用里按「注册插件 → 消费 context → 返回对象或直接改 store → TS 模块增强」四步扩展 store 能力并在 airi 仓库中直接参照 tracing 与 synced 两个真实插件实现验证 action 生命周期追踪与多窗口状态选举的落地细节。【免费下载链接】airi Self hosted, you-owned Grok Companion, a container of souls of waifu, cyber livings to bring them into our worlds, wishing to achieve Neuro-samas altitude. Capable of realtime voice chat, Minecraft, Factorio playing. Web / macOS / Windows supported.项目地址: https://gitcode.com/GitHub_Trending/ai/airi创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
上一篇/下一篇内容由系统自动关联
返回资讯列表 →