Skip to content

Pinia 状态建模

Pinia 的价值不是“把数据都搬到 Store”,而是给跨组件、跨页面且带业务动作的状态一个可追踪边界。单页局部 UI 状态留在组件,能从 URL 推导的状态留在路由。

用业务域划分 Store

ts
export const useCartStore = defineStore('cart', () => {
  const items = ref<CartItem[]>([])
  const submitting = ref(false)
  const total = computed(() => sum(items.value))

  async function submitOrder() {
    if (submitting.value) return
    submitting.value = true
    try {
      await checkoutService.submit(items.value)
      items.value = []
    } finally {
      submitting.value = false
    }
  }

  return { items, submitting, total, submitOrder }
})

Store 表达状态与业务动作,API client、序列化和领域服务可以独立,便于复用、替换和测试。不要把所有接口都塞进一个全局 Store。

Setup Store 与 Option Store

Option Store 的 state/getters/actions 边界直观,适合规则统一的团队;Setup Store 适合组合 composable、watch 和复杂依赖,但必须返回所有需要被 Pinia 识别的状态,否则 SSR、DevTools、插件和持久化可能出现隐形状态。

选择哪种风格不如同一业务域保持一致重要。

解构与响应式

直接解构 state/getter 会失去响应式连接。需要解构时使用 storeToRefs;action 是已经绑定 Store 的函数,可直接解构。

ts
const cart = useCartStore()
const { items, total } = storeToRefs(cart)
const { submitOrder } = cart

跨 Store 依赖

Store 可以在 action/getter 中调用其他 Store,但两个 Setup Store 不应在初始化阶段互相直接读取,避免循环。交叉读取可延迟到 computed/action;更复杂的业务工作流放到 service/use-case 层,避免 Store 成为双向依赖中心。

持久化不是默认能力

持久化前要明确:哪些字段需要跨会话、版本如何迁移、过期如何处理、是否包含敏感数据。购物车可保存商品标识与数量,但价格、库存和权限必须由服务端重新确认。

可测试性

  • Store 单测:每个用例创建新 Pinia,验证状态转移和 getter。
  • 组件测试:使用 createTestingPinia,明确 action 是 stub 还是真实执行。
  • 权限 Store:覆盖未登录、权限拉取失败、过期、角色切换和动态路由重建。
ts
beforeEach(() => setActivePinia(createPinia()))

it('提交成功后清空购物车', async () => {
  const cart = useCartStore()
  cart.items.push(product)
  await cart.submitOrder()
  expect(cart.items).toHaveLength(0)
})

Pinia v3 已移除 Vue 2 支持,维护老项目时先核对 Vue、Pinia 与插件版本,不要把新文档直接套到旧运行时。

参考:Pinia IntroductionComposing StoresTesting Stores