首页
/ airi 中的 Pinia 实战:Store 定义、State、Getters 与 Actions 全解析

airi 中的 Pinia 实战:Store 定义、State、Getters 与 Actions 全解析

2026-09-05 10:15:22作者:廉彬冶Miranda

本文以 airi 仓库的 Pinia 参考文档 core-stores.md 为主体,系统讲解 defineStore() 的两种定义风格(Options Store 与 Setup Store)、state / getters / actions 三大核心概念、storeToRefs$patch$subscribe$onAction 等 API,并结合 airi 仓库中 聊天会话 storePinia 行为追踪插件 的真实源码,验证这些概念在大型 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() → state,computed() → getters,function() → 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 内组合了多个 refsessionMessagessessionMetasindex 等)、computedisReady),以及大量非响应式的内部守卫变量(如 ensureActiveEpochreconcileEpoch),展示了 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
</script>

airi 的 session-store.ts 在 store 内部跨 store 引用时正是这样用的——注意它把 useAuthStore() 的返回交给 storeToRefs 再解构,保证 userIdauthToken 在后续 getter/action 中仍是响应式引用:

export const useChatSessionStore = defineStore('chat-session', () => {
  const { userId, token: authToken } = storeToRefs(useAuthStore())
  const { activeCardId, systemPrompt } = storeToRefs(useAiriCardStore())
  // ...
})

airi 仓库中 stage-web 的 App.vuestage-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.count++
<input v-model="store.count" type="number" />

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 重置 State

Options Store 自带 $reset();Setup Store 则需自行实现一个语义等价的动作:

export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)

  function $reset() {
    count.value = 0
  }

  return { count, $reset }
})

3.6 订阅状态变更:$subscribe

cartStore.$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 基础 Getter

getters: {
  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 中访问其他 Store

import { useOtherStore } from './other-store'

getters: {
  combined(state) {
    const otherStore = useOtherStore()
    return state.localData + otherStore.data
  },
},

这与 airi 的 session-store.ts 的组织方式一致:它把"当前选中会话"拆到独立的 chat-session-selection store,再通过 computedget/set 桥接为主 store 的 activeSessionId,源码注释解释动机是"选中的会话属于某个窗口,同步的会话数据不应让另一个窗口跳转到同一会话"——这正是跨 store 派生值设计要解决的典型问题。

5. Actions:业务逻辑与异步处理

Actions 是方法,与 getters 的关键区别是可以异步

5.1 定义 Actions

actions: {
  increment() {
    this.count++
  },
  randomizeCounter() {
    this.count = Math.round(100 * Math.random())
  },
},

5.2 异步 Actions

actions: {
  async registerUser(login: string, password: string) {
    try {
      this.userData = await api.post({ login, password })
    } catch (error) {
      return error
    }
  },
},

5.3 在 Actions 中访问其他 Store

import { 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 提供了 mapStatemapWritableStatemapActions(注意:getter 不需要单独映射,mapState 同样映射只读的 getter):

import { 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 中访问全局 Provider

Setup 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 的 authairi-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 Store:ref → state,computed → getters,函数 → actions;务必 return 所有需要追踪的响应式属性
  • 组件中解构 state/getter 一律走 storeToRefs,actions 可直接解构;跨 store 引用同样先 storeToRefs
  • 批量修改用 $patch(对象或函数语法),Options Store 用 $reset,Setup Store 自定义同名动作;
  • $subscribe(可配 flush / detached)做持久化与变更审计,用 $onActionafter / onError 做性能度量和错误上报;
  • 参数化 getter 要利用外层缓存;异步 action 中在 await 前完成所有 useStore() 调用;
  • airi 的 pinia-plugin-tracing.ts 提供了 $subscribe + $onAction 组合成开发期追踪插件的完整参考实现。
登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
982
503
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384