深入浅出 Pinia:Vue 状态管理新选择(附实战代码)
本文详解 Vue 3 官方推荐状态管理工具 Pinia(Vuex 继任者),含实战代码。基础部分:说明 Pinia 支持 Vue 2/3(推荐 Vue 3+Node.js 14.0+),提供 npm/yarn 安装命令,及在main.js/main.ts中创建、挂载 Pinia 实例的代码;讲解用defineStore定义 Store(含 state、getters、actions,action
前言
在 Vue 3 生态逐渐成为主流的当下,状态管理工具也迎来了新的 “官方推荐”——Pinia。作为 Vuex 的继任者,Pinia 不仅简化了 Vuex 中繁琐的Mutations设计,还带来了更友好的 TypeScript 支持、更灵活的 Store 组织方式,以及更轻量的体积。无论是新手入门 Vue 状态管理,还是老项目从 Vuex 迁移,Pinia 都是现阶段的最优解之一。本文将从基础安装到高级特性,带你一步步掌握 Pinia 的核心用法,所有代码均经过实战验证,可直接复制到项目中使用。
一、Pinia 基础:从安装到挂载
1. 环境要求
Pinia 支持 Vue 2 和 Vue 3,但更推荐在 Vue 3 项目中使用(能更好发挥 Composition API 优势),同时需要 Node.js 14.0 + 环境。
2. 安装 Pinia
打开项目终端,执行以下命令安装:
bash
# npm 安装
npm install pinia --save
# yarn 安装(若使用yarn)
yarn add pinia
3. 全局挂载 Pinia
在项目入口文件(通常是main.js或main.ts)中,创建 Pinia 实例并挂载到 Vue 应用上:
javascript
// main.js(Vue 3 + Options API)
import { createApp } from 'vue'
import { createPinia } from 'pinia' // 引入Pinia
import App from './App.vue'
import router from './router' // 若有路由,可正常引入
// 1. 创建Pinia实例
const pinia = createPinia()
// 2. 创建Vue应用
const app = createApp(App)
// 3. 挂载Pinia、路由到应用
app.use(pinia)
app.use(router)
app.mount('#app')
如果你的项目使用 TypeScript,代码结构基本一致,仅需注意类型声明:
typescript
// main.ts(Vue 3 + TypeScript)
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(createPinia()).use(router).mount('#app')
二、核心用法:定义与使用 Store
Store 是 Pinia 的核心概念,相当于 “状态容器”,每个 Store 负责管理一块独立的业务状态(如用户信息、购物车、计数器等)。Pinia 中定义 Store 的方式非常灵活,支持 Options API 风格和 Composition API 风格,本文先从更易上手的 Options API 风格讲起。
1. 定义第一个 Store(Options API 风格)
推荐在项目根目录下创建stores文件夹,统一管理所有 Store 文件(如counter.js、user.js),避免状态分散。
以 “计数器” Store 为例,创建stores/counter.js:
javascript
// stores/counter.js
import { defineStore } from 'pinia'
// 关键:通过defineStore定义Store,第一个参数是Store的唯一ID(必须唯一)
export const useCounterStore = defineStore('counter', {
// 1. state:存储状态(类似Vue组件的data),必须是函数返回对象(避免跨实例污染)
state: () => ({
count: 0, // 计数器初始值
title: 'Pinia计数器', // 额外状态示例
isLoading: false // 加载状态示例
}),
// 2. getters:计算属性(类似Vue组件的computed),基于state派生新值
getters: {
// 基础用法:接收state参数,返回计算后的值
doubleCount: (state) => state.count * 2,
// 进阶用法:通过this访问其他getters(注意:此时不能用箭头函数)
doubleCountPlusOne() {
// this指向当前Store实例,可访问state和其他getters
return this.doubleCount + 1
},
// 带参数的getters:返回一个函数(本质是方法,但仍具有缓存特性)
getCountWithAdd: (state) => (num) => state.count + num
},
// 3. actions:修改状态的方法(类似Vuex的actions,但支持同步+异步)
actions: {
// 同步修改:直接通过this操作state
increment() {
this.count++ // 无需像Vuex那样通过commit调用Mutation
},
// 带参数的同步方法
incrementBy(num) {
this.count += num
},
// 异步操作:支持async/await(如请求接口、定时器等)
async incrementAsync() {
this.isLoading = true // 开启加载状态
// 模拟异步请求(如接口调用)
await new Promise((resolve) => setTimeout(resolve, 1000))
this.increment() // 异步完成后修改状态
this.isLoading = false // 关闭加载状态
}
}
})
2. 在 Vue 组件中使用 Store
在组件中使用 Store 的步骤非常简单:引入 Store 函数 → 调用函数获取实例 → 直接使用 state、getters、actions。
以 Vue 3 的<script setup>语法为例(最简洁的方式):
vue
<!-- components/Counter.vue -->
<template>
<div class="counter-container">
<h2>{{ counterStore.title }}</h2>
<!-- 直接使用state -->
<p>当前计数:{{ counterStore.count }}</p>
<!-- 使用getters -->
<p>计数的2倍:{{ counterStore.doubleCount }}</p>
<p>计数的2倍+1:{{ counterStore.doubleCountPlusOne }}</p>
<p>计数+5:{{ counterStore.getCountWithAdd(5) }}</p>
<!-- 调用actions -->
<button @click="counterStore.increment">+1</button>
<button @click="counterStore.incrementBy(2)">+2</button>
<button @click="counterStore.incrementAsync" :disabled="counterStore.isLoading">
{{ counterStore.isLoading ? '加载中...' : '异步+1' }}
</button>
</div>
</template>
<script setup>
// 1. 引入定义好的Store函数
import { useCounterStore } from '@/stores/counter'
// 2. 调用函数,获取Store实例(无需new,Pinia会自动管理)
const counterStore = useCounterStore()
</script>
<style scoped>
.counter-container {
margin: 20px;
padding: 20px;
border: 1px solid #eee;
border-radius: 8px;
}
button {
margin: 0 8px;
padding: 6px 12px;
cursor: pointer;
}
</style>
如果你的项目仍在使用 Options API,使用方式如下:
vue
<!-- components/CounterOptions.vue -->
<template>
<div>
<p>当前计数:{{ counterStore.count }}</p>
<button @click="counterStore.increment">+1</button>
</div>
</template>
<script>
import { useCounterStore } from '@/stores/counter'
export default {
// 通过computed获取Store实例(确保响应式)
computed: {
counterStore() {
return useCounterStore()
}
}
}
</script>
三、Pinia 高级特性:解锁更多实用功能
掌握基础用法后,再来看看 Pinia 的高级特性,这些功能能帮你应对复杂业务场景(如批量修改状态、监听状态变化、Store 间通信等)。
1. 状态修改的 3 种方式
除了通过actions修改状态,Pinia 还支持直接修改和批量修改,灵活度更高。
(1)直接修改状态(简单场景推荐)
适合简单的状态修改,无需封装到actions(Pinia 不强制要求所有修改都通过actions):
javascript
const counterStore = useCounterStore()
// 直接修改单个状态
counterStore.count = 10
// 直接修改多个状态
counterStore.title = '新的计数器标题'
counterStore.isLoading = true
(2)$patch批量修改(推荐)
当需要修改多个状态时,使用$patch更高效(内部会批量更新,减少响应式触发次数),支持对象形式和函数形式。
- 对象形式(适合固定字段修改):
javascript
counterStore.$patch({
count: 20, // 直接赋值
title: '批量修改后的标题',
isLoading: false // 覆盖原有值
})
- 函数形式(适合复杂逻辑修改,如数组操作):
javascript
// 假设state中有一个数组:list: [1, 2, 3]
counterStore.$patch((state) => {
state.list.push(4) // 直接操作数组
state.count = state.list.length // 基于数组长度修改count
})
(3)actions修改(复杂 / 异步场景推荐)
当修改逻辑复杂(如需要判断、计算)或涉及异步操作(如接口请求)时,必须通过actions封装,保证代码可维护性:
javascript
// stores/user.js
export const useUserStore = defineStore('user', {
state: () => ({
userInfo: {},
token: ''
}),
actions: {
// 封装登录逻辑(异步+复杂状态修改)
async login(username, password) {
this.isLoading = true
try {
// 调用登录接口
const res = await axios.post('/api/login', { username, password })
// 批量修改状态
this.$patch({
userInfo: res.data.user,
token: res.data.token
})
// 额外操作:存储token到localStorage
localStorage.setItem('token', res.data.token)
} catch (err) {
console.error('登录失败:', err)
throw err // 抛出错误,让组件捕获
} finally {
this.isLoading = false
}
}
}
})
2. 监听状态变化
Pinia 提供了两种监听状态的方式:$subscribe(监听整个 Store)和 Vue 的watch(监听单个 / 多个状态)。
(1)$subscribe监听整个 Store
适合需要监听 Store 中所有状态变化的场景(如状态持久化、日志记录),且支持detached选项(组件卸载后仍继续监听):
javascript
const counterStore = useCounterStore()
// 监听整个Store的状态变化
counterStore.$subscribe((mutation, state) => {
// mutation:包含状态变化的信息(如storeId、type、payload)
// state:变化后的完整状态
console.log('Store状态变化:', mutation, state)
// 示例:将状态持久化到localStorage
localStorage.setItem('counterState', JSON.stringify(state))
}, {
detached: true // 组件卸载后,监听仍生效(可选)
})
(2)watch监听单个 / 多个状态
适合精准监听某几个状态的变化,用法与 Vue 组件中的watch一致:
javascript
import { watch } from 'vue'
import { useCounterStore } from '@/stores/counter'
const counterStore = useCounterStore()
// 1. 监听单个状态
watch(
() => counterStore.count, // 监听的状态(必须是函数返回值,确保响应式)
(newVal, oldVal) => {
console.log(`count从${oldVal}变成了${newVal}`)
},
{ immediate: true } // 立即执行一次(可选)
)
// 2. 监听多个状态
watch(
[() => counterStore.count, () => counterStore.title], // 数组形式传入多个状态
([newCount, newTitle], [oldCount, oldTitle]) => {
console.log(`count变化:${oldCount}→${newCount}`)
console.log(`title变化:${oldTitle}→${newTitle}`)
}
)
3. Store 之间的通信
在复杂项目中,经常需要多个 Store 之间相互调用(如用户 Store 需要用到购物车 Store 的方法)。Pinia 中无需像 Vuex 那样通过rootGetters或modules嵌套,直接在一个 Store 中引入并使用另一个 Store 即可。
示例:用户登录后,清空购物车未登录状态
javascript
// 1. 定义购物车Store(stores/cart.js)
export const useCartStore = defineStore('cart', {
state: () => ({
list: [] // 购物车列表
}),
actions: {
// 清空购物车方法
clearCart() {
this.list = []
}
}
})
// 2. 在用户Store中使用购物车Store(stores/user.js)
import { useCartStore } from './cart' // 引入购物车Store
export const useUserStore = defineStore('user', {
actions: {
async login(username, password) {
// 登录逻辑...(省略)
// 登录成功后,调用购物车Store的clearCart方法
const cartStore = useCartStore()
cartStore.clearCart()
}
}
})
4. 状态持久化(pinia-plugin-persistedstate)
Pinia 默认不支持状态持久化(页面刷新后状态会丢失),可通过第三方插件pinia-plugin-persistedstate实现,无需手动写localStorage逻辑。
(1)安装插件
bash
npm install pinia-plugin-persistedstate --save
(2)配置插件
在main.js中注册插件:
javascript
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).mount('#app')
(3)开启 Store 持久化
在需要持久化的 Store 中,添加persist: true配置:
javascript
// stores/user.js(持久化用户信息)
export const useUserStore = defineStore('user', {
state: () => ({
token: '',
userInfo: {}
}),
// 开启持久化(默认存储到localStorage,key为Store的id)
persist: true,
// 进阶配置:自定义持久化规则(如指定存储字段、存储位置)
// persist: {
// key: 'my-user-store', // 自定义存储key
// storage: sessionStorage, // 存储到sessionStorage(默认localStorage)
// paths: ['token'] // 只持久化token字段(默认持久化所有state)
// }
})
四、Pinia vs Vuex:为什么选 Pinia?
很多同学会问:“我已经会 Vuex 了,为什么要迁移到 Pinia?” 这里用一张表对比两者的核心差异,帮你理解 Pinia 的优势:
| 特性 | Pinia | Vuex 3/Vuex 4 |
|---|---|---|
| 官方推荐 | Vue 3 官方推荐 | Vue 2 官方推荐(Vue 3 中已被 Pinia 替代) |
| API 复杂度 | 简洁(无 Mutations,直接用 actions) | 繁琐(需区分 Actions、Mutations、Commit) |
| TypeScript 支持 | 原生支持,类型推导完善 | 需手动写类型声明,支持较差 |
| Store 组织方式 | 平级组织,无需嵌套 Modules | 需通过 Modules 嵌套,复杂项目易冗余 |
| 体积 | 轻量(约 1KB) | 较笨重(Vuex 4 约 20KB) |
| 响应式原理 | 基于 Vue 3 的 Proxy(支持所有数据类型) | Vuex 3 基于 Object.defineProperty(有局限性) |
结论:新项目直接用 Pinia,老 Vuex 项目可逐步迁移到 Pinia——Pinia 不仅兼容 Vuex 的核心功能,还解决了 Vuex 的诸多痛点,学习成本低,迁移成本也低。
五、常见问题与注意事项
-
Store 实例是否需要缓存?
不需要!useCounterStore()每次调用返回的都是同一个实例,Pinia 内部会自动缓存,无需手动用ref或reactive包裹。 -
为什么 getters 中不能用箭头函数访问 this?
箭头函数没有自己的this,会绑定外层作用域的this,导致无法指向 Store 实例。若需访问其他 getters 或 state,必须用普通函数。 -
持久化插件不生效怎么办?
检查 3 点:① 插件是否在pinia上注册;② Store 中是否配置persist: true;③ 存储的字段是否是可序列化的(如 Function、Symbol 无法被JSON.stringify,会丢失)。
你在使用 Pinia 时遇到过哪些问题?比如状态修改不生效、持久化异常,或是 Store 通信的复杂场景?欢迎在评论区留言讨论,一起解决问题、深化对 Pinia 的理解!
更多推荐



所有评论(0)