基于全景审计分析,产出 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
10 KiB
前端工程化改进设计
日期: 2026-04-26 | 状态: draft | 主题: 组件拆分、重复模式统一、Bundle 优化
1. 背景
HMS Web 前端共 139 个源文件(77 TSX + 62 TS),总代码量 27,000 行。随着健康管理模块的持续迭代,工程化债务逐步积累,主要体现在四个方面:
- 组件膨胀 — 14 个文件超过 400 行,最大 872 行(PluginCRUDPage.tsx)
- 重复模式 — 错误处理、分页列表、ID→名称缓存三处重复,已有统一抽象但未被全面采用
- API 层风格混用 — 对象风格(
patientApi.list())与函数风格(listAlerts())并存 - 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 状态。增强为:
interface UseApiRequestReturn {
execute: <T>(fn: () => Promise<T>, successMsg?: string) => Promise<T | null>;
loading: boolean;
}
改动量:~10 行。已有调用点无需修改。
3.2.2 增强 usePaginatedData(P1)
当前问题:fetchFn 签名只支持 (page, pageSize, search)。增强为支持泛型筛选参数:
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 中增加重型依赖拆分:
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% |