
airi 中的 Pinia 实战Store 定义、State、Getters 与 Actions 全解析【免费下载链接】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本文以 airi 仓库的 Pinia 参考文档 core-stores.md 为主体系统讲解defineStore()的两种定义风格Options Store 与 Setup Store、state / getters / actions 三大核心概念、storeToRefs、$patch、$subscribe、$onAction等 API并结合 airi 仓库中 聊天会话 store 与 Pinia 行为追踪插件 的真实源码验证这些概念在大型 Vue 3 应用中的落地方式帮助你在 airi 这类 Web / 桌面多端项目中正确编写、订阅和调试 Pinia store。1. Store 的三大核心概念Pinia 中的每个 store 通过defineStore()以唯一名称定义包含三个核心概念state状态、getters派生值和actions行为。可以用 Vue 的概念类比state对应datagetters对应computedactions对应methods。2. 定义 Store 的两种风格2.1 Options Store与 Vue 的 Options API 类似state、getters、actions 分别写在各自字段中import { defineStore } from pinia export const useCounterStore defineStore(counter, { state: () ({ count: 0, name: Eduardo, }), getters: { doubleCount: (state) state.count * 2, }, actions: { increment() { this.count }, }, })Options Store 有一个实用特性内置$reset()方法可直接将 state 恢复为初始值。2.2 Setup Store官方推荐使用组合式 API 语法定义更灵活强大也是 airi 仓库中 store 的主流写法import { ref, computed } from vue import { defineStore } from pinia export const useCounterStore defineStore(counter, () { const count ref(0) const name ref(Eduardo) const doubleCount computed(() count.value * 2) function increment() { count.value } return { count, name, doubleCount, increment } })映射关系为ref()→ statecomputed()→ gettersfunction()→ actions。关键约束必须通过return返回所有需要被 Pinia 追踪的 state 属性未被返回的ref只是组件外的普通响应式变量无法通过 store 实例访问。在 airi 中可以看到两种典型用法。PWA 更新 store 是一个精简的 Setup Store它把内部依赖如 service worker 的updateSW句柄保留在闭包内只返回需要暴露的updateReadyHooks同时通过import.meta.env.SSR判断避免在服务端执行注册逻辑export const usePWAStore defineStore(pwa, () { const updateReadyHooks ref(() void)[]([]) // ...内部逻辑不返回保持私有 onMounted(async () { if (import.meta.env.SSR) return const { registerSW } await import(../modules/pwa) // 动态注册 service worker 并监听更新 }) })而更复杂的场景如 聊天会话 store在 Setup Store 内组合了多个refsessionMessages、sessionMetas、index等、computedisReady以及大量非响应式的内部守卫变量如ensureActiveEpoch、reconcileEpoch展示了 Setup Store 相对 Options Store 的灵活性任意闭包状态、单例 Promise、Set/Map 等都可以与响应式状态共存。2.3 使用 Store 与 storeToRefs 解构在script setup中调用返回的函数即可获得 store 实例script setup import { useCounterStore } from /stores/counter const store useCounterStore() // 访问store.count、store.doubleCount、store.increment() /script解构时有一个经典的响应性陷阱直接解构 state 和 getters 会丢失响应性必须用storeToRefs而 actions 是普通方法可以直接解构script setup import { storeToRefs } from pinia import { useCounterStore } from /stores/counter const store useCounterStore() // ❌ 破坏响应性 const { name, doubleCount } store // ✅ 对 state/getters 保留响应性 const { name, doubleCount } storeToRefs(store) // ✅ actions 可以直接解构 const { increment } store /scriptairi 的 session-store.ts 在 store 内部跨 store 引用时正是这样用的——注意它把useAuthStore()的返回交给storeToRefs再解构保证userId、authToken在后续 getter/action 中仍是响应式引用export const useChatSessionStore defineStore(chat-session, () { const { userId, token: authToken } storeToRefs(useAuthStore()) const { activeCardId, systemPrompt } storeToRefs(useAiriCardStore()) // ... })airi 仓库中 stage-web 的 App.vue、stage-tamagotchi 的多个 island 组件 等页面普遍采用这种storeToRefs解构模式说明这是该仓库组件层消费 store 的标准姿势。3. State定义、访问、修改与订阅3.1 State 定义为函数返回初始值State 是一个返回初始状态对象的函数延迟求值以避免共享可变对象。3.2 TypeScript 类型标注简单类型可以自动推断对复杂类型有两种标注方式。方式一在属性上用as断言interface UserInfo { name: string age: number } export const useUserStore defineStore(user, { state: () ({ userList: [] as UserInfo[], user: null as UserInfo | null, }), })方式二为state函数声明返回类型更整洁interface State { userList: UserInfo[] user: UserInfo | null } export const useUserStore defineStore(user, { state: (): State ({ userList: [], user: null, }), })3.3 访问与修改const store useStore() store.countinput v-modelstore.count typenumber /3.4 用$patch批量变更一次应用多处修改时比逐条赋值更清晰也便于统一追踪 mutation// 对象语法 store.$patch({ count: store.count 1, name: DIO, }) // 函数语法适合复杂变更如数组操作 store.$patch((state) { state.items.push({ name: shoes, quantity: 1 }) state.hasChanged true })airi 的 角色服务 与 provider 测试 中都可以看到$patch的实际使用测试里大量通过store.$patch构造初始状态再断言行为说明它是 airi store 测试中的标准手段。3.5 重置 StateOptions Store 自带$reset()Setup Store 则需自行实现一个语义等价的动作export const useCounterStore defineStore(counter, () { const count ref(0) function $reset() { count.value 0 } return { count, $reset } })3.6 订阅状态变更$subscribecartStore.$subscribe((mutation, state) { mutation.type // direct | patch object | patch function mutation.storeId // cart mutation.payload // patch object仅 patch object 类型时存在 localStorage.setItem(cart, JSON.stringify(state)) }) // 可选配置 cartStore.$subscribe(callback, { flush: sync }) // 立即执行而非默认的 pre cartStore.$subscribe(callback, { detached: true }) // 组件卸载后仍保留订阅airi 的 pinia-plugin-tracing.ts 是这一 API 的工程化范例该 Pinia 插件在开发模式下对每个 store 挂载$subscribe并显式传入{ detached: true, flush: sync }用于按 5 秒窗口统计每个 store 的 mutation 频率与类型分布direct/patch object/patch function输出[DEBUG-pinia-rate]汇总日志。从源码结构看detached: true是必要的——插件在 Pinia 创建期就完成订阅生命周期不依附任何组件flush: sync则确保计数与变更严格同步避免异步 flush 带来的时序偏差。4. Getters派生状态的所有姿势Getters 等价于 Vue 的computed()是带缓存的派生值。4.1 基础 Gettergetters: { doubleCount: (state) state.count * 2, }4.2 访问其他 Getters通过this访问同 store 的其他 getter注意建议显式标注返回类型getters: { doubleCount: (state) state.count * 2, doublePlusOne(): number { return this.doubleCount 1 }, },4.3 带参数的 Getter返回函数即可让 getter 接受参数但要注意此时 getter 缓存的只是返回查找函数这一步每次调用参数化函数本身都会重新执行因此缓存效果会弱化。推荐的写法是把不变部分如过滤结果留在外层被缓存把参数化查询放在内层// 每次调用都遍历全量用户 getters: { getUserById: (state) { return (userId: string) state.users.find((user) user.id userId) }, }, // 外层计算过滤 active 用户被缓存仅内层 find 随参数执行 getters: { getActiveUserById(state) { const activeUsers state.users.filter((user) user.active) return (userId: string) activeUsers.find((user) user.id userId) }, },4.4 在 Getter 中访问其他 Storeimport { useOtherStore } from ./other-store getters: { combined(state) { const otherStore useOtherStore() return state.localData otherStore.data }, },这与 airi 的 session-store.ts 的组织方式一致它把当前选中会话拆到独立的chat-session-selectionstore再通过computed的get/set桥接为主 store 的activeSessionId源码注释解释动机是选中的会话属于某个窗口同步的会话数据不应让另一个窗口跳转到同一会话——这正是跨 store 派生值设计要解决的典型问题。5. Actions业务逻辑与异步处理Actions 是方法与 getters 的关键区别是可以异步。5.1 定义 Actionsactions: { increment() { this.count }, randomizeCounter() { this.count Math.round(100 * Math.random()) }, },5.2 异步 Actionsactions: { async registerUser(login: string, password: string) { try { this.userData await api.post({ login, password }) } catch (error) { return error } }, },5.3 在 Actions 中访问其他 Storeimport { useAuthStore } from ./auth-store actions: { async fetchUserPreferences() { const auth useAuthStore() if (auth.isAuthenticated) { this.preferences await fetchPreferences() } }, },SSR 注意事项在任何await之前调用所有useStore()不要在await之后才调用否则在服务端渲染时可能绑定到错误的 Pinia 实例async orderCart() { // ✅ 在 await 前调用 store const user useUserStore() await apiOrderCart(user.token, this.items) // ❌ SSR 下不要在 await 之后调用 useStore() }5.4 订阅 Action 生命周期$onAction$onAction提供 action 调用前后的钩子是埋点、性能度量、错误上报的标准入口const unsubscribe someStore.$onAction( ({ name, store, args, after, onError }) { const startTime Date.now() console.log(Start ${name} with params [${args.join(, )}]) after((result) { console.log(Finished ${name} after ${Date.now() - startTime}ms) }) onError((error) { console.warn(Failed ${name}: ${error}) }) } ) unsubscribe() // 清理订阅传第二个参数true可让订阅在组件卸载后仍然保留。airi 的 pinia-plugin-tracing.ts 展示了$onAction的完整生产用法插件为每个 action 生成invocationId在 action 开始、完成after、失败onError三个节点通过BroadcastChannel广播started / completed / failed事件失败事件附带errorMessageFrom(error)提取的报错文本。该插件刻意不保留 action 的参数、结果或 state 快照只在开发模式下按窗口聚合输出速率统计——这与文档中订阅可用于性能度量的示例完全对应只是扩展成了跨窗口的分布式追踪。6. Options API 场景下的映射辅助函数对于仍在使用 Options API 的组件Pinia 提供了mapState、mapWritableState、mapActions注意getter 不需要单独映射mapState同样映射只读的 getterimport { mapState, mapWritableState, mapActions } from pinia import { useCounterStore } from ../stores/counter export default { computed: { // 只读的 state/getter ...mapState(useCounterStore, [count, doubleCount]), // 可写的 state ...mapWritableState(useCounterStore, [count]), }, methods: { ...mapActions(useCounterStore, [increment]), }, }7. 在 Setup Store 中访问全局 ProviderSetup Store 的函数体中可以使用inject()和useRoute()等组合式 API 获取全局注入的值。典型约定是这些依赖只用于内部逻辑不要返回到 store 中组件应自行获取import { inject } from vue import { useRoute } from vue-router import { defineStore } from pinia export const useSearchFilters defineStore(search-filters, () { const route useRoute() const appProvided inject(appProvided) // 不要返回这些值直接在组件中访问它们 return { /* ... */ } })8. 仓库中的验证测试与同步插件airi 的 store 测试进一步佐证了上述 API 的用法。session-store.browser.test.ts 在测试中现场defineStore出 mock 的auth、airi-card两个 setup store并通过vi.doMock替换真实模块说明 Setup Store 的 mock 方式就是返回 ref 对象这一最小形态。该测试还引入了pinia-plugin-synced插件store 定义时传入{ synced: { state: true } }选项从源码结构看airi 用它在浏览器端实现跨窗口的 store 状态同步这也解释了 session-store.ts 中多窗口、epoch 防串号等设计——Pinia store 在该应用中承担着跨标签页共享会话状态的角色。此外background.ts 展示了 store 的再导出模式stage-web 不重复定义而是export { useBackgroundStore } from proj-airi/stage-layouts/stores/background把共享 store 收敛到 stage-layouts 包 中供 web / pocket / tamagotchi 多个端复用。小结优先使用 Setup Storeref→ statecomputed→ getters函数 → actions务必 return 所有需要追踪的响应式属性组件中解构 state/getter 一律走storeToRefsactions 可直接解构跨 store 引用同样先storeToRefs批量修改用$patch对象或函数语法Options Store 用$resetSetup Store 自定义同名动作用$subscribe可配flush/detached做持久化与变更审计用$onAction的after/onError做性能度量和错误上报参数化 getter 要利用外层缓存异步 action 中在await前完成所有useStore()调用airi 的 pinia-plugin-tracing.ts 提供了$subscribe$onAction组合成开发期追踪插件的完整参考实现。【免费下载链接】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),仅供参考