尧图网站建设 尧图网络
  • 首页
  • 关于我们
  • 服务项目
  • 案例展示
  • 建站流程
  • 资讯中心
  • 联系我们
首页/资讯中心/详情

Vuex Modules 分层:UI状态、用户状态、权限状态如何各管各的

Vuex Modules 分层:UI状态、用户状态、权限状态如何各管各的
📅 发布时间:2026/7/6 4:42:21

一个大中后台系统,几十个状态变量,怎么分?

Vuex 官方文档告诉你用modules拆分 store,但没告诉你按什么原则分。全部放一个 module 里能跑,但随着页面增多,一个store/index.js可能膨胀到 500 行——找状态靠Ctrl+F,改一个 mutation 要翻半屏。

这套经典的中后台模板给出了一个非常清晰的划分标准:按"谁产生的"分,不按"谁用的"分。

四个 module 的职责地图

src/store/ ├── index.js ← require.context 自动注册所有 module ├── getters.js ← 全局 getter,所有组件使用的统一入口 └── modules/ ├── app.js ← UI 状态层 ├── user.js ← 认证状态层 ├── permission.js ← 权限控制层 └── tagsView.js ← 标签页状态层

module 1:app.js—— 只管界面长什么样

这是纯 UI 状态,不涉及任何业务数据:

// store/modules/app.js(简化)conststate={sidebar:{opened:true,// 侧边栏展开/折叠withoutAnimation:false// 是否跳过动画},device:'desktop',// 当前设备类型:desktop / mobilesize:'medium'// 全局组件尺寸:medium / small / mini}constmutations={TOGGLE_SIDEBAR(state){state.sidebar.opened=!state.sidebar.opened},CLOSE_SIDEBAR(state,withoutAnimation){state.sidebar.opened=falsestate.sidebar.withoutAnimation=withoutAnimation},TOGGLE_DEVICE(state,device){state.device=device}}

注意一个细节:sidebar不是简单的boolean,而是一个对象{ opened, withoutAnimation }。为什么?

因为折叠和展开需要的动画控制不同——移动端点击遮罩关闭时不需要动画(要立刻消失),PC 端点击 hamburger 按钮时要动画过渡。如果sidebar是裸boolean,你需要在调用方传animation参数,侵入到每个组件。包成对象后,调用方只需dispatch('app/closeSideBar', { withoutAnimation: true }),内部处理。

判断标准:如果一个状态的消费者多,且不同消费者需要不同的"附带信息",就包成对象;如果只有一个消费者,用裸值就行。

module 2:user.js—— 只管你是谁

用户模块存认证相关的全部状态:

// store/modules/user.js(简化)conststate={token:'',// 登录凭证name:'',// 用户姓名avatar:'',// 头像 URLroles:[],// 角色列表:['admin', 'editor']}constactions={login({commit},userInfo){returnapi.login(userInfo).then(res=>{commit('SET_TOKEN',res.token)})},logout({commit}){commit('SET_TOKEN','')},getInfo({commit}){returnapi.getUserInfo().then(res=>{commit('SET_NAME',res.name)commit('SET_AVATAR',res.avatar)commit('SET_ROLES',res.roles)})}}

用户模块的特点是:它是唯一一个涉及异步请求的 module(login、logout、getInfo 都调 API)。其他三个 module(app、permission、tagsView)都是同步的 UI 状态管理。

这是因为用户认证是应用的生命周期门槛——其他一切状态都在"用户已登录"的前提下存在。把认证逻辑单独隔离,意味着:

  • 如果认证方式换了(JWT → OAuth),只改这一个 module
  • 如果加新认证信息(如permissions字段),不影响 UI 层

module 3:permission.js—— 管你能看什么

权限模块只干一件事:根据用户角色,从全部路由里筛出"该用户有权访问的":

// store/modules/permission.js(简化)conststate={routes:[]// 动态计算后的可访问路由}// 递归过滤:只保留用户角色匹配的路由functionfilterByRole(routes,roles){returnroutes.filter(route=>{if(route.meta?.roles){if(!roles.some(r=>route.meta.roles.includes(r)))returnfalse}if(route.children){route.children=filterByRole(route.children,roles)}returntrue})}constactions={generateRoutes({commit},roles){letaccessedRoutesif(roles.includes('admin')){accessedRoutes=asyncRoutes// admin 看全部}else{accessedRoutes=filterByRole(asyncRoutes,roles)// 按角色过滤}commit('SET_ROUTES',accessedRoutes)returnaccessedRoutes}}

权限模块为什么值得独立?因为路由过滤逻辑在运行时变化。用户登录后才知道角色,角色决定路由——这条链路在user/loginaction 返回后触发permission/generateRoutes,两个 module 之间通过 dispatch 通信:

// 在 user.js 的 login action 里login({commit,dispatch},userInfo){returnapi.login(userInfo).then(res=>{commit('SET_TOKEN',res.token)dispatch('permission/generateRoutes',res.roles,{root:true})// ↑ 跨 module 调用,必须加 { root: true }})}

{ root: true }是 Vuex namespaced module 的跨模块调用标记。忘记加这个,Vuex 会在当前 module 里找permission/generateRoutes,找不到就静默失败——这是一个非常容易踩的坑。

module 4:tagsView.js—— 管理打开的标签页

标签页导航的增删改:

// store/modules/tagsView.js(简化)conststate={visitedViews:[],// 已打开的标签页列表cachedViews:[]// 通过 keep-alive 缓存的组件名列表}constmutations={ADD_VISITED_VIEW(state,view){...},DEL_VISITED_VIEW(state,view){...},DEL_CACHED_VIEW(state,view){...},}

