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前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。