在 Vue 3 项目里,随着组件层级加深,跨组件共享状态(用户登录信息、购物车、全局主题)变得越来越难维护。Pinia 作为 Vue 官方推荐的状态管理方案,用组合式 API 重塑了 Store 的写法:类型安全、零样板、模块化清晰。本文带你从零落地 Pinia,搞懂它相对 Vuex 的优势与最佳实践。
一、为什么 Pinia 取代了 Vuex
Vuex 时代最大的痛点,是 mutation 与 action 的概念分裂——同步改状态走 mutation,异步走 action,同一个逻辑被拆到两处;再加上 TypeScript 类型推断弱、modules 嵌套繁琐,大型项目里很容易写出难以维护的样板代码。Pinia 直接去掉了 mutation,把 state、getter、action 三者统一,并基于组合式 API 提供完整的类型推导,体积也更小(约 1KB+)。
| 维度 | Vuex 4 | Pinia |
|---|---|---|
| 概念模型 | state / mutation / action / getter | state / getter / action(去掉 mutation) |
| TypeScript | 类型推断弱,需额外声明 | 完整类型推导 |
| 模块化 | modules 嵌套 | 扁平 store,天然拆分 |
| 体积 | 较大 | 更小(约 1KB+) |
| DevTools | 支持 | 支持且时间旅行更友好 |
二、安装与创建第一个 Store
用你熟悉的包管理器安装,并在应用入口挂载 Pinia 插件:
npm install pinia
# 或使用 pnpm / yarn
pnpm add pinia
// 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')
选项式与组合式两种写法
defineStore 支持两种风格。选项式更接近 Vuex 心智模型,组合式则更贴近 <script setup> 的写法:
// 选项式 option store
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0, name: 'Pinia' }),
getters: {
double: (state) => state.count * 2,
},
actions: {
increment() {
this.count++
},
},
})
// 组合式 setup store
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const name = ref('Pinia')
const double = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, name, double, increment }
})
三、State、Getter、Action 三件套
响应式 State 与解构
在组件里可以直接 store.count++ 修改状态,或用 store.$patch() 批量更新。但注意:直接解构 state 会丢失响应式,必须用 storeToRefs:
import { storeToRefs } from 'pinia'
const store = useCounterStore()
const { count, name } = storeToRefs(store) // 保持响应式
const { increment } = store // action 可直接解构调用
Getter 派生状态
getter 用于从 state 派生计算值,可以通过 this 访问其他 getter,甚至跨 store 调用,非常适合做权限判断、价格计算等纯函数逻辑。
Action 处理异步
actions: {
async fetchUser(id: number) {
this.loading = true
try {
const res = await fetch(`/api/users/${id}`)
this.user = await res.json()
} finally {
this.loading = false
}
},
}
四、TypeScript 类型安全
Pinia 最大的卖点之一,是 state、getter、action 全部自动类型推导。你也可以给 state 显式声明接口,获得编辑器级别的智能提示与编译期检查:
interface UserState {
id: number
name: string
token: string
}
export const useUserStore = defineStore('user', {
state: (): UserState => ({ id: 0, name: '', token: '' }),
getters: {
isLogin: (s): boolean => s.id > 0,
},
})
五、持久化与插件生态
登录态、主题偏好这类状态,刷新页面后希望保留。借助官方生态插件 pinia-plugin-persistedstate,几行代码就能把 store 同步到 localStorage:
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
export const useUserStore = defineStore('user', {
state: () => ({ token: '' }),
persist: true, // 自动同步 localStorage
})
六、与 DevTools 及测试
Vue DevTools 能直接看到 Pinia 的 store 树,并支持时间旅行调试。单元测试则可用 @vue/test-utils 配合 Vitest 直接实例化 store,无需挂载完整组件,断言 action 的副作用非常直观。
七、生产落地清单与常见误区
| 误区 | 正确做法 |
|---|---|
| 在组件里直接修改 state 而不走 action | 简单赋值可直接改;复杂业务放 action 便于追踪与复用 |
| 用解构丢失响应式 | 用 storeToRefs 解构 state,action 才可直接解构 |
| 把异步请求写在 getter | getter 必须同步;异步逻辑一律放 action |
| 所有状态塞进一个 store | 按业务域(user / cart / theme)拆分多个 store |
八、结语
Pinia 用更少的样板、更强的类型推导,成为 Vue 3 状态管理的默认选择。想把工程化链路补齐,可进一步阅读《Vue3 + TypeScript 工程化:类型安全的前端架构》(p=93)、《Vite 工程化实战:从零配置到构建优化提速》(p=1485)以及《VS Code 高效配置:插件与调试实战》(p=267)。把这三篇串起来,你就拥有了一条从开发、构建到调试的完整 Vue 3 工作流。




