Appearance
Vuex 与 Pinia:状态管理的演进、差异与选型
很多人第一次接触 Vuex 或 Pinia 时,最容易把它们理解成:Vue 项目里用来“全局存东西”的库。
这不算错,但太浅。
实际写项目时,更多是这样:状态管理工具解决的是“共享状态怎么组织、怎么约束、怎么让复杂页面和多个组件协作时不失控”的问题。
1. 为什么 Vue 应用会需要状态管理
在一个小组件里,状态通常很简单:
- 本地输入值
- 当前选中项
- 一个弹窗开关
这类状态直接写在组件里就够了。
但项目一旦变复杂,就会出现这些问题:
- 多个组件都依赖同一份数据
- 数据要跨页面共享
- 用户信息、权限、主题、缓存条件需要统一管理
- 状态更新链路越来越难追
这时真正的问题不是“状态放哪”,而是:有没有一套更稳定的方式,让共享状态的来源、修改和消费都更可控。
2. 先分清什么状态不应该上全局
这点很重要。
不是所有状态都该进 Vuex 或 Pinia。
更适合放在组件本地的通常包括:
- 一个输入框当前值
- 某个弹窗是否打开
- 某个局部折叠面板状态
更适合考虑共享状态管理的通常包括:
- 登录用户信息
- 权限信息
- 购物车
- 跨页面筛选条件
- 多组件共用的缓存数据
3. Vuex 是什么
Vuex 可以看成 Vue2 时代最经典、最官方感的一套集中式状态管理方案。
它最核心的思想是:
- 共享状态集中放在
store - 修改状态要走明确路径
- 状态流尽量保持可追踪
Vuex 里最常见的几个词是:
stategettersmutationsactionsmodules
3.1 一段典型 Vuex 示例
ts
const store = new Vuex.Store({
state: {
count: 0
},
getters: {
doubleCount(state) {
return state.count * 2
}
},
mutations: {
increment(state) {
state.count++
}
},
actions: {
asyncIncrement({ commit }) {
setTimeout(() => {
commit('increment')
}, 300)
}
}
})这套设计的重点是:
- 同步修改通常走
mutation - 异步逻辑通常放
action - 派生值放
getter
4. Vuex 的优点和边界
4.1 优点
- 状态流清楚
- 组织方式规范
- 在中大型 Vue2 项目里非常常见
4.2 边界
- 样板代码偏多
- 心智负担不小
- 小到中型项目里会显得有些重
5. Pinia 是什么
Pinia 可以看成 Vue3 时代更轻、更现代、更贴近组合式 API 的状态管理方案。
它不是简单的“Vuex 改名版”,而是状态管理思路的一次收敛。
Pinia 更强调:
- 更少样板代码
- 更自然的类型推导
- 更贴近组合式 API 的使用方式
5.1 一段典型 Pinia 示例
ts
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0
}),
getters: {
doubleCount: (state) => state.count * 2
},
actions: {
increment() {
this.count++
}
}
})在组件里使用时通常会像这样:
ts
const counterStore = useCounterStore()
counterStore.increment()6. Pinia 为什么更受新项目欢迎
核心原因通常不是“它更新”,而是:
- API 更直接
- 代码更少
- 类型推导更自然
- 和 Vue3 的组合式思路更一致
也就是说,Pinia 更适合现代 Vue 项目的默认心智模型。
7. Vuex 和 Pinia 的核心差异
| 维度 | Vuex | Pinia |
|---|---|---|
| 常见时代背景 | Vue2 项目很常见 | Vue3 项目更常见 |
| API 复杂度 | 相对更重 | 更轻 |
| 样板代码 | 较多 | 较少 |
| 类型体验 | 一般 | 更自然 |
| 与组合式 API 协调 | 间接 | 更顺 |
8. 选型时真正要看什么
不要只按“新旧”二选一,更稳的判断是:
8.1 更适合 Vuex 的情况
- 维护 Vue2 历史项目
- 现有团队和代码已经深度依赖 Vuex
- 短期没有迁移计划
8.2 更适合 Pinia 的情况
- 新的 Vue3 项目
- 希望减少样板代码
- 对 TypeScript 体验更敏感
- 希望和组合式 API 更一致
9. 一个更贴近业务的 Pinia 示例
ts
import { defineStore } from 'pinia'
type UserInfo = {
id: number
name: string
role: string
}
export const useUserStore = defineStore('user', {
state: () => ({
userInfo: null as UserInfo | null,
token: ''
}),
getters: {
isAdmin: (state) => state.userInfo?.role === 'admin'
},
actions: {
setUserInfo(userInfo: UserInfo) {
this.userInfo = userInfo
},
clearLoginState() {
this.userInfo = null
this.token = ''
}
}
})这个例子最值得先理解的是:
- 登录用户信息是典型共享状态
- 权限判断可以做成派生状态
- 清理登录态适合做成明确动作
10. Pinia 持久化怎么做
在真实项目里,Pinia 很少只承担“页面打开期间的临时状态”。
很多状态会希望在刷新后继续保留,例如:
- 用户主题设置
- 登录后的部分会话信息
- 搜索筛选条件
- 表单草稿
Pinia 本身主要负责状态组织,不直接内置完整持久化方案。
工程里更常见的做法,是配合插件 pinia-plugin-persistedstate。
安装通常类似这样:
bash
pnpm add pinia-plugin-persistedstate先在创建 Pinia 实例时注册插件:
ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import App from './App.vue'
const app = createApp(App)
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
app.use(pinia)
app.mount('#app')然后在具体 store 上声明哪些内容要持久化:
ts
import { defineStore } from 'pinia'
type UserInfo = {
id: number
name: string
role: string
}
export const useUserStore = defineStore('user', {
state: () => ({
userInfo: null as UserInfo | null,
token: '',
theme: 'light',
}),
actions: {
setLoginState(userInfo: UserInfo, token: string) {
this.userInfo = userInfo
this.token = token
},
setTheme(theme: 'light' | 'dark') {
this.theme = theme
},
clearLoginState() {
this.userInfo = null
this.token = ''
}
},
persist: {
key: 'user-store',
storage: localStorage,
paths: ['userInfo', 'theme'],
},
})这段配置里最关键的是:
persist: true表示快速启用默认持久化persist: { ... }表示做更细的自定义配置paths用来限制只持久化哪些字段storage决定底层写到哪里,常见是localStorage或sessionStorage
如果你只想做最小配置,也可以直接写:
ts
persist: true但工程上更常见的做法仍然是显式指定 paths,避免把整个 store 都落盘。
11. Pinia 持久化时该怎么判断边界
不是所有 Pinia 状态都适合持久化。
更适合持久化的通常包括:
- 用户偏好设置
- 主题和语言
- 刷新后仍需保留的筛选条件
- 草稿信息
不太适合直接持久化的通常包括:
- 大体积列表结果
- 很容易过期的接口数据
- 只在当前页面临时存在的 UI 开关
- 高敏感信息
🌟 把这层分工记住:Pinia 负责状态建模,持久化插件负责“要不要把这部分状态保存到浏览器存储里”。
如果是登录态场景,更稳的工程习惯通常是:
- 持久化少量必要字段
- 不把过多敏感数据直接长期写入浏览器存储
- 应用启动后结合接口重新校验登录有效性
12. Vuex / Pinia 的持久化思路有什么共通点
虽然 Vuex 和 Pinia API 不一样,但持久化思路其实很像:
- store 负责组织共享状态
- 持久化插件负责把部分状态同步到浏览器存储
- 页面初始化时再把已保存数据回灌回来
如果是历史 Vue2 项目,Vuex 里也常见配合持久化插件或手写订阅逻辑,把指定模块写进 localStorage。
但如果是 Vue3 新项目,工程上更常见的默认组合通常是:
piniapinia-plugin-persistedstate
13. Pinia / Vuex 和 @tanstack/vue-query 是什么关系
如果只把前端状态分成“组件本地状态”和“全局共享状态”,很多工程问题还是解释不清。
因为还有一类状态更像:
- 接口返回的数据
- 请求后的缓存结果
- 何时重新拉取
- 数据失效后如何自动更新
- loading / error / success 这些请求生命周期状态
这类数据更贴近“服务端状态”,而不是普通客户端业务状态。
在 Vue 生态里,更常见的方案就是 @tanstack/vue-query。
安装通常类似这样:
bash
pnpm add @tanstack/vue-query它更像:专门管理服务端状态获取、缓存、失效、重试和后台刷新的库。
所以把边界放回原位看:
| 工具 | 更偏解决什么 |
|---|---|
| Vuex / Pinia | 客户端共享业务状态、权限状态、界面协作状态 |
@tanstack/vue-query | 服务端数据拉取、缓存、重试、失效和刷新 |
例如下面这些数据,更适合优先考虑 vue-query:
- 商品列表
- 用户详情
- 评论列表
- 订单分页数据
- 搜索结果
而下面这些状态,通常仍然更适合 Pinia 或 Vuex:
- 主题模式
- 当前登录会话中的客户端展示信息
- 多组件共享的 UI 协作状态
- 复杂本地编辑状态
🌟 要注意的是,接口数据不是普通的“全局变量”,它天然带有缓存、失效时间和重取策略。
14. 一个最小 vue-query 接入示例
先在应用入口注册 VueQueryPlugin:
ts
import { createApp } from 'vue'
import {
QueryClient,
VueQueryPlugin,
} from '@tanstack/vue-query'
import App from './App.vue'
const app = createApp(App)
const queryClient = new QueryClient()
app.use(VueQueryPlugin, {
queryClient,
})
app.mount('#app')然后在组件里使用 useQuery:
vue
<script setup lang="ts">
import { useQuery } from '@tanstack/vue-query'
async function fetchUserList() {
const response = await fetch('/api/users')
if (!response.ok) {
throw new Error('加载用户列表失败')
}
return response.json()
}
const { data, isLoading, error } = useQuery({
queryKey: ['users'],
queryFn: fetchUserList,
staleTime: 60_000,
})
</script>
<template>
<div v-if="isLoading">加载中...</div>
<div v-else-if="error">加载失败</div>
<ul v-else>
<li v-for="user in data" :key="user.id">
{{ user.name }}
</li>
</ul>
</template>这段示例最值得看懂的是:
queryKey是这份远程数据的身份标识queryFn是真正的拉取逻辑staleTime决定数据在多长时间内算“新鲜”
也就是说,组件不再自己重复维护一整套:
loadingerrordata- 缓存复用
- 重新请求策略
15. Pinia 和 vue-query 怎么一起用
现代 Vue3 项目里,很常见的组合其实是:
Pinia管客户端共享状态@tanstack/vue-query管服务端状态
例如一个后台页面里:
- 列表接口、详情接口、分页数据用
vue-query - 当前主题、侧边栏展开状态、复杂本地草稿状态用
Pinia
这样分工的好处是:
- Pinia 不需要硬塞大量接口缓存数据
- 接口数据天然拥有失效、缓存和重取能力
- 状态边界更清楚
如果是 Vue2 老项目,Vuex 仍然可以继续管理客户端共享状态;只是遇到服务端数据缓存问题时,也要优先思考“它到底是不是该放进 store”。
16. 状态管理最容易踩的坑
16.1 什么都往全局状态里放
最后会让 store 变成“全局大仓库”。
16.2 组件本地状态和共享状态边界不清
会导致很多状态既分散又重复。
16.3 只关注“怎么改状态”,不关注“状态建模”
真正影响长期维护成本的,往往是状态结构本身。
16.4 状态拆分不按业务域,而按页面临时感受拆
这样后面一旦跨页面复用,就会越来越乱。
17. 一份更稳的检查清单
当你考虑要不要用 Vuex / Pinia 时,至少可以问:
- 这份状态是不是多个组件共享
- 这份状态会不会跨页面持续存在
- 这份状态有没有统一修改入口的价值
- 这个项目是 Vue2 还是 Vue3
- 当前团队更适合延续旧方案,还是切到更现代的方案
18. 小结
如果把这篇压缩成一句话,可以记住:Vuex 和 Pinia 都是在解决共享状态治理问题,Vuex 更像 Vue2 时代的经典集中式方案,Pinia 更像 Vue3 时代更轻、更自然、更贴近组合式 API 的状态管理方案。
继续往下读,比较自然的下一步是进入 Vue Router:前端路由、导航守卫与页面组织。