查看: 276|回复: 3

Vue3 Pinia状态管理store与actions用法详解

[复制链接]
发表于 2 小时前 | 显示全部楼层 |阅读模式
Pinia 是 Vue 官方提供的最新状态管理库,用来替代 Vuex。它同时支持 Vue2 与 Vue3(主要面向 Vue3),并且对 TypeScript 的类型推断更友好。相比 Vuex,Pinia 去掉了 mutation 与 modules 概念,每个 store 都是独立模块,可以创建多个全局仓库,不需要再写嵌套模块,API 更少、结构更直观。本文按安装、store 定义、state、getters、actions 的顺序,梳理一套可直接落到项目里的写法,并记录几个容易踩坑的细节。

一、安装与根存储挂载

安装命令(任选其一):
  1. npm install pinia
  2. pnpm add pinia
复制代码

安装完成后需要在入口文件中创建 Pinia 根存储并挂载到应用上,main.js 写法如下:
  1. import { createApp } from 'vue'
  2. import App from './App.vue'
  3. // 引入 createPinia 函数
  4. import { createPinia } from 'pinia'
  5. const app = createApp(App)
  6. // 使用 createPinia() 创建 Pinia(根存储),并应用到整个应用
  7. app.use(createPinia())
  8. 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,便于在组件里识别:
  1. import { defineStore } from "pinia"
  2. import { computed, ref } from 'vue'
  3. // 参数一:名字,必填且唯一
  4. // 参数二:组合式写法采用函数形式
  5. export const useStore = defineStore('main', () => {
  6.   // ref 变量 —— state
  7.   // computed() 计算属性 —— getters
  8.   // function 函数 —— actions
  9.   return {
  10.     // 需要共享出去的变量、计算属性、函数
  11.   }
  12. })
复制代码

三、state:定义与组件访问

state 是 store 的核心,存放共享数据。选项式写法里 state 是一个返回对象的函数;组合式写法里就是函数内部定义的 ref 变量,最后 return 暴露。例如:
  1. import { defineStore } from "pinia"
  2. import { ref } from 'vue'
  3. export const useUserStore = defineStore('user', () => {
  4.   const age = ref(27)
  5.   const level = ref(5)
  6.   const account = ref('SD78906')
  7.   const nickname = ref('一个好人')
  8.   // 暴露数据,供组件共享
  9.   return { age, level, account, nickname }
  10. })
复制代码

在组合式 API 组件中,直接引入对应 store,通过 store 实例读写 state 即可。如果希望在本组件里用自定义变量名接收 state 并保持响应式,有两种推荐做法:

1. computed(() => store.dataName):具备响应式,但只读,赋值会失效或报错;
2. storeToRefs(store):从 store 解构出 state,保持响应式,可修改,且支持重命名。
  1. <script setup>
  2. import { useUserStore } from '@/store/useUserStore'
  3. import { storeToRefs } from 'pinia'
  4. import { computed } from 'vue'
  5. import UserVue from '@/components/User.vue'
  6. // 获取 store 实例
  7. const user_store = useUserStore()
  8. // 方式一:computed 映射,响应式但只读
  9. const user_age = computed(() => user_store.age)
  10. const user_nickname = computed(() => user_store.nickname)
  11. // 方式二:storeToRefs 解构,响应式且可修改,可自定义名称
  12. const {
  13.   age,
  14.   nickname: userNickname
  15. } = storeToRefs(user_store)
  16. </script>
  17. <template>
  18.   <UserVue></UserVue>
  19.   <h2>从 store 直接取 state</h2>
  20.   <ul>
  21.     <li>年龄:{{ user_store.age }}</li>
  22.     <li>昵称:{{ user_store.nickname }}</li>
  23.   </ul>
  24.   <button @click="user_store.age += 10">更改年龄</button>
  25.   <hr>
  26.   <h2>computed 映射为计算属性</h2>
  27.   <ul>
  28.     <li>年龄:{{ user_age }}</li>
  29.     <li>昵称:{{ user_nickname }}</li>
  30.   </ul>
  31.   <hr>
  32.   <h2>storeToRefs 解构成自己的数据</h2>
  33.   <ul>
  34.     <li>年龄:{{ age }}</li>
  35.     <li>昵称:{{ userNickname }}</li>
  36.   </ul>
  37.   <button @click="age += 10">更改年龄</button>
  38.   <button @click="userNickname += '='">更改昵称</button>
  39. </template>
复制代码

