Pinia 状态管理
一、为什么要讲 Pinia
原因:
-
Vue3 官方推荐的状态管理方案
-
用来替代 Vuex(Vuex 5 已停止推进)
-
更符合 Vue3 中 Composition API 的设计理念
-
比起 Vuex 使用更加方便
vuex 与 pinia 对比
| 特性 | Vuex (3/4) | Pinia |
|---|---|---|
| 核心组成 | State, Getters, Mutations, Actions | State, Getters, Actions |
| Mutations | 必须通过它修改 State (繁琐) | 废弃,Actions 即可修改 State |
| TypeScript | 支持较弱,需大量额外定义 | 原生完美支持,类型推断极佳 |
| 模块化 | 单一 Store 树,需嵌套 Modules | 多 Store 设计,扁平化结构,自动拆包 |
| 体积 | 约 10kb | 约 1kb (极度轻量) |
二、什么是状态管理(State Management)
什么是「状态」?
在前端应用中,状态(state)指的是:
-
状态 = 程序运行时"留在内存里、会变化的数据"
-
状态的核心特征
| 特征 | 说明 | 例子 |
|---|---|---|
| 存在于内存中 | 页面刷新即消失(除非持久化) | 用户登录后的 token |
| 会随时间变化 | 用户操作、API 响应会改变它 | 购物车商品数量增减 |
| 影响 UI 渲染 | 状态变 → 视图自动更新 | 切换主题色后界面变色 |
| 需要被追踪 | 框架通过响应式系统监控变化 | Vue 的 ref/reactive |
不使用状态管理会遇到什么问题?
当项目变大后,会出现:
-
父子组件层层传 props
-
兄弟组件通信复杂
-
数据分散在各个组件,难以维护
-
同一份数据被多处复制,容易不一致
为什么需要状态管理?
状态管理就是把这些状态从"散落在各个组件里"抽出来,集中存、集中改、集中通知 ,让任何组件都能同一份数据源、统一规则地读取和更新,避免:
-
父→子→孙,一层层
props传递 -
事件一层层
emit回去 -
不同组件各自 copy 一份数据,改完互相不一致
总结
状态 = 应用运行时需要被框架追踪、会影响视图、且可能被多个组件共享的"活"数据
(而状态管理 = 让这些"活"数据变得有序、可控、可预测的工具)
三、Pinia 在项目中的定位
plaintext
Vue App
├─ Components(组件)
├─ Router(路由)
├─ Pinia(全局状态) ← 负责跨组件数据共享
└─ Services / API
四、Pinia 的核心概念
Store 是什么
Store 是保存状态 ( state ) 和业务逻辑的实体,它并不与组件树绑定。
换句话说,它存储着全局的状态数据。
一个 Store 由哪几部分组成
| 部分 | 作用 |
|---|---|
| state | 定义状态数据 |
| getters | 类似计算属性(派生状态) |
| actions | 业务逻辑 / 异步操作 |
总结类比
-
可以把 pinia 类比为一个一个中心化的"仓库"。就像一个公司有一个中央档案室,不管哪个 部门(Vue 组件 )需要 看 或 修改 某个 文件(数据) ,++都得去这个档案室拿取++。
-
解释:
-
各部门:各个 Vue 组件
-
文件:State 数据
-
看:组件调用 Store 里面 State 状态数据
-
修改:Store 里面修改 State 状态数据的方法(Actions)
-
都得去这个档案室拿取
- 我们如果想要在 Vue 组件里面使用 Store 仓库里面的数据与方法,肯定要先在组件里引入使用这个 Store 仓库
-
五、Pinia 的基础使用流程
安装 Pinia
bash
npm install pinia
在 main.ts 中注册 Pinia
ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const app = createApp(App)
app.use(createPinia())
app.mount('#app')
定义一个 Store
ts
import { defineStore } from 'pinia'
// 'use' 是约定俗成的前缀,便于识别这是一个 store
// 'user' 是 store 的唯一 ID
export const useUserStore = defineStore('user', {
// Store 配置项
// state、getter 和 action 都写这里面
// 这里面包含了两种写法,我们会在下面介绍
})
在组件中使用 Store
ts
// 1. 导入
import { useUserStore } from '@/stores/user'
// 2. 调用
const userStore = useUserStore()
// 3. 获取调用结果里面的数据方法
userStore.setToken('abc')
console.log(userStore.isLogin)
六、Pinia 的两种写法
1. Options Store(类似 Vuex,适合新手)
ts
// 选项式写法
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
token: '',
userInfo: null
}),
getters: {
isLogin: (state) => !!state.token
},
actions: {
setToken(token: string) {
this.token = token
}
}
})
2. Setup Store(推荐写法)
ts
// 组合式写法(推荐)
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useMainStore = defineStore('main', () => {
// 1. 定义状态 (State) - 使用 ref 或 reactive
const count = ref(0)
const name = ref('Eduardo')
const user = ref({ name: 'John', age: 30 })
// 2. 定义计算属性 (Getters) - 使用 computed
const doubleCount = computed(() => count.value * 2)
const isEven = computed(() => count.value % 2 === 0)
// 3. 定义动作 (Actions) - 定义函数
function increment() {
count.value++
}
function decrement() {
count.value--
}
// 可以包含异步操作
async function fetchUserAge() {
try {
// 模拟 API 调用
await new Promise(resolve => setTimeout(resolve, 1000))
user.value.age += 1
} catch (error) {
console.error('Failed to fetch user age:', error)
}
}
// 4. 必须返回你想要暴露给外界的部分
return {
// State
count,
name,
user,
// Getters
doubleCount,
isEven,
// Actions
increment,
decrement,
fetchUserAge
}
})
Setup Store 与 Composition API 思想完全一致
七、Pinia 中的数据更新原则
1. 可以直接修改 state
ts
// 可以在引入 Store 仓库的组件里面直接更改
// 只是通常不建议这么做
store.count++
为什么不推荐:
-
虽然可以直改,但在中大型项目中,如果随处直接修改 Store 数据,会带来如下一些问题:
-
业务逻辑碎片化
-
如果修改数据的逻辑(比如:修改价格前要检查库存、计算折扣、验证权限)散落在 10 个不同的
.vue文件里。 -
后果 :当你需要修改这个逻辑时,你得满项目找这 10 个地方。如果写在
action里,你只需要改 Store 里的一个函数
-
-
调试与追踪困难
-
Pinia 的 Action 像是一个"带名字的事务所"
-
直接修改:在 DevTools 里,你可能只看到数据变了,但不知道是哪个组件、因为什么逻辑触发的
-
Action 修改 :DevTools 会记录下 Action 的名称和参数,你可以清晰地看到"用户点击了支付按钮 -> 触发了
checkoutAction -> 状态变更"
-
-
2. 推荐在 actions 中集中修改
优点:
-
逻辑集中
-
方便调试
-
易于维护
八、Pinia 数据持久化
-
介绍:
-
Pinia 数据持久化是指将 Store 中的数据自动保存到浏览器的本地存储
-
localStorage -
sessionStorage
-
-
这样即使用户刷新页面或关闭浏览器后再次打开,数据依然能够恢复
-
对于记住用户的登录状态、购物车内容、主题偏好等场景非常有用
-
-
Pinia 官方并没有内置持久化插件,但社区提供了一个非常流行且好用的官方推荐插件
pinia-plugin-persistedstate
-
pinia-plugin-persistedstate的使用方法:- 安装插件:首先,我们需要在我们的项目里面安装
pinia-plugin-persistedstate插件
powershellnpm install pinia-plugin-persistedstate # 或者 yarn add pinia-plugin-persistedstate # 或者 pnpm add pinia-plugin-persistedstate-
在 Pinia 实例中注册插件
javascript// main.js 或 store/index.js import { createApp } from 'vue'; import { createPinia } from 'pinia'; import persistedstate from 'pinia-plugin-persistedstate'; // 导入插件 const app = createApp(App); const pinia = createPinia(); pinia.use(persistedstate); // 注册插件 app.use(pinia); app.mount('#app'); -
在 Store 定义中启用持久化
-
在使用
defineStore定义 Store 时,通过添加persist: true或一个配置对象来启用持久化javascript// 示例1:简单的配置 // stores/counter.js import { defineStore } from 'pinia'; export const useCounterStore = defineStore('counter', () => { // Store 配置项 // state、getter 和 action 都写这里面 , { // 在这里添加持久化配置 persist: true // 最简单的形式,会持久化整个 Store 的 state }); // ================================================================ // 示例 2:更详细的配置 export const useUserStore = defineStore('user', () => { // Store 配置项 }, { persist: { // key: 自定义存储的键名,默认为 storeId key: 'my_user_store_key', // storage: 选择存储方式,默认为 localStorage storage: localStorage, // 或 sessionStorage // paths: 指定需要持久化的 state 字段(数组) // 如果不设置,则默认持久化整个 state paths: ['profile', 'preferences'], // 只持久化 profile 和 preferences // 可选:自定义序列化函数 (serializer) serializer: { serialize: JSON.stringify, deserialize: JSON.parse, }, // 可选:自定义存储逻辑 (advanced) // 如需要对存储的数据进行加密/解密等操作 } }); -
配置项详解:
-
persist: true:- 最简单的配置,会将整个 Store 的
state(即return出来的ref和reactive对象)完整地保存到localStorage中。
- 最简单的配置,会将整个 Store 的
-
persist: { ... }:-
key(string): 指定在浏览器存储中使用的键名。如果不指定,默认使用defineStore时的第一个参数(即storeId)。 -
storage(Storage): 指定使用的 Web Storage 接口。可以是localStorage(数据永久保存,除非手动清除)或sessionStorage(数据仅在当前会话期间有效,关闭浏览器标签页/窗口后消失)。默认为localStorage。 -
paths(Array<string>): 一个字符串数组,用于指定哪些state属性需要被持久化。如果设置了paths,则只有列出的属性会被保存,其他属性会被忽略。这对于不想持久化临时数据或敏感信息(如 token)非常有用。 -
serializer(object): 允许你自定义数据的序列化和反序列化方式。默认使用JSON.stringify和JSON.parse。在某些特殊情况下可能需要自定义。
-
-
-
- 安装插件:首先,我们需要在我们的项目里面安装