1. Pinia 状态管理基础认知
第一次接触 Pinia 是在去年重构一个 Vue 3 企业级中台项目时。当时项目中的 Vuex 代码已经变得难以维护,一个 store 文件动辄上千行代码,类型推导也十分困难。在技术选型阶段,Pinia 简洁的 API 设计和完美的 TypeScript 支持立刻吸引了我。
Pinia 作为 Vue 官方推荐的状态管理库,其核心优势在于:
- 去除了 mutations 的概念,所有状态变更都在 actions 中完成
- 完美的 TypeScript 支持,自动推导类型
- 支持 Composition API 和 Options API 两种写法
- 模块化设计,每个 store 都是独立的
- 轻量级,打包体积仅有 1kb 左右
2. Option Store 传统写法详解
2.1 基础结构解析
Option Store 的写法与 Vue 2 时代的 Vuex 非常相似,对于从 Vuex 迁移过来的开发者来说几乎没有学习成本。一个典型的 Option Store 结构如下:
import { defineStore } from 'pinia' export const useCounterStore = defineStore('counter', { state: () => ({ count: 0, user: null }), getters: { doubleCount: (state) => state.count * 2 }, actions: { increment() { this.count++ }, async fetchUser() { this.user = await api.getUser() } } })2.2 状态访问与修改
在组件中使用 Option Store 时,最常见的模式是:
<script setup> import { useCounterStore } from '@/stores/counter' const store = useCounterStore() </script> <template> <div>{{ store.count }}</div> <div>{{ store.doubleCount }}</div> <button @click="store.increment()">+1</button> </template>重要提示:虽然可以通过
store.count++直接修改状态,但官方推荐始终通过 actions 来修改状态,这样有利于维护状态变更的可追溯性。
2.3 类型推导的优势
Pinia 的一个巨大优势是完善的类型推导。在 Option Store 中,所有 state、getters 和 actions 都会自动获得类型提示。例如:
store.count // number store.doubleCount // number store.increment() // void3. Setup Store 现代写法剖析
3.1 组合式 API 风格
Setup Store 采用了与 Vue 3 Composition API 相似的写法,更适合复杂的状态逻辑组织。基本结构如下:
import { defineStore } from 'pinia' import { ref, computed } from 'vue' export const useCounterStore = defineStore('counter', () => { const count = ref(0) const user = ref(null) const doubleCount = computed(() => count.value * 2) function increment() { count.value++ } async function fetchUser() { user.value = await api.getUser() } return { count, user, doubleCount, increment, fetchUser } })3.2 响应式系统集成
Setup Store 最大的特点是直接使用 Vue 的响应式 API(ref、reactive、computed 等),这使得状态管理更加灵活:
const count = ref(0) // 等同于 Option Store 中的 state: () => ({ count: 0 })3.3 复杂状态组织
对于复杂的状态逻辑,Setup Store 可以像 setup 函数一样组织代码:
export const useAuthStore = defineStore('auth', () => { // 状态 const token = ref('') const user = ref(null) // getters const isLoggedIn = computed(() => !!token.value) // actions async function login(credentials) { const res = await api.login(credentials) token.value = res.token user.value = res.user } function logout() { token.value = '' user.value = null } return { token, user, isLoggedIn, login, logout } })4. 两种写法的深度对比
4.1 代码风格差异
| 特性 | Option Store | Setup Store |
|---|---|---|
| 写法风格 | 对象字面量 | 函数式 |
| 状态定义 | state 函数 | ref/reactive |
| 计算属性 | getters 对象 | computed 函数 |
| 方法定义 | actions 对象 | 普通函数 |
| 类型推导 | 自动 | 自动 |
| this 使用 | 需要 | 不需要 |
4.2 适用场景分析
Option Store 更适合:
- 从 Vuex 迁移的项目
- 偏好传统面向对象风格的团队
- 简单的状态管理需求
- 需要快速上手的场景
Setup Store 更适合:
- 新开始的 Vue 3 项目
- 复杂的状态逻辑
- 需要组合多个响应式数据源的场景
- 偏好函数式编程的团队
4.3 性能考量
在实际项目中,两种写法在性能上没有显著差异。但 Setup Store 由于直接使用 Vue 的响应式系统,在某些极端情况下可能会有微小的性能优势:
- 对于超大型应用,Setup Store 的 tree-shaking 效果更好
- 频繁更新的状态,Setup Store 的响应式更新路径更短
- 计算属性的缓存机制在两种写法中表现一致
5. 响应式陷阱与解决方案
5.1 解构丢失响应性
这是 Pinia 新手最常见的坑之一:
<script setup> import { useCounterStore } from '@/stores/counter' // ❌ 错误做法:直接解构会丢失响应性 const { count, doubleCount } = useCounterStore() </script>5.2 正确解构方法
Pinia 提供了 storeToRefs 工具函数来保持响应式:
<script setup> import { storeToRefs } from 'pinia' import { useCounterStore } from '@/stores/counter' const store = useCounterStore() // ✅ 正确做法:使用 storeToRefs const { count, doubleCount } = storeToRefs(store) </script>5.3 原理剖析
直接解构之所以会丢失响应性,是因为 Pinia 的状态在内部是通过 reactive 包装的。当直接解构时,实际上获取的是原始值的副本,而不是响应式引用。
storeToRefs 的工作原理类似于 toRefs,它会为每个属性创建 ref 引用,保持与源属性的响应式连接。
6. 实战经验分享
6.1 项目结构组织
在大型项目中,我推荐按功能模块组织 stores:
stores/ auth/ index.ts # 主 store 文件 types.ts # 类型定义 mock.ts # 模拟数据 user/ index.ts types.ts settings/ index.ts6.2 类型安全最佳实践
即使是使用 JavaScript 的项目,也建议添加 JSDoc 类型注释:
/** * @typedef {Object} User * @property {string} id * @property {string} name */ export const useUserStore = defineStore('user', { state: () => ({ /** @type {User|null} */ currentUser: null }) })6.3 调试技巧
在开发过程中,可以通过以下方式调试 store:
- 浏览器控制台直接访问
pinia全局变量 - 使用 Pinia 的 devtools 插件
- 添加 store 订阅:
store.$subscribe((mutation, state) => { console.log('状态变更:', mutation) console.log('新状态:', state) })7. 迁移策略与渐进式采用
7.1 从 Vuex 迁移
对于 Vuex 项目,可以采取渐进式迁移策略:
- 先在新模块中使用 Pinia
- 逐步将 Vuex modules 改写为 Pinia stores
- 最后移除 Vuex 依赖
7.2 混合使用注意事项
在过渡期,如果需要在 Pinia 中访问 Vuex store:
import { useStore } from 'vuex' export const useAuthStore = defineStore('auth', () => { const vuexStore = useStore() // 访问 Vuex state const oldState = computed(() => vuexStore.state.someModule) return { oldState } })8. 高级模式与插件开发
8.1 自定义插件示例
Pinia 的插件系统非常强大,可以用于:
- 持久化存储
- 统一错误处理
- 性能监控
function localStoragePlugin(context) { const key = `pinia-${context.store.$id}` // 从 localStorage 恢复状态 const savedState = localStorage.getItem(key) if (savedState) { context.store.$patch(JSON.parse(savedState)) } // 订阅状态变更 context.store.$subscribe((_, state) => { localStorage.setItem(key, JSON.stringify(state)) }) }8.2 SSR 支持
在 Nuxt.js 中使用 Pinia 需要特别注意:
// nuxt.config.js export default { buildModules: [ ['@pinia/nuxt', { disableVuex: true }] ] }9. 测试策略
9.1 单元测试示例
使用 vitest 测试 Pinia store:
import { setActivePinia, createPinia } from 'pinia' import { useCounterStore } from '@/stores/counter' describe('Counter Store', () => { beforeEach(() => { setActivePinia(createPinia()) }) it('increments count', () => { const store = useCounterStore() expect(store.count).toBe(0) store.increment() expect(store.count).toBe(1) }) })9.2 组件测试技巧
在测试组件时,可以 mock store:
import { createTestingPinia } from '@pinia/testing' test('displays count', () => { const wrapper = mount(CounterComponent, { global: { plugins: [ createTestingPinia({ initialState: { counter: { count: 10 } } }) ] } }) expect(wrapper.text()).toContain('10') })10. 性能优化实践
10.1 选择性订阅
对于大型 store,可以使用 computed 选择性订阅部分状态:
const expensiveData = computed(() => store.largeData.filter(/* 复杂逻辑 */))10.2 批量更新
使用 $patch 进行批量更新:
store.$patch({ count: store.count + 1, lastUpdated: new Date() })10.3 惰性加载
对于不常用的 store,可以动态导入:
const store = computed(() => useSomeStore())