当前位置: 首页 > news >正文

Vue3 Pinia 状态管理规范:状态拆分、Actions 写法、持久化实战,避坑状态污染|状态管理与路由规范篇

【Pinia 状态管理】+【中后台前端实战】:从状态拆分、actions 规范到持久化落地,掌握可维护状态管理写法,避开状态污染与全局状态地狱!

  • Pinia 官方文档

📑 文章目录

  • 一、开篇:为什么还要学 Pinia 规范?
  • 二、Pinia 基础速览(5 分钟扫盲)
    • 2.1 和 Vuex 的区别(你只需要记住这些)
    • 2.2 一个最小的 Store 长什么样?
  • 三、核心规范一:状态拆分 —— 按领域还是按功能?
    • 3.1 问题:一个 store 塞满所有状态会怎样?
    • 3.2 推荐做法:按业务领域拆分
    • 3.3 什么时候需要跨 store 拿数据?
  • 四、核心规范二:Actions 怎么写才不乱?
    • 4.1 三个原则
    • 4.2 标准异步 Action 写法
    • 4.3 在组件里怎么用?
    • 4.4 容易踩的坑:直接解构 store
  • 五、核心规范三:持久化 —— 刷新不丢数据
    • 5.1 什么需要持久化?
    • 5.2 方案一:pinia-plugin-persistedstate(推荐)
    • 5.3 方案二:自己封装(理解原理)
    • 5.4 持久化需要注意的点
  • 六、核心规范四:避免状态污染 —— 深浅拷贝要分清
    • 6.1 什么是状态污染?
    • 6.2 正确写法:深拷贝、明确归属
    • 6.3 Getter 返回引用时要注意
  • 七、完整示例:用户 + 应用配置 + 持久化
  • 八、小结速查表
  • 九、延伸阅读建议
  • 🔍 系列模块导航

同学们好,我是 Eugene(尤金),一名多年中后台前端开发工程师。

(Eugene 发音 /juːˈdʒiːn/,大家怎么顺口怎么叫就好)

很多前端开发者都会遇到一个瓶颈:

代码能跑,但不够规范;功能能实现,但维护起来特别痛苦;一个人写没问题,一到团队协作就各种混乱、踩坑、返工。

想写出干净、优雅、可维护的专业代码,靠的不是天赋,而是体系化的规范 + 真实实战经验

这一系列《前端规范实战》,我会用大白话 + 真实业务场景,不讲玄学、不堆理论,只分享能直接落地的规范、标准与避坑指南。

帮你从「会写代码」真正升级为「会写优质、可维护、团队级别的代码」。


一、开篇:为什么还要学 Pinia 规范?

Vue 3 的推荐状态管理是 Pinia。如果只会写业务,但不清楚怎么拆分 store、怎么设计 actions、如何做持久化和避免污染,项目一复杂就会变成“全局状态地狱”。

这篇文章从日常开发怎么选、为什么这么选、容易踩哪些坑三个角度,帮你把 Pinia 用得更稳、更清晰。

适用读者:

  • 会写 JS,但对状态管理概念还比较模糊的人
  • 想从零搭建 Vue 3 项目的初学者
  • 有经验但想梳理一遍习惯的开发者

⬆ 返回目录


二、Pinia 基础速览(5 分钟扫盲)

2.1 和 Vuex 的区别(你只需要记住这些)

对比项VuexPinia
API 风格mutation / action 分离只有 actions
模块化modules注册每个 store 是独立文件
TypeScript支持一般支持更好
包体积较大更小
组合式写法需要 mapState 等直接用storeToRefs

一句话:Pinia 更简单,功能不弱,官方推荐优先用它。

⬆ 返回目录

2.2 一个最小的 Store 长什么样?

// stores/counter.jsimport{defineStore}from'pinia'exportconstuseCounterStore=defineStore('counter',{state:()=>({count:0,}),getters:{doubleCount:(state)=>state.count*2,},actions:{increment(){this.count++},},})

在组件里用:

<scriptsetup>import{useCounterStore}from'@/stores/counter'constcounterStore=useCounterStore()</script><template><div>{{ counterStore.count }}</div><button@click="counterStore.increment">+1</button></template>

defineStore第一个参数是 store 的唯一 id,第二个是配置对象(或 setup 函数)。下文会围绕这个基础,讲怎么拆、怎么改、怎么持久化。

⬆ 返回目录


三、核心规范一:状态拆分 —— 按领域还是按功能?

3.1 问题:一个 store 塞满所有状态会怎样?

  • 文件超大,难以维护
  • 修改一处,到处影响
  • 团队协作容易冲突
  • 难以做按需持久化

⬆ 返回目录

3.2 推荐做法:按业务领域拆分

