
前言
在 Vue 项目开发中,状态管理是核心环节 ------ 小到用户登录状态、购物车数据,大到全局权限配置、业务数据缓存,都需要一套清晰、高效的状态管理方案。相较于 Vuex 的繁琐(如 Module 嵌套、Mutation 同步限制、TS 支持差),Pinia 作为 Vue 官方推荐的新一代状态管理库,凭借无 Mutation、天然支持 TS、极简 API、多 Store 扁平化管理等优势,已成为中大型 Vue 项目的首选。
本文从Pinia 基础配置→核心特性拆解→多 Store 管理→持久化存储四大核心模块,结合电商、后台管理系统等真实业务场景,手把手教你吃透 Pinia 的全流程用法。内容兼顾 Vue2/Vue3 适配、实战避坑和性能优化,无论是刚从 Vuex 迁移的开发者,还是零基础入门 Pinia 的新手,都能快速上手并落地到项目中。建议收藏本文,遇到状态管理问题时直接对照实操!
1. Pinia 基础:从安装到状态的访问与修改
Pinia 的核心设计理念是扁平化、极简、类型友好,入门门槛远低于 Vuex,先从最基础的安装、Store 创建和状态操作开始拆解。
1.1 安装与环境适配(Vue2/Vue3 区别)
Pinia 对 Vue2 和 Vue3 提供完整支持,但安装和初始化方式略有差异,需注意版本匹配:
-
Vue3 + Vite 项目(推荐) :
bash# 安装核心库 npm install pinia --save # 或yarn/pnpm yarn add pinia pnpm add pinia -
Vue2 项目 :需额外安装适配桥接包
bash# 核心库 + Vue2适配包 npm install pinia @vue/composition-api --save
初始化 Pinia(挂载到 Vue 实例)
Vue3 写法(src/main.js)
javascript
import { createApp } from 'vue'
import App from './App.vue'
// 导入Pinia核心方法
import { createPinia } from 'pinia'
const app = createApp(App)
// 创建Pinia实例并挂载
const pinia = createPinia()
app.use(pinia)
app.mount('#app')
Vue2 写法(src/main.js)
javascript
import Vue from 'vue'
import App from './App.vue'
import { createPinia, PiniaVuePlugin } from 'pinia'
// 安装Pinia插件
Vue.use(PiniaVuePlugin)
// 创建并挂载Pinia实例
const pinia = createPinia()
new Vue({
el: '#app',
pinia, // 挂载到Vue实例
render: h => h(App)
})
避坑提示:Vue2 项目若报错Cannot find module '@vue/composition-api',需先安装
@vue/composition-api并在 main.js 中提前导入:import '@vue/composition-api'。
1.2 Store 定义:defineStore 核心用法
Store 是 Pinia 的核心单元,每个 Store 对应一个独立的状态模块,通过defineStore创建,核心参数为唯一 ID和配置对象 / 函数。
基础示例:创建用户 Store(src/store/user.js)
javascript
import { defineStore } from 'pinia'
// ❶ 定义并导出Store(唯一ID:userStore,不可重复)
export const useUserStore = defineStore('userStore', {
// ❷ state:存储核心状态(类似Vue的data)
state: () => ({
userId: '',
username: '',
token: '',
isLogin: false,
permissions: [] // 用户权限列表
}),
// ❸ getters:计算状态(类似Vue的computed,后续详解)
getters: {},
// ❹ actions:修改状态的方法(支持同步/异步,后续详解)
actions: {}
})
Store 定义的两种风格
-
选项式 API(如上):贴近 Vuex 写法,适合 Vue2 开发者快速迁移;
-
组合式 API :更灵活,适合 Vue3 组合式开发风格:
javascriptexport const useUserStore = defineStore('userStore', () => { // 替代state:直接定义响应式变量 const userId = ref('') const username = ref('') const isLogin = ref(false) // 替代getters:计算属性 const hasAdminPerm = computed(() => permissions.value.includes('admin')) // 替代actions:普通函数(支持同步/异步) const login = (userInfo) => { userId.value = userInfo.id username.value = userInfo.name isLogin.value = true } // 必须返回需要暴露的状态和方法 return { userId, username, isLogin, hasAdminPerm, login } })
核心原则:一个业务模块对应一个 Store,Store ID 必须全局唯一(否则会导致状态冲突)。
1.3 状态访问与修改:三种方式对比
创建 Store 后,需在组件中访问和修改状态,Pinia 提供三种常用方式,各有适用场景:
方式 1:直接访问 / 修改(最简单,适合简单场景)
html
<template>
<div>
<div>用户名:{{ userStore.username }}</div>
<button @click="login">模拟登录</button>
</div>
</template>
<script setup>
// ❶ 导入并实例化Store(组合式API)
import { useUserStore } from '@/store/user'
const userStore = useUserStore()
// ❷ 直接修改状态(Pinia允许直接修改,无需Mutation)
const login = () => {
userStore.username = '张三'
userStore.isLogin = true
userStore.token = 'xxx-xxx-xxx'
}
</script>
<!-- Vue2选项式API写法 -->
<script>
import { useUserStore } from '@/store/user'
export default {
methods: {
login() {
const userStore = useUserStore()
userStore.username = '张三'
}
}
}
</script>
方式 2:$patch 批量修改(推荐,减少响应式触发次数)
当需要修改多个状态时,$patch更高效(批量更新,仅触发一次响应式更新),支持对象式和函数式两种写法:
javascript
// ❶ 对象式:适合简单批量修改
userStore.$patch({
username: '李四',
isLogin: true,
permissions: ['user:view', 'user:edit']
})
// ❷ 函数式:适合复杂修改(如数组操作、条件修改)
userStore.$patch((state) => {
state.permissions.push('order:view')
if (state.username === '李四') {
state.isLogin = true
}
})
方式 3:Actions 修改(推荐,复杂业务逻辑)
将状态修改逻辑封装到 Store 的actions中,便于复用和维护:
javascript
// 1. 在Store中定义actions
export const useUserStore = defineStore('userStore', {
state: () => ({ /* ... */ }),
actions: {
// 封装登录逻辑
loginAction(userInfo) {
// 可执行复杂逻辑:请求接口、数据处理、状态修改
this.userId = userInfo.id
this.username = userInfo.name
this.token = userInfo.token
this.isLogin = true
// 支持异步操作(后续详解)
// return axios.post('/api/login', userInfo)
},
// 封装退出登录逻辑
logoutAction() {
this.$reset() // 重置状态到初始值(核心API)
}
}
})
// 2. 组件中调用actions
const login = () => {
userStore.loginAction({
id: '1001',
name: '张三',
token: 'xxx-xxx-xxx'
})
}
状态操作方式对比图解

2. Pinia 核心特性:Getters/Actions 与 Mutation 移除原因
Pinia 的核心优势集中在 Getters 和 Actions 的设计上,同时彻底移除了 Vuex 的 Mutation,这一节拆解核心特性并分析背后的设计逻辑。
2.1 Getters:计算状态的定义与进阶用法
Getters 对应 Vue 的computed,用于基于 State 派生新状态,支持缓存、传参、跨 Getters 调用,是实现状态复用的核心。
2.1.1 基础定义与访问
javascript
// Store中定义Getters
export const useUserStore = defineStore('userStore', {
state: () => ({
userId: '1001',
username: '张三',
permissions: ['user:view', 'user:edit', 'admin']
}),
getters: {
// ❶ 基础用法:基于state派生状态
isAdmin: (state) => {
return state.permissions.includes('admin')
},
// ❷ 访问其他Getters:通过this(需指定返回值类型,TS友好)
userInfo: (state) => ({
id: state.userId,
name: state.username,
isAdmin: this.isAdmin // 调用当前Store的其他Getters
})
}
})
// 组件中访问Getters(与访问State一致)
<template>
<div>是否管理员:{{ userStore.isAdmin }}</div>
<div>用户信息:{{ userStore.userInfo }}</div>
</template>
2.1.2 进阶用法:Getters 传参
Getters 本身是计算属性,默认不能传参,但可通过返回函数实现传参(注意:此时缓存失效,每次调用都会重新计算):
javascript
getters: {
// 传参:判断用户是否有指定权限
hasPermission: (state) => {
return (perm) => state.permissions.includes(perm)
}
}
// 组件中调用
<template>
<div>是否有编辑权限:{{ userStore.hasPermission('user:edit') }}</div>
<div>是否有订单权限:{{ userStore.hasPermission('order:view') }}</div>
</template>
2.2 Actions:同步 / 异步操作的统一处理
Pinia 的 Actions 替代了 Vuex 的 Mutation 和 Action,支持同步、异步操作,无需区分,写法更统一,是处理业务逻辑 + 状态修改的核心。
2.2.1 同步 Actions(替代 Vuex 的 Mutation)
javascript
actions: {
// 同步修改状态
updateUsername(newName) {
this.username = newName // 直接修改state
},
// 批量修改
resetPermissions() {
this.permissions = ['user:view']
}
}
2.2.2 异步 Actions(替代 Vuex 的 Action)
支持 async/await,可直接在 Actions 中调用接口,处理异步逻辑:
javascript
import axios from 'axios'
actions: {
// 异步登录:调用接口+修改状态
async loginAsync(userForm) {
try {
// 调用登录接口
const res = await axios.post('/api/login', userForm)
const { id, name, token } = res.data
// 修改状态
this.userId = id
this.username = name
this.token = token
this.isLogin = true
return res.data // 返回结果,组件可接收
} catch (err) {
console.error('登录失败:', err)
throw err // 抛出错误,组件可捕获
}
}
}
// 组件中调用异步Actions
const handleLogin = async () => {
try {
await userStore.loginAsync({ username: '张三', password: '123456' })
ElMessage.success('登录成功')
} catch (err) {
ElMessage.error('登录失败')
}
}
2.3 为什么 Pinia 移除了 Mutations?(深度解析)
这是 Pinia 与 Vuex 最核心的区别,也是 Pinia 更易用的关键,背后有三层核心原因:
- 简化开发流程:Vuex 要求同步修改用 Mutation、异步用 Action,增加了心智负担;Pinia 的 Actions 支持同步 / 异步,无需区分,代码量减少 50%;
- TypeScript 友好:Mutation 的类型定义繁琐,而 Pinia 的 Actions 天然支持 TS 类型推导,无需额外配置;
- 性能与灵活性平衡 :Vuex 设计 Mutation 的初衷是追踪状态变更,但 Pinia 通过
$patch和 Actions 封装,既保留了状态修改的可追溯性,又避免了冗余的 Mutation 定义; - 符合组合式 API 设计理念:Pinia 的设计贴合 Vue3 组合式 API,强调简洁、灵活、低侵入,Mutation 的存在与这一理念冲突。
实战结论:无需纠结是否违反单向数据流,Pinia 的 Actions 封装 +
$patch批量更新,既保证了代码的可维护性,又兼顾了开发效率。
3. 多 Store 管理:模块划分与跨 Store 通信
Pinia 采用扁平化多 Store设计,没有 Vuex 的 Module 嵌套,而是通过按业务拆分独立 Store实现模块化,更清晰、易维护。
3.1 按业务拆分 Store(最佳实践)
中大型项目建议按业务域拆分 Store,避免单个 Store 臃肿,示例结构:
bash
src/store/
├── index.js # 统一导出所有Store(可选)
├── user.js # 用户状态(登录、权限、个人信息)
├── cart.js # 购物车状态(商品列表、数量、总价)
├── permission.js # 权限状态(菜单、按钮权限)
└── app.js # 应用状态(主题、语言、布局)
示例:购物车 Store(src/store/cart.js)
javascript
import { defineStore } from 'pinia'
export const useCartStore = defineStore('cartStore', {
state: () => ({
items: [], // 购物车商品列表:[{ id, name, price, count }]
totalPrice: 0 // 总价
}),
getters: {
// 计算总价(实时更新)
calcTotalPrice: (state) => {
return state.items.reduce((sum, item) => sum + item.price * item.count, 0)
},
// 商品总数
itemCount: (state) => state.items.length
},
actions: {
// 添加商品到购物车
addItem(product) {
const existItem = this.items.find(item => item.id === product.id)
if (existItem) {
existItem.count += 1
} else {
this.items.push({ ...product, count: 1 })
}
// 同步总价(也可通过getters实时计算,无需手动维护)
this.totalPrice = this.calcTotalPrice
},
// 清空购物车
clearCart() {
this.items = []
this.totalPrice = 0
}
}
})
3.2 Store 间通信:跨 Store 调用与数据共享
Pinia 的 Store 间通信无需像 Vuex 那样通过rootState或namespaced,直接导入并调用即可,支持两种场景:
场景 1:组件中同时使用多个 Store
html
<template>
<div>
<div>当前用户:{{ userStore.username }}</div>
<div>购物车商品数:{{ cartStore.itemCount }}</div>
<button @click="addToCart">添加商品到购物车</button>
</div>
</template>
<script setup>
// 导入多个Store
import { useUserStore } from '@/store/user'
import { useCartStore } from '@/store/cart'
const userStore = useUserStore()
const cartStore = useCartStore()
// 调用多个Store的方法
const addToCart = () => {
// 仅登录用户可添加购物车(依赖userStore的状态)
if (userStore.isLogin) {
cartStore.addItem({ id: '2001', name: 'Vue实战教程', price: 99 })
} else {
ElMessage.warning('请先登录')
}
}
</script>
场景 2:Store 内部调用其他 Store(深度耦合场景)
javascript
// 在cartStore中调用userStore
import { useUserStore } from './user'
export const useCartStore = defineStore('cartStore', {
actions: {
addItem(product) {
// ❶ 获取userStore实例
const userStore = useUserStore()
// ❷ 校验用户状态
if (!userStore.isLogin) {
throw new Error('未登录,无法添加购物车')
}
// ❸ 校验用户权限
if (!userStore.hasPermission('cart:add')) {
throw new Error('无添加购物车权限')
}
// ❹ 执行添加逻辑
const existItem = this.items.find(item => item.id === product.id)
if (existItem) existItem.count += 1
else this.items.push({ ...product, count: 1 })
}
}
})
最佳实践:尽量在组件中协调多个 Store(低耦合),仅在核心业务逻辑中才在 Store 内部调用其他 Store。
4. Pinia 持久化存储:pinia-plugin-persistedstate 实战
Pinia 的状态默认存储在内存中,页面刷新后会丢失,通过pinia-plugin-persistedstate插件可实现状态持久化(存储到 localStorage/sessionStorage),是项目必备功能。
4.1 插件安装与基础配置
步骤 1:安装插件
bash
npm install pinia-plugin-persistedstate --save
# 或
yarn add pinia-plugin-persistedstate
步骤 2:挂载插件到 Pinia
javascript
// src/main.js(Vue3示例)
import { createApp } from 'vue'
import { createPinia } from 'pinia'
// 导入持久化插件
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const app = createApp(App)
const pinia = createPinia()
// 挂载插件
pinia.use(piniaPluginPersistedstate)
app.use(pinia)
app.mount('#app')
步骤 3:开启 Store 持久化
在 Store 中添加persist: true,即可实现全量状态持久化:
javascript
export const useUserStore = defineStore('userStore', {
state: () => ({
userId: '',
username: '',
token: '',
isLogin: false
}),
// 开启持久化
persist: true
})
4.2 进阶用法:指定字段持久化 / 自定义存储方式
默认配置会将整个 Store 存储到 localStorage,key 为 Store ID,实际项目中常需自定义配置:
javascript
export const useUserStore = defineStore('userStore', {
state: () => ({ /* ... */ }),
// 自定义持久化配置
persist: {
// ❶ 自定义存储key(默认是Store ID:userStore)
key: 'vue3-user-state',
// ❷ 指定存储位置:localStorage(默认)/ sessionStorage
storage: sessionStorage,
// ❸ 仅持久化指定字段(白名单)
paths: ['userId', 'token', 'isLogin'],
// ❹ 持久化前修改数据(可选)
serialize: (state) => JSON.stringify({ ...state, token: state.token + '_encrypt' }),
// ❺ 恢复数据前处理(可选)
deserialize: (value) => {
const state = JSON.parse(value)
return { ...state, token: state.token.replace('_encrypt', '') }
}
}
})
多 Store 不同持久化配置示例
javascript
// userStore:存储到localStorage,仅持久化token和isLogin
export const useUserStore = defineStore('userStore', {
persist: {
key: 'user-info',
paths: ['token', 'isLogin']
}
})
// cartStore:存储到sessionStorage,全量持久化
export const useCartStore = defineStore('cartStore', {
persist: {
key: 'cart-data',
storage: sessionStorage
}
})
4.3 持久化避坑:异步数据与存储加密
坑点 1:异步数据无法持久化
问题:Actions 中异步获取的状态(如登录接口返回的 token),刷新后未持久化?原因:插件仅持久化 State 的初始值和同步修改的值,异步修改需确保状态已更新。解决方案:异步操作完成后,通过$patch或 Actions 修改状态(插件会自动监听)。
坑点 2:敏感数据(如 token)明文存储
问题:localStorage 明文存储 token 有安全风险?解决方案:持久化前加密,恢复后解密(如上述serialize/deserialize示例),或使用加密库(如 crypto-js):
javascript
import CryptoJS from 'crypto-js'
const SECRET_KEY = 'your-secret-key' // 项目中需从环境变量读取
persist: {
serialize: (state) => {
// 加密token
const encryptToken = CryptoJS.AES.encrypt(state.token, SECRET_KEY).toString()
return JSON.stringify({ ...state, token: encryptToken })
},
deserialize: (value) => {
const state = JSON.parse(value)
// 解密token
const decryptToken = CryptoJS.AES.decrypt(state.token, SECRET_KEY).toString(CryptoJS.enc.Utf8)
return { ...state, token: decryptToken }
}
}
持久化流程图解

5. 实战避坑指南:高频问题与解决方案
5.1 Store 实例重复创建导致状态异常?
-
问题:组件中多次调用
useUserStore()创建实例,导致状态修改失效; -
原因:Pinia 的
useXxxStore是单例模式,多次调用返回同一个实例,但重复解构会丢失响应式; -
解决方案:
javascript// 错误:解构丢失响应式 const { username, isLogin } = useUserStore() // 正确:直接使用Store实例,或用storeToRefs解构 import { storeToRefs } from 'pinia' const userStore = useUserStore() const { username, isLogin } = storeToRefs(userStore)
5.2 持久化配置不生效?
- 常见原因:
- 插件未挂载到 Pinia 实例;
paths配置的字段名错误(如path: ['token']写成path: ['Token']);- 存储位置无权限(如隐身模式下 localStorage 不可用);
- 解决方案:检查插件挂载流程,核对字段名,添加存储异常捕获。
5.3 Vue2 中使用 Pinia 报错?
- 核心原因:Vue2 未安装
@vue/composition-api,或版本不兼容; - 解决方案:
- 安装
@vue/composition-api:npm install @vue/composition-api --save; - 在 main.js 最顶部导入:
import '@vue/composition-api'; - 确保 Pinia 版本≤2.1.7(高版本对 Vue2 支持差)。
- 安装
5.4 跨 Store 调用导致死循环?
- 问题:A Store 调用 B Store,B Store 又调用 A Store;
- 解决方案:拆解核心逻辑到独立工具函数,避免 Store 间循环依赖。
6. 总结与进阶学习方向
本文从基础到进阶,拆解了 Pinia 的核心用法:从 Store 的创建、状态操作,到 Getters/Actions 的特性,再到多 Store 管理和持久化存储,覆盖了 90% 的项目实战场景。相较于 Vuex,Pinia 的优势在于极简 API、TS 友好、扁平化管理、无冗余概念,是 Vue3 时代状态管理的最优解。
核心要点回顾
- Store 创建:通过
defineStore定义,ID 全局唯一,支持选项式 / 组合式两种风格; - 状态修改:简单场景直接修改,批量修改用
$patch,复杂逻辑用 Actions 封装; - 多 Store 管理:按业务拆分,跨 Store 通信优先在组件中协调,降低耦合;
- 持久化存储:使用
pinia-plugin-persistedstate,自定义paths和存储方式,敏感数据加密。
进阶学习方向
- Pinia 与 Vue DevTools 集成:调试状态变更流程;
- Pinia 中间件:自定义插件实现状态日志、权限校验;
- Pinia 与 SSR 结合:服务端渲染场景下的状态管理;
- 状态设计模式:统一 Store 命名规范、状态拆分原则。
如果你在使用 Pinia 时遇到了其他问题,或者有更好的实战技巧,欢迎在评论区留言交流!如果本文对你有帮助,别忘了点赞 + 收藏 + 关注,后续会更新更多 Vue 实战干货
你在从 Vuex 迁移到 Pinia 的过程中,遇到过哪些印象深刻的问题?是怎么解决的?评论区聊聊你的经历,让更多开发者少走弯路!
