Pinia是啥?为啥Vue3推荐用它代替Vuex?
现在Vue3成了前端项目的主流框架,状态管理这块很多人从Vuex转向了Pinia,为啥选Pinia?Vue3里咋上手Pinia?不同场景下怎么用它解决状态共享问题?这篇文章用问答形式把Pinia的关键知识点和实战技巧拆明白,不管是刚接触的新手还是想优化项目的开发者,看完能少走不少弯路。
Pinia是Vue生态里的**状态管理库**,专门给Vue.js(尤其是Vue3)做响应式状态共享用的,它和Vuex关系很有意思——俩项目作者是同一个人,Pinia可以理解成“Vuex的下一代”,解决了Vuex之前被吐槽的痛点:- 代码更简洁:Vuex里有state、mutation、action、getter、module这些概念,Pinia直接砍掉mutation(修改状态不用再区分同步异步,action里想咋改咋改),只保留state、getter、action,写代码时少了很多“仪式感”。
- 对Composition API更友好:Vue3主推组合式API,Pinia的API设计和setup语法天然契合,用起来像写组件逻辑一样顺。
- TS支持拉满:现在项目基本都用TypeScript,Pinia在类型推导上做了优化,定义Store时能自动推断类型,不用像Vuex那样写一堆泛型声明,对TS新手友好太多。
- 体积更小,性能更好:Pinia打包后体积比Vuex小很多,内部用Vue3的
reactive和computed做响应式,性能和组件内状态管理一致,没有额外性能开销。
Vue官方文档里也能看到,现在更推荐用Pinia做状态管理,Vuex虽然还维护,但新项目优先选Pinia是趋势。
怎么在Vue3项目里装Pinia并初始化?
分两步:安装包 + 挂载到Vue应用里。
安装Pinia
用包管理器装,比如npm:
npm install pinia
yarn或pnpm也一样,换命令就行。
在main.js里初始化
Vue3的项目入口一般是main.js(或main.ts),要创建Pinia实例,然后用app.use()挂载:
// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const app = createApp(App)
// 创建Pinia实例
const pinia = createPinia()
// 挂载到Vue应用
app.use(pinia)
app.mount('#app')
这一步做完,整个项目就有了全局的状态管理容器,接下来就能定义各种Store了。
怎么定义第一个Store?
Store是Pinia里的“状态模块”,可以理解成一个独立的逻辑单元,把状态、计算逻辑、方法包在一起,用defineStore函数定义,步骤如下:
创建Store文件
一般在src/store目录下新建文件,比如counterStore.js(TS项目用.ts)。
用defineStore定义结构
defineStore需要两个参数:唯一ID(整个应用里不能重复)、配置对象(包含state、getters、actions)。
举个“计数器”的例子:
// src/store/counterStore.js
import { defineStore } from 'pinia'
// 定义并导出Store
export const useCounterStore = defineStore('counter', {
// state:存数据,返回对象(和Vuex的state类似)
state: () => ({
count: 0
}),
// getters:计算属性,依赖state,和组件的computed一样
getters: {
doubleCount: (state) => state.count * 2
},
// actions:方法,同步/异步都能写,用来修改state
actions: {
increment() {
this.count++ // 直接修改state,Pinia允许这么做
},
async incrementAsync() {
// 模拟异步延迟
await new Promise(resolve => setTimeout(resolve, 1000))
this.count++
}
}
})
这里注意几个点:
- ID
'counter'要全局唯一,不然会冲突; state是函数,返回初始状态对象(避免所有实例共享同一对象);getters里的函数能拿到state参数,也能通过this访问state(但TS下用参数更安全);actions里的this指向当前Store实例,所以能直接改state,不用像Vuex那样commit mutation。
组件里咋用Store的状态和方法?
在Vue3的组合式API(setup语法)里,用useStore函数获取Store实例,然后访问state、getters、actions。
示例:在组件里用计数器Store
新建Counter.vue组件:
<template>
<div>
<p>当前计数:{{ count }}</p>
<p>双倍计数:{{ doubleCount }}</p>
<button @click="increment">+1</button>
<button @click="incrementAsync">异步+1</button>
</div>
</template>
<script setup>
import { useCounterStore } from '../store/counterStore'
// 注意:直接解构state会丢失响应式,要用storeToRefs
import { storeToRefs } from 'pinia'
// 获取Store实例
const counterStore = useCounterStore()
// 解构state和getters,保持响应式
const { count, doubleCount } = storeToRefs(counterStore)
// 直接拿actions(方法本身不需要ref,因为调用时是触发动作)
const { increment, incrementAsync } = counterStore
</script>
这里关键是storeToRefs——因为Pinia的state是响应式的,但用对象解构后,普通变量会丢失响应式,storeToRefs能把state和getters转成带ref的结构,保证组件里数据变化时UI更新,而actions是函数,不属于响应式数据,直接解构没问题。
State的修改方式有哪些?哪种场景用哪种?
Pinia里改state很灵活,常用三种方式:
直接修改(最简单)
在action里或组件里,直接给state属性赋值:
// action里直接改
actions: {
increment() {
this.count++ // 允许!
}
}
// 组件里直接改(不推荐,但能行)
counterStore.count++
这种方式适合简单场景,比如点击按钮直接+1,但如果是批量修改多个状态,或者想优化性能(减少响应式追踪次数),可以用$patch。
$patch方法(批量修改)
$patch有两种用法:传对象(适合改多个已知属性)、传函数(适合复杂逻辑或动态修改)。
示例1:传对象(一次性改多个属性)
假设state里有count和message:
counterStore.$patch({
count: counterStore.count + 2,
message: '更新后'
})
示例2:传函数(基于当前state计算新值)
适合需要先读旧值再修改的场景:
counterStore.$patch((state) => {
state.count += 3
state.message = `新消息${state.count}`
})
$patch的优势是批量更新,Vue会把多个状态变化合并成一个响应式更新,减少组件重渲染次数,性能更好。
在action里修改(适合复杂/异步逻辑)
如果修改state的逻辑很复杂,或者需要调接口(异步),把逻辑包在action里更清晰:
actions: {
async fetchDataAndUpdate() {
// 调接口(异步)
const res = await api.get('/data')
// 改state
this.count = res.data.count
this.message = res.data.msg
}
}
组件里只需要调用这个action:counterStore.fetchDataAndUpdate(),逻辑内聚在Store里,代码更整洁。
Getter怎么用?和组件计算属性有啥区别?
Getter是Store里的“计算属性”,作用和组件的computed类似,但复用性更强——多个组件需要同一个计算逻辑时,把它放到Store的getter里,不用在每个组件重复写。
基本用法
比如前面计数器的doubleCount:
getters: {
doubleCount: (state) => state.count * 2
}
组件里通过store.doubleCount访问,和访问state一样。
Getter支持传参(返回函数)
如果需要根据不同参数计算,Getter可以返回一个函数:
getters: {
// 根据传入的乘数,返回对应结果
multiplyCount: (state) => (multiplier) => state.count * multiplier
}
// 组件里用:
counterStore.multiplyCount(3) // 得到count*3的结果
和组件computed的区别
- 作用域不同:组件computed只在当前组件生效;Getter在Store里,所有组件都能复用。
- 依赖源不同:computed依赖组件内的响应式数据;Getter依赖Store的state。
- 缓存机制:和computed一样,Getter也有缓存——只有依赖的state变化时,才会重新计算,性能友好。
Action里处理异步逻辑咋做?举个实际例子?
开发中常见的场景:调后端接口,拿数据后更新state,用Pinia的action写异步逻辑很丝滑,结合async/await就行。
示例:用户登录状态管理
假设要做一个用户Store,处理登录、登出、获取用户信息:
// src/store/userStore.js
import { defineStore } from 'pinia'
import { apiLogin, apiGetUserInfo } from '../api/user' // 假设的接口函数
export const useUserStore = defineStore('user', {
state: () => ({
token: localStorage.getItem('token') || '', // 从本地缓存取token
userInfo: null
}),
actions: {
// 登录(异步)
async login(account, password) {
try {
// 调登录接口,拿token
const res = await apiLogin({ account, password })
this.token = res.data.token
localStorage.setItem('token', this.token) // 存到本地
// 登录成功后,拉取用户信息
await this.fetchUserInfo()
return true // 登录成功
} catch (err) {
console.error('登录失败', err)
return false
}
},
// 获取用户信息(异步)
async fetchUserInfo() {
const res = await apiGetUserInfo()
this.userInfo = res.data
},
// 登出
logout() {
this.token = ''
this.userInfo = null
localStorage.removeItem('token')
}
}
})
组件里调用登录逻辑:
<template>
<button @click="handleLogin">登录</button>
</template>
<script setup>
import { useUserStore } from '../store/userStore'
const userStore = useUserStore()
const handleLogin = async () => {
const success = await userStore.login('test', '123456')
if (success) {
// 登录成功,跳转到首页等逻辑
}
}
</script>
这里能看到:
- action里可以用
async/await处理异步流程; - 多个action可以互相调用(比如
login里调用fetchUserInfo); - 结合本地存储(localStorage)做持久化(后面会讲更完善的持久化方案)。
多个Store之间怎么互相调用?比如用户Store要调商品Store的方法?
实际项目里,不同Store可能有依赖,比如用户登录后,需要更新购物车数据,这时候可以在一个Store的action里,用useOtherStore获取其他Store的实例。
示例:用户登录后更新购物车
假设有userStore和cartStore,用户登录后要刷新购物车列表:
// src/store/userStore.js
import { defineStore } from 'pinia'
import { useCartStore } from './cartStore' // 导入其他Store
export const useUserStore = defineStore('user', {
actions: {
async login(account, password) {
// 登录逻辑...(省略)
// 登录成功后,获取购物车Store实例
const cartStore = useCartStore()
// 调用cartStore的action刷新购物车
await cartStore.fetchCartList()
}
}
})
// src/store/cartStore.js
export const useCartStore = defineStore('cart', {
state: () => ({
cartList: []
}),
actions: {
async fetchCartList() {
// 调接口拿购物车数据
const res = await apiGetCartList()
this.cartList = res.data
}
}
})
这种跨Store调用的关键是:在需要的地方用useOtherStore获取实例,和组件里用Store的方式一样,Pinia会自动管理Store的单例性,不用担心重复创建。
Pinia的状态持久化怎么实现?刷新页面数据不丢?
默认情况下,Pinia的state存在内存里,页面刷新就没了,要实现持久化(比如存localStorage/sessionStorage),可以用社区插件pinia-plugin-persistedstate。
步骤:
-
安装插件:
npm install pinia-plugin-persistedstate
-
在main.js里注册插件:
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里配置`persist`选项:
以用户Store为例,让`token`和`userInfo`刷新后保留:
```js
export const useUserStore = defineStore('user', {
state: () => ({
token: '',
userInfo: null
}),
// 配置持久化
persist: {
key: 'user-store', // 存储的key(默认是Store的id)
storage: localStorage, // 存哪里:localStorage/sessionStorage,默认localStorage
paths: ['token', 'userInfo'] // 要持久化的state字段,默认全存
},
actions: { /* ... */ }
})
配置项说明:
key:自定义存储的键名,避免和其他项目冲突;storage:选localStorage(永久存储)或sessionStorage(会话级存储,关闭标签页清空);paths:数组,指定要持久化的state属性,比如只存token就写['token'],减少存储体积。
这样配置后,state里的token和userInfo会自动存到localStorage,页面刷新时Pinia会自动读取并恢复状态。
大型项目里,Pinia怎么拆分模块更合理?
大型项目状态多,全放一个Store里会很臃肿。按业务模块拆分Store是关键,比如用户模块、购物车模块、商品模块、订单模块,每个模块一个Store文件。
拆分步骤:
-
建
src/store目录,按模块建文件:store/ ├─ user.js // 用户相关状态 ├─ cart.js // 购物车相关 ├─ product.js // 商品列表相关 ├─ index.js // (可选)统一导出所有Store -
每个模块独立定义Store:
比如product.js负责商品列表的获取和筛选:
// src/store/product.js
import { defineStore } from 'pinia'
export const useProductStore = defineStore('product', {
state: () => ({
list: [],
filter: 'all' // 筛选条件:all/sale/new
}),
actions: {
async fetchProductList() {
const res = await apiGetProductList(this.filter)
this.list = res.data
},
setFilter(newFilter) {
this.filter = newFilter
this.fetchProductList() // 切换筛选后重新拉数据
}
}
})
-
组件里按需导入Store:
需要商品列表的组件里,只导入useProductStore,不需要关心其他模块的状态,代码解耦。 -
(可选)用index.js统一导出:
如果觉得每次导入路径麻烦,可以在store/index.js里集中导出:
export { useUserStore } from './user'
export { useCartStore } from './cart'
export { useProductStore } from './product'
组件里就可以这样导入:
import { useUserStore, useCartStore } from '../store'
Pinia和
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
code前端网



发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。