子组件同样可以只解构自己关心的字段,配合 storeToRefs 使用即可实现跨组件同步:
  1. <script setup>
  2. import { useUserStore } from '@/store/useUserStore'
  3. import { storeToRefs } from 'pinia'
  4. const user_store = useUserStore()
  5. const { age, level } = storeToRefs(user_store)
  6. </script>
  7. <template>
  8.   <ul>
  9.     <li>年龄:{{ age }}</li>
  10.     <li>等级:{{ level }}</li>
  11.   </ul>
  12. </template>
复制代码

四、getters:派生数据与只读约束

getters 是由 state 计算得到的共享数据,依赖变化时会重新计算,属于只读数据,组件和 store 自身都不应直接修改它。

选项式写法中,getters 声明的函数即计算属性,函数内可用 this 拿到 store 实例,也可以通过函数的第一个参数拿到 store 实例;如果写成箭头函数就无法使用 this,此时必须依靠第一个参数传入 store。组合式写法中则直接用 computed() 计算并 return:
  1. import { defineStore } from "pinia"
  2. import { computed, ref } from 'vue'
  3. export const useUserStore = defineStore('user', () => {
  4.   const birthday = ref('1992-12-27')
  5.   const age = ref(30)
  6.   // 计算得到的共享数据,只读,依赖变化时会重新计算
  7.   const month = computed(() => birthday.value.split('-')[1])
  8.   const ageState = computed(() => {
  9.     if (age.value < 18) return '未成年'
  10.     if (age.value < 35) return '青年'
  11.     if (age.value < 50) return '中年'
  12.     if (age.value >= 50) return '老年'
  13.   })
  14.   return { birthday, age, month, ageState }
  15. })
复制代码

组件里访问 getters 与访问 state 类似:组合式 API 中直接通过 store 实例读取,或用 computed 映射,或用 storeToRefs 解构(解构出来的仍是计算属性,只读)。选项式 API 中可用 mapState() 帮助器映射为只读计算属性;如果误用 mapWritableState() 把 getters 映射成可写属性,虽然仍有响应式,但一旦赋值就会报错,属于典型误用。
  1. <script setup>
  2. import { useUserStore } from '@/store/useUserStore'
  3. import { storeToRefs } from 'pinia'
  4. import { computed } from 'vue'
  5. const user_store = useUserStore()
  6. // computed 映射为本地计算属性,只读
  7. const birthday_month = computed(() => user_store.month)
  8. const user_age_state = computed(() => user_store.ageState)
  9. // storeToRefs 解构 getters,响应式但只读
  10. const { month, ageState: userAgeState } = storeToRefs(user_store)
  11. // state 可正常解构并修改
  12. const { birthday, age } = storeToRefs(user_store)
  13. </script>
  14. <template>
  15.   <h3>通过 store 直接获取 getters</h3>
  16.   <ul>
  17.     <li>月份:{{ user_store.month }}</li>
  18.     <li>年龄阶段:{{ user_store.ageState }}</li>
  19.   </ul>
  20.   <h3>通过 storeToRefs 映射为自己的计算属性</h3>
  21.   <ul>
  22.     <li>月份:{{ month }}</li>
  23.     <li>年龄阶段:{{ userAgeState }}</li>
  24.   </ul>
  25.   <hr>
  26.   生日:<input type="date" v-model="birthday">
  27.   年龄:<input type="number" min="1" max="120" v-model="age">
  28. </template>
复制代码

当修改 birthday 或 age 时,month 与 ageState 会自动重算,这正是“改 state、不动 getter”的正确用法。

五、actions:业务逻辑与异步调用

actions 通常是对 state 做修改的业务逻辑函数,也可以是异步的,内部可以 await 任意接口调用或其他操作。

选项式写法在 actions 中通过 this 获取整个 store 实例;组合式写法则是声明普通函数并 return 暴露,函数内直接操作 ref 的 .value,不需要 this。写函数时注意形参不要和 ref 变量同名,否则容易出现赋值互相覆盖的问题。
  1. import { defineStore } from "pinia"
  2. import { ref } from 'vue'
  3. export const useUserStore = defineStore('user', () => {
  4.   const nickname = ref('一个好人')
  5.   const age = ref(20)
  6.   // 形参名不要与 ref 名冲突
  7.   function setUserInfo(user_nickname, user_age) {
  8.     nickname.value = user_nickname
  9.     age.value = user_age
  10.   }
  11.   function setUserInfoByObject(user) {
  12.     nickname.value = user.nickname
  13.     age.value = user.age
  14.   }
  15.   // 暴露函数即共享 actions
  16.   return { nickname, age, setUserInfo, setUserInfoByObject }
  17. })
复制代码

