Airi 中的 Pinia 插件机制:为所有 Store 注入自定义属性、状态与行为的完整实践

发布时间:2026/9/10 0:42:21

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),仅供参考
延伸阅读

更多相关文章

2026/9/7 22:07:51

Umi-OCR 评测:免费离线OCR工具,一张截图、整本PDF都能识别

Umi-OCR 评测:免费离线OCR工具,一张截图、整本PDF都能识别 【免费下载链接】Umi-OCR OCR software, free and offline. 开源、免费的离线OCR软件。支持截屏/批量导入图片,PDF文档识别,排除水印/页眉页脚,扫描/生成二维…

2026/9/9 23:37:00

DeepTutor 本地部署 5 步跑通指南

DeepTutor 本地部署 5 步跑通指南 【免费下载链接】DeepTutor DeepTutor: Lifelong Personalized Tutoring. https://deeptutor.info/. 项目地址: https://gitcode.com/GitHub_Trending/dee/DeepTutor 你手头有几份 PDF 讲义,想找一个能照着讲义答题、帮你出…

2026/9/8 6:00:18

Pixelle-Video AI短视频生成上手手册:从一句主题到成片出街

Pixelle-Video AI短视频生成上手手册:从一句主题到成片出街 【免费下载链接】Pixelle-Video 🚀 AI 全自动短视频引擎 | AI Fully Automated Short Video Engine 项目地址: https://gitcode.com/GitHub_Trending/pi/Pixelle-Video 把一段产品文案丢…

2026/9/10 0:41:01

低代码平台动态引擎设计与实践:Liquor规则配置化与热更新

做低代码平台这几年,我最大的体会是:平台好不好用,不看你拖拽组件做了多少,而看业务逻辑能不能“动”起来。我说的“动”,不是改个字段、换张表单,而是不重新发版、不重启服务,就能动态修改一段…

2026/9/10 0:41:01

Matlab/Simulink双馈风机仿真:MPPT控制与参数观测实战

1. 选Matlab 2019而不是追新版本:双馈风机模型架构的搭建前提1.1 我为什么把项目锁死在Matlab 2019上先说个现实问题:做双馈风机(DFIG)仿真,版本选择真的会卡住进度。我这几年接触过不少做新能源控制的团队&#xff0c…

2026/9/10 0:41:01

光电测试技术全解析:从AOMTI 2026看行业未来趋势

1. 光电测试技术被低估的这些年:高精度背后全是细节仗光电测试技术这个行当,在外行眼里就是"拿个仪器测一测光",但在真正干这行的人心里,它几乎是现代工业的隐形地基。芯片制造里的光刻对准、光纤通信里每0.01dB的损耗波…

2026/9/10 0:36:01

PySide6开发桌面天气应用全攻略:从API对接、界面设计到打包部署

“桌面版天气预报应用”这个名字听起来简单,但真正动手做的时候,你会发现它几乎能逼你把桌面开发、网络请求、数据解析、状态管理、异常处理、打包分发这条路完整走一遍。我最初想做个桌面天气应用,纯粹是因为受够了手机天气推送的过度设计—…

2026/9/9 13:11:35

超人会飞不算本事:系统稳定依赖清晰规则与边界设计

开头先不绕弯子。“#斯坦李吐槽dc 所以超人是无缘无故会飞的嘛哈哈哈哈哈哈哈锤哥真是技术人才啊!#雷神 #复联”这类调侃式短标题,第一波冲击力在于它把两个宇宙的角色塞进同一个吐槽箱里,但细想一下就能发现,它真正碰到的根本不是…

2026/9/8 7:15:15

超人VS蜘蛛侠:拆解超级IP的影响力与传播方法论

把“蜘蛛侠 vs 超人”放在 CSDN 上聊,可能很多人第一反应是走错片场了。但如果把这两个角色看成“两个持续运营了 80 多年的文化产品”,你会发现,这场比较本质上是两个不同 IP 策略的长期结果对比:超人赢在定义了整个超级英雄题材…

2026/9/9 16:31:09

基于CNN的调制信号识别:MATLAB实现时频图分类实战

简介:本资源是一套面向通信工程与信号处理方向学习者、研究者的深度学习实践方案,聚焦调制信号自动检测与识别这一典型无线通信任务,解决传统方法依赖人工特征、低信噪比下性能下降等痛点。压缩包共12个文件(10.73MB)&…

2026/9/10 0:00:55

目录对比去重实战:用哈希算法精准清理重复文件

我电脑里现在还有一块换了三次机的“数据墓地”硬盘,里面存着2016年以前所有旧笔记本的完整备份。平时不觉得有什么,直到前阵子想把它整理归档,发现同一个安装包、同一批照片、同一份论文草稿,在几个不同的备份目录里反复出现。更…

2026/9/10 0:00:55

Leaflet离线地图完整Demo合集:内网部署与坐标纠偏实战

简介:这是一份面向Web GIS开发者的LeafLet离线地图示例合集,帮助开发者快速掌握离线地图从搭建到交互的完整流程。压缩包共723个文件,大小14.06MB,以319个js脚本、175个html页面和29个css样式文件为主体,配合png/svg图…

2026/9/10 0:00:55

MATLAB读取Rinex 3.02观测文件:多系统GNSS数据解析实战

简介:基于MATLAB开发的Rinex3.02版观测文件(o文件)读取代码包,面向卫星定位导航方向的学习者与研究人员,用于解决新版观测文件的数据解析、历元提取与时间转换问题。压缩包共4个文件,包含两个m脚本、一个19…

2026/9/7 16:23:03

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/7 22:46:00

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/9 10:21:54

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
咨询二维码