基于全景审计分析,产出 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
248 lines
10 KiB
Markdown
248 lines
10 KiB
Markdown
# 前端工程化改进设计
|
||
|
||
> 日期: 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 增强 useApiRequest(P0)
|
||
|
||
当前问题:缺少 loading 状态。增强为:
|
||
|
||
```typescript
|
||
interface UseApiRequestReturn {
|
||
execute: <T>(fn: () => Promise<T>, successMsg?: string) => Promise<T | null>;
|
||
loading: boolean;
|
||
}
|
||
```
|
||
|
||
改动量:~10 行。已有调用点无需修改。
|
||
|
||
#### 3.2.2 增强 usePaginatedData(P1)
|
||
|
||
当前问题: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 降至 < 400KB,chunkSizeWarningLimit 可降至 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% |
|