组件中调用有两种粒度:直接通过 store 实例调用,或者把 actions 解构成组件自己的函数并自定义名称。注意解构 actions 不能用 storeToRefs,直接从 store 解构即可。
  1. <script setup>
  2. import { useUserStore } from '@/store/useUserStore'
  3. import { storeToRefs } from 'pinia'
  4. const user_store = useUserStore()
  5. const { nickname, age } = storeToRefs(user_store)
  6. // 解构 actions 并自定义函数名,不使用 storeToRefs
  7. const { setUserInfo, setUserInfoByObject: set_user_info_object } = user_store
  8. </script>
  9. <template>
  10.   <ul>
  11.     <li>昵称:{{ nickname }}</li>
  12.     <li>年龄:{{ age }}</li>
  13.   </ul>
  14.   <span>直接使用 store 执行 actions:</span>
  15.   <button @click="user_store.setUserInfo('Tom', 15)">修改信息</button>
  16.   <button @click="user_store.setUserInfoByObject({ nickname: 'John', age: 40 })">修改信息</button>
  17.   <hr>
  18.   <span>将 actions 映射成自己的函数:</span>
  19.   <button @click="setUserInfo('Mike', 35)">修改信息</button>
  20.   <button @click="set_user_info_object({ nickname: 'Yadown', age: 20 })">修改信息</button>
  21. </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 的类型推断也更省心。
回复

使用道具 举报

发表于 2 小时前 | 显示全部楼层

Re: Vue3 Pinia状态管理store与actions用法详解

感谢分享,这篇从安装挂载到 store 定义、再到 state 的写法梳理得很顺,尤其适合刚上手 Pinia 的人按顺序过一遍。组合式写法里把 ref 对应 state、computed 对应 getters、function 对应 actions 这个类比挺直观,比单纯讲概念好记多了。 state 那部分提到的两种映射方式很实用:computed 映射响应式但只读,适合只展示的场景;storeToRefs 解构出来保持响应式又可修改,还能重命名,正好覆盖两种需求。之前直接解构 store 拿变量会丢响应性,这个坑确实常见,文中点出来能帮不少人避开。 两个小建议供参考:一是 defineStore 的 name 全局唯一,按业务拆文件时命名最好带上业务前缀,避免后面模块多了撞名;二是 storeToRefs 主要解构 state 和 getters,actions 一般直接从 store 实例上调用,文中如果能顺带提一句,读者会更清楚边界。 期待后面的 getters 和 actions 部分,特别是 actions 里做异步请求、组件里调用时怎么处理 loading 和错误,以及组合式写法里 actions 内部直接引用 ref 还是走返回值,这些落到项目里都挺关键。
回复 支持 反对

使用道具 举报

发表于 2 小时前 | 显示全部楼层

Re: Vue3 Pinia状态管理store与actions用法详解

楼主这篇梳理得挺清楚的,按安装、定义、state、getters、actions 的顺序走,确实能直接照着落到项目里。 几个点我觉得写得比较到位: 一是把 Pinia 和 Vuex 的差异点明了。去掉 mutation 和 modules,每个 store 独立成一个模块,不用再写嵌套模块,对刚上手的人来说心智负担小很多,API 少了以后也更容易记住。 二是组合式写法的对应关系讲得很直观。ref 对应 state、computed 对应 getters、function 对应 actions,这个类比把选项式和组合式两种写法串起来了,之前看官方文档容易在两种写法之间来回切换搞混,这么一对照就清晰了。 三是 storeToRefs 和 computed 映射的区别。这点确实容易踩坑——用 computed 包一层虽然响应式还在,但其实是个只读的计算属性,想改值得回到 store 实例上操作;而 storeToRefs 解构出来的既能保持响应式又能改,还支持重命名,实际项目里用这个更省事。楼主把两种方式并排写出来,还配了组件示例,比只讲一种要实用。 另外提醒一句,“每个 store 都是独立模块、可以创建多个全局仓库”这点很关键,命名(defineStore 的第一个参数)全局唯一这点也值得强调,命名重复在调试工具里排查起来挺头疼的。 后面 getters 和 actions 的部分好
回复 支持 反对

使用道具 举报

发表于 2 小时前 | 显示全部楼层

Re: Vue3 Pinia状态管理store与actions用法详解

这篇整理得很系统,安装挂载、defineStore 的命名和两种写法、state 在组合式组件里的访问方式都讲得比较清楚。尤其是 computed 映射只读、storeToRefs 解构后保持响应式且可重命名这两个细节,实际项目里很容易混淆,这样对比着写很实用。按业务拆分 store、统一用 useXxxStore 命名也很利于维护。后面如果继续写 getters 和 actions,可以重点结合异步请求、loading 状态和错误处理来讲,这样整套状态管理的落地感会更强。
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-10-31 05:41 , Processed in 0.028579 second(s), 28 queries , Gzip On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部