visitedViews存的是路由对象(path、title 等),cachedViews存的是组件名(用于<keep-alive :include="cachedViews">)。两者是同一个标签页在"UI 显示"和"性能缓存"两个维度的投影,放在同一个 module 里方便同步增删。

module 间通信的两种方式

方式一:dispatch 跨模块调用(如上)

dispatch('permission/generateRoutes',roles,{root:true})

方式二:getters 跨模块取值

所有 module 的状态都通过全局getters.js暴露,组件不直接访问state.app.sidebar,而是通过 getter:

// store/getters.jsconstgetters={sidebar:state=>state.app.sidebar,device:state=>state.app.device,token:state=>state.user.token,roles:state=>state.user.roles,menuRoutes:state=>state.permission.routes,visitedViews:state=>state.tagsView.visitedViews,}

这个文件的价值在于:组件不需要知道sidebar在哪个 module 里。组件只写mapGetters(['sidebar']),至于 sidebar 是state.app.sidebar还是state.ui.sidebar,getters.js 承担了映射。

如果将来重构把app.js拆成ui.js和layout.js,你只需要改getters.js里的一行映射,所有组件不受影响。

自动注册 module:require.context

注意store/index.js里没有import app from './modules/app',而是用 webpack 的require.context自动扫描:

constmodulesFiles=require.context('./modules',true,/\.js$/)constmodules=modulesFiles.keys().reduce((modules,modulePath)=>{constmoduleName=modulePath.replace(/^\.\/(.*)\.\w+$/,'$1')constvalue=modulesFiles(modulePath)modules[moduleName]=value.defaultreturnmodules},{})conststore=newVuex.Store({modules,getters})

这段代码的效果:你往modules/目录里加一个analytics.js文件,不需要改任何入口文件,module 自动生效。文件名即 module 名(analytics.js→ namespaceanalytics)。

这个设计解决了"多人同时开发时,store/index.js 频繁冲突"的问题——每个人在自己的 module 文件里开发,不需要碰入口文件。

我的验证:复现同样的 module 分层

为了验证这套分层逻辑,我搭了一个最小 demo:

// store/index.jsimportVuefrom'vue'importVuexfrom'vuex'conststore=newVuex.Store({modules:{ui:{namespaced:true,state:{sidebar:{opened:true},device:'desktop'},mutations:{TOGGLE_SIDEBAR(s){s.sidebar.opened=!s.sidebar.opened}}},auth:{namespaced:true,state:{token:'',roles:[]},mutations:{SET_TOKEN(s,token){s.token=token}}}},getters:{sidebar:s=>s.ui.sidebar,token:s=>s.auth.token,}})

然后在组件里验证了两点:

// 验证1:组件不感知 module 位置computed:{...mapGetters(['sidebar'])// 不知道它来自 ui module}// 验证2:跨 module dispatchthis.$store.dispatch('auth/login',credentials,{root:true})

两个验证都通过。核心结论:只要 getters 层封住了 module 边界,上层组件完全不需要知道 store 的内部结构。

总结:三个分层原则

  1. 按"谁产生的"分 module,不按"谁用的"分。sidebar 是 UI 产生的,归 ui module;token 是认证产生的,归 auth module。组件可以同时消费两个 module 的数据,但产生方是唯一的。

  2. getters 是 module 边界的封装层。组件永远通过 getter 取值,不直接访问state.xxx.yyy。这给未来的重构留了退路。

  3. 跨 module 通信必须显式声明{ root: true }。这是 Vuex namespaced 的安全机制——防止一个 module 意外修改另一个 module 的数据。

相关新闻

  • Power BI Copilot 与 Fabric 深度集成原理与工程实践
  • 了解CSS的查找匹配原理,让CSS更简洁、高效
  • Mac M1 安装了DevEco Studio,如何把内置的npm配置到系统的环境变量中全局生效

最新新闻

  • YOLOv10 vs YOLOv11 vs YOLOv12:Nature论文实测三代数模型在零售自助结账场景下的精度-速度权衡
  • DXVK:打破Windows游戏在Linux上的性能壁垒
  • 数据库作业
  • 基于libhv与JWT的HTTP认证中间件实战指南
  • Bilibili视频下载终极方案:免费解锁大会员4K和充电专属内容
  • IPXWrapper技术实现深度解析:Windows平台经典网络协议兼容性解决方案

日新闻

  • AI智能体安全防护框架AgentGuard:从原理到实战部署指南
  • KMX63与PIC18F26K40硬件组合及低功耗设计实践
  • 基于YOLO13改进的门体检测模型:C3k2模块与PoolingFormer技术解析

周新闻

  • 基于YOLOv12的番茄成熟度智能检测系统开发
  • 终极RimWorld模组管理指南:用RimSort告别模组冲突烦恼
  • AI Agent框架开发:从理论到实践的完整指南

月新闻

  • 2026年6月公司网站搭建最新热门渠道测评:四大低成本/零代码平台对比+避坑
  • 【Linux】Linux arm 编译QT程序,出现expected “}“报错
  • 【MATLAB例程】四基站二维AOA定位与距离辅助增强对比仿真。基于角度观测和测距修正的固定目标平面定位精度分析

关于尧图

  • 公司简介
  • 团队介绍
  • 企业文化
  • 荣誉资质

服务项目

  • 定制开发
  • 电商建站
  • UI 设计
  • 运维服务

快速链接

  • 案例展示
  • 建站流程
  • 常见问题
  • 资讯中心

联系方式

  • 📍北京市朝阳区互联网产业园 A 座 10 层
  • 📞400-888-8888
  • ✉️contact@rkmt.cn
  • 🕐周一至周日 9:00-21:00

© 2024 北京尧图网络科技有限公司 版权所有 | 京 ICP 备 XXXXXXXX 号