Vue 3 中 Pinia 的使用总结
本文原文写于 2022 年,主要介绍 Pinia 的基础用法。下面尽量保留原文的学习路径和示例,同时补充 Vue 3.5.x、Pinia 2/3 的版本边界、Setup Store、SSR、插件和当前状态管理建议。
文中的“Pinia 比 Vuex 简洁”“Pinia 是 Vuex 的后继方向”等属于作者当时的生态观点,不是 API 保证。Pinia 3 及更高版本面向 Vue 3;维护 Vue 2.7 的旧项目时应按兼容矩阵固定 Pinia 2,不要只执行不带版本约束的安装命令。
Pinia 的简介和优势
Pinia 是 Vue 生态中的状态管理库,用来在多个组件或页面之间共享状态。它由 Vuex 的维护者参与开发,吸收了 Composition API 的思想,也是 Vue 官方当前推荐的新项目状态管理方案之一。
相较于 Vuex 3/4,Pinia 常见的优势包括:
- API 更直接:没有强制的 mutations 层,state 可以直接修改,也可以通过 actions 集中处理业务逻辑。
- Store 更扁平:不需要 Vuex 风格的嵌套 modules 和 namespaced 字符串。
- TypeScript 推导更自然:
defineStore、state、getters 和 actions 都能获得较好的类型推导。 - Composition API 友好:既可以使用 Options Store,也可以使用 Setup Store。
- 开发工具和生态完善:支持 Devtools、插件、SSR、测试工具和热更新。
- 按 Store 拆分代码:每个 Store 可以放在独立文件中,构建工具也可以按模块组织代码。
Pinia 不等于“所有项目都必须使用的全局变量容器”。局部状态优先放在组件或 composable 中;只有多个组件真正共享的状态才需要放入 Store。
Vue 2.7 与 Vue 3 的版本说明
- Vue 3.5.x:本文示例按 Pinia 3 的 Vue 3 API 编写。实际安装时请根据项目 lockfile 固定兼容版本。
- Vue 2.7:旧项目可以使用 Pinia 2;Pinia 3 不支持 Vue 2。
- Vue 2.6 及更早版本:还需要额外的 Composition API 兼容方案,不建议新项目采用。
- Vue 2 已于 2023 年 12 月 31 日结束维护;Pinia 2 的 Vue 2 支持只适合作为迁移期或旧项目方案。
Vue 3 环境安装
如果需要新建 Vue 3 项目,可以使用 Vite:
pnpm create vite my-vue-app --template vue-ts
cd my-vue-app
pnpm install
原文中的命令截图保留如下:


如果项目已经存在,直接安装 Pinia。Vue 3 项目示例:
pnpm add pinia
如果要明确使用 Pinia 3,可以在项目升级策略允许时写成:
pnpm add pinia@^3
Vue 2.7 项目应使用与项目 Vue 版本匹配的 Pinia 2:
pnpm add pinia@^2
不要只根据文章截图中的版本号判断当前版本;应以 package.json、lockfile 和官方 peer dependencies 为准。

可以在 package.json 或包管理器的依赖树中查看实际安装的版本。
Pinia 的安装
在 src/main.ts 中创建 Pinia 实例,并在挂载应用前安装:
// src/main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const app = createApp(App)
const pinia = createPinia()
app.use(pinia)
app.mount('#app')
createPinia() 返回的是当前应用的 Pinia 实例。一个页面中可以有多个 Vue app,但每个 app 都应安装自己对应的 Pinia 实例。

创建 Store 状态管理库
通常在 src/stores/ 下为每个业务域创建一个文件,例如 src/stores/counter.ts:

// src/stores/counter.ts
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
name: 'Vue',
items: [] as string[],
}),
getters: {
doubleCount: state => state.count * 2,
itemCount(): number {
return this.items.length
},
},
actions: {
increment() {
this.count++
},
async reload() {
// 可以在这里请求接口,再更新 this.items
},
},
})
defineStore() 的第一个参数是 Store 的唯一 id,用于 Devtools、插件和 Pinia 内部识别。返回值通常命名为 useCounterStore,它是一个在组件中调用后才创建/取得 Store 实例的函数。
Options Store 的三个主要选项可以这样理解:
state:类似组件的data,必须返回初始状态对象。需要使用的状态字段应在这里预先声明,即使初值是undefined。getters:类似 computed,用于派生数据,不是 watch 监听器。actions:类似 methods,用于封装同步或异步业务逻辑,可以通过this访问 Store。