原则:一个 store 只管一类业务,边界清晰。

示例项目结构:

stores/ ├── index.js # 统一导出 ├── user.js # 用户:登录、权限、资料 ├── app.js # 应用:主题、语言、布局 ├── cart.js # 购物车 └── product.js # 商品列表、搜索条件

错误示范:

// ❌ 错误:一个 store 包揽所有exportconstuseMainStore=defineStore('main',{state:()=>({user:null,token:'',theme:'light',cartItems:[],productList:[],// ... 越来越多}),})

正确示范:

// ✅ stores/user.js - 只管用户相关exportconstuseUserStore=defineStore('user',{state:()=>({userInfo:null,token:'',permissions:[],}),// ...})// ✅ stores/app.js - 只管应用配置exportconstuseAppStore=defineStore('app',{state:()=>({theme:'light',language:'zh-CN',sidebarCollapsed:false,}),// ...})

⬆ 返回目录

3.3 什么时候需要跨 store 拿数据?

用 getter 引用其他 store:

// stores/cart.jsimport{defineStore}from'pinia'import{useUserStore}from'./user'exportconstuseCartStore=defineStore('cart',{state:()=>({items:[],}),getters:{// 跨 store 获取用户是否登录canCheckout:()=>{constuserStore=useUserStore()returnuserStore.token&&userStore.userInfo},},})

要点:只在 getter 里读其他 store,不要在 action 里直接改别的 store 的 state。

⬆ 返回目录


四、核心规范二:Actions 怎么写才不乱?

4.1 三个原则

  1. 异步放在 actions 里:请求、副作用都集中到 actions
  2. 状态只通过 actions 改:组件不要直接改 state
  3. 一个 action 只做一件事:粒度小、好测试、易复用

⬆ 返回目录

4.2 标准异步 Action 写法

// stores/user.jsimport{defineStore}from'pinia'import{loginApi,getUserInfoApi}from'@/api/user'exportconstuseUserStore=defineStore('user',{state:()=>({userInfo:null,token:'',loading:false,error:null,}),actions:{asynclogin(credentials){this.loading=truethis.error=nulltry{constres=awaitloginApi(credentials)this.token=res.data.tokenthis.userInfo=res.data.userreturn{success:true}}catch(err){this.error=err.message||'登录失败'return{success:false,message:this.error}}finally{this.loading=false}},asyncfetchUserInfo(){if(!this.token)returntry{constres=awaitgetUserInfoApi()this.userInfo=res.data}catch(err){this.error=err.message// 可以考虑清除 token,触发重新登录}},logout(){this.token=''this.userInfo=nullthis.error=null},},})

要点:用loading/error描述过程,用try/catch/finally统一处理。

⬆ 返回目录

4.3 在组件里怎么用?

<scriptsetup>import{storeToRefs}from'pinia'import{useUserStore}from'@/stores/user'constuserStore=useUserStore()// 只把需要响应式的拿出来,避免整个 store 被解构const{userInfo,loading,error}=storeToRefs(userStore)asyncfunctionhandleLogin(){constresult=awaituserStore.login({username,password})if(result.success){// 跳转首页等}}</script><template><form@submit.prevent="handleLogin"><divv-if="error"class="error">{{ error }}</div><button:disabled="loading">{{ loading ? '登录中...' : '登录' }}</button></form></template>

⬆ 返回目录

4.4 容易踩的坑:直接解构 store

// ❌ 错误:直接解构会丢失响应式const{count,increment}=useCounterStore()// count 是普通值,改了不会更新视图// ✅ 正确:用 storeToRefs 拿 state/getterconst{count}=storeToRefs(useCounterStore())// actions 直接从 store 上取const{increment}=useCounterStore()

⬆ 返回目录


五、核心规范三:持久化 —— 刷新不丢数据

5.1 什么需要持久化?

  • 需要持久化:token、主题、语言、布局偏好、购物车
  • 一般不需要:临时列表、弹窗状态、加载状态

⬆ 返回目录

5.2 方案一:pinia-plugin-persistedstate(推荐)

安装:

npminstallpinia-plugin-persistedstate

配置:

// main.jsimport{createApp}from'vue'import{createPinia}from'pinia'importpiniaPluginPersistedstatefrom'pinia-plugin-persistedstate'importAppfrom'./App.vue'constpinia=createPinia()pinia.use(piniaPluginPersistedstate)createApp(App).use(pinia).mount('#app')

在 store 里开启:

// stores/user.jsexportconstuseUserStore=defineStore('user',{state:()=>({token:'',userInfo:null,}),persist:true,// 全部持久化})

更细粒度的配置:

exportconstuseUserStore=defineStore('user',{state:()=>({token:'',userInfo:null,tempData:null,// 不想持久化}),persist:{key:'my-app-user',// 存到 localStorage 的 keystorage:localStorage,// 默认就是 localStoragepaths:['token','userInfo'],// 只持久化这两个字段},})

⬆ 返回目录

5.3 方案二:自己封装(理解原理)

// utils/persist.jsexportfunctionusePersist(store,key=store.$id){// 初始化时从 localStorage 恢复constsaved=localStorage.getItem(key)if(saved){try{store.$patch(JSON.parse(saved))}catch(e){console.warn('恢复状态失败',e)}}// 监听变化并保存store.$subscribe((mutation,state)=>{localStorage.setItem(key,JSON.stringify(state))})}

main.js或入口里按需调用:

constuserStore=useUserStore()usePersist(userStore,'my-user')

⬆ 返回目录

5.4 持久化需要注意的点

  1. 不要存敏感数据:token 可以用,但尽量用 httpOnly cookie 更安全
  2. 注意体积:避免把大数组、大对象全丢进 localStorage
  3. 版本兼容:结构变了,要做兼容或清空旧数据

⬆ 返回目录


六、核心规范四:避免状态污染 —— 深浅拷贝要分清

6.1 什么是状态污染?

组件或接口拿到的是 state 的引用,直接改了这个引用,就会污染 store,导致:

  • 状态难追踪
  • 撤销/重做不好做
  • 时间旅行调试失效

典型错误:

// ❌ 污染:直接改传入的对象actions:{updateUser(updates){// updates 可能是外部传入的,直接合并会污染Object.assign(this.userInfo,updates)},}// 组件里constform={name:'张三',age:20}userStore.updateUser(form)// form 可能被 store 内部修改影响

⬆ 返回目录

6.2 正确写法:深拷贝、明确归属

actions:{updateUser(updates){if(!this.userInfo)return// 基于当前 state 做拷贝,再合并this.userInfo={...this.userInfo,...JSON.parse(JSON.stringify(updates)),}},setCartItems(newItems){// 用拷贝,避免外部直接改 storethis.items=[...newItems]},}

复杂对象可以用structuredClonelodash.cloneDeep

⬆ 返回目录

6.3 Getter 返回引用时要注意

getters:{// ❌ 返回内部数组引用,外部修改会污染cartItems:(state)=>state.items,// ✅ 返回拷贝,外部改不影响 storecartItemsSafe:(state)=>[...state.items],}

如果 getter 只是做过滤、映射,建议返回新数组/新对象。

⬆ 返回目录


七、完整示例:用户 + 应用配置 + 持久化

下面是一个可直接拷贝、改名的示例。

// stores/user.jsimport{defineStore}from'pinia'import{loginApi,getUserInfoApi}from'@/api/user'exportconstuseUserStore=defineStore('user',{state:()=>({token:'',userInfo:null,loading:false,error:null,}),getters:{isLoggedIn:(state)=>!!state.token,userName:(state)=>state.userInfo?.name??'',},actions:{asynclogin(credentials){this.loading=truethis.error=nulltry{constres=awaitloginApi(credentials)this.token=res.data.tokenawaitthis.fetchUserInfo()return{success:true}}catch(err){this.error=err.message||'登录失败'return{success:false,message:this.error}}finally{this.loading=false}},asyncfetchUserInfo(){if(!this.token)returntry{constres=awaitgetUserInfoApi()this.userInfo={...res.data}}catch{this.logout()}},logout(){this.token=''this.userInfo=nullthis.error=null},},persist:{key:'app-user',paths:['token','userInfo'],},})
// stores/app.jsimport{defineStore}from'pinia'exportconstuseAppStore=defineStore('app',{state:()=>({theme:'light',sidebarCollapsed:false,}),actions:{toggleTheme(){this.theme=this.theme==='light'?'dark':'light'},toggleSidebar(){this.sidebarCollapsed=!this.sidebarCollapsed},},persist:true,})
<!-- components/UserProfile.vue --><scriptsetup>import{storeToRefs}from'pinia'import{useUserStore}from'@/stores/user'constuserStore=useUserStore()const{userInfo,loading,error}=storeToRefs(userStore)functionhandleLogout(){userStore.logout()}</script><template><divv-if="userInfo"><p>欢迎,{{ userInfo.name }}</p><button:disabled="loading"@click="handleLogout">退出</button></div><pv-if="error"class="error">{{ error }}</p></template>

⬆ 返回目录


八、小结速查表

场景推荐做法避免
拆分按业务领域拆 store一个 store 包所有状态
异步统一在 actions 里,用 loading/error在组件里散落请求
解构storeToRefs拿 state/getter直接解构 store
持久化pinia-plugin-persistedstate+ paths全量持久化大对象
防污染用拷贝更新 state,getter 返回拷贝直接改引用、getter 返回内部引用

⬆ 返回目录

🔍 系列模块导航

📝 状态管理与路由规范

一、《Vue3 Pinia 状态管理规范:状态拆分、Actions 写法、持久化实战,避坑状态污染|状态管理与路由规范篇》
二、《Vue3 Pinia 状态管理规范:何时用 Pinia 何时用本地状态|状态管理与路由规范篇》
三、《Vue Router 实战规范:path/name/meta 配置 + 动态 / 嵌套路由,统一团队标准|状态管理与路由规范篇》
四、《Vue3 + Vue Router + Pinia 路由守卫规范:beforeEach 应做 / 不应做,避死循环、防重复请求|状态管理与路由规范篇》
五、《Vue keep-alive 实战避坑:include/exclude + 路由 meta 标记,中后台路由缓存精准可控|状态管理与路由规范篇》

👉 跟着系列慢慢学,把技术功底扎扎实实地打牢~

📚 系列总览

前端规范实战系列」正在持续更新中,后续会整理一篇《前端规范实战系列全系列目录导航》,包含每篇文章简介 + 直达链接,方便大家按顺序、体系化学习。

更新中,敬请期待~

⬆ 返回目录


技术成长,从来不是比谁写得快,而是比谁写得稳、规范、可维护

哪怕每次只吃透一条规范,长期下来,差距会非常明显。

后续我会持续更新前端规范、工程化、可维护代码相关实战干货,帮你告别面条代码、维护噩梦,在开发与面试中更有底气。

觉得有用欢迎点赞 + 收藏 + 关注,不错过每一篇实战内容。

我是 Eugene,与你一起写规范、写优质代码,我们下篇干货见~

http://www.jsqmd.com/news/523582/

相关文章:

  • 品牌方如何利用TRO有效打击线上假货
  • 高光谱遥感影像分类必备:五大经典数据集详解与避坑指南
  • AMCL定位避坑指南:如何解决ROS导航中粒子发散问题(附可视化调试方法)
  • 洗板机品牌推荐与选购指南:国产哪家强?性价比之王是它! - 品牌推荐大师
  • 2026热门浓香白酒选款指南,性价比高的低度顺口浓香白酒品牌汇总 - 博客万
  • ggplot2进阶:打造可发表级别的单细胞UMAP可视化
  • Amazon Linux 2023 上 Docker 安装避坑指南:从零到一键部署
  • 从沉默到自信表达,大咖素质训练营的教育智慧
  • 黑客大佬私藏!这20款神级工具,小白也能玩转网络安全?
  • 收藏!小白程序员必看:轻松入门大模型(训练、微调与推理全解析)
  • 3个维度掌握Real-ESRGAN-ncnn-vulkan:从图像模糊到细节清晰的超分辨率实践指南
  • 树莓派4B串口通信实战:从硬件配置到软件调试的完整避坑指南
  • 【统信UOS实战】离线部署MySQL 5.7:从依赖缺失到服务自启的完整避坑指南
  • 嵌入式按键消抖与GPIO输入可靠性设计
  • 告别蓝屏!GHO镜像安装Windows 7的5个关键步骤与常见错误排查指南
  • C语言入门必备!掌握开发环境搭建及C-Free 5安装要点
  • 中国罗茨鼓风机市场占有率与品牌竞争力分析报告
  • AI审核加持的IACheck:塔吊与施工电梯安全监测系统检测报告如何实现高效合规与风险可控
  • MQTT 3.1.1协议实战:从零搭建物联网消息服务器(附Python代码示例)
  • 保姆级教程:用STM32CubeMX配置STM32F429的串口DMA双缓存,并集成FreeRTOS消息队列
  • TMS320F28P550开发板硬件设计与实时控制实践
  • Maxwell16.0电机仿真避坑指南:从空载到有载的完整流程(附常见报错解决方案)
  • 收藏!面24家大模型企业拿9个offer,小白程序员必看的入行干货+高频考点
  • 2026年3月口碑好的振动平台品牌大盘点,正规的振动平台源头厂家口碑分析特昌振动诚信务实提供高性价比服务 - 品牌推荐师
  • FPGA新手避坑指南:手把手教你用Verilog仿真SPI通信(附Testbench代码)
  • FireRedASR Pro实战教程:用语音识别辅助学习,听课录音一键整理
  • Mac OS X系统下用Xcode创建项目运行C语言程序教程(适合初学者)
  • 不止于HELLO:用RH850F1KMS1的UART DMA实现稳定可靠的长数据帧收发
  • 3个技巧让MuJoCo物理仿真性能翻倍:从单机到云端的实战指南
  • Python+Selenium实战:手把手教你破解大麦网反爬机制(附完整代码)