Pinia 是 Vue 官方提供的最新状态管理库,用来替代 Vuex。它同时支持 Vue2 与 Vue3(主要面向 Vue3),并且对 TypeScript 的类型推断更友好。相比 Vuex,Pinia 去掉了 mutation 与 modules 概念,每个 store 都是独立模块,可以创建多个全局仓库,不需要再写嵌套模块,API 更少、结构更直观。本文按安装、store 定义、state、getters、actions 的顺序,梳理一套可直接落到项目里的写法,并记录几个容易踩坑的细节。
一、安装与根存储挂载
安装命令(任选其一):
- npm install pinia
- pnpm add pinia
复制代码
安装完成后需要在入口文件中创建 Pinia 根存储并挂载到应用上,main.js 写法如下:
- import { createApp } from 'vue'
- import App from './App.vue'
- // 引入 createPinia 函数
- import { createPinia } from 'pinia'
- const app = createApp(App)
- // 使用 createPinia() 创建 Pinia(根存储),并应用到整个应用
- app.use(createPinia())
- app.mount('#app')
复制代码
这一步完成后,任意组件都可以通过 useXxxStore() 拿到同一份全局状态。
二、store 的定义方式
store 是保存状态与业务逻辑的实体,不与组件树绑定,承载的是全局状态,可以理解为“永远存在的组件”,每个组件都能读写它。store 内部有三个概念:state、getters、actions,分别对应组件里的 data、computed、methods。
工程里通常在 src 下新建 store 目录,再建 store.js 或按业务拆分文件。定义统一使用 defineStore(name, function | options):
1. 参数 name:store 名称,必填且全局唯一;
2. 参数 function | options:对象形式即选项式写法,在对象里配置 state、getters、actions;函数形式即组合式写法,函数内定义响应式变量与方法,最后 return 出去,其中 ref() 相当于 state,computed() 相当于 getters,function 相当于 actions。
建议把返回值命名为 use...Store,便于在组件里识别:
- import { defineStore } from "pinia"
- import { computed, ref } from 'vue'
- // 参数一:名字,必填且唯一
- // 参数二:组合式写法采用函数形式
- export const useStore = defineStore('main', () => {
- // ref 变量 —— state
- // computed() 计算属性 —— getters
- // function 函数 —— actions
- return {
- // 需要共享出去的变量、计算属性、函数
- }
- })
复制代码
三、state:定义与组件访问
state 是 store 的核心,存放共享数据。选项式写法里 state 是一个返回对象的函数;组合式写法里就是函数内部定义的 ref 变量,最后 return 暴露。例如:
- import { defineStore } from "pinia"
- import { ref } from 'vue'
- export const useUserStore = defineStore('user', () => {
- const age = ref(27)
- const level = ref(5)
- const account = ref('SD78906')
- const nickname = ref('一个好人')
- // 暴露数据,供组件共享
- return { age, level, account, nickname }
- })
复制代码
在组合式 API 组件中,直接引入对应 store,通过 store 实例读写 state 即可。如果希望在本组件里用自定义变量名接收 state 并保持响应式,有两种推荐做法:
1. computed(() => store.dataName):具备响应式,但只读,赋值会失效或报错;
2. storeToRefs(store):从 store 解构出 state,保持响应式,可修改,且支持重命名。
- <script setup>
- import { useUserStore } from '@/store/useUserStore'
- import { storeToRefs } from 'pinia'
- import { computed } from 'vue'
- import UserVue from '@/components/User.vue'
- // 获取 store 实例
- const user_store = useUserStore()
- // 方式一:computed 映射,响应式但只读
- const user_age = computed(() => user_store.age)
- const user_nickname = computed(() => user_store.nickname)
- // 方式二:storeToRefs 解构,响应式且可修改,可自定义名称
- const {
- age,
- nickname: userNickname
- } = storeToRefs(user_store)
- </script>
- <template>
- <UserVue></UserVue>
- <h2>从 store 直接取 state</h2>
- <ul>
- <li>年龄:{{ user_store.age }}</li>
- <li>昵称:{{ user_store.nickname }}</li>
- </ul>
- <button @click="user_store.age += 10">更改年龄</button>
- <hr>
- <h2>computed 映射为计算属性</h2>
- <ul>
- <li>年龄:{{ user_age }}</li>
- <li>昵称:{{ user_nickname }}</li>
- </ul>
- <hr>
- <h2>storeToRefs 解构成自己的数据</h2>
- <ul>
- <li>年龄:{{ age }}</li>
- <li>昵称:{{ userNickname }}</li>
- </ul>
- <button @click="age += 10">更改年龄</button>
- <button @click="userNickname += '='">更改昵称</button>
- </template>
复制代码
子组件同样可以只解构自己关心的字段,配合 storeToRefs 使用即可实现跨组件同步:
- <script setup>
- import { useUserStore } from '@/store/useUserStore'
- import { storeToRefs } from 'pinia'
- const user_store = useUserStore()
- const { age, level } = storeToRefs(user_store)
- </script>
- <template>
- <ul>
- <li>年龄:{{ age }}</li>
- <li>等级:{{ level }}</li>
- </ul>
- </template>
复制代码
四、getters:派生数据与只读约束
getters 是由 state 计算得到的共享数据,依赖变化时会重新计算,属于只读数据,组件和 store 自身都不应直接修改它。
选项式写法中,getters 声明的函数即计算属性,函数内可用 this 拿到 store 实例,也可以通过函数的第一个参数拿到 store 实例;如果写成箭头函数就无法使用 this,此时必须依靠第一个参数传入 store。组合式写法中则直接用 computed() 计算并 return:
- import { defineStore } from "pinia"
- import { computed, ref } from 'vue'
- export const useUserStore = defineStore('user', () => {
- const birthday = ref('1992-12-27')
- const age = ref(30)
- // 计算得到的共享数据,只读,依赖变化时会重新计算
- const month = computed(() => birthday.value.split('-')[1])
- const ageState = computed(() => {
- if (age.value < 18) return '未成年'
- if (age.value < 35) return '青年'
- if (age.value < 50) return '中年'
- if (age.value >= 50) return '老年'
- })
- return { birthday, age, month, ageState }
- })
复制代码
组件里访问 getters 与访问 state 类似:组合式 API 中直接通过 store 实例读取,或用 computed 映射,或用 storeToRefs 解构(解构出来的仍是计算属性,只读)。选项式 API 中可用 mapState() 帮助器映射为只读计算属性;如果误用 mapWritableState() 把 getters 映射成可写属性,虽然仍有响应式,但一旦赋值就会报错,属于典型误用。
- <script setup>
- import { useUserStore } from '@/store/useUserStore'
- import { storeToRefs } from 'pinia'
- import { computed } from 'vue'
- const user_store = useUserStore()
- // computed 映射为本地计算属性,只读
- const birthday_month = computed(() => user_store.month)
- const user_age_state = computed(() => user_store.ageState)
- // storeToRefs 解构 getters,响应式但只读
- const { month, ageState: userAgeState } = storeToRefs(user_store)
- // state 可正常解构并修改
- const { birthday, age } = storeToRefs(user_store)
- </script>
- <template>
- <h3>通过 store 直接获取 getters</h3>
- <ul>
- <li>月份:{{ user_store.month }}</li>
- <li>年龄阶段:{{ user_store.ageState }}</li>
- </ul>
- <h3>通过 storeToRefs 映射为自己的计算属性</h3>
- <ul>
- <li>月份:{{ month }}</li>
- <li>年龄阶段:{{ userAgeState }}</li>
- </ul>
- <hr>
- 生日:<input type="date" v-model="birthday">
- 年龄:<input type="number" min="1" max="120" v-model="age">
- </template>
复制代码
当修改 birthday 或 age 时,month 与 ageState 会自动重算,这正是“改 state、不动 getter”的正确用法。
五、actions:业务逻辑与异步调用
actions 通常是对 state 做修改的业务逻辑函数,也可以是异步的,内部可以 await 任意接口调用或其他操作。
选项式写法在 actions 中通过 this 获取整个 store 实例;组合式写法则是声明普通函数并 return 暴露,函数内直接操作 ref 的 .value,不需要 this。写函数时注意形参不要和 ref 变量同名,否则容易出现赋值互相覆盖的问题。
- import { defineStore } from "pinia"
- import { ref } from 'vue'
- export const useUserStore = defineStore('user', () => {
- const nickname = ref('一个好人')
- const age = ref(20)
- // 形参名不要与 ref 名冲突
- function setUserInfo(user_nickname, user_age) {
- nickname.value = user_nickname
- age.value = user_age
- }
- function setUserInfoByObject(user) {
- nickname.value = user.nickname
- age.value = user.age
- }
- // 暴露函数即共享 actions
- return { nickname, age, setUserInfo, setUserInfoByObject }
- })
复制代码
组件中调用有两种粒度:直接通过 store 实例调用,或者把 actions 解构成组件自己的函数并自定义名称。注意解构 actions 不能用 storeToRefs,直接从 store 解构即可。
- <script setup>
- import { useUserStore } from '@/store/useUserStore'
- import { storeToRefs } from 'pinia'
- const user_store = useUserStore()
- const { nickname, age } = storeToRefs(user_store)
- // 解构 actions 并自定义函数名,不使用 storeToRefs
- const { setUserInfo, setUserInfoByObject: set_user_info_object } = user_store
- </script>
- <template>
- <ul>
- <li>昵称:{{ nickname }}</li>
- <li>年龄:{{ age }}</li>
- </ul>
- <span>直接使用 store 执行 actions:</span>
- <button @click="user_store.setUserInfo('Tom', 15)">修改信息</button>
- <button @click="user_store.setUserInfoByObject({ nickname: 'John', age: 40 })">修改信息</button>
- <hr>
- <span>将 actions 映射成自己的函数:</span>
- <button @click="setUserInfo('Mike', 35)">修改信息</button>
- <button @click="set_user_info_object({ nickname: 'Yadown', age: 20 })">修改信息</button>
- </template>
复制代码
选项式 API 组件中,则可用 mapActions(storeObj, array | object) 把 actions 映射为组件方法:第一个参数是引入的 store 对象;第二个参数传字符串数组时,直接把同名 actions 映射为组件方法,不能自定义名称;传对象时,key 为当前组件自定义的函数名,value 为 store 中 actions 的函数名。
六、常见问题排查清单
1. 修改 getter 报错:getters 本质是计算属性,任何对 store 中 getter 的赋值都会报错,正确做法是修改它依赖的 state。
2. 用 storeToRefs 解构 actions 拿到空值:storeToRefs 只处理 state 与 getters,actions 直接从 store 解构。
3. 用 computed 接收 state 后赋值无效:computed 返回只读引用,需要可写就用 storeToRefs。
4. 选项式写法中 getters 里 this 为 undefined:说明用了箭头函数,改为普通函数或使用第一个参数获取 store 实例。
5. actions 形参与 ref 同名导致数据错乱:重命名形参,如 user_nickname、user_age。
6. defineStore 第一个参数重复:name 必须全局唯一,多模块拆分时按业务命名。
七、适用场景小结
需要跨组件、跨页面共享登录用户信息、全局配置、购物车、字典数据等场景,用 Pinia 一个独立 store 就能完成,不必像 Vuex 那样套 modules。组合式项目建议统一采用函数形式定义 store,把 ref 当 state、computed 当 getters、普通函数当 actions,读写边界清晰,配合 TypeScript 的类型推断也更省心。 |