# 前端工程化改进设计 > 日期: 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(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>` 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: (fn: () => Promise, successMsg?: string) => Promise; loading: boolean; } ``` 改动量:~10 行。已有调用点无需修改。 #### 3.2.2 增强 usePaginatedData(P1) 当前问题:fetchFn 签名只支持 (page, pageSize, search)。增强为支持泛型筛选参数: ```typescript interface UsePaginatedDataOptions { 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% |