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

362 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 前端引入 Pinia 方案(已完成)
## 当前状态
当前方案涉及的核心改造已经完成,现状如下:
- `Pinia` 已正式接入项目
- `auth / appearance / menu / notification` 四个 store 已完成落地
- “建议进入 Pinia”的状态范围已全部进入对应 store
- 认证、外观、菜单、通知、租户切换上下文均已由 store 统一收口
- 页面侧对认证与外观状态的消费已改为直接读取 store
-`utils/auth.ts``utils/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-updated``in-app-unread-changed``storage` 事件做组件间和多 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` 下新增如下目录:
```text
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)`
- 新增空的 `authStore``appearanceStore``menuStore``notificationStore`
产出:
- 项目具备 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 的 `reset``reload`
### 风险 3组件和 store 职责混乱
表现:
- 视图层继续直接读写 `localStorage`
- store 同时处理过多页面局部逻辑
应对:
- 代码评审时明确约束
- 新增共享状态优先进入 store
- 页面局部状态继续留在页面组件
## 12. 预估改造顺序
推荐顺序:
1. `pinia` 基础接入
2. `appearanceStore`
3. `authStore`
4. `menuStore`
5. `notificationStore`
6. 页面侧逐步替换直接读取 `localStorage` 的逻辑
这样做的原因:
- `appearance``auth` 边界最清晰,迁移成本最低。
- `menu``notification` 与租户切换耦合更深,适合第二阶段处理。
- 页面层替换可以跟随 store 稳定后分批推进。
## 13. 验收结果(已完成)
完成首轮引入后,应满足以下标准:
- 项目已正式接入 `Pinia`
- 认证态、外观态不再以分散的 `localStorage` 读取为主入口
- 租户切换存在明确的 store 级清理机制
- 菜单和通知逻辑从 `AppLayout.vue` 中部分或全部抽离
- 现有登录、登出、刷新 token、切换租户、多 Tab 同步能力不回退
## 14. 最终状态(已完成)
建议采用“引入基础设施 + 迁移最核心全局状态 + 保留页面局部状态”的方式推进。
具体建议如下:
- 建议引入 `Pinia`
- 不建议一次性重构所有页面
- 第一批只迁移 `auth / appearance / menu / notification`
- 明确把“租户切换后的 store 清理机制”作为本次方案的硬性要求
该方案执行后,前端状态管理会从“工具函数 + 布局组件 + 浏览器事件”的分散模式过渡到“store 统一收口、页面按需消费”的结构,更适合当前项目继续演进。