Setup Store 写法
Pinia 也支持与 Composition API 类似的 Setup Store:
// src/stores/user.ts
import { computed, ref } from 'vue'
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', () => {
const token = ref<string | null>(null)
const isLoggedIn = computed(() => token.value !== null)
function setToken(value: string | null) {
token.value = value
}
return {
token,
isLoggedIn,
setToken,
}
})
Setup Store 中:
- 返回的
ref会成为 state; - 返回的
computed会成为 getter; - 返回的函数会成为 action;
- 所有希望被 Pinia、Devtools、插件和 SSR 识别的状态都必须返回,不能隐藏一部分私有 state。
Setup Store 更灵活,可以在 Store 中使用 composable 或同步创建 watcher;但 SSR 场景也需要更仔细地处理浏览器 API和请求级状态。
在 Vue 3 组件中读取 Store 数据
在 <script setup> 中调用 Store 函数得到实例:
<script setup lang="ts">
import { useCounterStore } from '@/stores/counter'
const counterStore = useCounterStore()
</script>
<template>
<p>count:{{ counterStore.count }}</p>
<p>double:{{ counterStore.doubleCount }}</p>
<button @click="counterStore.increment()">增加</button>
</template>
Store 实例本身被 Vue 的 reactive() 包装,因此模板和脚本中直接访问 counterStore.count 不需要 .value。
原文的多个“读取 Store”截图已本地化:



解构 Store 时使用 storeToRefs
直接解构 Store 的 state 或 getter 会丢失响应式连接:
const store = useCounterStore()
// 不推荐:取出的 count 不会随着 store.count 更新
const { count, doubleCount } = store
正确做法是使用 storeToRefs():
import { storeToRefs } from 'pinia'
const store = useCounterStore()
const { count, doubleCount } = storeToRefs(store)
// action 可以直接解构,因为 Pinia 会绑定它的 this
const { increment } = store
storeToRefs() 会为 state、getter 以及插件加入的响应式属性创建 refs;普通方法和非响应式属性不会被转换。




Pinia 修改状态数据的方式
1. 直接修改
Pinia 允许直接修改已声明的 state:
const store = useCounterStore()
store.count++
store.name = 'Pinia'
这不是绕过响应式系统的修改,Vue 和 Pinia Devtools 仍然可以追踪它。不能随意增加没有在 state() 中声明的字段。

2. 使用 $patch 对象
一次修改多个字段时,可以使用对象形式:
store.$patch({
count: 10,
name: 'Pinia',
})
3. 使用 $patch 函数
数组、对象等复杂修改可以使用函数形式:
store.$patch(state => {
state.items.push('new item')
state.count++
})
$patch() 的主要价值是把一组修改作为一个 Devtools 记录,便于追踪和时间旅行;官方没有保证它“一定更快”,不要把 $patch 当成无条件的性能优化。


4. 在 action 中集中处理复杂逻辑
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
}),
actions: {
async incrementAfterRequest() {
await Promise.resolve()
this.count++
},
},
})
组件只负责调用 action:
const store = useCounterStore()
await store.incrementAfterRequest()
Action 可以接收任意参数、返回普通值或 Promise,也可以调用其他 Store:
const authStore = useAuthStore()
const settingsStore = useSettingsStore()
if (authStore.isLoggedIn) {
await settingsStore.load()
}
使用普通函数 action 时不要改成箭头函数,因为 Options Store 的 action 需要通过 this 访问 Store。


