Files
workspace/code/fms/.qoder/repowiki/zh/content/前端架构/状态管理.md
T
2026-08-23 21:02:41 +08:00

15 KiB
Raw Blame History

状态管理

**本文引用的文件** - [auth.js](file://fms-vue/src/stores/auth.js) - [app.js](file://fms-vue/src/stores/app.js) - [permissions.js](file://fms-vue/src/stores/permissions.js) - [main.js](file://fms-vue/src/main.js) - [router/index.js](file://fms-vue/src/router/index.js) - [api.js](file://fms-vue/src/services/api.js) - [theme/index.js](file://fms-vue/src/theme/index.js) - [stores.spec.js](file://fms-vue/tests/stores/stores.spec.js) - [permissions.spec.js](file://fms-vue/tests/permissions.spec.js)

目录

  1. 简介
  2. 项目结构
  3. 核心组件
  4. 架构总览
  5. 详细组件分析
  6. 依赖关系分析
  7. 性能考虑
  8. 故障排查指南
  9. 结论
  10. 附录

简介

本文件为 FMS 前端(Vue + Pinia)的状态管理文档,聚焦以下目标:

  • 基于 Pinia 的 store 创建、模块化与数据持久化策略
  • 用户认证状态:登录态、用户信息、JWT 令牌处理
  • 应用全局状态:主题、语言偏好、UI 状态(侧边栏、页签等)
  • 权限状态:菜单权限、按钮/功能点权限控制逻辑
  • 状态更新最佳实践与性能优化
  • 状态调试与测试方法

项目结构

FMS 前端采用按领域划分的 store 模块:

  • stores/auth.js:认证与会话、用户回显信息
  • stores/app.js:应用级 UI 状态(主题、语言、页签、路由刷新计数)
  • stores/permissions.js:权限数据加载与访问/功能点判断
  • router/index.js:路由守卫统一触发权限加载与鉴权
  • services/api.js:后端接口封装(登录、数据加载等)
  • theme/index.js:主题切换与主色变量写入 DOM
  • main.js:Pinia 初始化、插件启用、主题同步初始化
graph TB
A["main.js<br/>创建 Pinia、启用持久化插件"] --> B["stores/app.js<br/>应用状态与主题同步"]
A --> C["stores/auth.js<br/>认证与会话持久化"]
A --> D["stores/permissions.js<br/>权限数据与判断"]
E["router/index.js<br/>beforeEach 守卫"] --> C
E --> D
D --> F["services/api.js<br/>loadDataApi 等"]
B --> G["theme/index.js<br/>applyDarkMode / applyPrimaryColor"]

图表来源

章节来源

核心组件

  • 认证 Store(auth)
    • 维护会话凭证(token、user)与跨会话用户回显信息(orgId、account)
    • 通过 pinia-plugin-persistedstate 将会话写 sessionStorage,用户回显写 localStorage
    • 提供 setSession、clearSession、saveLastLogin 等方法
  • 应用 Store(app)
    • 管理暗色模式、主色、侧边栏折叠、语言、已访问页签、路由刷新计数
    • 通过 watch 将状态同步到 DOM(主题类名与 CSS 变量)
    • 仅持久化主题与语言相关字段
  • 权限 Store(permissions)
    • 拉取模块、用户授权、功能点数据,并缓存结果
    • 支持管理员提权(g3soft)直接可见全部启用模块
    • 提供 canAccess(菜单/页面访问)、canPower(功能点/按钮)判断
    • 监听认证主体变化自动清理缓存,避免脏数据

章节来源

架构总览

下图展示了从路由守卫到权限加载、再到视图渲染的整体流程。

sequenceDiagram
participant U as "用户"
participant R as "router/index.js"
participant A as "stores/auth.js"
participant P as "stores/permissions.js"
participant S as "services/api.js"
participant V as "视图组件"
U->>R : 访问受保护路由
R->>A : 检查 isAuthenticated
alt 未登录
R-->>U : 重定向到 /login?redirect=...
else 已登录
R->>P : await load()
P->>S : loadDataApi(...) 多次并发
S-->>P : 返回模块/授权/功能点数据
P-->>R : 完成加载
R->>V : 允许进入并渲染
end

图表来源

详细组件分析

认证 Store(auth.js)

  • 状态设计
    • loginInfo:包含 token 与 user,用于当前会话鉴权
    • userInfo:包含 orgId 与 account,用于登录页回显,退出登录不清除
    • isAuthenticated:计算属性,基于 token 是否存在
  • 关键方法
    • setSession:兼容多种字段形态(token/access_token、user/user_info),设置 orgId
    • clearSession:清空会话凭证,保留 userInfo
    • saveLastLogin:保存机构与账号,供下次打开登录页回显
  • 持久化策略
    • 使用 pinia-plugin-persistedstate
    • 会话凭证(loginInfo)持久化到 sessionStorage,键名为 fms-login
    • 用户回显(userInfo)持久化到 localStorage,键名为 fms-user
  • 安全与体验
    • 会话随标签页关闭失效;回显数据跨会话保留,提升用户体验
flowchart TD
Start(["调用 setSession"]) --> Normalize["规范化字段<br/>token/access_token, user/user_info"]
Normalize --> SetState["写入 loginInfo 与 userInfo.orgId"]
SetState --> PersistSS["持久化到 sessionStorage(fms-login)"]
SetState --> PersistLS["持久化到 localStorage(fms-user)"]
PersistSS --> End(["完成"])
PersistLS --> End

图表来源

章节来源

应用 Store(app.js)

  • 状态设计
    • darkMode、primaryColor:主题相关
    • sidebarCollapsed:侧边栏折叠状态
    • locale:语言偏好(占位)
    • visitedTabs:已访问页签列表(工作台固定不可关闭)
    • routeRefreshKeys:路由刷新计数器,key 为 fullPath
  • 计算属性
    • cachedViews:根据 visitedTabs 生成 keep-alive 的 include 列表(去重)
  • 动作
    • 主题切换:setDark/toggleDark/setPrimary
    • 侧边栏:toggleSidebar/setSidebar
    • 语言:toggleLocale
    • 页签管理:addVisitedTab/removeVisitedTab/closeOthers/closeLeft/closeRight/closeAll
    • 路由刷新:refreshRoute/getRouteRefreshKey/pruneOrphanRefreshKeys
  • 持久化
    • 仅持久化 darkMode、primaryColor、locale
  • 主题同步
    • initThemeSync:在 Pinia 实例激活后建立 watch,将状态同步到 DOM(class 与 CSS 变量)
classDiagram
class AppStore {
+boolean darkMode
+string primaryColor
+boolean sidebarCollapsed
+string locale
+Object[] visitedTabs
+Object~number~ routeRefreshKeys
+cachedViews() string[]
+setDark(value) void
+toggleDark() void
+setPrimary(color) void
+toggleSidebar() void
+setSidebar(collapsed) void
+toggleLocale() void
+addVisitedTab(tab) void
+removeVisitedTab(key) void
+closeOthers(key) void
+closeLeft(key) void
+closeRight(key) void
+closeAll() void
+pruneOrphanRefreshKeys() void
+getRouteRefreshKey(key) number
+refreshRoute(key) void
}

图表来源

章节来源

权限 Store(permissions.js)

  • 数据模型
    • modules:可用模块列表
    • grantedPageIds:用户授权的页面 ID 集合
    • moduleCodes:用户可访问的模块编码集合
    • powerCodes:用户可用的功能点编码集合(形如 moduleId.powerCode)
    • isG3soft:是否为管理员提权账号
  • 加载逻辑
    • 管理员提权:仅加载启用模块,跳过授权表查询
    • 普通用户:并发加载 s_module、b_user_module、b_user_power、s_module_power,并组装权限集合
    • 主体去重:同一 orgId:userId 重复调用不重复请求,并发共享同一 Promise
  • 权限判断
    • canAccess(moduleCode):判断是否可访问某模块(含父 page 类型模块)
    • canPower(scopeCode, powerCode):判断是否拥有某功能点(含父 page 类型模块)
  • 清理机制
    • 监听 authStore 的变化(orgId、userId、account、token),在主体变更时清理缓存
    • clear 方法区分“进行中请求属于其他主体”的情况,作废旧请求,避免脏数据
flowchart TD
LStart(["调用 load()"]) --> CheckAdmin{"是否 g3soft?"}
CheckAdmin --> |是| LoadModules["加载启用模块(s_module)"]
CheckAdmin --> |否| ValidateUser{"校验 userId"}
ValidateUser --> |无效| ThrowErr["抛出错误"]
ValidateUser --> |有效| CheckDedup{"是否已加载该主体?"}
CheckDedup --> |是| ReturnCached["返回缓存"]
CheckDedup --> |否| ParallelLoad["并发加载 4 个接口"]
ParallelLoad --> Assemble["组装 modules/grantedPageIds/powerCodes"]
Assemble --> MarkLoaded["记录 loadedPrincipalKey"]
MarkLoaded --> LEnd(["完成"])
ThrowErr --> LEnd
ReturnCached --> LEnd

图表来源

章节来源

路由守卫与状态联动

  • 登录态检查:未登录访问受保护路由跳转 /login,带 redirect 参数
  • 权限加载:在 beforeEach 中统一调用 permissionStore.load(),确保首屏与业务页挂载前已完成
  • 访问控制:若路由 meta.moduleId 存在且无权限,则跳转 /dashboard
sequenceDiagram
participant Router as "router/index.js"
participant Auth as "stores/auth.js"
participant Perm as "stores/permissions.js"
Router->>Auth : 读取 isAuthenticated
alt 未登录
Router-->>Router : 重定向到 /login?redirect=...
else 已登录
Router->>Perm : await load()
Perm-->>Router : 完成
Router->>Router : 校验 canAccess(moduleId)
Router-->>Router : 放行或跳转 /dashboard
end

图表来源

章节来源

依赖关系分析

  • main.js
    • 创建 Pinia 并启用 pinia-plugin-persistedstate
    • 在挂载前调用 initThemeSync,确保主题同步
  • app.js
    • 依赖 theme/index.js 的 applyDarkMode、applyPrimaryColor
  • permissions.js
    • 依赖 services/api.js 的 loadDataApi
    • 依赖 auth.js 获取当前用户信息
  • router/index.js
    • 依赖 auth.js 与 permissions.js 进行鉴权与权限加载
graph LR
M["main.js"] --> A["app.js"]
M --> Au["auth.js"]
M --> P["permissions.js"]
A --> T["theme/index.js"]
P --> API["services/api.js"]
P --> Au
R["router/index.js"] --> Au
R --> P

图表来源

章节来源

性能考虑

  • 并发与去重
    • 权限加载对同一主体去重,并发调用共享 Promise,减少重复网络请求
    • 管理员提权路径简化请求数量
  • 内存与状态清理
    • 关闭页签时清理 routeRefreshKeys,避免无限增长
    • 权限 store 在主体变更时清理缓存,防止脏数据
  • 首屏体验
    • 路由守卫在 mount 前完成权限加载,避免菜单“跳出”
    • 主题同步在挂载前执行,避免首屏闪烁
  • 持久化粒度
    • 仅持久化必要状态(主题、语言、用户回显),降低存储体积与读写开销

[本节为通用性能建议,无需特定文件引用]

故障排查指南

  • 登录后菜单空白
    • 检查权限 store 是否在路由守卫中被正确 await
    • 确认主体变化时 clear 不会作废正在进行的请求(同主体)
  • 主题未生效
    • 确认 initThemeSync 已在 Pinia 实例激活后调用
    • 检查 applyDarkMode/applyPrimaryColor 是否正确操作 DOM
  • 登录态异常
    • 检查 sessionStorage/localStorage 中的键名与内容
    • 确认 clearSession 仅清除会话凭证,不影响用户回显
  • 权限判断不准确
    • 确认 canAccess/canPower 的 scopeCode 与 powerCode 格式正确
    • 检查模块树解析(resolvePageId)是否能找到父 page 类型模块

章节来源

结论

FMS 前端通过 Pinia 实现了清晰、可维护的状态管理:

  • 认证、应用、权限三大 store 职责明确,耦合度低
  • 通过路由守卫集中管理鉴权与权限加载,保证首屏体验
  • 使用 pinia-plugin-persistedstate 实现细粒度的持久化策略
  • 完善的测试覆盖验证了关键行为与边界条件

[本节为总结性内容,无需特定文件引用]

附录

状态更新最佳实践

  • 单一数据源:所有状态变更通过 store actions 进行,组件内不直接修改状态
  • 副作用隔离:主题同步、DOM 操作集中在 app store 的初始化与 watch 中
  • 幂等与去重:权限加载对同一主体去重,避免重复请求
  • 最小持久化:仅持久化必要状态,减少存储压力
  • 错误处理:权限加载失败时静默处理,避免阻断路由导航

[本节为通用实践建议,无需特定文件引用]

状态调试与测试方法

  • 单元测试
    • 使用 vitest 与 createTestPinia 创建独立 Pinia 实例
    • 模拟 API 响应,验证权限加载与判断逻辑
    • 验证持久化键名与内容(sessionStorage/localStorage)
  • 断言要点
    • 认证:setSession/clearSession 的行为与持久化键
    • 应用:主题切换、页签管理、刷新计数
    • 权限:主体去重、管理员提权、并发请求复用
  • 调试技巧
    • 在浏览器控制台查看 Pinia state(通过 devtools)
    • 观察路由守卫日志与 NProgress 进度条,定位加载时机

章节来源