writeOff/frontend_pinia_adoption_plan.md
haomingming 815aa04fe8 first
2026-05-20 18:21:39 +08:00

11 KiB
Raw Permalink Blame History

前端引入 Pinia 方案(已完成)

当前状态

当前方案涉及的核心改造已经完成,现状如下:

  • Pinia 已正式接入项目
  • auth / appearance / menu / notification 四个 store 已完成落地
  • “建议进入 Pinia”的状态范围已全部进入对应 store
  • 认证、外观、菜单、通知、租户切换上下文均已由 store 统一收口
  • 页面侧对认证与外观状态的消费已改为直接读取 store
  • utils/auth.tsutils/appearance.ts 兼容层已完成清理
  • 前端构建验证已通过

1. 背景

当前前端技术栈为 Vue 3 + Vue Router + Element Plus + Axios,尚未引入 Pinia。从现有代码看,跨页面共享状态主要通过以下方式维护:

  • frontend/src/utils/auth.ts
    • 使用 localStorage 持久化 token、用户信息、租户信息、权限、界面偏好。
  • frontend/src/utils/appearance.ts
    • 使用 reactive 单例维护主题和密度状态。
  • frontend/src/views/layout/AppLayout.vue
    • 集中维护菜单、租户切换、未读站内信、WebSocket 连接、跨标签页同步等全局状态。
  • window.dispatchEvent / window.addEventListener
    • 通过 auth:token-updatedin-app-unread-changedstorage 事件做组件间和多 Tab 同步。

该方案在当前阶段可以工作,但随着前端功能继续增加,已经出现以下趋势:

  • 全局状态来源分散,排查问题时需要同时阅读 utils、布局组件和页面组件。
  • 业务状态与持久化逻辑、事件广播逻辑耦合较深。
  • 租户切换、登录态变更、菜单刷新、通知未读数刷新等链路缺少统一的 store 边界。
  • 后续若继续增加全局缓存,会放大“旧租户状态残留”和“页面使用过期缓存”的风险。

结论:建议引入 Pinia,但采用渐进式迁移,不进行一次性重构。

2. 目标

本方案目标如下:

  • 建立统一的前端全局状态管理入口。
  • 将认证、租户、菜单、外观、通知等跨页面共享状态从工具函数和布局组件中抽离。
  • 降低租户切换、重新登录、刷新 token、多标签页同步等复杂链路的维护成本。
  • 为后续增加全局缓存、数据预取、业务上下文复用提供稳定边界。

3. 非目标

本次方案不包含以下内容:

  • 不将所有页面内 ref/reactive 状态迁入 Pinia。
  • 不将表单输入、弹窗开关、分页条件等纯页面局部状态全局化。
  • 不在首轮引入额外状态管理插件或复杂中间件。
  • 不在首轮重构全部页面的接口调用方式。

4. 是否建议引入

建议引入,原因如下:

  • 项目已经存在明确的全局共享状态。
  • 当前共享状态分散在 localStorage、响应式单例、布局组件和浏览器事件之间。
  • 多租户切换已经是核心链路,继续扩展时需要稳定的 store 清理机制。
  • 后续若继续做全局搜索、通知中心、更多个性化设置、页面缓存Pinia 会显著降低复杂度。

不建议一次性全面迁移,原因如下:

  • 大部分业务页面仍以单页局部状态为主,强行迁移收益不高。
  • 一次性改动风险大,容易引入登录态、权限态和租户态回归问题。

5. 适合进入 Pinia 的状态范围(已完成)

已完成进入 Pinia

  • 认证态
    • token
    • 当前用户
    • 当前租户
    • scope
    • roles
    • permissions
  • 外观态
    • themeMode
    • density
    • 初始化状态
  • 菜单与布局态
    • 当前菜单列表
    • 当前租户 logo
    • 是否正在切换租户
  • 通知态
    • 未读数
    • 通知列表
    • 轮询和 WebSocket 生命周期状态
  • 租户切换上下文
    • 当前租户 ID
    • 切换后是否需要清理的业务缓存版本号