5. 使用 Getters
Getter 类似 computed:
export const useUserStore = defineStore('user', {
state: () => ({
phone: '13812345678',
}),
getters: {
hiddenPhone: state => {
return state.phone.replace(/^(\d{3})\d{4}(\d{4})$/, '$1****$2')
},
},
})
Getter 可以在模板中直接访问:
<script setup>
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
</script>
<template>
<p>{{ userStore.hiddenPhone }}</p>
</template>
Getter 的结果具有 computed 的缓存语义;如果 getter 返回一个函数来接收参数,则这个函数调用本身不会自动缓存每个参数的结果。


6. action 和 getter 中的 this
Options Store 的 action 可以使用 this 访问整个 Store:
export const useCartStore = defineStore('cart', {
state: () => ({
items: [] as { price: number }[],
}),
getters: {
total(): number {
return this.items.reduce((sum, item) => sum + item.price, 0)
},
},
actions: {
clear() {
this.items = []
},
},
})
如果 TypeScript getter 通过 this 访问其他 getter,通常需要显式声明返回类型。原文截图中的 String 应改为小写的 string,避免把 JavaScript 包装对象类型带入业务代码。

Store 之间相互调用
在一个 Store 中直接调用另一个 Store:
// stores/settings.ts
import { defineStore } from 'pinia'
import { useAuthStore } from './auth'
export const useSettingsStore = defineStore('settings', {
state: () => ({
theme: 'light',
}),
actions: {
async loadForCurrentUser() {
const auth = useAuthStore()
if (!auth.isLoggedIn) return
// 根据当前用户加载设置
},
},
})
要注意循环依赖:如果两个 Store 在模块顶层就互相调用,而不是在 action/getter 执行时调用,可能造成初始化顺序问题。

Store 的重置、订阅和插件
$reset
Options Store 自带 $reset():
const store = useCounterStore()
store.$reset()
Setup Store 需要自己实现:
function $reset() {
count.value = 0
}
$subscribe 与 $onAction
store.$subscribe((mutation, state) => {
console.log(mutation.type, state)
})
const unsubscribe = store.$onAction(({ name, after, onError }) => {
console.log('开始 action:', name)
after(result => console.log('完成:', result))
onError(error => console.error(error))
})
unsubscribe()
在组件 setup() 中同步注册的订阅通常会随组件卸载自动清理;需要脱离组件生命周期时,应使用官方提供的 detached 选项或手动取消。
Pinia 插件
插件通过 Pinia 实例安装,而不是通过 app.use() 直接传插件:
const pinia = createPinia()
pinia.use(({ store, app, options }) => {
// 可以给 Store 增加方法或属性
return {
createdAt: new Date(),
}
})
插件应在 Store 创建前注册。注入 router、第三方实例等非响应式对象时,可以使用 markRaw(),避免不必要的代理。
SSR 注意事项
Pinia 支持 SSR,但服务端必须避免多个请求共享同一个可变 Store:
- SSR 应为每个请求创建对应的 app 和 Pinia 实例。
- 在组件外使用 Store(例如 router 守卫)时,显式传入当前
pinia实例:useStore(pinia)。 - 服务端 state 注入 HTML 前要安全序列化,防止 XSS;客户端应在首次使用 Store 前完成 hydration。
localStorage、window、document只存在于浏览器环境。不要在 action 的模块顶层或 SSR 路径无条件访问localStorage。
小结
Pinia 的核心使用流程是:
- 创建 app 和 Pinia,并在 app 上安装。
- 使用唯一 id 定义 Store。
- 在 state 中声明初始状态。
- 用 getters 派生数据,用 actions 封装业务逻辑。
- 组件中保留 Store 实例,或用
storeToRefs解构响应式 state/getter。 - 需要持久化、插件或 SSR 时,按官方 API 处理生命周期和安全边界。
官方参考
- Pinia Introduction
- Pinia Core Concepts
- Pinia State
- Pinia Getters
- Pinia Actions
- Pinia Plugins
- Pinia SSR
- Pinia v2 到 v3 迁移
- Vue 状态管理
原文出处
原文参考地址:https://i.cnblogs.com/posts/edit
原链接是博客后台编辑地址,不是稳定的公开文章地址;本文保留该出处信息,但当前 API 以 Pinia 和 Vue 官方文档为准。