目录
[一、为什么选择 Pinia?](#一、为什么选择 Pinia?)
[1. 最大的改进:去掉了冗余的 Mutations](#1. 最大的改进:去掉了冗余的 Mutations)
[2. 原生完美支持 TypeScript](#2. 原生完美支持 TypeScript)
[3. 与组合式 API 无缝集成](#3. 与组合式 API 无缝集成)
[4. 扁平化模块化设计](#4. 扁平化模块化设计)
[5. 轻量高效](#5. 轻量高效)
[Pinia vs Vuex 核心对比](#Pinia vs Vuex 核心对比)
[三、快速上手:5 分钟写第一个 Pinia 例子](#三、快速上手:5 分钟写第一个 Pinia 例子)
[1. 安装 Pinia](#1. 安装 Pinia)
[2. 在 main.js 中注册 Pinia](#2. 在 main.js 中注册 Pinia)
[3. 定义第一个 Store](#3. 定义第一个 Store)
[方式 1:选项式 API(适合 Vue2 转 Vue3 的开发者)](#方式 1:选项式 API(适合 Vue2 转 Vue3 的开发者))
[方式 2:组合式 API(推荐,Vue3 标准写法)](#方式 2:组合式 API(推荐,Vue3 标准写法))
[4. 在组件中使用 Store](#4. 在组件中使用 Store)
[四、核心 API 详解:不同场景的最佳实践](#四、核心 API 详解:不同场景的最佳实践)
[1. State:三种修改方式,分别适合什么场景?](#1. State:三种修改方式,分别适合什么场景?)
[方式 1:直接修改(最简单,适合单个属性修改)](#方式 1:直接修改(最简单,适合单个属性修改))
[方式 2:patch 批量修改(适合同时修改多个属性)](#方式 2:patch 批量修改(适合同时修改多个属性))
[方式 3:通过 Actions 修改(推荐,适合复杂逻辑)](#方式 3:通过 Actions 修改(推荐,适合复杂逻辑))
[2. Getters:派生状态的最佳实践](#2. Getters:派生状态的最佳实践)
[带参数的 Getters](#带参数的 Getters)
[3. Actions:同步和异步操作都在这里](#3. Actions:同步和异步操作都在这里)
[同步 Action](#同步 Action)
[异步 Action](#异步 Action)
[调用其他 Action](#调用其他 Action)
[1. 重置 Store 到初始状态](#1. 重置 Store 到初始状态)
[2. 监听状态变化](#2. 监听状态变化)
[3. 状态持久化(最常用插件)](#3. 状态持久化(最常用插件))
[4. 跨 Store 调用](#4. 跨 Store 调用)
[六、新手最容易踩的 8 个坑](#六、新手最容易踩的 8 个坑)
[1. 直接解构 Store 丢失响应式](#1. 直接解构 Store 丢失响应式)
[2. 忘记注册 Pinia 实例](#2. 忘记注册 Pinia 实例)
[3. 在组合式 Store 中忘记返回属性](#3. 在组合式 Store 中忘记返回属性)
[4. 在 Actions 中使用箭头函数](#4. 在 Actions 中使用箭头函数)
[5. 把所有状态都放在 Pinia 里](#5. 把所有状态都放在 Pinia 里)
[6. 持久化插件配置错误](#6. 持久化插件配置错误)
[7. 多个 Store 循环依赖](#7. 多个 Store 循环依赖)
[8. 不使用 TypeScript](#8. 不使用 TypeScript)
[八、什么时候该用 Pinia?什么时候不用?](#八、什么时候该用 Pinia?什么时候不用?)
[应该用 Pinia 的场景](#应该用 Pinia 的场景)
[不应该用 Pinia 的场景](#不应该用 Pinia 的场景)
如果你正在学习 Vue3,那么 Pinia 是你必须掌握的核心技能之一。作为 Vue 官方推荐的新一代状态管理库,Pinia 彻底解决了 Vuex 的诸多痛点,以更简洁的 API、更友好的开发体验和更强大的类型支持,成为了 Vue3 生态中状态管理的标准方案。
本文将从新手的角度出发,由浅入深地讲解 Pinia 的所有核心知识,结合不同场景的实战分析,帮助你快速掌握 Pinia,并能在实际项目中灵活运用。
一、为什么选择 Pinia?
在学习 Pinia 之前,我们首先要明白一个问题:为什么 Vue 官方要推出 Pinia 来替代 Vuex?Pinia 到底好在哪里?
1. 最大的改进:去掉了冗余的 Mutations
这是 Pinia 最具革命性的改变。在 Vuex 中,我们必须通过commit(mutation)来同步修改状态,异步操作必须放在actions中,这导致了大量的样板代码。
而在 Pinia 中,只有 Actions,同步和异步操作都可以在 Actions 中处理,甚至可以直接修改状态。代码量直接减少了 50% 以上。
2. 原生完美支持 TypeScript
Pinia 是用 TypeScript 编写的,开箱即用的类型推断,不需要额外的类型声明。所有属性和方法都有完整的自动补全和类型检查,开发时能及时发现很多潜在错误。
3. 与组合式 API 无缝集成
Pinia 完全是为 Vue3 的组合式 API 设计的,你可以像写普通的 composable 一样定义 Store,完美支持<script setup>语法。
4. 扁平化模块化设计
Pinia 没有 Vuex 那样复杂的嵌套 modules 和命名空间,每个 Store 都是一个独立的模块,通过不同的文件区分。自动代码分割,按需加载,性能更好。
5. 轻量高效
Pinia 的核心代码仅约 1KB(gzip 压缩后),比 Vuex 更轻量,性能也更优秀。同时支持服务端渲染(SSR)和热更新。
Pinia vs Vuex 核心对比
| 特性 | Vuex | Pinia |
|---|---|---|
| 官方状态 | 维护模式,不再更新 | 官方推荐,活跃开发 |
| 核心概念 | State、Getters、Mutations、Actions、Modules | State、Getters、Actions |
| 修改状态 | 必须通过 commit (mutation) | 直接修改或通过 actions |
| TypeScript 支持 | 需要大量手动类型声明 | 原生支持,自动类型推断 |
| 组合式 API | 支持但不友好 | 原生设计,完美契合 |
| 代码量 | 较多,样板代码多 | 简洁,减少约 50% 代码 |
二、核心概念速览
Pinia 的核心概念非常简单,只有四个,新手很容易理解:
- Store(仓库):Pinia 的核心,每个 Store 都是一个独立的容器,存储着应用的一部分状态和相关的业务逻辑。
- State(状态) :Store 中存储的原始数据,是响应式的,相当于组件中的
data。 - Getters(计算属性) :基于 State 派生出来的状态,有缓存机制,相当于组件中的
computed。 - Actions(动作) :处理业务逻辑的地方,可以是同步或异步操作,相当于组件中的
methods。
记住:State 负责存储数据,Getters 负责派生数据,Actions 负责修改数据。
三、快速上手:5 分钟写第一个 Pinia 例子
1. 安装 Pinia
bash
npm install pinia
# 或
yarn add pinia
2. 在 main.js 中注册 Pinia
javascript
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const app = createApp(App)
// 创建Pinia实例并注册到应用
app.use(createPinia())
app.mount('#app')
3. 定义第一个 Store
在src目录下创建stores文件夹,然后创建counter.js文件:
Pinia 支持两种定义 Store 的方式:选项式 API 和组合式 API。
方式 1:选项式 API(适合 Vue2 转 Vue3 的开发者)
javascript
// stores/counter.js
import { defineStore } from 'pinia'
// 第一个参数是Store的唯一ID,不能重复
export const useCounterStore = defineStore('counter', {
// State:存储状态数据,必须是一个函数
state: () => ({
count: 0
}),
// Getters:计算派生状态
getters: {
doubleCount: (state) => state.count * 2
},
// Actions:处理业务逻辑
actions: {
increment() {
this.count++
},
decrement() {
this.count--
}
}
})
方式 2:组合式 API(推荐,Vue3 标准写法)
javascript
// stores/counter.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useCounterStore = defineStore('counter', () => {
// 相当于state
const count = ref(0)
// 相当于getters
const doubleCount = computed(() => count.value * 2)
// 相当于actions
const increment = () => {
count.value++
}
const decrement = () => {
count.value--
}
// 必须返回所有要暴露的属性和方法
return { count, doubleCount, increment, decrement }
})
4. 在组件中使用 Store
javascript
<template>
<div>
<p>计数:{{ counter.count }}</p>
<p>双倍计数:{{ counter.doubleCount }}</p>
<button @click="counter.increment">+1</button>
<button @click="counter.decrement">-1</button>
</div>
</template>
<script setup>
// 导入并实例化Store
import { useCounterStore } from '@/stores/counter'
const counter = useCounterStore()
</script>
就是这么简单!你已经完成了第一个 Pinia 应用。
四、核心 API 详解:不同场景的最佳实践
1. State:三种修改方式,分别适合什么场景?
Pinia 提供了三种修改 State 的方式,不同的场景应该选择不同的方式。
方式 1:直接修改(最简单,适合单个属性修改)
javascript
const counter = useCounterStore()
// 直接修改单个属性
counter.count = 10
适用场景:简单的单个属性修改,代码最简洁。
方式 2:$patch 批量修改(适合同时修改多个属性)
javascript
const userStore = useUserStore()
// 批量修改多个属性
userStore.$patch({
name: '张三',
age: 25,
email: 'zhangsan@example.com'
})
// 也可以用函数形式,适合修改数组或对象
userStore.$patch((state) => {
state.hobbies.push('篮球')
state.age++
})
适用场景:同时修改多个属性,或者修改数组 / 对象的元素。比多次直接修改性能更好。
方式 3:通过 Actions 修改(推荐,适合复杂逻辑)
javascript
// 在Store中定义Action
actions: {
updateUserInfo(newInfo) {
// 可以在这里添加验证、日志等复杂逻辑
if (newInfo.age < 0) {
console.error('年龄不能为负数')
return
}
this.name = newInfo.name
this.age = newInfo.age
}
}
// 在组件中调用
userStore.updateUserInfo({ name: '李四', age: 30 })
适用场景:包含复杂逻辑的状态修改,如数据验证、异步操作、日志记录等。这是最推荐的方式,保持代码的可维护性。
2. Getters:派生状态的最佳实践
Getters 是基于 State 派生出来的状态,有缓存机制,只有当依赖的 State 发生变化时才会重新计算。
基本用法
javascript
getters: {
// 接收state作为参数
adultCount: (state) => {
return state.users.filter(user => user.age >= 18).length
},
// 可以访问其他getter,使用this
adultAverageAge() {
const adults = this.users.filter(user => user.age >= 18)
if (adults.length === 0) return 0
return adults.reduce((sum, user) => sum + user.age, 0) / adults.length
}
}
带参数的 Getters
如果需要传递参数,可以返回一个函数:
javascript
getters: {
getUserById: (state) => {
return (id) => state.users.find(user => user.id === id)
}
}
在组件中使用:
javascript
const user = userStore.getUserById(1)
3. Actions:同步和异步操作都在这里
Actions 是处理业务逻辑的地方,支持同步和异步操作。
同步 Action
javascript
actions: {
increment(num) {
this.count += num
}
}
异步 Action
javascript
actions: {
async fetchUserList() {
// 显示加载状态
this.loading = true
try {
// 调用接口获取数据
const res = await api.getUserList()
this.userList = res.data
} catch (error) {
console.error('获取用户列表失败', error)
} finally {
// 隐藏加载状态
this.loading = false
}
}
}
调用其他 Action
在一个 Action 中可以调用同一个 Store 的其他 Action:
javascript
actions: {
async fetchAndProcessUsers() {
await this.fetchUserList()
this.processUserData()
},
processUserData() {
// 处理用户数据
}
}
五、高级常用特性
1. 重置 Store 到初始状态
一键将 Store 的所有状态重置为初始值,非常适合退出登录时清空用户数据:
javascript
const userStore = useUserStore()
userStore.$reset()
2. 监听状态变化
监听整个 Store 的状态变化,可以用来做日志记录、状态持久化等:
javascript
userStore.$subscribe((mutation, state) => {
console.log('状态发生了变化', mutation)
console.log('最新状态', state)
// 将状态保存到localStorage
localStorage.setItem('user', JSON.stringify(state))
})
3. 状态持久化(最常用插件)
使用pinia-plugin-persistedstate插件可以轻松实现状态持久化到 localStorage,不需要手动写代码。
安装插件
bash
npm install pinia-plugin-persistedstate
注册插件
javascript
// main.js
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
使用持久化
javascript
// stores/user.js
export const useUserStore = defineStore('user', {
state: () => ({
token: '',
username: ''
}),
// 开启持久化,自动保存到localStorage
persist: true
})
4. 跨 Store 调用
在一个 Store 中可以调用其他 Store 的方法:
javascript
// stores/user.js
import { useCartStore } from './cart'
export const useUserStore = defineStore('user', {
actions: {
async logout() {
this.token = ''
this.username = ''
// 调用购物车Store的清空方法
const cartStore = useCartStore()
cartStore.clearCart()
}
}
})
六、新手最容易踩的 8 个坑
1. 直接解构 Store 丢失响应式
javascript
// 错误:直接解构会丢失响应式
const { count, doubleCount } = useCounterStore()
// 正确:使用storeToRefs保持响应式
import { storeToRefs } from 'pinia'
const { count, doubleCount } = storeToRefs(useCounterStore())
注意:方法不需要用 storeToRefs,直接解构即可:
javascript
const { increment, decrement } = useCounterStore()
2. 忘记注册 Pinia 实例
如果在 main.js 中忘记注册 Pinia,会报错:"getActivePinia()" was called but there was no active Pinia。
3. 在组合式 Store 中忘记返回属性
组合式 Store 必须在最后返回所有要暴露的属性和方法,否则组件中无法访问。
4. 在 Actions 中使用箭头函数
javascript
// 错误:箭头函数会导致this指向错误
actions: {
increment: () => {
this.count++ // this是undefined
}
}
// 正确:使用普通函数
actions: {
increment() {
this.count++
}
}
5. 把所有状态都放在 Pinia 里
Pinia 是用来管理全局共享状态 的,不是所有状态都要放在 Pinia 里。组件内部的局部状态应该用ref或reactive管理。
6. 持久化插件配置错误
如果开启了持久化但数据没有保存,检查是否在 main.js 中正确注册了插件,以及是否在 Store 中设置了persist: true。
7. 多个 Store 循环依赖
避免在 Store A 中导入 Store B,同时在 Store B 中导入 Store A,这会导致循环依赖错误。
8. 不使用 TypeScript
Pinia 对 TypeScript 的支持非常好,使用 TypeScript 可以获得完整的类型检查和自动补全,大幅提升开发效率和代码质量。
七、最佳实践
- 按功能模块划分 Store :不要把所有状态都放在一个 Store 里,一个业务模块对应一个 Store(如
userStore、cartStore、orderStore)。 - 复杂逻辑放在 Actions 中:组件只负责调用 actions,不要在组件中写复杂的业务逻辑。
- 使用 Getters 派生状态:避免在组件中重复计算相同的派生状态。
- 合理使用状态持久化:只保存需要持久化的数据(如 token、用户信息),不要保存所有状态。
- 保持 Store 的纯粹性:不要在 Store 中操作 DOM 或路由,保持 Store 只负责状态管理。
- 配合 TypeScript 使用:充分利用 Pinia 的类型系统,减少运行时错误。
八、什么时候该用 Pinia?什么时候不用?
应该用 Pinia 的场景
- 多个组件共享的全局状态(如用户信息、购物车、主题设置)
- 需要在不同页面之间传递的数据
- 复杂的业务逻辑需要统一管理
- 需要持久化到本地的数据
不应该用 Pinia 的场景
- 组件内部的局部状态
- 父子组件之间传递的数据(用 Props 和事件即可)
- 临时的、不需要共享的数据
总结
Pinia 是 Vue3 时代状态管理的最佳选择,它继承了 Vuex 的优点,同时解决了 Vuex 的诸多痛点。对于新手来说,Pinia 的学习曲线非常平缓,只要你掌握了 Vue3 的基础,就能很快上手。
记住:Pinia 的核心思想是简单、直观、高效。不要把简单的事情复杂化,根据实际场景选择合适的用法,才能发挥 Pinia 的最大优势。
现在,打开你的编辑器,动手写第一个 Pinia 应用吧!实践是最好的老师。