不建议进入 Pinia

  • 单页面查询条件
  • 表单内容
  • 弹窗显隐
  • 临时上传进度
  • 仅在单个页面使用的表格数据

6. 推荐目录结构

建议在 frontend/src 下新增如下目录:

stores/
  index.ts
  auth.ts
  appearance.ts
  menu.ts
  notification.ts

职责建议如下:

  • stores/index.ts
    • 统一创建 pinia 实例。
  • stores/auth.ts
    • 管理登录态、用户信息、权限、租户信息、登录态广播、登出清理。
  • stores/appearance.ts
    • 管理主题、密度、文档样式同步。
  • stores/menu.ts
    • 管理租户端和平台端菜单加载、菜单刷新。
  • stores/notification.ts
    • 管理未读数、通知列表、轮询、WebSocket 连接和重连。

7. 设计原则

7.1 store 负责状态和动作,工具函数只保留纯函数能力

例如:

  • utils/auth.ts
    • 逐步收缩为解析、序列化、兼容读取等纯工具。
  • 登录保存、登出清理、权限变更广播
    • 迁移到 authStore

7.2 持久化与内存态分层

  • Pinia 中维护运行时状态。
  • localStorage 仅作为持久化载体,不再作为主要读取入口。
  • 页面和组件默认从 store 读取,不直接散落读取 localStorage

7.3 租户切换必须具备显式清理机制

租户切换后,至少要做到:

  • 刷新 authStore
  • 刷新 menuStore
  • 刷新 notificationStore
  • 清空或失效租户相关业务缓存
  • 触发页面重新拉数或重建

建议在 authStore 中增加:

  • authVersion
  • tenantContextVersion

通过版本号驱动需要重新加载的数据,而不是只依赖浏览器事件和页面手工处理。

7.4 不引入“超大 store”

避免把全部全局状态堆进一个 appStore。按领域拆分,降低耦合。

8. 分阶段实施方案

阶段 1基础接入已完成

目标:

  • 引入 pinia
  • 完成基础目录和初始化接入
  • 不改变现有业务行为

实施内容:

  • 安装 pinia
  • 新增 frontend/src/stores/index.ts
  • frontend/src/main.ts 中接入 app.use(pinia)
  • 新增空的 authStoreappearanceStoremenuStorenotificationStore

产出:

  • 项目具备 store 基础设施
  • 不触发业务逻辑迁移

阶段 2迁移认证与外观已完成

目标:

  • 优先迁移最稳定、最全局的状态

实施内容:

  • utils/auth.ts 中的状态读写入口迁移到 authStore
  • utils/appearance.ts 中的响应式单例迁移到 appearanceStore
  • 保留 localStorage 持久化,但统一由 store 写入
  • 页面不再直接读取 localStorage 获取认证信息

涉及文件重点:

  • frontend/src/utils/auth.ts
  • frontend/src/utils/appearance.ts
  • frontend/src/main.ts
  • frontend/src/App.vue
  • frontend/src/router/index.ts
  • frontend/src/views/modules/ProfilePage.vue

验收标准:

  • 登录、刷新、退出登录功能不回归
  • 主题和密度切换行为不回归
  • 多 Tab 基本同步仍然可用

阶段 3迁移菜单与租户切换链路已完成

目标:

  • 将当前集中在 AppLayout.vue 的布局级共享状态抽出

实施内容:

  • 将菜单加载迁移到 menuStore
  • 将租户切换迁移到 authStore 或专门的 tenant context action
  • 将租户 Logo、租户切换状态从布局组件迁移到 store
  • 明确切租户后的 store 级缓存清理动作

涉及文件重点:

  • frontend/src/views/layout/AppLayout.vue
  • frontend/src/api/modules.ts
  • frontend/src/router/index.ts

