Pinia 是 Vue 官方推荐的状态管理库,以其简洁的 API 和完整的 TypeScript 支持取代了 Vuex。本文系统讲解 Pinia 的核心概念、模块化设计、持久化方案以及从 Vuex 迁移的完整指南。
一、为什么选择 Pinia
Pinia vs Vuex
| 特性 | Vuex 4 | Pinia |
|---|---|---|
| API 风格 | Mutation + Action | 只有 Action(同步异步统一) |
| TypeScript | 支持较弱 | 完整类型推导 |
| 模块化 | 嵌套模块 + namespace | 扁平 Store,天然模块化 |
| 代码量 | 较多样板代码 | 极简 |
| 组合式 API | 适配一般 | 原生设计 |
| DevTools | 支持 | 支持(时间旅行更佳) |
| SSR | 支持 | 支持 |
| 热更新 | 部分 | 完整支持 |
Pinia 移除了什么
- Mutations:直接修改 state 或在 action 中修改
- 嵌套模块:每个 Store 独立扁平
- 命名空间:Store 天然隔离
- 辅助函数:
mapState/mapGetters不再必要(组合式写法)
二、快速上手
安装与初始化
// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const app = createApp(App)
app.use(createPinia())
app.mount('#app')定义第一个 Store
// stores/counter.js
import { defineStore } from 'pinia'
// 选项式写法(类似组件的 data/computed/methods)
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
name: '计数器'
}),
getters: {
double: (state) => state.count * 2,
doublePlusOne() {
// 通过 this 访问其他 getter
return this.double + 1
}
},
actions: {
increment() {
this.count++
},
async incrementAsync() {
// 直接写异步逻辑,无需 dispatch
await new Promise(r => setTimeout(r, 1000))
this.count++
}
}
})组合式写法(推荐)
// stores/counter.js — Setup Store 写法
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
// state
const count = ref(0)
const name = ref('计数器')
// getters
const double = computed(() => count.value * 2)
const doublePlusOne = computed(() => double.value + 1)
// actions
function increment() {
count.value++
}
async function incrementAsync() {
await new Promise(r => setTimeout(r, 1000))
count.value++
}
// 必须返回所有使用的属性
return { count, name, double, doublePlusOne, increment, incrementAsync }
})在组件中使用
<script setup>
import { useCounterStore } from '@/stores/counter'
import { storeToRefs } from 'pinia'
const counter = useCounterStore()
// ⚠️ 解构会丢失响应式,必须用 storeToRefs
const { count, double } = storeToRefs(counter)
// action 可以直接解构(它是函数)
const { increment } = counter
// 也可以不解构直接用
console.log(counter.count)
counter.increment()
</script>
<template>
<div>
<p>{{ counter.name }}: {{ count }}</p>
<p>双倍: {{ double }}</p>
<button @click="increment">+1</button>
</div>
</template>三、核心概念详解
State
// stores/user.js
export const useUserStore = defineStore('user', {
state: () => ({
user: null,
token: '',
permissions: [],
settings: {
theme: 'light',
language: 'zh-CN'
}
}),
actions: {
// 重置整个 state 到初始值
resetState() {
this.$reset()
},
// 批量修改(替代 Vuex 的 mutation)
setUser(user) {
this.user = user
this.permissions = user.permissions
},
// $patch 批量更新,性能更好
updateSettings(partial) {
this.$patch({
settings: { ...this.settings, ...partial }
})
// 函数式写法
this.$patch((state) => {
state.settings.theme = 'dark'
})
}
}
})Getters
export const useCartStore = defineStore('cart', {
state: () => ({
items: []
}),
getters: {
// 基础 getter
itemCount: (state) => state.items.length,
// 带参数的 getter(返回函数,无缓存优势)
itemById: (state) => {
return (id) => state.items.find(item => item.id === id)
},
// 访问其他 getter
subtotal() {
return this.items.reduce((sum, item) => sum + item.price * item.qty, 0)
},
// 访问其他 Store
total() {
const userStore = useUserStore()
const discount = userStore.user?.discount ?? 1
return this.subtotal * discount
}
}
})Actions
// stores/products.js
import { defineStore } from 'pinia'
import api from '@/api'
export const useProductStore = defineStore('products', () => {
const products = ref([])
const loading = ref(false)
const error = ref(null)
// 获取产品列表
async function fetchProducts(params) {
loading.value = true
error.value = null
try {
const { data } = await api.get('/products', { params })
products.value = data
} catch (e) {
error.value = e.message
} finally {
loading.value = false
}
}
// 跨 Store 调用
async function purchase(productId) {
const cartStore = useCartStore()
const product = products.value.find(p => p.id === productId)
await api.post(`/cart/items`, { productId, qty: 1 })
cartStore.addItem(product)
}
return { products, loading, error, fetchProducts, purchase }
})四、插件系统
持久化插件
// plugins/persist.js
import { watch } from 'vue'
export function createPersistPlugin() {
return ({ store }) => {
// 从 localStorage 恢复
const saved = localStorage.getItem(`pinia-${store.$id}`)
if (saved) {
store.$patch(JSON.parse(saved))
}
// 订阅变化并保存
store.$subscribe((mutation, state) => {
localStorage.setItem(`pinia-${store.$id}`, JSON.stringify(state))
})
}
}// main.js
import { createPinia } from 'pinia'
import { createPersistPlugin } from './plugins/persist'
const pinia = createPinia()
pinia.use(createPersistPlugin())使用官方持久化插件
npm install pinia-plugin-persistedstate// stores/user.js
export const useUserStore = defineStore('user', {
state: () => ({
user: null,
token: ''
}),
// 持久化配置
persist: {
key: 'my-app-user',
storage: localStorage, // 或 sessionStorage
// 只持久化部分字段
pick: ['token', 'user.id', 'user.name']
},
actions: {
login(credentials) { /* ... */ },
logout() {
this.user = null
this.token = ''
}
}
})自定义日志插件
// plugins/logger.js
export function createLoggerPlugin() {
return ({ store }) => {
store.$subscribe((mutation, state) => {
console.log(
`[Pinia] ${mutation.storeId} ${mutation.type}:`,
mutation.type === 'patch'
? mutation.events
: `${mutation.payload}`
)
})
store.$onAction(({ name, args, after, onError }) => {
const startTime = Date.now()
console.log(`[Action] ${store.$id}/${name} 开始`, args)
after((result) => {
console.log(`[Action] ${store.$id}/${name} 完成 (${Date.now() - startTime}ms)`)
})
onError((error) => {
console.error(`[Action] ${store.$id}/${name} 失败:`, error)
})
})
}
}五、Store 组合与模块化
Store 之间相互调用
// stores/cart.js
import { defineStore } from 'pinia'
import { useUserStore } from './user'
import { useProductStore } from './products'
export const useCartStore = defineStore('cart', () => {
const items = ref([])
const userStore = useUserStore() // 在 setup 内调用
const total = computed(() => {
const discount = userStore.user?.discount ?? 1
return items.value.reduce(
(sum, item) => sum + item.price * item.qty, 0
) * discount
})
return { items, total }
})组合式 Store 设计模式
// stores/composables/useEntityCRUD.js
// 通用的 CRUD Store 工厂
export function createCrudStore(entityName, apiEndpoint) {
return defineStore(entityName, () => {
const items = ref([])
const current = ref(null)
const loading = ref(false)
async function fetchAll(params = {}) {
loading.value = true
try {
const { data } = await api.get(apiEndpoint, { params })
items.value = data
} finally {
loading.value = false
}
}
async function fetchOne(id) {
const { data } = await api.get(`${apiEndpoint}/${id}`)
current.value = data
}
async function create(payload) {
const { data } = await api.post(apiEndpoint, payload)
items.value.unshift(data)
return data
}
async function update(id, payload) {
const { data } = await api.put(`${apiEndpoint}/${id}`, payload)
const index = items.value.findIndex(i => i.id === id)
if (index > -1) items.value[index] = data
return data
}
async function remove(id) {
await api.delete(`${apiEndpoint}/${id}`)
items.value = items.value.filter(i => i.id !== id)
}
return { items, current, loading, fetchAll, fetchOne, create, update, remove }
})
}
// 使用工厂创建具体的 Store
export const usePostStore = createCrudStore('posts', '/api/posts')
export const useCategoryStore = createCrudStore('categories', '/api/categories')六、从 Vuex 迁移
对照转换
// === Vuex 4 ===
// store/index.js
export default createStore({
state: () => ({
count: 0
}),
mutations: {
increment(state) {
state.count++
}
},
actions: {
incrementAsync({ commit }) {
setTimeout(() => commit('increment'), 1000)
}
},
getters: {
double: (state) => state.count * 2
}
})
// === Pinia 等价写法 ===
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
// mutation 直接变成 action
actions: {
increment() {
this.count++
},
incrementAsync() {
setTimeout(() => this.increment(), 1000)
}
},
getters: {
double: (state) => state.count * 2
}
})// === 组件中使用对比 ===
// Vuex
// this.$store.state.count
// this.$store.commit('increment')
// this.$store.dispatch('incrementAsync')
// mapState('counter', ['count'])
// mapGetters('counter', ['double'])
// Pinia
const counter = useCounterStore()
counter.count
counter.increment() // 同步异步统一调用
counter.incrementAsync()
const { count } = storeToRefs(counter)渐进式迁移策略
- 同时安装两个库,逐模块迁移
- 从叶子模块开始(无其他模块依赖的)
- 用组合函数桥接:
// 桥接层:让旧 Vuex 代码访问新 Pinia
// stores/bridge.js
export function useCounterCompat() {
const counter = useCounterStore()
return {
get count() { return counter.count },
increment: counter.increment
}
}- 迁移完成移除 Vuex
七、SSR 中的注意事项
// Nuxt 3 中使用
// stores/counter.js
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
return { count }
})
// 组件中(Nuxt 自动导入 Pinia)
const counter = useCounterStore()
// SSR 中跨请求状态污染防范
// ✅ 在 setup 内调用 useStore(),而不是模块顶层
// ❌ const store = useCounterStore() // 模块顶层会共享状态八、小结
Pinia 以极简的 API 提供了完整的状态管理能力:去掉 mutation 减少样板代码,扁平 Store 天然模块化,完整的 TypeScript 和 DevTools 支持。从 Vuex 迁移成本低,新项目直接上 Pinia 是唯一正确选择。
本文由 inspirecl.asia 原创,转载请注明出处。
评论 (0)