在 Vue3 生态中,Pinia 已经成为官方推荐的状态管理库,彻底替代了 Vue2 时代的 Vuex。相比于 Vuex,Pinia 移除了 mutations、modules 等繁琐概念,简化了语法,同时完美适配 Vue3 组合式 API,支持 TypeScript 类型推断,上手成本极低,成为中小型项目乃至大型项目的首选状态管理方案。
本文将从新手视角出发,用「通俗讲解 + 可直接复制的实战代码」,覆盖 Pinia 从环境搭建、基础使用(定义Store、存取状态、修改状态),到进阶技巧(Getters、Actions、持久化),再到实战场景的全流程,同时兼容非TS和TS两种写法,新手看完就能上手,老手也能查漏补缺,彻底掌握 Pinia 的核心用法。
一、为什么选择 Pinia?(对比Vuex的优势)
在学习 Pinia 之前,先搞清楚一个问题:为什么 Vue3 官方推荐用 Pinia 替代 Vuex?核心优势总结为 5 点,看完你就明白它有多香:
- 语法更简洁:移除了 Vuex 中繁琐的 mutations(提交修改)、modules(模块)嵌套,直接用 Actions 修改状态,代码量减少 30%+;
- 完美适配组合式 API:和 Vue3
- 原生支持 TypeScript:自带类型推断,无需手动编写大量类型声明,TS 开发体验拉满(非TS项目也能正常使用);
- 无需手动注册:创建 Store 后可直接在组件中使用,无需像 Vuex 那样在 main.js 中注册 Store 实例;
- 轻量无依赖:体积极小(仅 1KB 左右),无需引入额外依赖,不增加项目负担。
一句话总结:Pinia 就是 Vue3 时代「更简单、更高效、更友好」的 Vuex 替代方案,无论你是新手还是老手,都能快速上手。
二、环境搭建(Vue3 + Pinia,非TS/TS通用)
首先我们完成 Pinia 的环境搭建,分为「新项目初始化」和「已有项目集成」两种场景,步骤简单,全程复制命令即可。
1. 前提条件
确保你的项目是 Vue3 项目(Vue2 不支持 Pinia),如果是 Vue2 项目,需先升级到 Vue3,或继续使用 Vuex。
2. 安装 Pinia
打开终端,进入项目根目录,执行以下命令(npm 和 yarn 二选一):
# npm 安装(推荐)
npm install pinia
# yarn 安装
yarn add pinia
3. 初始化 Pinia(main.js 配置)
安装完成后,需要在 main.js 中创建 Pinia 实例,并挂载到 Vue 应用上,这一步是全局唯一的。
非TS版(Vue3)
// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia' // 引入 createPinia
import App from './App.vue'
const app = createApp(App)
const pinia = createPinia() // 创建 Pinia 实例
app.use(pinia) // 挂载 Pinia 到 Vue 应用
app.mount('#app')
TS版(Vue3 + TS )
// main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const app = createApp(App)
app.use(createPinia()) // 可简化写法,无需单独定义 pinia 变量
app.mount('#app')
至此,Pinia 环境搭建完成,接下来就可以创建 Store,开始状态管理了。
三、Pinia 核心基础:定义Store(最关键一步)
在 Pinia 中,Store 是核心,可以理解为「一个全局的响应式数据容器」,用于存储和管理全局共享的状态(比如用户信息、购物车数据、主题配置等)。
每个 Store 都是独立的,互不干扰,我们可以根据业务需求,创建多个 Store(比如用户 Store、购物车 Store、设置 Store),实现状态的模块化管理(无需像 Vuex 那样嵌套 modules)。
1. Store 创建规范
推荐在项目根目录下创建 stores 文件夹(注意是复数),用于存放所有 Store 文件,每个 Store 单独创建一个 js/ts 文件,命名规范:xxxStore.js/ts(比如 userStore.js、cartStore.ts)。
2. 基础 Store 定义(非TS版,新手首选)
以「用户 Store」为例,创建 stores/userStore.js,代码如下(核心包含 3 部分:state、getters、actions):
// stores/userStore.js
import { defineStore } from 'pinia' // 引入 Pinia 内置的 defineStore
// 1. 定义并导出 Store(参数1:Store唯一标识,参数2:Store配置对象)
// 唯一标识(id):整个应用中唯一,不能重复,建议和文件名对应
export const useUserStore = defineStore('user', {
// 2. state:存储全局状态(类似组件中的 data),返回一个对象
state: () => ({
username: '游客', // 初始用户名
token: '', // 用户token
isLogin: false, // 登录状态
userInfo: {} // 用户详细信息(对象类型)
}),
// 3. getters:处理状态(类似组件中的 computed),用于对 state 进行计算或过滤
getters: {
// 示例1:简单计算(获取用户名的大写形式)
upperUsername: (state) => {
return state.username.toUpperCase()
},
// 示例2:依赖其他 getters(判断是否是管理员,假设 userInfo 中有 role 字段)
isAdmin: (state, getters) => {
// getters 可以访问当前 Store 中的其他 getters
return state.userInfo.role === 'admin' && getters.upperUsername
}
},
// 4. actions:修改状态(类似组件中的 methods),支持同步和异步
actions: {
// 示例1:同步修改状态(登录,修改用户信息)
login(data) {
// 直接修改 state 中的数据(无需像 Vuex 那样提交 mutation)
this.username = data.username
this.token = data.token
this.isLogin = true
this.userInfo = data.userInfo
},
// 示例2:异步修改状态(退出登录,模拟接口请求)
async logout() {
// 模拟接口请求(比如调用后端退出接口)
await new Promise((resolve) => {
setTimeout(() => {
resolve()
}, 500)
})
// 接口请求成功后,重置状态
this.username = '游客'
this.token = ''
this.isLogin = false
this.userInfo = {}
},
// 示例3:修改单个状态(单独修改用户名)
updateUsername(name) {
this.username = name
}
}
})
3. 基础 Store 定义(TS版,类型安全)
TS版会对 state、getters、actions 的参数和返回值进行类型约束,避免数据类型错误,代码如下:
// stores/userStore.ts
import { defineStore } from 'pinia'
// 定义 UserInfo 类型接口(约束 userInfo 的结构)
interface UserInfo {
id?: number
name?: string
role?: 'admin' | 'user' // 角色只能是 admin 或 user
}
// 定义 State 类型接口(约束 state 的结构)
interface UserState {
username: string
token: string
isLogin: boolean
userInfo: UserInfo
}
// 定义并导出 Store
export const useUserStore = defineStore('user', {
// 对 state 进行类型约束
state: (): UserState => ({
username: '游客',
token: '',
isLogin: false,
userInfo: {}
}),
getters: {
upperUsername: (state: UserState): string => {
return state.username.toUpperCase()
},
isAdmin: (state: UserState, getters): boolean => {
return state.userInfo.role === 'admin' && getters.upperUsername
}
},
actions: {
// 对参数 data 进行类型约束
login(data: { username: string; token: string; userInfo: UserInfo }): void {
this.username = data.username
this.token = data.token
this.isLogin = true
this.userInfo = data.userInfo
},
// 异步 action,返回 Promise
async logout(): Promise<void> {
await new Promise((resolve) => {
setTimeout(() => {
resolve()
}, 500)
})
this.username = '游客'
this.token = ''
this.isLogin = false
this.userInfo = {}
},
updateUsername(name: string): void {
this.username = name
}
}
})
核心细节说明(新手必看)
defineStore是 Pinia 内置宏,无需导入额外依赖,第一个参数(id)必须全局唯一,否则会导致状态混乱;- state 必须是一个「函数」,返回一个对象,目的是避免多个组件复用 Store 时,出现状态污染;
- getters 支持两种写法:箭头函数(推荐,简洁)和普通函数,箭头函数的第一个参数是 state,第二个参数是 getters(可访问当前 Store 的其他 getters);
- actions 中可以直接通过
this访问和修改 state 中的数据,支持同步和异步(async/await),无需像 Vuex 那样区分 mutations(同步)和 actions(异步); - Store 命名规范:函数名以
useXXXStore开头(比如 useUserStore),符合 Vue3 组合式 API 的命名习惯。
四、Pinia 基础使用:组件中存取/修改状态
创建好 Store 后,就可以在任意组件中使用它了——核心步骤:引入 Store → 创建 Store 实例 → 存取/修改状态,步骤简单,无需注册,直接使用。
1. 组件中使用 Store(非TS版)
<template>
<div class="demo">
<h3>Pinia 组件使用示例(非TS版)</h3>
<!-- 直接使用 state 中的数据 -->
<p>当前用户:{{ userStore.username }}</p>
<p>用户名大写:{{ userStore.upperUsername }}</p>
<p>登录状态:{{ userStore.isLogin ? '已登录' : '未登录' }}</p>
<!-- 调用 actions 中的方法,修改状态 -->
<button @click="handleLogin">模拟登录</button>
<button @click="handleLogout" style="margin-left: 10px;">模拟退出</button>
<button @click="handleUpdateName" style="margin-left: 10px;">修改用户名</button>
</div>
</template>
<script setup>
// 1. 引入创建好的 Store
import { useUserStore } from '@/stores/userStore'
// 2. 创建 Store 实例(必须调用 useUserStore(),不能直接赋值)
const userStore = useUserStore()
// 3. 调用 actions 中的方法,修改状态
const handleLogin = () => {
// 模拟登录数据
const loginData = {
username: '掘金用户',
token: '1234567890abcdef',
userInfo: {
id: 1001,
name: '掘金用户',
role: 'user'
}
}
// 调用同步 action
userStore.login(loginData)
}
const handleLogout = () => {
// 调用异步 action(注意:异步方法需要加 await)
userStore.logout()
}
const handleUpdateName = () => {
// 调用 action 修改单个状态
userStore.updateUsername('新的用户名')
}
</script>
2. 组件中使用 Store(TS版)
TS版和非TS版写法基本一致,唯一区别是 TS 会自动进行类型推断,无需手动声明类型,代码更安全:
<template>
<div class="demo">
<h3>Pinia 组件使用示例(TS版)</h3>
<p>当前用户:{{ userStore.username }}</p>
<p>用户名大写:{{ userStore.upperUsername }}</p>
<button @click="handleLogin">模拟登录</button>
</div>
</template>
<script setup lang="ts">
// 引入 Store(TS 会自动推断类型)
import { useUserStore } from '@/stores/userStore'
const userStore = useUserStore()
// 调用 action 时,TS 会自动校验参数类型
const handleLogin = () => {
const loginData = {
username: 'TS用户',
token: 'ts123456',
userInfo: {
id: 1002,
name: 'TS用户',
role: 'admin' // 符合 UserInfo 接口的 role 类型
}
}
userStore.login(loginData)
}
</script>
3. 状态修改的3种方式(实战常用)
Pinia 提供了 3 种修改 state 状态的方式,根据场景灵活选择,推荐优先使用前两种(符合规范):
方式1:调用 actions 中的方法(推荐,最规范)
这是最推荐的方式,将状态修改逻辑封装在 actions 中,便于维护和复用,尤其是复杂状态修改和异步操作:
// 组件中
const userStore = useUserStore()
// 调用 actions 方法修改状态
userStore.login(loginData)
userStore.updateUsername('新用户名')
方式2:直接修改 state 中的数据(简单场景可用)
如果是简单的单个状态修改,可直接通过 Store 实例修改,无需封装 actions(简化代码):
const userStore = useUserStore()
// 直接修改单个状态
userStore.username = '直接修改用户名'
userStore.isLogin = true
// 直接修改对象中的属性
userStore.userInfo.name = '修改用户姓名'
方式3:使用 $patch 批量修改状态(批量修改可用)
如果需要同时修改多个状态,使用$patch 方法,批量修改,代码更简洁:
const userStore = useUserStore()
// 批量修改状态(对象写法)
userStore.$patch({
username: '批量修改用户名',
isLogin: true,
token: 'patch123456'
})
// 批量修改状态(函数写法,适合复杂逻辑)
userStore.$patch((state) => {
state.username = '批量修改(函数写法)'
state.userInfo.role = 'admin'
state.isLogin = true
})
五、Pinia 进阶技巧:实战必备功能
掌握基础用法后,再学习几个进阶技巧,覆盖实际开发中的高频需求,让 Pinia 用起来更高效。
1. Getters 进阶:缓存与依赖
Pinia 的 getters 具有「缓存特性」——只要依赖的 state 数据不变化,getters 的计算结果就会被缓存,多次访问不会重复计算,提升性能。
// stores/userStore.js
getters: {
// 缓存示例:只有 state.username 变化时,才会重新计算
upperUsername: (state) => {
console.log('getters 计算执行了') // 仅在 username 变化时打印
return state.username.toUpperCase()
},
// 依赖其他 Store 的 getters(跨 Store 访问)
// 假设还有一个 settingStore,用于存储主题配置
themeAndUser: (state, getters) => {
const settingStore = useSettingStore() // 引入其他 Store
return `${getters.upperUsername} - ${settingStore.theme}`
}
}
2. Actions 进阶:异步操作与跨 Store 调用
Actions 支持 async/await 异步操作(比如调用后端接口),同时也能调用其他 Store 的 actions 或修改其他 Store 的状态,实现跨 Store 交互。
// stores/userStore.js
import { useCartStore } from './cartStore' // 引入其他 Store
actions: {
// 异步 action:调用后端登录接口(实战常用)
async loginByApi(formData) {
try {
// 调用后端登录接口(模拟)
const res = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify(formData)
})
const data = await res.json()
// 登录成功后,修改当前 Store 状态
this.username = data.username
this.token = data.token
this.isLogin = true
// 跨 Store 调用:调用 cartStore 的 actions(比如同步购物车数据)
const cartStore = useCartStore()
cartStore.syncCart(data.token) // 传递 token,同步购物车
return data // 可返回数据,供组件使用
} catch (err) {
console.error('登录失败:', err)
throw err // 抛出错误,供组件捕获处理
}
}
}
3. 状态持久化(实战必备)
Pinia 的状态默认存储在内存中,页面刷新后会丢失(比如登录状态、购物车数据),因此需要做「状态持久化」——将状态存储到 localStorage/sessionStorage 中,页面刷新后自动恢复。
推荐使用第三方插件 pinia-plugin-persistedstate,配置简单,一键实现持久化。
步骤1:安装插件
npm install pinia-plugin-persistedstate
# 或 yarn 安装
yarn add pinia-plugin-persistedstate
步骤2:main.js 中配置插件
// main.js(非TS版)
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' // 引入插件
import App from './App.vue'
const app = createApp(App)
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate) // 挂载持久化插件
app.use(pinia)
app.mount('#app')
步骤3:给指定 Store 开启持久化
在 defineStore 中添加 persist: true,即可开启当前 Store 的持久化(默认存储到 localStorage):
// stores/userStore.js
export const useUserStore = defineStore('user', {
state: () => ({ /* ... */ }),
getters: { /* ... */ },
actions: { /* ... */ },
persist: true // 开启持久化,页面刷新后状态不丢失
})
进阶配置:自定义持久化规则
可自定义持久化方式(localStorage/sessionStorage)、存储的键名、需要持久化的状态字段:
persist: {
key: 'userStore', // 存储到本地的键名(默认是 Store 的 id)
storage: sessionStorage, // 存储方式:sessionStorage(页面关闭后丢失)
paths: ['username', 'token', 'isLogin'] // 只持久化这3个字段,其他字段不持久化
}
4. 解构 Store 数据(避免响应式丢失)
如果直接解构 Store 实例中的 state 数据,会导致数据丢失响应式(修改数据后,页面不更新),解决方案:使用 Pinia 内置的storeToRefs 方法。
// 错误写法:直接解构,丢失响应式
const { username, isLogin } = useUserStore()
username = '新用户名' // 页面不更新
// 正确写法:使用 storeToRefs 解构,保留响应式
import { storeToRefs } from 'pinia' // 引入 storeToRefs
const userStore = useUserStore()
const { username, isLogin } = storeToRefs(userStore)
// 此时修改数据(需通过 Store 实例,解构后的变量是只读的)
userStore.username = '新用户名' // 页面正常更新
说明:storeToRefs 只会解构 state 中的数据,并不会解构 getters 和 actions,getters 和 actions 仍需通过 Store 实例访问。
六、实战场景:Pinia 实现购物车功能
结合实际开发中的「购物车」场景,完整演示 Pinia 的使用流程(非TS版),代码可直接复制到项目中使用。
1. 创建购物车 Store(stores/cartStore.js)
// stores/cartStore.js
import { defineStore } from 'pinia'
export const useCartStore = defineStore('cart', {
state: () => ({
cartList: [] // 购物车列表,每个项包含 id、name、price、count、checked
}),
getters: {
// 计算购物车总数量
cartTotalCount: (state) => {
return state.cartList.reduce((total, item) => total + item.count, 0)
},
// 计算购物车总价(只算选中的商品)
cartTotalPrice: (state) => {
return state.cartList
.filter(item => item.checked)
.reduce((total, item) => total + item.price * item.count, 0)
},
// 判断是否全选
isAllChecked: (state) => {
return state.cartList.length > 0 && state.cartList.every(item => item.checked)
}
},
actions: {
// 1. 添加商品到购物车(重复商品数量+1)
addToCart(goods) {
const existGoods = state.cartList.find(item => item.id === goods.id)
if (existGoods) {
existGoods.count += goods.count
} else {
state.cartList.push({ ...goods, checked: true }) // 默认选中
}
},
// 2. 减少商品数量(数量为1时删除商品)
reduceCartCount(id) {
const existGoods = state.cartList.find(item => item.id === id)
if (existGoods) {
if (existGoods.count === 1) {
this.removeFromCart(id) // 调用当前 Store 的其他 action
} else {
existGoods.count--
}
}
},
// 3. 从购物车删除商品
removeFromCart(id) {
state.cartList = state.cartList.filter(item => item.id !== id)
},
// 4. 切换商品选中状态
toggleChecked(id) {
const existGoods = state.cartList.find(item => item.id === id)
if (existGoods) {
existGoods.checked = !existGoods.checked
}
},
// 5. 全选/取消全选
toggleAllChecked(checked) {
state.cartList.forEach(item => {
item.checked = checked
})
},
// 6. 清空购物车
clearCart() {
state.cartList = []
}
},
// 开启持久化,避免页面刷新后购物车数据丢失
persist: {
key: 'cartStore',
paths: ['cartList']
}
})
2. 组件中使用购物车 Store
<template>
<div class="cart-page">
<h3>购物车页面</h3>
<div class="cart-header">
<input
type="checkbox"
v-model="isAllChecked"
@change="handleToggleAllChecked"
/>
<span>全选</span>
<span class="total">总数量:{{ cartStore.cartTotalCount }}</span>
<span class="total-price">总价:¥{{ cartStore.cartTotalPrice.toFixed(2) }}</span>
<button @click="cartStore.clearCart" class="clear-btn">清空购物车</button>
</div>
<div class="cart-list">
<div class="cart-item" v-for="item in cartStore.cartList" :key="item.id">
<input
type="checkbox"
v-model="item.checked"
@change="cartStore.toggleChecked(item.id)"
/>
<span class="goods-name">{{ item.name }}</span>
<span class="goods-price">¥{{ item.price.toFixed(2) }}</span>
<div class="count-btn">
<button @click="cartStore.reduceCartCount(item.id)">-</button>
<span>{{ item.count }}</span>
<button @click="cartStore.addToCart({ ...item, count: 1 })">+</button>
</div>
<button @click="cartStore.removeFromCart(item.id)" class="delete-btn">删除</button>
</div>
</div>
<div class="add-goods">
<button @click="handleAddGoods">添加商品到购物车</button>
</div>
</div>
</template>
<script setup>
import { useCartStore } from '@/stores/cartStore'
import { storeToRefs } from 'pinia'
const cartStore = useCartStore()
// 解构 getters,保留响应式
const { isAllChecked } = storeToRefs(cartStore)
// 全选/取消全选
const handleToggleAllChecked = () => {
cartStore.toggleAllChecked(isAllChecked.value)
}
// 模拟添加商品
const handleAddGoods = () => {
const goods = {
id: Math.floor(Math.random() * 1000),
name: `商品${Math.floor(Math.random() * 100)}`,
price: Math.floor(Math.random() * 100) + 10,
count: 1
}
cartStore.addToCart(goods)
}
</script>
七、常见坑点避坑指南(新手必看)
很多新手在使用 Pinia 时,会遇到「响应式丢失」「状态刷新丢失」「Store 调用错误」等问题,以下是最常见的 5 个坑点,帮你快速避坑。
坑点1:直接解构 Store 实例,丢失响应式
错误写法:const { username } = useUserStore();
正确写法:使用 storeToRefs 解构,const { username } = storeToRefs(useUserStore())。
坑点2:忘记开启状态持久化,页面刷新后状态丢失
解决方案:安装 pinia-plugin-persistedstate 插件,在 Store 中添加 persist: true。
坑点3:创建 Store 实例时,未调用 useXXXStore()
错误写法:const userStore = useUserStore(未加括号,赋值的是函数本身,不是实例);
正确写法:const userStore = useUserStore()(必须加括号,调用函数创建实例)。
坑点4:actions 中使用 async/await 时,未加 await 调用
错误写法:userStore.logout()(异步方法未加 await,可能导致后续操作执行顺序错误);
正确写法:await userStore.logout()(在 async 函数中调用,等待异步操作完成)。
坑点5:多个 Store 的 id 重复,导致状态混乱
解决方案:Store 的 id 必须全局唯一,推荐和文件名对应(比如 userStore 的 id 为 'user',cartStore 的 id 为 'cart')。
八、总结:Pinia 核心要点回顾
Pinia 作为 Vue3 官方推荐的状态管理库,核心优势是「简单、高效、友好」,掌握以下核心要点,就能应对所有项目场景:
- 核心结构:每个 Store 包含
state(存储状态)、getters(计算状态)、actions(修改状态),无需 modules 和 mutations; - 基础流程:安装 Pinia → 初始化挂载 → 创建 Store → 组件中引入并使用;
- 状态修改:优先调用 actions 方法,简单场景可直接修改,批量修改用 $patch;
- 进阶技巧:getters 有缓存特性,actions 支持异步和跨 Store 调用,状态持久化用 pinia-plugin-persistedstate;
- 避坑关键:用 storeToRefs 解构保留响应式,开启持久化避免刷新丢失,Store id 全局唯一。
相比于 Vuex,Pinia 的上手成本极低,即使是新手,也能在1-2小时内掌握核心用法,并且完美适配 Vue3 组合式 API 和 TypeScript,是当前 Vue3 项目状态管理的最优解。