Store 的职责与响应式消费
Pinia 解决的是跨组件共享的领域状态,不是把所有变量搬进全局。先问“谁拥有这份状态”:输入框只被一个组件使用,就留在组件;购物车、当前用户和筛选条件被多个页面读写,才值得成为 store;服务器返回的列表还需要缓存、失效和重取策略时,应优先交给 server-state 工具。
从一个可观察的状态开始
下面的例子只依赖 Pinia 的公开 API,先看数据流再看语法:
import { computed, ref } from 'vue'
import { defineStore, storeToRefs } from 'pinia'
export const useCartStore = defineStore('cart', () => {
const items = ref<{ id: string; price: number; quantity: number }[]>([])
const count = computed(() => items.value.reduce((n, item) => n + item.quantity, 0))
const total = computed(() => items.value.reduce((n, item) => n + item.price * item.quantity, 0))
function add(item: { id: string; price: number }) {
const existing = items.value.find((entry) => entry.id === item.id)
if (existing) existing.quantity += 1
else items.value.push({ ...item, quantity: 1 })
}
return { items, count, total, add }
})
const cart = useCartStore()
const { count, total } = storeToRefs(cart)items 是唯一事实,count 和 total 是派生事实,add 是改变事实的动作。组件解构 store 时必须对状态和 getter 使用 storeToRefs,否则直接解构会把响应式引用变成一次性快照;action 可以直接解构,因为函数身份不依赖响应式追踪。
两种定义风格如何取舍
Setup Store 用 ref、computed 和函数表达领域模型,适合复用组合式函数与精确类型;Option Store 用 state、getters、actions 分栏,迁移 Vuex 或让初学者快速定位职责时更直观。两者都创建同一种 store,选择标准是团队约定、迁移成本和类型复杂度,而不是“哪一种更现代”。
边界
不要在 getter 里写请求、修改 state 或读取当前时间;getter 应当是可重复的派生计算。不要把 isLoading、error 和 items.length === 0 混成一个状态:空结果、加载中和失败是不同事实。跨 store 读取时保持单向依赖,避免 user 读 cart、cart 又读 user 形成初始化环。
练习
把购物车改成支持“删除一项”和“按商品 id 查询数量”,要求:删除不存在的 id 不改变数组引用;商品数量为 0 时移除;组件仍能观察到 count 与 total 的变化。先写两个输入序列,再说明每一步可观察结果。
下一步
完成练习后打开本章答案,再回到Pinia 课程总览。