
Pinia 是 Vue 官方推荐的状态管理库,用于跨组件、跨页面共享状态,其功能定位与 Vuex 一致,但具备更简洁的 API 设计、更好的 TypeScript 支持,且移除了 Vuex 中繁琐的 mutations 概念,大幅降低了学习与使用成本。作为 Vuex 的替代方案,Pinia 更轻量、灵活,是 Vue 3 项目状态管理的首选方案。
二、安装 Pinia
支持 yarn、npm、pnpm 三种包管理器安装,选择任意一种即可:
bash
yarn add pinia
# 或使用 npm
npm install pinia
# 或使用 pnpm
pnpm add pinia
安装完成后,重启项目使依赖生效。
三、Pinia 封装
3.1 封装目的
将 Pinia 实例创建、持久化插件引入等统一管理,避免在 main.js 中写入过多冗余代码,提升项目结构的可维护性(后续持久化插件的配置将在此基础上扩展)。
3.2 封装步骤
-
在
src目录下创建stores文件夹(约定俗成的状态管理目录),并在该文件夹下创建index.js文件; -
在
index.js中创建并导出 Pinia 实例:`// src/stores/index.js
import { createPinia } from "pinia"; // 引入 Pinia 核心方法
const pinia = createPinia(); // 创建 Pinia 实例
export default pinia; // 导出实例,供 main.js 注册`
- 在 main.js 中引入并使用 Pinia:
`// src/main.js
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import './assets/reset.css';
import ElementPlus from 'element-plus';
import pinia from './stores'; // 引入封装好的 Pinia 实例
// 注意:此处修正原文重复 use(router) 的错误
createApp(App)
.use(pinia) // 注册 Pinia
.use(router) // 注册路由
.use(ElementPlus) // 注册 ElementPlus
.mount('#app');`
四、定义 Store
4.1 核心规则
-
Store 通过
defineStore()函数定义,第一个参数为唯一名称(id),用于连接开发者工具,不可重复; -
约定将返回的 Store 函数命名为
useXxxStore格式(如 useUserInfoStore),符合 Vue 可组合项的命名规范; -
Store 文件建议放在
stores目录下,文件名见名知意(如用户相关状态用 user.js)。
4.2 具体实现(以用户状态为例)
-
在
stores目录下创建user.js文件; -
编写 Store 定义代码:
`// src/stores/user.js
import { defineStore } from "pinia";
// 定义并导出用户信息 Store
const useUserInfoStore = defineStore('userInfo', {
// state:存储核心状态数据,必须是函数(避免跨组件共享引用问题)
state: () => ({
username: '赫赫',
age: 30,
like: 'girl',
obj: { money: 100, friend: 10 },
hobby: [
{ id: 1, name: '篮球', level: 1 },
{ id: 2, name: 'rap', level: 10 }
]
})
});
export default useUserInfoStore;`
五、Store 三大核心概念
Pinia 的核心功能围绕 State、Getters、Actions 展开,分别对应「状态数据」「派生数据」「业务逻辑」三大核心场景。
5.1 State:核心状态数据
State 类似于组件中的 data,用于存储全局共享的原始数据。
5.1.1 访问 State 数据
Store 实例化后,可直接通过实例访问 State 数据,但需注意:不可直接解构 Store 实例 ,否则会丢失数据的响应式特性。若需解构,需使用 Pinia 提供的 storeToRefs() 方法(推荐)或 Vue 的 toRefs() 方法。
vue
<template>
<!-- 直接使用响应式数据 -->
<div>我叫 {{ username }},今年 {{ age }} 岁,喜欢 {{ like }}</div>
<div>爱好列表:</div>
<div v-for="item in hobby" :key="item.id">
{{ item.name }}(熟练度:{{ item.level }})
</div>
<button @click="editPiniaHandler">逐个修改数据</button>
<button @click="editAll">批量修改数据</button>
<button @click="replaceAll">替换整个 State</button>
<button @click="resetBtn">重置 State</button>
</template>
<script setup>
import { storeToRefs } from "pinia"; // 引入 Pinia 响应式解构工具
import useUserInfoStore from "@/stores/user"; // 引入用户 Store
// 实例化 Store(Store 会在首次调用时创建)
const userInfoStore = useUserInfoStore();
// 响应式解构 State 数据(推荐使用 storeToRefs)
const { username, age, like, hobby } = storeToRefs(userInfoStore);
// 错误示范:直接解构会丢失响应式
// const { username, age } = userInfoStore;
</script>
5.1.2 修改 State 数据
Pinia 支持 4 种修改 State 的方式,根据场景灵活选择:
-
直接修改 :适用于单个数据的简单修改
// 逐个修改数据 const editPiniaHandler = () => { userInfoStore.username += "嘎"; userInfoStore.age += 1; userInfoStore.like = "boy"; }; -
**KaTeX parse error: Expected '}', got 'EOF' at end of input: ... userInfoStore.patch({
username: "鸭蛋",
age: 21,
obj: { money: 200, friend: 15 } // 注意:对象会被完整替换
});
// 若需修改对象/数组中的部分属性,可使用函数形式
userInfoStore.$patch((state) => {
state.obj.money += 50;
state.hobby.push({ id: 3, name: '游泳', level: 5 });
});
};`
-
**KaTeX parse error: Expected '}', got 'EOF' at end of input: ... userInfoStore.state = {
username: '狗子',
age: 22,
like: 'boy',
obj: { money: 10, friend: 1 },
hobby: []
};
};`
-
**KaTeX parse error: Expected '}', got 'EOF' at end of input: ... userInfoStore.reset();
};`
5.2 Getters:派生状态数据
Getters 类似于组件中的 computed,用于基于 State 派生出新数据,具备缓存特性(依赖的 State 未变化时,多次访问会复用缓存结果)。
5.2.1 定义 Getters
在 defineStore 中与 state 同级定义,支持两种写法:接收 state 参数访问状态,或通过 this 访问其他 Getters。
javascript
// src/stores/user.js
import { defineStore } from "pinia";
const useUserInfoStore = defineStore('userInfo', {
state: () => ({
username: '赫赫',
age: 30,
like: 'girl',
obj: { money: 100, friend: 10 },
hobby: [
{ id: 1, name: '篮球', level: 1 },
{ id: 2, name: 'rap', level: 10 }
]
}),
// 定义 Getters
getters: {
// 基础用法:通过 state 参数访问状态
doubleAge: (state) => state.age * 2,
// 进阶用法:通过 this 访问其他 Getters(注意:不可使用箭头函数,否则 this 指向异常)
addOneAge() {
return this.doubleAge + 1;
},
// 传参用法:返回函数实现动态传参(此时缓存失效,每次调用都会重新计算)
getHobbyById: (state) => (id) => {
return state.hobby.find(item => item.id === id) || '未找到该爱好';
},
// 复杂派生:计算总资产(假设 money 为现金,friend 为好友数量,此处仅为示例)
totalAssets() {
return this.obj.money + this.obj.friend * 10;
}
}
});
export default useUserInfoStore;
5.2.2 使用 Getters
直接通过 Store 实例访问 Getters,传参时直接调用函数并传入参数:
vue
<template>
<div>年龄翻倍:{{ userInfoStore.doubleAge }}</div>
<div>年龄翻倍+1:{{ userInfoStore.addOneAge }}</div>
<div>ID=1 的爱好:{{ userInfoStore.getHobbyById(1) }}</div>
<div>总资产:{{ userInfoStore.totalAssets }}</div>
</template>
<script setup>
import useUserInfoStore from "@/stores/user";
const userInfoStore = useUserInfoStore();
</script>
5.3 Actions:业务逻辑封装
Actions 类似于组件中的 methods,用于封装业务逻辑,支持同步和异步操作,是修改 State 的推荐方式(集中管理逻辑,便于维护)。与 Vuex 不同,Pinia 的 Actions 可直接修改 State,无需通过 mutations。
5.3.1 定义 Actions
在 defineStore 中与 state、getters 同级定义,可通过 this 访问 Store 实例的 state、getters 和其他 actions。
javascript
// src/stores/user.js
import { defineStore } from "pinia";
const useUserInfoStore = defineStore('userInfo', {
state: () => ({ /* 省略 state 定义 */ }),
getters: { /* 省略 getters 定义 */ },
// 定义 Actions
actions: {
// 同步 Action:接收外部参数
addAge(step) {
console.log('接收的步长参数:', step);
this.age += step; // 直接修改 State
this.updateAssets(step); // 调用其他 Action
},
// 辅助 Action:更新资产(内部调用,不对外暴露)
updateAssets(step) {
this.obj.money += step * 10;
},
// 异步 Action:模拟 API 请求
async fetchUserInfo() {
try {
// 模拟网络请求(2 秒延迟)
const res = await new Promise((resolve) => {
setTimeout(() => {
resolve({
username: '小明',
age: 25,
like: 'coding'
});
}, 2000);
});
// 更新 State
this.$patch(res);
return res; // 向外返回结果
} catch (error) {
console.error('获取用户信息失败:', error);
throw error; // 抛出错误,供组件捕获
}
},
// 复杂异步场景:调用其他异步 Action
async loadUserAndHobby() {
const userInfo = await this.fetchUserInfo();
// 模拟加载爱好列表
const hobbies = await this.fetchHobbies(userInfo.username);
this.hobby = hobbies;
},
// 模拟加载爱好列表
fetchHobbies(username) {
return new Promise((resolve) => {
setTimeout(() => {
resolve([
{ id: 1, name: '编程', level: 8 },
{ id: 2, name: '健身', level: 6 }
]);
}, 1000);
});
}
}
});
export default useUserInfoStore;
5.3.2 使用 Actions
直接通过 Store 实例调用 Actions,同步 Action 直接调用,异步 Action 可使用 await 捕获结果或处理错误:
javascript
<script setup>
import useUserInfoStore from "@/stores/user";
const userInfoStore = useUserInfoStore();
// 调用同步 Action
const add = () => {
userInfoStore.addAge(5);
};
// 调用异步 Action
const loadUser = async () => {
try {
const res = await userInfoStore.fetchUserInfo();
console.log('获取用户信息成功:', res);
} catch (error) {
console.error('获取失败:', error);
}
};
// 调用依赖其他异步 Action 的方法
const loadAll = async () => {
await userInfoStore.loadUserAndHobby();
console.log('用户信息和爱好加载完成');
};
</script>
六、Pinia 持久化存储
默认情况下,Pinia 的 State 存储在内存中,页面刷新后会丢失。通过 pinia-plugin-persistedstate 插件可实现 State 持久化,支持 localStorage、sessionStorage 等存储方式。
6.1 安装持久化插件
bash
npm install pinia-plugin-persistedstate
# 或 yarn
yarn add pinia-plugin-persistedstate
# 或 pnpm
pnpm add pinia-plugin-persistedstate
6.2 注册插件
在之前封装的src/stores/index.js 中引入并注册插件:
javascript
// src/stores/index.js
import { createPinia } from "pinia";
// 引入持久化插件
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
const pinia = createPinia();
// 注册插件
pinia.use(piniaPluginPersistedstate);
export default pinia;
6.3 持久化配置
在定义 Store 时,通过 persist 选项配置持久化规则,支持基础配置和高级配置两种方式。
6.3.1 基础配置(默认规则)
将 persist 设为 true,使用默认配置:
-
存储方式:localStorage
-
存储键名:Store 的 id(如 userInfo)
-
序列化/反序列化:JSON.stringify / JSON.parse
-
持久化范围:整个 State
javascript
// src/stores/user.js
import { defineStore } from "pinia";
const useUserInfoStore = defineStore('userInfo', {
state: () => ({
username: '赫赫',
age: 30,
like: 'girl'
}),
getters: { /* 省略 */ },
actions: { /* 省略 */ },
// 开启持久化(默认配置)
persist: true
});
export default useUserInfoStore;
6.3.2 高级配置(自定义规则)
通过对象形式配置 persist,支持自定义存储键名、存储方式、持久化字段等,核心配置项如下:
-
key:自定义存储键名(默认是 Store 的 id); -
storage:指定存储方式(localStorage / sessionStorage); -
paths:指定需要持久化的字段(数组形式,不指定则全量持久化); -
serializer:自定义序列化/反序列化方法(默认 JSON 处理)。
javascript
// src/stores/user.js
import { defineStore } from "pinia";
const useUserInfoStore = defineStore('userInfo', {
state: () => ({
username: '赫赫',
age: 30,
like: 'girl',
obj: { money: 100, friend: 10 },
hobby: []
}),
getters: { /* 省略 */ },
actions: { /* 省略 */ },
// 高级持久化配置
persist: {
key: 'user_info_storage', // 自定义存储键名
storage: sessionStorage, // 存储方式改为 sessionStorage(页面关闭后丢失)
paths: ['username', 'age', 'obj.money'], // 仅持久化 username、age、obj.money 字段
// 自定义序列化/反序列化(可选)
serializer: {
serialize: (value) => JSON.stringify(value),
deserialize: (value) => JSON.parse(value)
}
}
});
export default useUserInfoStore;
6.3.3 多 Store 独立配置
每个 Store 可单独配置持久化规则,互不影响。例如,用户信息用 localStorage 持久化,购物车用 sessionStorage 持久化:
javascript
// 购物车 Store(src/stores/cart.js)
import { defineStore } from "pinia";
const useCartStore = defineStore('cart', {
state: () => ({
items: [],
total: 0
}),
persist: {
key: 'cart_storage',
storage: sessionStorage, // 购物车仅在当前会话有效
paths: ['items'] // 仅持久化商品列表
}
});
export default useCartStore;
七、Pinia 两种定义方式对比
Pinia 支持两种 Store 定义方式,分别对应 Vue 的 Options API 和 Composition API 范式,可根据项目需求选择:
7.1 两种定义方式示例
javascript
// 1. Options API 风格(本文默认方式,结构化清晰,适合 Vuex 迁移)
const useUserStore = defineStore('user', {
state: () => ({ username: '' }),
getters: { fullName: (state) => `Mr. ${state.username}` },
actions: { setUsername(name) { this.username = name } }
});
// 2. Composition API 风格(灵活度高,类型推断更好,适合复杂逻辑复用)
import { ref, computed } from 'vue';
import { defineStore } from 'pinia';
const useUserStore = defineStore('user', () => {
// 状态:对应 state
const username = ref('');
// 计算属性:对应 getters
const fullName = computed(() => `Mr. ${username.value}`);
// 方法:对应 actions
const setUsername = (name) => {
username.value = name;
};
// 必须返回需要暴露的属性和方法
return { username, fullName, setUsername };
});
7.2 核心区别对比
| 对比维度 | Options API 风格 | Composition API 风格 |
|---|---|---|
| 状态定义 | state 函数返回对象 | 使用 ref/reactive 定义 |
| 计算属性 | getters 对象定义 | 使用 computed 函数定义 |
| 方法定义 | actions 对象定义 | 直接定义函数 |
| 类型推断 | 一般 | 优秀 |
| 逻辑复用 | 较困难 | 容易(可抽离独立函数) |
| 适用场景 | 简单状态、Vuex 迁移项目 | 复杂逻辑、TypeScript 项目 |
八、常见问题与最佳实践
8.1 常见问题
-
问题 1:解构 Store 后数据丢失响应式?
解答:使用
storeToRefs()替代直接解构,该方法会为 State 数据创建响应式引用。 -
问题 2:页面刷新后 State 丢失?
解答:开启持久化存储,通过
pinia-plugin-persistedstate插件配置 localStorage 存储。 -
问题 3:Actions 中 this 指向异常?
解答:Actions 中的方法不可使用箭头函数,否则 this 无法指向 Store 实例。
-
问题 4:多个组件调用同一 Action 会重复请求?
解答:在 Action 中添加缓存逻辑,例如通过标志位判断请求是否正在进行,避免重复调用。
8.2 最佳实践
-
Store 拆分原则:按业务模块拆分(如 user.js、cart.js、setting.js),避免单个 Store 过于庞大。
-
命名规范 :Store 文件名、id 均使用小写+下划线命名(如 user_info),Store 函数名遵循
useXxxStore格式。 -
异步请求位置:需要跨组件共享的数据,异步请求放在 Actions 中;仅组件内部使用的请求,直接在组件内处理。
-
状态修改规范:复杂逻辑的状态修改统一放在 Actions 中,便于维护和调试;简单修改可直接操作 State。
-
开发工具使用:Pinia 内置支持 Vue DevTools,可通过开发者工具查看 State 变化、回溯 Actions 调用记录,提升调试效率。
九、参考资料
-
Pinia 官方文档:https://pinia.vuejs.org/
-
pinia-plugin-persistedstate 官方文档:https://prazdevs.github.io/pinia-plugin-persistedstate/