Pinia 取代 Vuex 不是意外
Vuex 4 还在用 mutations、actions、getters 三段式,写起来像填表格。Pinia 一把梭:一个 store 就是一组相关的状态和方法,没有 mutation 概念,直接改。
Vue 官方现在推荐 Pinia 作为默认状态管理方案。如果你的项目还在用 Vuex,可以开始迁了。
基础:一个完整的 Store
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| import { defineStore } from 'pinia' import { ref, computed } from 'vue'
export const useUserStore = defineStore('user', () => { const token = ref('') const profile = ref<UserProfile | null>(null) const isLoggedIn = computed(() => !!token.value)
async function login(phone: string, code: string) { const res = await api.login(phone, code) token.value = res.token profile.value = res.user }
function logout() { token.value = '' profile.value = null router.push('/login') }
return { token, profile, isLoggedIn, login, logout } })
|
关键区别:不需要 commit('SET_TOKEN'),直接 token.value = xxx。简洁到让你怀疑是不是漏了什么步骤——但这就是 Pinia。
Pinia vs Vuex:一张表说清
|
Vuex 4 |
Pinia |
| 概念数量 |
State / Getters / Mutations / Actions / Modules |
State / Getters / Actions |
| 类型推断 |
弱,需要额外声明 |
原生 TypeScript 支持 |
| 模块化 |
modules 嵌套,命名空间 |
平铺,每个 store 独立文件 |
| 修改状态 |
必须通过 commit mutation |
直接赋值 |
| DevTools |
支持 |
支持,且更好用 |
| 体积 |
~10KB |
~5KB |
| 组合式 API |
不原生支持 |
原生支持 Setup Store |
四种常用模式
1. Setup Store(推荐)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| export const useCartStore = defineStore('cart', () => { const items = ref<CartItem[]>([])
const totalPrice = computed(() => items.value.reduce((sum, i) => sum + i.price * i.quantity, 0) )
const itemCount = computed(() => items.value.length)
function addItem(item: CartItem) { const existing = items.value.find(i => i.id === item.id) if (existing) { existing.quantity++ } else { items.value.push({ ...item, quantity: 1 }) } }
function removeItem(id: string) { items.value = items.value.filter(i => i.id !== id) }
return { items, totalPrice, itemCount, addItem, removeItem } })
|
Setup Store 本质就是一个 composable——所有你熟悉的 Vue API(ref、computed、watch)都能在里面用。
2. 模块化拆分
不要把所有状态塞进一个 store:
1 2 3 4 5 6
| stores/ ├── auth.ts # 登录、Token、用户信息 ├── cart.ts # 购物车 ├── notification.ts # 通知、Toast ├── theme.ts # 暗色模式、语言 └── app.ts # 全局 Loading、路由状态
|
每个 store 职责单一,组件按需引用。
3. Store 之间互相引用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import { useCartStore } from './cart' import { useAuthStore } from './auth'
export const useCheckoutStore = defineStore('checkout', () => { const cart = useCartStore() const auth = useAuthStore()
async function submitOrder() { if (!auth.isLoggedIn) throw new Error('请先登录') await api.createOrder({ items: cart.items, total: cart.totalPrice, userId: auth.profile!.id, }) cart.clear() }
return { submitOrder } })
|
跨 store 引用就像引用普通 composable,不需要 rootState、rootGetters 这些间接调用。
4. 持久化(刷新不丢)
1
| npm install pinia-plugin-persistedstate
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import { createPinia } from 'pinia' import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia() pinia.use(piniaPluginPersistedstate)
export const useAuthStore = defineStore('auth', () => { const token = ref('')
return { token } }, { persist: { key: 'auth', storage: localStorage, paths: ['token'], } })
|
组件中使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <script setup lang="ts"> import { useCartStore } from '@/stores/cart' import { storeToRefs } from 'pinia'
const cart = useCartStore()
// 解构响应式数据必须用 storeToRefs const { items, totalPrice, itemCount } = storeToRefs(cart)
// 方法可以直接解构(不是响应式,不需要 storeToRefs) const { addItem, removeItem } = cart </script>
<template> <div>共 {{ itemCount }} 件,合计 ¥{{ totalPrice }}</div> <button @click="addItem(product)">加入购物车</button> </template>
|
常见坑: 不用 storeToRefs 直接解构会丢失响应式。
从 Vuex 迁移的步骤
- 安装 Pinia,和 Vuex 可以共存
- 新功能全部用 Pinia store
- 逐个模块迁移:先改一个小 store,验证没问题再推
- 最后一个模块迁完,卸载 Vuex
什么时候不需要 Pinia
- 组件内部状态(表单输入、展开/折叠)→
ref 就行
- 父子组件通信 → props + emits
- 跨层级但范围有限的共享 →
provide / inject
- URL 参数(筛选条件、页码)→ 路由参数
provide/inject 示例:
1 2 3 4 5 6
| const theme = ref('light') provide('theme', theme)
const theme = inject('theme')
|
不需要全局状态就别往 store 放。
总结
Pinia 的核心价值就一句话:用写 composable 的方式写状态管理。没有新概念、没有魔法字符串、没有 commit。Vue 开发者 10 分钟上手,30 分钟就能在项目里用起来。