- frontend-architecture-refactor: 拆分 hooks/组件、类型筛选器动态化 - http-checker-quality-hardening: ReDoS 防护、failure 格式修正、测试补全
146 lines
6.4 KiB
Markdown
146 lines
6.4 KiB
Markdown
## Context
|
||
|
||
当前前端代码约 970 行,功能完整但存在以下架构问题:
|
||
|
||
1. **hook 职责过重**:`hooks/useTargetDetail.ts`(113 行)同时承载全局查询(`useSummary`、`useTargets`)、Drawer 状态管理、条件查询和通用 `fetchJson` 封装,文件名与实际职责不匹配
|
||
2. **组件体积膨胀**:`TargetDetailDrawer.tsx`(228 行)混合了时间选择逻辑、两个 Tab 的完整渲染、列定义和统计计算
|
||
3. **类型维护重复**:`target-type-display.ts` 和 `target-table-filters.ts` 各自硬编码 checker 类型列表,新增 checker 需改两处前端文件
|
||
4. **测试覆盖不足**:仅 `constants/` 下 4 个纯函数有测试,`utils/time.ts` 和组件内统计逻辑未覆盖
|
||
5. **小问题**:`StatusDonut` 用数组索引做 key、`StatusBar` 硬编码 30 格、`TrendChart` 冗余 loading prop
|
||
|
||
后端 `CheckerRegistry` 已有 `supportedTypes` 属性,可直接暴露给前端。项目未上线,不需要向前兼容。
|
||
|
||
## Goals / Non-Goals
|
||
|
||
**Goals:**
|
||
|
||
- hook 按职责拆分,文件名匹配实际内容
|
||
- `TargetDetailDrawer` 拆分为 3 个组件,每个 < 100 行
|
||
- 类型筛选器由后端 meta API 驱动,新增 checker 前端零改动
|
||
- 删除 type label 转换层,直接使用 type 原始文本
|
||
- 补齐前端纯函数测试
|
||
- 修复已知小问题
|
||
|
||
**Non-Goals:**
|
||
|
||
- 不引入路由库或状态管理库
|
||
- 不重构后端 API 结构
|
||
- 不改变现有轮询策略和 QueryClient 配置
|
||
- 不新增 CSS 文件(继续使用单一 `styles.css`)
|
||
|
||
## Decisions
|
||
|
||
### Decision 1: hook 拆分策略
|
||
|
||
**选择**:按查询层级拆分为 `use-queries.ts` 和 `use-target-detail.ts`
|
||
|
||
- `use-queries.ts`:`queryKeys`、`fetchJson`(不导出)、`useSummary`、`useTargets`、`useMeta`
|
||
- `use-target-detail.ts`:仅保留 Drawer 状态管理(`selectedTargetId`、时间范围、分页、`openDrawer`/`closeDrawer`)和条件查询(trend/history)
|
||
|
||
**理由**:全局查询(summary/targets/meta)是面板级别的,与 Drawer 详情无关。拆分后各文件职责单一,命名自解释。`fetchJson` 仅被 query 层使用,留在 `use-queries.ts` 内部不导出。
|
||
|
||
**备选方案**:
|
||
- 仅重命名为 `useQueries.ts` — 解决命名问题但不解决职责混合
|
||
- 每个 query 一个文件 — 过度拆分,增加文件数量但无实质收益
|
||
|
||
### Decision 2: TargetDetailDrawer 拆分方式
|
||
|
||
**选择**:拆为 3 个组件 + 1 个常量文件
|
||
|
||
```
|
||
TargetDetailDrawer.tsx ← Drawer 壳 + 时间选择 + Tab 切换
|
||
OverviewTab.tsx ← 统计 + TrendChart + StatusDonut + Descriptions
|
||
HistoryTab.tsx ← PrimaryTable + 分页
|
||
constants/history-table-columns.tsx ← HISTORY_COLUMNS
|
||
```
|
||
|
||
**理由**:两个 Tab 的内容完全独立,拆分后各组件 < 100 行。`HISTORY_COLUMNS` 与 `TARGET_TABLE_COLUMNS` 性质相同,应放在 `constants/` 下保持一致。
|
||
|
||
统计计算逻辑(`totalChecks`/`upChecks`/`downChecks`)提取为 `utils/stats.ts` 纯函数,便于测试和 `useMemo`。
|
||
|
||
### Decision 3: Meta API 设计
|
||
|
||
**选择**:`GET /api/meta` 返回 `{ checkerTypes: string[] }`
|
||
|
||
```typescript
|
||
// src/shared/api.ts
|
||
export interface MetaResponse {
|
||
checkerTypes: string[];
|
||
}
|
||
```
|
||
|
||
**理由**:
|
||
- 从 `checkerRegistry.supportedTypes` 直接获取,无需额外维护
|
||
- 返回 `string[]` 而非 `{ key, label }[]`,因为决策是不做 label 转换
|
||
- 端点命名为 `/api/meta` 而非 `/api/types`,为未来扩展预留空间(如版本号、功能开关等)
|
||
- `staleTime: Infinity`,应用生命周期内只请求一次
|
||
|
||
**备选方案**:
|
||
- 从 targets 响应中动态提取 — 只能获取"当前有数据的类型",不能获取"系统支持的全部类型"
|
||
- 在 health 端点中附带 — 语义不匹配,health 应保持最小化
|
||
|
||
### Decision 4: 类型展示策略
|
||
|
||
**选择**:删除 `target-type-display.ts`,所有展示位置直接使用 `target.type` 原始文本
|
||
|
||
**影响位置**:
|
||
- `target-table-columns.tsx` 类型列 cell:`row.type` 直接渲染
|
||
- `TargetDetailDrawer.tsx` 标题栏 Tag:`target.type` 直接渲染
|
||
- `typeFilter` 列表:从 meta API 获取,label 和 value 均为原始 type 文本
|
||
|
||
**理由**:type 文本本身已足够清晰(`http`、`command`、`tcp`),无需额外映射层。消除了前后端重复维护的问题。
|
||
|
||
### Decision 5: 列定义动态化
|
||
|
||
**选择**:`TARGET_TABLE_COLUMNS` 从静态常量改为工厂函数
|
||
|
||
```typescript
|
||
export function createTargetTableColumns(checkerTypes: string[]): PrimaryTableCol<TargetStatus>[] {
|
||
const typeFilter = {
|
||
list: [
|
||
{ label: "全部", value: "" },
|
||
...checkerTypes.map(t => ({ label: t, value: t })),
|
||
],
|
||
type: "single" as const,
|
||
};
|
||
// ... 返回列定义数组
|
||
}
|
||
```
|
||
|
||
**数据流**:`useMeta()` → `checkerTypes` → `createTargetTableColumns(checkerTypes)` → `TargetGroup` columns prop
|
||
|
||
**理由**:`typeFilter` 是唯一需要动态数据的部分,通过工厂函数注入参数,保持列定义的纯函数特性。`statusFilter` 保持静态(UP/DOWN 是固定的)。
|
||
|
||
`TargetGroup` 新增 `columns` prop 接收动态列定义,`TargetBoard` 负责调用工厂函数并传递。
|
||
|
||
### Decision 6: StatusBar maxSlots 参数化
|
||
|
||
**选择**:新增 `maxSlots` prop(默认 30),组件根据 prop 渲染格数
|
||
|
||
```typescript
|
||
interface StatusBarProps {
|
||
samples: Array<{ up: boolean }>;
|
||
maxSlots?: number;
|
||
}
|
||
```
|
||
|
||
**理由**:消除硬编码魔数,使组件可复用。默认值 30 保持向后兼容。
|
||
|
||
### Decision 7: TrendChart 移除 loading prop
|
||
|
||
**选择**:移除 `loading` prop,组件只接收 `data: TrendPoint[]`
|
||
|
||
**理由**:调用方(`OverviewTab`)已用 `Skeleton` 处理 loading 状态,`TrendChart` 只在有数据时渲染。组件内部的 `if (loading)` 分支和 `loading={false}` 传参都是死代码。
|
||
|
||
## Risks / Trade-offs
|
||
|
||
| Risk | Mitigation |
|
||
|------|-----------|
|
||
| 列定义改为函数后,每次 `checkerTypes` 变化会重新创建列数组 | `useMemo` 包裹 `createTargetTableColumns` 调用;meta 数据 `staleTime: Infinity` 确保不会频繁变化 |
|
||
| meta API 在 targets 之前未返回时,筛选器暂时为空 | meta 请求极轻量(无 DB 查询),通常先于 targets 返回;即使晚到,筛选器会在数据到达后自动出现 |
|
||
| 拆分后组件间 props 传递增多 | 层级仅增加一层(Drawer → Tab),props 类型明确,不会造成 prop drilling 问题 |
|
||
|
||
## Open Questions
|
||
|
||
无。方案已在 explore 阶段与用户确认。
|