为什么选择 Pinia 而不是 Vuex
Pinia 已经成为 Vue 3 官方推荐的状态管理方案。与 Vuex 相比,Pinia 去掉了 mutation 的概念,简化了 API 设计。它天然支持 TypeScript 类型推断,不需要手写复杂的类型声明文件。Pinia 支持 multiple store,每个 store 独立管理,不再需要巨型单一状态树。此外,Pinia 的体积仅约 1KB(gzip 后),模块化方式也支持懒加载。
定义 Store:Options API 风格
Options Store 的结构类似于 Vue 组件选项式 API,通过 defineStore 传入配置对象来定义 state、getters 和 actions。
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
name: '计数器'
}),
getters: {
doubleCount: (state) => state.count * 2,
// 访问其他 getter 时使用 this
formattedName(): string {
return `【${this.name}】当前值:${this.doubleCount}`
}
},
actions: {
increment() {
this.count++ // 直接修改,无需 mutation
},
async fetchCount() {
const res = await fetch('/api/count')
this.count = await res.json()
}
}
})
定义 Store:Setup 语法
Setup Store 使用 ref、computed 和普通函数,与组合式 API 风格一致。对于习惯 Composition API 的开发者来说更加直观,也更容易实现逻辑复用。
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useCartStore = defineStore('cart', () => {
// state
const items = ref<Array<{ id: number; name: string; price: number }>>([])
// getters
const total = computed(() =>
items.value.reduce((sum, item) => sum + item.price, 0)
)
const itemCount = computed(() => items.value.length)
// actions
function addItem(item: { id: number; name: string; price: number }) {
items.value.push(item)
}
function removeItem(id: number) {
items.value = items.value.filter(item => item.id !== id)
}
async function loadCart() {
const res = await fetch('/api/cart')
items.value = await res.json()
}
return { items, total, itemCount, addItem, removeItem, loadCart }
})
在组件中使用 Store
<script setup lang="ts">
import { useCounterStore } from '@/stores/counter'
import { storeToRefs } from 'pinia'
const counter = useCounterStore()
// 解构时保持响应式
const { count, doubleCount } = storeToRefs(counter)
// actions 直接解构即可
const { increment } = counter
</script>
<template>
<div>
<p>计数:{{ count }},双倍:{{ doubleCount }}</p>
<button @click="increment">+1</button>
</div>
</template>
TypeScript 类型支持
Pinia 在设计之初就充分考虑了 TypeScript 的集成。Setup Store 中的 ref 和 computed 自动推导类型;Options Store 也通过泛型推断 state 和 getters 的类型。使用 storeToRefs 解构时,类型信息完整保留。
// 为 store 的 state 补充类型
interface UserState {
name: string
email: string
role: 'admin' | 'editor' | 'viewer'
}
const useUserStore = defineStore('user', {
state: (): UserState => ({
name: '',
email: '',
role: 'viewer'
}),
actions: {
setRole(role: UserState['role']) {
this.role = role // 完整的类型检查
}
}
})
插件系统
Pinia 插件可以扩展 store 的功能,常见的用途包括持久化存储、路由权限控制、全局错误处理等。插件通过 pinia.use() 注册,每个 store 创建时都会执行插件函数。
import { createPinia } from 'pinia'
function persistPlugin({ store }) {
// 从 localStorage 恢复状态
const saved = localStorage.getItem(store.$id)
if (saved) {
store.$patch(JSON.parse(saved))
}
// 监听变化并保存
store.$subscribe((mutation, state) => {
localStorage.setItem(store.$id, JSON.stringify(state))
})
}
const pinia = createPinia()
pinia.use(persistPlugin)
Store 组合与复用
在一个 store 中可以引用另一个 store,实现逻辑的组合。这在 Setup Store 中尤为自然,直接调用另一个 store 的组合函数即可。
import { defineStore } from 'pinia'
import { useUserStore } from './user'
export const useCartStore = defineStore('cart', () => {
const userStore = useUserStore()
const items = ref([])
const canCheckout = computed(() => {
return items.value.length > 0 && userStore.role !== 'viewer'
})
function checkout() {
if (!canCheckout.value) return
// 结算逻辑
}
return { items, canCheckout, checkout }
})
从 Vuex 迁移
从 Vuex 迁移到 Pinia 可以渐进式进行。核心映射关系为:Vuex 的 state 对应 Pinia 的 state,getters 对应 getters,mutations 和 actions 统一合并为 actions。Pinia 不再需要 dispatch 和 commit,直接调用 store 实例上的方法即可。模块命名空间也被扁平化的独立 store 取代,每个 store 通过唯一 ID 区分。
// Vuex 模块
const vuexModule = {
namespaced: true,
state: () => ({ count: 0 }),
mutations: { INCREMENT(state) { state.count++ } },
actions: { increment({ commit }) { commit('INCREMENT') } },
getters: { double: state => state.count * 2 }
}
// 对应的 Pinia Store
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
getters: { double: state => state.count * 2 },
actions: { increment() { this.count++ } }
})