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 与插件版本,不要把新文档直接套到旧运行时。