mcpField
V
vue-pinia

vue-pinia

github.com/mindrally开发28766 次安装3 周前更新
代码仓库

关于 vue-pinia

用于在 Vue 3 项目中使用 Pinia 的开发技能,帮助开发者将共享客户端状态组织为 setup store,并区分组件本地状态与服务端数据。

主要功能

  1. 01Composition API setup store
  2. 02共享状态、getter 与 action
  3. 03使用 storeToRefs 保持响应性
  4. 04适配 SSR 的 store 用法
  5. 05持久化与测试指导

适用场景

  1. 01在路由和组件间共享状态
  2. 02用 store action 建模业务流程
  3. 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 的工作流

  1. 判断 Pinia 是否适用 — 确认该状态是在路由或组件间共享的,而非服务端缓存数据,也不是纯粹的本地 UI 状态(参见下文的状态归属)。
  2. 创建 store 文件 — 在 stores/ 下按领域添加文件,例如 stores/cart.ts,并使用 defineStore('cart', () => { ... })。
  3. 定义状态、getter 和 action — 使用 ref/reactive 声明状态,使用 computed 派生值,并将所有写入和副作用放在函数(action)中。
  4. 从 setup 函数返回所有内容 — 返回每一项状态、每个 getter 和每个 action,让 Pinia devtools、SSR 和插件能够追踪它们。
  5. 在组件中使用 — 在 <script setup> 顶部调用 store,使用 storeToRefs() 解构响应式状态,并直接解构 action。
  6. 处理 SSR/路由守卫中的使用 — 在 setup、getter 或 action 内调用 store;在 setup 上下文之外显式传入当前活动的 Pinia 实例。
  7. 添加持久化和测试 — 将允许持久化的字段列入白名单,校验重新水合的数据,并使用 @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 API defineStore({ 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