Files
hms/docs/superpowers/specs/2026-04-26-frontend-engineering-design.md
iven d1ab8074a3
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled
docs: 多专家组头脑风暴产出 — 5 份设计规格
基于全景审计分析,产出 5 份跨领域设计规格:

1. 性能优化 — 后端批量INSERT/合并COUNT/告警预加载 + 前端N+1内联name
2. 安全纵深防御 — PostgreSQL RLS/行级数据范围/session_key Redis/审计哈希链
3. 事件驱动架构增强 — 6个业务域11个缺失事件补发 + Outbox LISTEN/NOTIFY
4. 前端工程化 — 14个大组件拆分 + 3个重复模式统一 + Bundle优化
5. 可观测性与运维 — 深度健康检查/Prometheus/OpenTelemetry/生产Docker
2026-04-27 07:46:36 +08:00

248 lines
10 KiB
Markdown
Raw 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.
# 前端工程化改进设计
> 日期: 2026-04-26 | 状态: draft | 主题: 组件拆分、重复模式统一、Bundle 优化
## 1. 背景
HMS Web 前端共 139 个源文件77 TSX + 62 TS总代码量 27,000 行。随着健康管理模块的持续迭代,工程化债务逐步积累,主要体现在四个方面:
1. **组件膨胀** — 14 个文件超过 400 行,最大 872 行PluginCRUDPage.tsx
2. **重复模式** — 错误处理、分页列表、ID→名称缓存三处重复已有统一抽象但未被全面采用
3. **API 层风格混用** — 对象风格(`patientApi.list()`)与函数风格(`listAlerts()`)并存
4. **Bundle 体积** — 大型依赖未拆独立 chunk`chunkSizeWarningLimit` 已提升至 600KB
### 1.1 数据概览
| 指标 | 数值 |
|------|------|
| 超过 400 行的组件 | 14 个 |
| 超过 500 行的组件 | 7 个 |
| 未使用 usePaginatedData 的健康列表页 | 6 个 |
| 自建 nameCache 的页面 | 2 个AppointmentList、PointsOrderList|
| API 层文件 | 33 个(对象风格 8 个,函数风格 25 个) |
## 2. 问题分析
### 2.1 组件膨胀分析
TOP 7 大组件:
| 文件 | 行数 | 职责混杂点 |
|------|------|-----------|
| PluginCRUDPage.tsx | 872 | 表格渲染 + 表单校验 + Drawer + 导入导出 + Timeline |
| PluginGraphPage.tsx | 759 | Canvas 绑定 + 数据加载 + 布局计算 + 动画控制 |
| Organizations.tsx | 622 | 三栏树形 + 组织 CRUD + 部门管理 + 人员分配 |
| StatisticsDashboard.tsx | 580 | 五个并行统计 API + 图表渲染 + 时间筛选 |
| ArticleEditor.tsx | 554 | 富文本编辑器 + 表单 + 标签选择 + 封面上传 |
| FollowUpTaskList.tsx | 547 | 列表 + 筛选面板 + 状态流转弹窗 + 批量操作 |
| MainLayout.tsx | 535 | 侧边栏 + 动态菜单 + 插件菜单注入 + Header |
**根因:** React 组件未按"展示/容器/Hook"分层,状态逻辑和 UI 渲染耦合在同一文件中。
### 2.2 重复模式分析
**模式一:错误处理**
已有统一方案:`client.ts` 全局拦截器处理 401/403/500`handleApiError()` 处理业务错误,`useApiRequest()` hook 封装 try-catch。
实际情况:组件仍大量内联 `catch (err) { message.error(...) }`。原因useApiRequest 缺少 loading 状态返回,部分场景需要更细粒度的错误控制。
**模式二:分页列表**
已有 `usePaginatedData<T>(fetchFn, pageSize)` hook封装 data/total/page/loading/refresh。
未使用的健康列表页PatientList、AppointmentList、ConsultationList、FollowUpTaskList、OfflineEventList、PointsProductList。原因fetchFn 签名只支持 (page, pageSize, search)部分页面需要额外筛选参数status/dateRange/tag
**模式三ID→名称缓存**
已有 `useHealthStore` 提供 `resolvePatientName(id)` / `getPatientName(id)` + 自动去重加载。
AppointmentList.tsx 和 PointsOrderList.tsx 仍自建 `useState<Record<string, string>>` nameCache。原因store 未提供批量解析接口。
### 2.3 API 风格混用
- **对象风格**8 个文件):`patientApi.list()`, `doctorApi.list()`
- **函数风格**25 个文件):`listAlerts()`, `acknowledgeAlert()`
结论:不强制统一(改动量大、收益有限),新增 API 文件统一采用对象风格。
### 2.4 Bundle 体积分析
当前 manualChunks 仅拆分了 react/react-dom/antd/axios/zustand。未拆分的大型依赖
| 依赖 | 估算大小 (gzip) | 使用范围 |
|------|----------------|---------|
| @ant-design/charts | ~180KB | 仅 StatisticsDashboard |
| @xyflow/react | ~120KB | 仅 PluginGraphPage |
| @wangeditor/editor | ~200KB | 仅 ArticleEditor |
`chunkSizeWarningLimit: 600` 说明单个 chunk 已超过 Vite 默认 500KB 警告阈值。
## 3. 解决方案
### 3.1 组件拆分策略
统一采用 **Container + Presentational + Hook** 三层模式:
```
原组件 (500+ 行)
├── hooks/useXxxData.ts — 数据获取、状态管理、业务逻辑
├── components/XxxTable.tsx — 纯展示表格
├── components/XxxForm.tsx — 表单(含校验)
└── XxxPage.tsx — 容器组件(组装 hooks + 子组件)
```
#### PluginCRUDPage.tsx (872行) — P0
| 拆分目标 | 类型 | 预估行数 | 职责 |
|----------|------|---------|------|
| usePluginData.ts | Hook | ~120 | CRUD 操作、导入导出逻辑 |
| PluginTable.tsx | 展示 | ~150 | 表格列定义、行操作按钮 |
| PluginForm.tsx | 展示 | ~180 | 新增/编辑表单 + Drawer |
| PluginImportExport.tsx | 展示 | ~100 | 导入导出面板 |
| PluginTimeline.tsx | 展示 | ~80 | 操作历史 Timeline |
| PluginCRUDPage.tsx | 容器 | ~80 | 组装子组件 |
#### PluginGraphPage.tsx (759行) — P1
| 拆分目标 | 类型 | 预估行数 | 职责 |
|----------|------|---------|------|
| useGraphLayout.ts | Hook | ~100 | 布局算法、节点位置计算 |
| useGraphData.ts | Hook | ~80 | 数据加载、边/节点转换 |
| GraphCanvas.tsx | 展示 | ~200 | ReactFlow 渲染、节点样式 |
| GraphToolbar.tsx | 展示 | ~60 | 工具栏(缩放/自动布局) |
| PluginGraphPage.tsx | 容器 | ~60 | 组装 |
#### Organizations.tsx (622行) — P1
| 拆分目标 | 类型 | 预估行数 | 职责 |
|----------|------|---------|------|
| useOrgTree.ts | Hook | ~80 | 树数据加载、CRUD 操作 |
| OrgTree.tsx | 展示 | ~120 | 左侧树形选择 |
| OrgDetail.tsx | 展示 | ~150 | 右侧组织详情/编辑 |
| DeptMemberList.tsx | 展示 | ~100 | 部门成员列表 + 分配 |
| Organizations.tsx | 容器 | ~60 | 三栏布局组装 |
#### StatisticsDashboard.tsx (580行) — P1
| 拆分目标 | 类型 | 预估行数 | 职责 |
|----------|------|---------|------|
| useStatsData.ts | Hook | ~100 | 五个统计 API 并行加载 |
| PatientTrendChart.tsx | 展示 | ~80 | 患者趋势图 |
| AppointmentStats.tsx | 展示 | ~80 | 预约统计图 |
| OverviewCards.tsx | 展示 | ~60 | 概览卡片组 |
| TimeRangeSelector.tsx | 展示 | ~40 | 时间范围选择 |
| StatisticsDashboard.tsx | 容器 | ~50 | 组装 |
#### ArticleEditor.tsx (554行) — P2
| 拆分目标 | 类型 | 预估行数 | 职责 |
|----------|------|---------|------|
| useArticleEditor.ts | Hook | ~100 | 文章加载/保存/发布逻辑 |
| RichTextEditor.tsx | 展示 | ~150 | WangEditor 封装 |
| ArticleMetaForm.tsx | 展示 | ~120 | 标题/分类/标签/封面表单 |
| ArticleEditor.tsx | 容器 | ~60 | 组装 |
#### FollowUpTaskList.tsx (547行) — P2
| 拆分目标 | 类型 | 预估行数 | 职责 |
|----------|------|---------|------|
| useFollowUpTasks.ts | Hook | ~100 | 列表加载/筛选/状态流转 |
| FollowUpTable.tsx | 展示 | ~120 | 表格 + 行操作 |
| FollowUpFilter.tsx | 展示 | ~80 | 筛选面板 |
| TaskStatusModal.tsx | 展示 | ~80 | 状态变更弹窗 |
| FollowUpTaskList.tsx | 容器 | ~50 | 组装 |
#### MainLayout.tsx (535行) — P1
| 拆分目标 | 类型 | 预估行数 | 职责 |
|----------|------|---------|------|
| useMenuBuilder.ts | Hook | ~100 | 菜单数据构建、插件菜单合并 |
| AppSidebar.tsx | 展示 | ~120 | 侧边栏渲染 |
| AppHeader.tsx | 展示 | ~80 | 顶部 Header |
| MainLayout.tsx | 容器 | ~60 | 布局骨架 |
### 3.2 重复模式统一方案
#### 3.2.1 增强 useApiRequestP0
当前问题:缺少 loading 状态。增强为:
```typescript
interface UseApiRequestReturn {
execute: <T>(fn: () => Promise<T>, successMsg?: string) => Promise<T | null>;
loading: boolean;
}
```
改动量:~10 行。已有调用点无需修改。
#### 3.2.2 增强 usePaginatedDataP1
当前问题fetchFn 签名只支持 (page, pageSize, search)。增强为支持泛型筛选参数:
```typescript
interface UsePaginatedDataOptions<T, F> {
fetchFn: (page: number, pageSize: number, filters: F) => Promise<{ data: T[]; total: number }>;
pageSize?: number;
defaultFilters: F;
autoFetch?: boolean;
}
```
改动量:~30 行。保持旧签名兼容函数重载6 个列表页渐进迁移。
#### 3.2.3 增强 useHealthStore 批量解析P2
新增 `batchResolvePatientNames(ids)` / `batchResolveDoctorNames(ids)`。内部实现:去重 → 批量并发(限制并发数 5→ 写入缓存。
改动量stores/health.ts ~30 行。删除 AppointmentList/PointsOrderList 自建 nameCache 代码。
### 3.3 API 风格策略
不强制统一现有代码。新增规则:新建 API 文件统一采用对象风格修改已有文件时顺手迁移Boy Scout Rule
### 3.4 Bundle 优化方案
在 vite.config.ts 的 manualChunks 中增加重型依赖拆分:
```typescript
if (id.includes('@ant-design/charts') || id.includes('@antv/')) return 'vendor-charts';
if (id.includes('@xyflow/react') || id.includes('@reactflow/')) return 'vendor-flow';
if (id.includes('@wangeditor/')) return 'vendor-editor';
```
配合路由级 `React.lazy()` 加载,使独立 chunk 仅在访问对应页面时下载。预期主 chunk 从 > 600KB 降至 < 400KBchunkSizeWarningLimit 可降至 500。
## 4. 实施步骤
| Phase | 任务 | 工期 | 优先级 |
|-------|------|------|--------|
| 1: 基础设施增强 | 增强 useApiRequest + usePaginatedData + manualChunks + 路由懒加载 | 1 天 | P0-P1 |
| 2: 核心组件拆分 | PluginCRUDPage + MainLayout + PluginGraphPage | 2-3 天 | P0-P1 |
| 3: 健康模块拆分 | StatisticsDashboard + ArticleEditor + FollowUpTaskList | 2 天 | P1-P2 |
| 4: 重复模式迁移 | 6 个列表页迁移 usePaginatedData + 2 个 nameCache 迁移 | 2 天 | P2 |
| 5: 验证回归 | pnpm build 验证 + 功能回归 + ESLint max-lines-per-file 规则 | 1 天 | P2 |
## 5. 风险与缓解
| 风险 | 缓解措施 |
|------|---------|
| 拆分引入 re-render 性能退化 | React.memo 包装展示组件DevTools Profiler 验证 |
| usePaginatedData 泛型重构破坏现有调用点 | 保持旧签名兼容(函数重载),渐进迁移 |
| 拆分后导入路径变化导致循环依赖 | 每个拆分完成后立即 `pnpm build` 验证 |
| Bundle 拆分过度导致请求数增加 | HTTP/2 多路复用下影响有限 |
| WangEditor 封装层与编辑器生命周期冲突 | useRef 管理 editor 实例,严格 cleanup |
**不做的事情:** 不重写现有组件;不强制统一 API 风格;不引入新状态管理库;不做 SSR/SSG。
**成功指标:**
| 指标 | 当前值 | 目标值 |
|------|--------|--------|
| >400 行的组件数 | 14 | <= 5 |
| >500 行的组件数 | 7 | 0 |
| 主 chunk 体积 | > 600KB | < 400KB |
| usePaginatedData 覆盖率 | ~30% | > 80% |
| useApiRequest 覆盖率 | ~20% | > 60% |