关于 vue-pinia
用于在 Vue 3 项目中使用 Pinia 的开发技能,帮助开发者将共享客户端状态组织为 setup store,并区分组件本地状态与服务端数据。
主要功能
- 01Composition API setup store
- 02共享状态、getter 与 action
- 03使用 storeToRefs 保持响应性
- 04适配 SSR 的 store 用法
- 05持久化与测试指导
适用场景
- 01在路由和组件间共享状态
- 02用 store action 建模业务流程
- 03独立测试 store 行为
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ mindrally/ skills --skill vue-pinia安装后在 Claude Code 中输入/vue-pinia确认已加载。
详细介绍
Vue + Pinia
Pinia 是 Vue 3 的标准状态管理库。使用组合式 API 构建的 setup store,能让共享的应用状态拥有与本地 ref/computed 状态相同的使用体验。
添加 Pinia Store 的工作流
- 判断 Pinia 是否适用 — 确认该状态是在路由或组件间共享的,而非服务端缓存数据,也不是纯粹的本地 UI 状态(参见下文的状态归属)。
- 创建 store 文件 — 在
stores/下按领域添加文件,例如stores/cart.ts,并使用defineStore('cart', () => { ... })。 - 定义状态、getter 和 action — 使用
ref/reactive声明状态,使用computed派生值,并将所有写入和副作用放在函数(action)中。 - 从 setup 函数返回所有内容 — 返回每一项状态、每个 getter 和每个 action,让 Pinia devtools、SSR 和插件能够追踪它们。
- 在组件中使用 — 在
<script setup>顶部调用 store,使用storeToRefs()解构响应式状态,并直接解构 action。 - 处理 SSR/路由守卫中的使用 — 在 setup、getter 或 action 内调用 store;在 setup 上下文之外显式传入当前活动的 Pinia 实例。
- 添加持久化和测试 — 将允许持久化的字段列入白名单,校验重新水合的数据,并使用
@pinia/testing直接测试 action。
状态归属
- 仅属于组件的状态应使用
ref、reactive或computed保留在组件中。 - 对于跨路由、布局或互不相关的组件树共享的客户端状态,使用 Pinia。
- 对于可分享的导航状态,使用路由参数和查询字符串;不要将其重复存入 store。
- 对于服务端状态,使用 Nuxt
useFetch/useAsyncData、TanStack Query for Vue 或现有的 API 层;Pinia 用于客户端状态,而不是数据获取缓存。 - 除非是有意创建的可编辑草稿、离线缓存或具有自身生命周期的工作流快照,否则不要将服务端缓存数据复制到 Pinia。
Store 结构
- 对于 Vue 3 组合式 API 项目,优先使用
defineStore('name', () => { ... })创建 setup store,而不是较旧的 options APIdefineStore({ state, getters, actions })形式。 - 在
stores/下按领域为每个 store 创建一个文件,例如stores/cart.ts或stores/session.ts。 - 当状态、计算属性 getter 和 action 属于同一个连贯领域时,将它们放在一起。
- 从 setup store 返回每个状态属性,以便 Pinia 在 devtools、SSR 和插件中追踪它们;未返回的
ref对系统其他部分不可见。
// stores/cart.ts
import { computed, ref } from 'vue'
import { defineStore } from 'pinia'
interface CartLine {
id: string
name: string
quantity: number
unitPrice: number
}
export const useCartStore = defineStore('cart', () => {
const lines = ref<CartLine[]>([])
const itemCount = computed(() =>
lines.value.reduce((total, line) => total + line.quantity, 0),
)
const subtotal = computed(() =>
lines.value.reduce((total, line) => total + line.quantity * line.unitPrice, 0),
)
function addLine(line: CartLine) {
const existing = lines.value.find((item) => item.id === line.id)
if (existing) {
existing.quantity += line.quantity
return
}
lines.value.push(line)
}
function removeLine(id: string) {
lines.value = lines.value.filter((line) => line.id !== id)
}
function clearCart() {
lines.value = []
}
return {
lines,
itemCount,
subtotal,
addLine,
removeLine,
clearCart,
}
})
在组件中使用
- 在
<script setup>顶部或 setup 函数、getter 和 action 内调用 store;不要有条件地调用,也不要在导入时加载的普通.js/.ts模块中调用。 - 在组件中解构 store 状态或 getter 时使用
storeToRefs(),以保留响应性。 - 需要时可直接解构 action;action 仍绑定到 store,无需使用
storeToRefs()。 - 避免在组件中编写复杂的业务工作流;将其移至 store action 或组合式函数中,以便测试和复用。
- 派生状态时优先使用计算属性,而不是 watcher;如果
watch只是重新计算一个值,通常应改用computed。
<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { useCartStore } from '@/stores/cart'
const cart = useCartStore()
const { itemCount, subtotal } = storeToRefs(cart)
const { clearCart } = cart
</script>
<template>
<div>
<p>{{ itemCount }} items — ${{ subtotal.toFixed(2) }}</p>
<button type="button" :disabled="itemCount === 0" @click="clearCart">
Clear cart
</button>
</div>
</template>
TypeScript
- 明确标注 store 状态、action 参数和 API 响应的类型。
- 避免使用
any;使用unknown,并在将外部输入(API 响应、持久化存储)写入状态前进行收窄。 - 对于跨组件共享或跨越 API 边界的对象状态,使用 interface。
- 工作流状态和错误状态优先使用可辨识联合类型,例如
{ status: 'idle' } | { status: 'loading' } | { status: 'error'; error: string }。 - 保持 store ID(
defineStore的第一个参数)稳定且具有描述性,因为它们会显示在 devtools 和持久化键中。
操作与副作用
- 操作可以是同步或异步的;每个操作都应专注于一个用户或领域工作流。
- 在边界处校验操作输入,然后再修改 store 状态。
- 如果 UI 依赖异步工作流,请使用明确的
status、error和lastUpdatedAt字段表示状态。 - 重试异步操作前,先重置过期错误,避免 UI 中残留先前的失败信息。
- 除非 store 负责其生命周期和清理(例如提供明确的
connect()/disconnect()操作对),否则应将订阅、间隔计时器、套接字和浏览器监听器放在 store 之外。
SSR、Nuxt 和路由守卫
- 在 SSR 上下文中,在 setup、getter 或操作中使用 store,以便 Pinia 能解析当前活动的应用实例。
- 在 setup 外使用 store(例如在路由守卫中)时,如果框架有此要求,请传入当前活动的 Pinia 实例(Nuxt 的自动导入会自动处理)。
- 服务端渲染期间不要读取仅限浏览器的存储(
localStorage、window)——使用import.meta.client(Nuxt)或onMounted检查进行保护。 - 在 Nuxt 中,优先使用
@pinia/nuxt集成和 SSR 安全的数据获取组合式函数,而不是手动连接 Pinia 实例。 - 依赖框架为每个请求创建的 Pinia 实例,而不是模块级单例,以避免单例状态在请求间泄漏。
// router guard example (outside setup)
router.beforeEach((to) => {
const auth = useAuthStore(pinia) // pass the active Pinia instance explicitly
if (to.meta.requiresAuth && !auth.isAuthenticated) {
return { name: 'login' }
}
})
持久化
- 仅持久化必须在重新加载后保留的字段,例如偏好设置或未完成的本地草稿。
- 切勿在浏览器存储中持久化机密、访问令牌、刷新令牌、原始个人身份信息或授权决策。
- 对持久化的 store 架构使用字段允许列表和带版本的迁移,避免旧的持久化结构导致新版本 store 崩溃。
- 将持久化状态视为不可信输入,并在用于关键工作流前进行校验——用户可以直接编辑
localStorage。 - 在渲染依赖持久化值的 UI 前,考虑水合时机;尤其是在 SSR 下,服务端无法访问浏览器存储。
测试与工具
- 对需要 store 的组件测试,使用
@pinia/testing(createTestingPinia()),其操作默认会被存根。 - 直接测试 store 操作的领域行为和边缘情况,不依赖任何组件。
- 在测试之间重置 Pinia,避免测试用例间共享状态泄漏。
- 如果项目采用 Vite HMR 模式,请在 store 中使用
acceptHMRUpdate()添加 HMR 支持。 - 使用清晰的状态名称和专注于单一领域的 store,让 store 易于在 Vue Devtools 中检查。
import { setActivePinia, createPinia } from 'pinia'
import { describe, it, expect, beforeEach } from 'vitest'
import { useCartStore } from '@/stores/cart'
describe('cart store', () => {
beforeEach(() => {
setActivePinia(createPinia())
})
it('merges quantities for duplicate line items', () => {
const cart = useCartStore()
cart.addLine({ id: '1', name: 'Widget', quantity: 1, unitPrice: 10 })
cart.addLine({ id: '1', name: 'Widget', quantity: 2, unitPrice: 10 })
expect(cart.itemCount).toBe(3)
})
})
反模式
- 不要把 Pinia 当作所有响应式值的存放处——大多数组件状态应保留在本地。
- 不要直接从 store 解构状态而不使用
storeToRefs();这会在不发出提示的情况下破坏响应性。 - 不要通过 store 操作修改 props 或路由对象。
- 不要将仅限服务端的对象、请求实例、DOM 节点或计时器放入 store 状态中——它们无法序列化,会破坏 SSR/devtools。
- 不要在 setup 函数中让 store 之间形成循环读取;应通过操作或计算值组合 store,而不是让两个 store 在模块作用域相互读取。
评论
支持 Markdown