验收标准:

  • 租户切换后菜单正确刷新
  • 原路由保留逻辑仍然成立
  • 不出现旧租户菜单、旧租户 logo、旧租户权限残留

阶段 4迁移通知中心与 WebSocket 生命周期(已完成)

目标:

  • 将布局中的通知状态与连接管理从视图层抽离

实施内容:

  • 将未读数、通知列表迁移到 notificationStore
  • 将轮询、WebSocket 建连、重连、关闭逻辑迁移到 store action
  • 保留 AppLayout.vue 作为纯展示和交互入口

验收标准:

  • 未读数更新正常
  • 标记已读/全部已读正常
  • 页面切换不导致重复连接
  • 登出或切租户后连接被正确关闭

9. 与现有代码的映射关系(已完成)

建议按如下方式收口:

  • 当前 frontend/src/utils/auth.ts
    • 收口到 authStore
  • 当前 frontend/src/utils/appearance.ts
    • 收口到 appearanceStore
  • 当前 frontend/src/views/layout/AppLayout.vue
    • 收口到 menuStore + notificationStore + authStore
  • 当前直接调用 hasPermission() 的页面
    • 后续逐步改为从 authStore 暴露的 getter 或 helper 读取

10. 持久化策略建议

首轮建议不引入额外持久化插件,采用手工持久化,原因如下:

  • 当前状态字段较少,手工维护可控。
  • 可以保留现有 localStorage 键,降低兼容成本。
  • 可以在迁移期同时兼容旧逻辑,便于灰度替换。

后续如 store 数量和持久化字段继续增多,再评估是否引入持久化插件。

11. 风险与应对

风险 1登录态回归

表现:

  • 登录后页面未刷新权限
  • token 刷新后页面仍使用旧状态

应对:

  • 认证迁移阶段保留兼容读取逻辑
  • 为登录、登出、refreshAuth、tenant switch 增加回归测试清单

风险 2租户切换后残留旧缓存

表现:

  • 菜单、通知、页面数据、权限按钮残留旧租户信息

应对:

  • 引入 tenantContextVersion
  • 在切换租户 action 中显式调用各 store 的 resetreload

风险 3组件和 store 职责混乱

表现:

  • 视图层继续直接读写 localStorage
  • store 同时处理过多页面局部逻辑

应对:

  • 代码评审时明确约束
  • 新增共享状态优先进入 store
  • 页面局部状态继续留在页面组件

12. 预估改造顺序

推荐顺序:

  1. pinia 基础接入
  2. appearanceStore
  3. authStore
  4. menuStore
  5. notificationStore
  6. 页面侧逐步替换直接读取 localStorage 的逻辑

这样做的原因:

  • appearanceauth 边界最清晰,迁移成本最低。
  • menunotification 与租户切换耦合更深,适合第二阶段处理。
  • 页面层替换可以跟随 store 稳定后分批推进。

13. 验收结果(已完成)

完成首轮引入后,应满足以下标准:

  • 项目已正式接入 Pinia
  • 认证态、外观态不再以分散的 localStorage 读取为主入口
  • 租户切换存在明确的 store 级清理机制
  • 菜单和通知逻辑从 AppLayout.vue 中部分或全部抽离
  • 现有登录、登出、刷新 token、切换租户、多 Tab 同步能力不回退

14. 最终状态(已完成)

建议采用“引入基础设施 + 迁移最核心全局状态 + 保留页面局部状态”的方式推进。

具体建议如下:

  • 建议引入 Pinia
  • 不建议一次性重构所有页面
  • 第一批只迁移 auth / appearance / menu / notification
  • 明确把“租户切换后的 store 清理机制”作为本次方案的硬性要求

该方案执行后,前端状态管理会从“工具函数 + 布局组件 + 浏览器事件”的分散模式过渡到“store 统一收口、页面按需消费”的结构,更适合当前项目继续演进。