1
0
Files
DiAL/openspec/changes/frontend-architecture-refactor/design.md
lanyuanxiaoyao 76b47006fe feat: 新增两个 OpenSpec 变更提案 — 前端架构重构与 HTTP Checker 质量加固
- frontend-architecture-refactor: 拆分 hooks/组件、类型筛选器动态化
- http-checker-quality-hardening: ReDoS 防护、failure 格式修正、测试补全
2026-05-13 18:40:08 +08:00

146 lines
6.4 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.
## 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 → Tabprops 类型明确,不会造成 prop drilling 问题 |
## Open Questions
无。方案已在 explore 阶段与用户确认。