Pinia 详细学习笔记
面向 Vue 3 的状态管理库,官方钦定的 Vuex 5.0 替代方案。
1. Pinia 简介与动机
Pinia 是 Vue 生态中的轻量级状态管理库,起初是 Vuex 5 的实验性实现,后因设计优秀被官方收编,现在已是 Vue 官方推荐的状态管理工具(甚至对 Vue 2 也提供支持)。
核心优势:
- 完整的 TypeScript 支持:类型推导极其自然,无需额外的类型声明。
- 模块化设计:不再有
modules嵌套,每个 store 都是独立的,按需引入。 - 删除 mutations:actions 直接支持同步/异步操作,代码更简洁。
- 极轻量:压缩后仅约 1KB。
- 支持 Composition API 和 Options API:写法灵活。
- 开发者工具集成:完美支持 Vue Devtools,时间旅行、状态快照。
- 插件扩展:可自定义插件实现日志、持久化等能力。
- SSR 友好:内置服务端渲染场景的状态管理方案。
2. 安装与初始化
npm install pinia
# 或
yarn add pinia
# 或
pnpm add pinia
在 Vue 应用中注册:
// main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const app = createApp(App)
app.use(createPinia())
app.mount('#app')
若在使用 Vue 2,需要额外安装 @vue/composition-api 并使用 PiniaVuePlugin,此处仅讨论 Vue 3 场景。
3. 定义 Store
Pinia 支持两种语法定义 store:Option Store(类似 Vuex)和 Setup Store(类组合式 API)。两者完全等价,按喜好选用。
3.1 Option Store
与 Vuex 类似,通过选项对象定义 state、getters、actions。
// stores/counter.ts
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
name: 'Counter',
}),
getters: {
doubleCount: (state) => state.count * 2,
// 使用 this 访问其他 getter(需明确返回值类型,否则 TS 推导出错)
doublePlusOne(): number {
return this.doubleCount + 1
},
},
actions: {
increment() {
this.count++
},
async fetchAndSetCount(url: string) {
const res = await fetch(url)
const data = await res.json()
this.count = data.value
},
},
})
3.2 Setup Store
使用类似 Vue 组件 setup() 的语法,可以利用组合式函数(composables)并更自由地组织代码。
// stores/counter.ts
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const name = ref('Counter')
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
async function fetchAndSetCount(url: string) {
const res = await fetch(url)
const data = await res.json()
count.value = data.value
}
return { count, name, doubleCount, increment, fetchAndSetCount }
})
两种写法的选择:
- 熟悉 Vuex / Options API 推荐 Option Store。
- 喜欢 Composition API、要复用逻辑推荐 Setup Store。
- Setup Store 中所有 return 出的内容都会成为 store 的公共属性和方法,不能使用私有变量了吗?可以,不 return 的变量即为私有。
4. State
4.1 访问 state
import { useCounterStore } from '@/stores/counter'
const counter = useCounterStore()
// 直接读写
console.log(counter.count)
counter.count++
通过 storeToRefs 提取响应式引用(解构时保持响应性):
import { storeToRefs } from 'pinia'
const { count, name } = storeToRefs(counter)
// count 是 Ref<number>,name 是 Ref<string>
注意: 直接解构 store 会丢失响应性(如同解构 reactive),必须使用 storeToRefs。actions 可以直接解构,因为它们是普通函数。
4.2 重置 state
Option Store 下可直接调用 $reset():
counter.$reset() // 将 state 重置为初始值
但 Setup Store 默认不支持 $reset(),因为 Pinia 无法得知如何重建初始状态。需要自定义实现:
// 在 setup store 内手动提供 $reset
const initialState = { count: 0, name: 'Counter' } // 或用函数返回
const count = ref(initialState.count)
...
function $reset() {
count.value = initialState.count
name.value = initialState.name
}
return { count, name, $reset, ... }
4.3 修改 state 的多种方式
- 直接修改(推荐)
- 通过
$patch批量更新:
counter.$patch({
count: counter.count + 1,
name: 'New Name',
})
// 或函数形式
counter.$patch((state) => {
state.count++
state.name = 'New Name'
})
- 替换整个 state(不推荐,可能破坏响应性):
counter.$state = { count: 10, name: 'Replaced' }
5. Getters
Getters 等价于 store 的计算属性。
Option Store:
getters: {
doubleCount(state) { return state.count * 2 },
// 使用 this 访问其他 getter,但需要明确返回类型(TS限制)
doublePlusOne(): number { return this.doubleCount + 1 },
// 返回函数实现可传参的 getter(但不缓存)
countMultipliedBy: (state) => (multiplier: number) => state.count * multiplier,
}
Setup Store:
直接使用 computed:
const doubleCount = computed(() => count.value * 2)
// 可传参
const multiplied = computed(() => (m: number) => count.value * m)
注意事项:
- Getters 默认是缓存的。
- 若 getter 返回函数,则函数结果不缓存(因为每次调用返回新函数)。
- 在 Option Store 中通过
this访问其他 getter 时,由于 TS 无法推导this类型,必须明确标注返回值类型。
6. Actions
Actions 相当于组件里的方法,支持同步/异步操作。在 Option Store 中通过 this 访问 state 和其他 actions。
actions: {
async fetchData() {
this.loading = true
try {
const data = await api.get('/data')
this.data = data
} finally {
this.loading = false
}
},
// 调用其他 action
async init() {
await this.fetchData()
}
}
在 Setup Store 中,actions 就是普通函数。
调用:
const store = useCounterStore()
store.increment()
Pinia 会自动将 actions 绑定到 store 实例,所以解构出来也可以直接调用,不会丢失 this:
const { increment } = store
increment() // 正常运作
7. 在组件中使用 Store
7.1 组合式 API (Composition API)
<script setup lang="ts">
import { useCounterStore } from '@/stores/counter'
import { storeToRefs } from 'pinia'
const counter = useCounterStore()
const { count, doubleCount } = storeToRefs(counter)
const { increment } = counter // action 可直接解构
function handleClick() {
increment()
}
</script>
<template>
<div>
<p>Count: {{ count }}</p>
<p>Double: {{ doubleCount }}</p>
<button @click="handleClick">+1</button>
</div>
</template>
7.2 选项式 API (Options API)
在没有 setup 的场景(如纯 Options API 组件),可以通过 mapStores、mapState、mapActions 辅助函数映射。
<script lang="ts">
import { mapStores, mapState, mapActions } from 'pinia'
import { useCounterStore } from '@/stores/counter'
export default {
computed: {
// 将整个 store 映射进来
...mapStores(useCounterStore),
// 或映射特定 state/getter
...mapState(useCounterStore, ['count', 'doubleCount']),
// 支持重命名
...mapState(useCounterStore, {
myCount: 'count',
double: (store) => store.doubleCount,
}),
},
methods: {
...mapActions(useCounterStore, ['increment']),
...mapActions(useCounterStore, { plusOne: 'increment' }),
},
}
</script>
注意:
mapStores会生成counterStore这样的属性,通过this.counterStore.count访问。- 使用
mapState需要传递 store 的定义函数,而不是实例。 - Option API 下 store 需要在
setup()内手动调用一次才能激活,或者在映射前确保 store 已被使用。通过mapStores会自动调用。
8. Store 间相互引用
一个 store 可以导入并使用另一个 store。
Setup Store 示例:
// stores/user.ts
export const useUserStore = defineStore('user', () => {
const name = ref('Alice')
return { name }
})
// stores/cart.ts
import { useUserStore } from './user'
export const useCartStore = defineStore('cart', () => {
const user = useUserStore() // 直接在 setup 内调用
const items = ref([])
function addItem(item) {
items.value.push({ ...item, addedBy: user.name })
}
return { items, addItem }
})
Option Store 示例:
import { useUserStore } from './user'
export const useCartStore = defineStore('cart', {
state: () => ({ items: [] }),
actions: {
addItem(item) {
const user = useUserStore() // 在 action 内部调用
this.items.push({ ...item, addedBy: user.name })
},
},
})
注意:
- 可以在任何地方调用
useXxxStore(),只要在 Pinia 注册之后。 - 若在 setup 外部(如 action 内部)调用另一个 store,完全没问题,因为此时 Pinia 一定已挂载。
- 避免在 store 的顶层直接调用另一个 store 时出现循环依赖,需要按需在内部使用。
9. 插件系统
Pinia 插件是一个函数,接收一个 context 对象,可以在 store 上增加属性、方法,或者拦截行为。
// plugins/myPlugin.ts
import { PiniaPluginContext } from 'pinia'
export function myPiniaPlugin(context: PiniaPluginContext) {
// context: { app, pinia, store, options }
const { store } = context
// 添加全局属性
store.$myGlobal = 'Hello'
// 监听 action
store.$onAction(({ name, store, args, after, onError }) => {
console.log(`Action ${name} started`)
after(() => console.log(`Action ${name} finished`))
onError((error) => console.error(`Action ${name} failed`, error))
})
// 添加持久化逻辑(简易版)
const savedState = localStorage.getItem(store.$id)
if (savedState) {
store.$patch(JSON.parse(savedState))
}
store.$subscribe((mutation, state) => {
localStorage.setItem(store.$id, JSON.stringify(state))
})
}
注册插件:
const pinia = createPinia()
pinia.use(myPiniaPlugin)
app.use(pinia)
常用插件生态:
- pinia-plugin-persistedstate:状态持久化。
- 自行封装日志、权限、重置插件。
10. 服务端渲染(SSR)
Pinia 为 SSR 设计了安全的状态管理方案,防止跨请求状态污染。
核心原则:
- 每个请求都创建全新的 Pinia 实例。
- 避免在模块顶层直接使用
useStore(),应在setup或请求处理函数内调用。
典型流程(如 Nuxt 3 / 手动 SSR):
// server.ts
import { createPinia } from 'pinia'
import { createSSRApp } from 'vue'
export function createApp() {
const app = createSSRApp(App)
const pinia = createPinia()
app.use(pinia)
return { app, pinia }
}
// 请求处理
async function render(url, manifest) {
const { app, pinia } = createApp()
// 设置当前请求的 router 等
// 渲染前可预先填充数据
const store = useSomeStore(pinia) // 传入 pinia 实例确保在正确实例上操作
await store.fetchData()
// 序列化状态
const state = JSON.stringify(pinia.state.value)
// 渲染并将状态注入到 HTML 中
}
在客户端激活时,替换状态:
// entry-client.ts
const { app, pinia } = createApp()
if (window.__PINIA_STATE__) {
pinia.state.value = JSON.parse(window.__PINIA_STATE__)
}
app.mount('#app')
Nuxt 3 自动处理了这些细节,你只需定义 store,并在 setup 中调用。
11. 开发工具支持
- Vue Devtools:支持 Pinia 状态查看、时间旅行、action 追踪。
- 时间旅行调试:需要开启
defineStore的第三个参数中的history选项(仅开发模式):defineStore('id', { ... }, { history: true }) - Pinia 内部支持 action 追踪,插件中可通过
$onAction监听。
12. 与 Vuex 的对比
| 特性 | Pinia | Vuex 4 |
|---|---|---|
| 模块化 | 扁平化,每个 store 独立 | 树形嵌套 modules,命名空间 |
| mutations | 移除,直接 actions 修改 | 必须有 mutations 同步修改 |
| TypeScript | 完美推导,零额外代码 | 需要复杂类型声明,推导弱 |
| 体积 | ~1KB | ~10KB+ |
| API 风格 | Options + Setup Store | 仅有 Options API |
| 开发者工具 | 支持时间旅行 | 支持时间旅行 |
| 社区生态 | 快速增长,官方维护 | 成熟但不再新功能开发 |
| 代码结构 | 灵活,可自由组织 | 较死板,state/getters/mutations/actions 分离 |
结论: 新项目无脑选 Pinia;老项目若已用 Vuex,可渐进迁移,两者可在同一项目共存。
13. 最佳实践
- 按功能领域拆分 store:如
useUserStore、useCartStore、useProductStore,保持单一职责。 - 避免过度集中:不要把所有状态丢进一个大 store,失去了模块化优势。
- Setup Store 优先:更接近 Composition API,方便逻辑抽取、复用、TS 推导最好。
- 使用
storeToRefs解构:保持 state/getters 的响应性,actions 直接解构。 - action 建议返回 Promise:方便在组件中链式调用和等待。
actions: { async fetchData() { ... } } // 组件内 await store.fetchData() - 考虑 SSR 安全性:不要在全局作用域直接调用
useStore(),始终在函数内调用(setup、action、请求处理函数)。 - 持久化用插件:
pinia-plugin-persistedstate配置简单,避免手写$subscribe逻辑。 - 复杂更新优先用
$patch:一次修改多个属性更清晰,且能触发一次响应式更新。 - 通过 TypeScript 增强健壮性:使用接口定义 state 类型(Option Store)或利用自动推导(Setup Store)。
- 测试友好:可以在单元测试中创建独立的 Pinia 实例并注入,避免状态污染。
14. 常见问题 FAQ
Q:能在 Pinia 中使用 Vuex 的 mapGetters 等辅助函数吗?
A:不能直接使用 Vuex 的,但 Pinia 提供了自己的 mapState、mapActions,用法相似。
Q:如何监听 state 变化?
A:通过 store.$subscribe(callback, options),类似于 Vue 的 watch,可指定 { detached: true } 使组件销毁后仍监听。
Q:action 是否必须是异步的?
A:不必须,可以是同步函数。Pinia 去掉了 mutation 就是为了让 action 统一处理同步和异步。
Q:为什么 $reset() 在 setup store 不可用?
A:Setup Store 的状态是通过 ref/reactive 构建的,没有最初的工厂函数来自动重建状态。你可以自己实现一个 $reset 方法并导出。
Q:能否在 Pinia store 中使用 watch 或 watchEffect?
A:可以,在 Setup Store 内直接使用 Vue 的 watch,但要注意在组件卸载时 store 不会自动销毁,因此可能导致内存泄漏。需要手动停止或仅在必要时使用。
Q:如何实现 store 间的循环依赖?
A:尽量避免。如果必须,可以在 action 内部延迟引用另一个 store(在运行时调用函数),而不是在顶层导入,这样打破循环。
15. 总结
Pinia 作为 Vue 3 的首选状态管理方案,凭借简洁的 API、出色的 TypeScript 体验和灵活的模块设计,极大地提升了开发效率。掌握其两种 store 定义方式、在组件中的正确使用姿势、插件机制与 SSR 策略,就能从容应对大多数复杂应用的状态管理需求。建议动手实践,从一个小项目开始全部使用 Pinia,很快便能感受到其丝般顺滑的开发体验。