1
0

docs: 完善 DEVELOPMENT.md,按前端/后端/其他三大章节重构开发指引

This commit is contained in:
2026-05-12 15:26:49 +08:00
parent f7facb7232
commit e1c33b4002

View File

@@ -4,6 +4,17 @@
用户使用说明请参阅 [README.md](README.md)。
## 目录
- [项目结构](#项目结构)
- [一、后端开发指引](#一后端开发指引)
- [二、前端开发指引](#二前端开发指引)
- [三、项目运行、集成与打包](#三项目运行集成与打包)
- [代码质量](#代码质量)
- [已知限制](#已知限制)
---
## 项目结构
```text
@@ -11,11 +22,11 @@ src/
server/
app.ts Bun HTTP 路由入口(路由分发 + API 汇聚)
config.ts CLI 参数解析
dev.ts 开发启动入口
server.ts HTTP server 启动
dev.ts 生产/开发启动入口
server.ts HTTP server 启动工厂
helpers.ts 共享响应格式化工具jsonResponse、createHeaders 等)
middleware.ts API 参数校验中间件guardGetHead、validateTargetId 等)
static.ts 静态资源服务 与 SPA fallback
static.ts 静态资源服务与 SPA fallback
routes/ API 路由 handler按端点拆分
health.ts GET /health
summary.ts GET /api/summary
@@ -47,53 +58,15 @@ tests/ Bun test 测试
openspec/ OpenSpec 变更与规格文档
```
## 构建 executable
```bash
bun run build
```
构建流程:
1. 运行 `vite build`,输出前端资源到 `dist/web`
2. 生成临时 `.build/static-assets.ts`,嵌入 Vite 产物
3. 生成临时 `.build/server-entry.ts`,作为生产入口
4. 运行 `Bun.build({ compile })`,输出 `dist/dial-server`
运行 executable
```bash
./dist/dial-server probes.yaml
```
## 代码质量
```bash
bun run lint
bun run format:check
bun run format
bun run check
```
- `check` 依次运行 `typecheck``lint``format:check` 和单元测试。
## 测试
```bash
bun run check
bun run verify
```
- `check` 适合日常开发包含类型检查、lint、格式检查和单元测试。
- `verify` 先运行 `check`,再重新构建生产 executable 并运行 smoke test。
## 前后端边界
前端只通过 HTTP 调用后端API 路径为 `/api/*`。共享类型放在 `src/shared`,前端不得 import `src/server` 的运行时实现。
## 后端开发指引
---
### 架构概览
## 一、后端开发指引
### 1.1 架构概览
```
启动流程:
@@ -110,7 +83,7 @@ HTTP 请求:
→ middleware.ts(参数校验) → helpers.ts(响应格式化) → Response
```
### 库使用优先级
### 1.2 库使用优先级
后端代码开发遵循严格的库选择顺序:
@@ -122,7 +95,9 @@ HTTP 请求:
| 4 | 主流三方库 | cheerioHTML 解析、xpath + @xmldom/xmldomXML 解析) |
| 5 | 自行实现 | 仅在以上都无法满足时(如 `parseDuration``parseSize``evaluateJsonPath` 等专项逻辑) |
### API 路由开发
**原则**:新增依赖前先检查上述每一层级是否已有可用方案。禁止随意引入新依赖。
### 1.3 API 路由开发
路由文件位于 `src/server/routes/`每个端点一个文件。handler 函数签名统一为:
@@ -142,24 +117,25 @@ export function handleXxx(params, store: ProbeStore, method: string, mode: Runti
1.`src/server/routes/` 下创建 `<name>.ts`
2. 实现 handler 函数并 export
3.`app.ts``handleApiRoute` 中注册路径匹配和调用
3.`app.ts``createFetchHandler` 中注册路径匹配和调用
4.`tests/server/app.test.ts` 中添加对应测试
### 共享工具
### 1.4 共享工具
- **`helpers.ts`**:跨路由共用的响应工具函数(`jsonResponse``createHeaders``createApiError``mapCheckResult``formatDuration``createHealthResponse`
- **`middleware.ts`**API 参数校验函数(`guardGetHead``validateTargetId``validateTimeRange``validatePagination`
- **`static.ts`**:生产模式下的静态资源服务与 SPA fallback
### 类型定义规范
### 1.5 类型定义规范
- **共享类型**以 `src/shared/api.ts` 为唯一源头,前后端共同引用
- 前端不得 `import src/server/` 下的任何文件
- **严格联合类型**优先于宽类型:如 `phase: "status" | "duration" | ...` 而非 `phase: string`
- **后端内部扩展**`checker/types.ts``CheckResult` 通过 `extends` 共享版本的 `ApiCheckResult` 增加 `targetName` 等内部字段
- 存储层类型(`StoredTarget``StoredCheckResult`)独立定义,与 API 类型分离
- 配置类型(`ProbeConfig``TargetConfig`)支持 discriminated union通过 `type` 字段区分 http/command
### 数据存储规范
### 1.6 数据存储规范
基于 `bun:sqlite`WAL 模式运行,数据库文件位于配置的 `dataDir` 下。
@@ -182,15 +158,16 @@ export function handleXxx(params, store: ProbeStore, method: string, mode: Runti
- `check_results`target_idFK CASCADE、timestamp、matched0/1、duration_ms、status_detail、failureJSON
- 复合索引:`(target_id, timestamp)`
### 拨测引擎
### 1.7 拨测引擎
- **调度**`ProbeEngine``es-toolkit/groupBy` 按 interval 分组,每组独立 `setInterval` 定时触发
- **并发控制**`es-toolkit/Semaphore` 限制全局最大并发数(`maxConcurrentChecks``acquire()` 阻塞等待
- **Runner 选择**`engine.runCheck()``target.type` 分发到 `runHttpCheck``runCommandCheck`
- **超时控制**HTTP 用 `AbortController`Command 用 `setTimeout` + `proc.kill()`
- **结果写入**:检查结果通过 `store.insertCheckResult()` 写入 SQLiteengine 通过 `targetNameToId` 缓存 name→id 映射
- **生命周期**`start()`/`stop()` 管理定时器,`stop()` 清理所有 `setInterval`
### expect 断言系统
### 1.8 expect 断言系统
两层模型:**观测值收集** → **规则校验**
@@ -220,23 +197,193 @@ runCommandCheck → 收集观测(exitCode/stdout/stderr/durationMs)
**操作符**`equals`(深度比较,`es-toolkit/isEqual`)、`contains``match`(正则)、`empty``isNil`+`isEmptyObject`)、`exists``gte`/`lte`/`gt`/`lt`
### 错误模式
### 1.9 错误模式
- **API 错误**`{ error: "描述", status: <code> }`,状态码 400/404/405/503
- **CheckFailure**`{ kind: "error"|"mismatch", phase, path, expected?, actual?, message }`
- **错误处理**expect 校验失败记录首个失败原因;网络/超时/进程崩溃统一为 `kind:"error"`
- **日志**:解析失败等非致命异常用 `console.warn`,启动失败用 `console.error` + `process.exit(1)`
### 测试规范
### 1.10 测试规范
- 测试文件与源文件对应:`tests/server/checker/store.test.ts``src/server/checker/store.ts`
- 使用 `bun:test` 框架(`describe`/`test`/`expect`),测试数据库用临时目录 + `tmpdir()`
- 新增 store 方法必须编写单元测试;新增 API 端点必须在 `app.test.ts` 中添加集成测试
- 测试后清理:`afterAll``store.close()` + `rm(tempDir, { recursive: true })`
## 前端样式规范
---
前端基于 TDesign React 构建UI样式开发遵循以下优先级从高到低
## 二、前端开发指引
### 2.1 技术栈概览
| 层面 | 技术 | 用途 |
| -------- | ----------------------------------- | ------------------------------ |
| 框架 | React 19 | UI 组件开发 |
| 构建 | Vite 8 | 开发服务与生产构建 |
| 语言 | TypeScript 6 | 类型安全 |
| UI 库 | TDesign React + tdesign-icons-react | UI 组件与图标 |
| 数据层 | TanStack Query (React Query) | 服务端状态管理与自动轮询 |
| 图表 | Recharts | 拨测趋势折线图与状态环状图 |
| 路由 | 无(单页面 Dashboard | 仅需 Drawer/Tab 做页面内导航 |
**不引入的依赖**React Router单页面场景不需要、状态管理库TanStack Query 即服务端状态层,组件内用 `useState` 足够)
### 2.2 组件树与数据流
```
main.tsx
└── QueryClientProviderTanStack Query 全局挂载)
└── App根组件
├── SummaryCards总览统计卡片
│ └── useSummary() ─── GET /api/summary8s 轮询)
└── TargetBoard目标列表
├── useTargets() ─── GET /api/targets8s 轮询)
└── TargetGroup[](按 group 字段分组)
└── PrimaryTable ← TARGET_TABLE_COLUMNS列定义排序/筛选/渲染)
└── TargetDetailDrawer目标详情抽屉
└── useTargetDetail() ── 按需发起 trend + history 查询
├── Tab: 概览 → Statistic + TrendChart + StatusDonut + Descriptions
└── Tab: 记录 → PrimaryTable分页历史记录
```
**数据层架构**
```
hooks/useTargetDetail.ts唯一的数据层入口
├── queryKeys结构化 query key确保缓存粒度精确
├── useSummary() → /api/summary8s 自动轮询)
├── useTargets() → /api/targets8s 自动轮询)
└── useTargetDetail()(组合 hook管理 Drawer 全部状态)
├── 内部复用 useTargets() 的缓存来查找 selectedTarget
├── useQuery(/api/targets/:id/trend)条件查询enabled 仅当 Drawer 打开且时间范围有效)
└── useQuery(/api/targets/:id/history)(条件查询:含分页)
```
### 2.3 TanStack Query 数据层
#### Query Key 规范
```typescript
const queryKeys = {
summary: () => ["summary"] as const,
targets: () => ["targets"] as const,
trend: (targetId: number, from: string, to: string) => ["trend", targetId, from, to] as const,
history: (targetId: number, from: string, to: string, page: number) => ["history", targetId, from, to, page] as const,
};
```
- Key 使用 **structured array**(非字符串),以便精确匹配和按 prefix 失效
- 使用 `as const` 保持字面量类型
- 排序scope → id → 参数(粒度从粗到细)
#### 查询配置规范
```typescript
// 全局面板级查询(需要持续刷新)
useQuery({
queryKey: queryKeys.summary(),
queryFn: () => fetchJson<SummaryResponse>("/api/summary"),
refetchInterval: 8000, // 自动轮询间隔
refetchIntervalInBackground: false, // 切后台不轮询
});
// 详情级查询(按需加载)
useQuery({
queryKey: selectedTargetId ? queryKeys.trend(id, from, to) : ["trend", "disabled"],
queryFn: () => fetchJson(`/api/targets/${id}/trend?...`),
enabled: selectedTargetId !== null && !!timeFrom && !!timeTo, // 条件查询
});
```
#### fetch 封装
```typescript
async function fetchJson<T>(url: string): Promise<T> {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json() as Promise<T>;
}
```
- 统一使用 `fetch`(不引入 axios与后端共享 Web API 生态
- 错误抛异常,由 TanStack Query 的 `error` 状态承接
#### QueryClient 全局配置
```typescript
new QueryClient({
defaultOptions: {
queries: {
retry: 1, // 失败重试 1 次
refetchOnWindowFocus: true, // 窗口聚焦时刷新
staleTime: 5000, // 5s 内视为 fresh避免重复请求
},
},
});
```
### 2.4 组件开发规范
#### 文件命名与导入
- 每个 React 组件一个 `.tsx` 文件,文件名使用 PascalCase`StatusDot.tsx`
- 组件 props 定义为 `interface XxxProps`,紧邻组件函数声明
- 类型从 `../../shared/api` 导入,使用 `type` 导入(`import type { ... }`
```typescript
import type { TargetStatus } from "../../shared/api";
import { StatusDot } from "./StatusDot";
interface TargetGroupProps {
name: string;
targets: TargetStatus[];
onTargetClick: (target: TargetStatus) => void;
}
export function TargetGroup({ name, targets, onTargetClick }: TargetGroupProps) {
// ...
}
```
#### 组件拆分原则
- **展示组件**`components/`):纯渲染逻辑,通过 props 接收数据,通过回调返回事件
- **容器逻辑**放在 hooks 中,组件只做数据消费
- **常量数据**(列定义、排序器、筛选器)放在 `constants/`,不放在组件内部
- **工具函数**(时间处理等)放在 `utils/`,保持纯函数无副作用
#### 现有组件清单
| 组件 | 文件 | 用途 |
| ----------------------- | -------------------------- | ---------------------------- |
| `App` | `app.tsx` | 根组件,编排全局状态与布局 |
| `SummaryCards` | `components/SummaryCards.tsx` | 总览统计卡片(全部/正常/异常) |
| `TargetBoard` | `components/TargetBoard.tsx` | 按分组渲染目标表格列表 |
| `TargetGroup` | `components/TargetGroup.tsx` | 单个分组标题 + PrimaryTable |
| `TargetDetailDrawer` | `components/TargetDetailDrawer.tsx` | 目标详情抽屉(概览/记录 Tab |
| `TrendChart` | `components/TrendChart.tsx` | Recharts 双轴折线图(耗时/可用率) |
| `StatusDonut` | `components/StatusDonut.tsx` | Recharts 环状图UP/DOWN 分布) |
| `StatusDot` | `components/StatusDot.tsx` | 圆形状态指示点(绿/红) |
| `StatusBar` | `components/StatusBar.tsx` | 最近采样状态条(多色块) |
| `GroupHeader` | `components/GroupHeader.tsx` | 分组标题(名称 + 统计) |
### 2.5 新增功能开发步骤
以"新增一个详情页面 Tab"为例:
1. **确认数据需求**:是已有 API 数据还是需要新端点?
- 如有新端点,先在 `src/server/routes/` 添加,参考 [1.3 新增路由步骤](#13-api-路由开发)
- 如有新字段,更新 `src/shared/api.ts` 类型定义
2. **实现 hooks**:在 `src/web/hooks/useTargetDetail.ts` 中新增 `useQuery`(写好 `queryKey``enabled` 条件)
3. **编写组件**:在 `src/web/components/` 创建组件文件
-`TargetDetailDrawer.tsx` 中新增 `<Tabs.TabPanel>` 引用
4. **编写常量**:如有列定义/排序器/筛选器,放在 `src/web/constants/`
5. **编写测试**:在 `tests/web/` 下添加对应的单元测试
### 2.6 样式开发规范
前端基于 TDesign React 构建 UI样式开发遵循以下优先级从高到低
1. **使用 TDesign 组件**:布局、间距、排版优先使用 TDesign 组件(如 Space、Divider、Typography
2. **使用 TDesign 组件 props**:通过组件的 props 参数控制外观(如 `theme``variant``size`
@@ -251,6 +398,256 @@ runCommandCheck → 收集观测(exitCode/stdout/stderr/durationMs)
- **严禁使用 `!important`**
- 颜色统一使用 TDesign CSS tokens`--td-success-color``--td-error-color``--td-warning-color` 等),不使用硬编码色值
**styles.css 组织**
- 自定义 CSS 变量(如可用率渐变色 `--avail-0` ~ `--avail-9`)定义在 `:root`
- 布局类(`.dashboard``.dashboard-header`)定义全局页面结构
- 组件修饰类(`.status-dot--up``.latency-ok`)为自定义视觉组件提供样式变体
- TDesign 表格行高亮(`.row-down`)通过 `rowClassName` prop 应用
### 2.7 前端测试规范
- 测试目录:`tests/web/`,结构对应 `src/web/`
- 重点测试 **constants/** 中的纯函数(排序器、筛选器、颜色阈值等)
- 使用 `bun:test` 框架
---
## 三、项目运行、集成与打包
### 3.1 开发期运行
#### 同时启动前后端
```bash
bun run dev probes.yaml
```
`scripts/dev.ts` 通过 `Bun.spawn` 同时启动两个子进程:
```
bun run dev probes.yaml
├── bun run dev:server probes.yaml → Bun HTTP 后端(默认 3000 端口)
└── bun run dev:web → Vite 前端开发服务器5173 端口)
```
- 任一子进程退出会导致整体退出
- `SIGINT`/`SIGTERM` 信号会同时终止两个子进程
- `BACKEND_PORT` 环境变量可覆盖后端端口
#### 分别启动
```bash
# 启动后端(含 watch 模式自动重启)
bun run dev:server probes.yaml
# 另开终端启动前端
bun run dev:web
```
### 3.2 前后端集成方式
#### 开发期代理
Vite 配置了开发代理(`vite.config.ts`
```typescript
server: {
proxy: {
"/api": {
target: `http://127.0.0.1:${backendPort}`,
changeOrigin: true,
},
},
}
```
前端访问 `/api/*`Vite 开发服务器自动转发到后端 `http://127.0.0.1:${backendPort}`,无需 CORS 配置。
前端开发地址为 `http://127.0.0.1:5173`(严格端口 `strictPort: true`)。
后端在开发模式下不提供静态资源服务,访问 `http://127.0.0.1:3000` 会提示"请通过 Vite 前端地址访问"。
#### 生产期集成
生产可执行文件是单体应用:前端静态资源嵌入 binary后端同时提供 API 和静态文件服务。
```
./dist/dial-server probes.yaml
启动后:
访问 http://127.0.0.1:3000/ → 返回前端 SPAindex.html
访问 http://127.0.0.1:3000/api/* → 返回后端 API
访问 /assets/* → 返回带不可变缓存的静态资源
```
SPA fallback 逻辑(`src/server/static.ts`
- `/` → index.html
- 匹配 `/assets/*` → 返回对应文件(未匹配则 404
- 其他路径(如 `/dashboard`)→ fallback 到 index.htmlSPA 路由)
### 3.3 构建打包
#### 构建命令
```bash
bun run build
```
#### 构建流程详解
`scripts/build.ts` 执行以下步骤:
```
1. vite build
├── 入口src/web/index.html
└── 输出dist/web/index.html + assets/
2. 生成 .build/static-assets.ts临时文件
├── import Vite 产物为 Bun.file
└── 导出 staticAssets: StaticAssets 对象
3. 生成 .build/server-entry.ts临时文件
└── import 后端入口模块 + staticAssets作为 Bun.build 入口
4. Bun.build({ compile, minify, sourcemap: "linked" })
└── 输出dist/dial-server单文件可执行 binary
```
#### 产物
| 产物 | 用途 |
| ---------------------------- | ------------------------ |
| `dist/dial-server` | 生产可执行文件 |
| `dist/web/` | Vite 构建产物(中间产物) |
| `.build/` | 临时生成文件(构建后清理) |
#### 构建参数
| 环境变量 | 说明 |
| ------------------- | ----------------------------------------- |
| `BUN_TARGET`/`BUILD_TARGET` | 交叉编译目标平台(如 `bun-linux-x64` |
#### 运行可执行文件
```bash
./dist/dial-server probes.yaml
```
#### 清理
```bash
bun run clean
# 清理 .build/ 缓存和 *.bun-build 临时文件
```
### 3.4 开发工作流
#### 日常开发循环
```bash
bun run dev probes.yaml # 启动开发环境
# 修改代码 → Vite HMR前端/ bun --watch后端自动重启
bun run check # 提交前运行完整质量检查
```
#### 完整验证流程
```bash
bun run verify
# = bun run check + bun run build + bun run test:smoke
```
`verify` 适合 CI 或正式提交前会完整验证类型检查、lint、格式、单元测试、构建、smoke test。
### 3.5 Smoke Test
```bash
bun run test:smoke
```
`scripts/smoke.ts` 构建后验证流程:
1. 动态分配空闲端口
2. 用临时配置文件启动 `dist/dial-server`
3. 等待健康检查通过
4. 验证所有 API 端点返回正确数据
5. 验证静态资源服务(含 SPA fallback 和 404 处理)
6. 验证安全 headers
7. 测试结束清理临时目录和进程
### 3.6 脚本说明
| 脚本 | 文件 | 说明 |
| ----------------------- | ------------------ | ---------------------------------- |
| `bun run dev` | `scripts/dev.ts` | 同时启动前后端开发服务 |
| `bun run build` | `scripts/build.ts` | Vite 构建 + Bun 编译可执行文件 |
| `bun run test:smoke` | `scripts/smoke.ts` | 构建后的端到端验证 |
| `bun run clean` | `scripts/clean.ts` | 清理构建缓存与临时文件 |
### 3.7 环境变量
| 变量 | 用途 | 默认值 |
| ----------------------- | ------------------------------------------- | ------ |
| `PORT`/`BACKEND_PORT` | 后端监听端口(开发期 Vite 代理目标、生产期监听端口) | `3000` |
| `BUN_TARGET`/`BUILD_TARGET` | 交叉编译目标平台(仅在 `bun run build` 时有效) | 当前平台 |
### 3.8 项目配置文件
| 文件 | 用途 |
| --------------------- | -------------------------------------- |
| `package.json` | 项目信息、脚本、依赖声明 |
| `tsconfig.json` | TypeScript 配置ESNext 模块、严格模式)|
| `vite.config.ts` | Vite 开发代理与构建配置 |
| `eslint.config.js` | ESLint 规则(含前端不得 import server 的检查) |
| `.prettierrc.json` | Prettier 格式化规则(`printWidth: 120` |
| `.prettierignore` | Prettier 排除路径 |
| `probes.example.yaml` | 配置文件示例 |
| `opencode.json` | OpenCode 工具配置TDesign MCP server |
### 3.9 依赖管理
- **包管理器**:仅使用 `bun`,禁止使用 npm、pnpm、yarn
- **安装依赖**`bun install`
- **运行工具**:使用 `bunx`,禁止使用 `npx``pnpx`
- **锁文件**`bun.lock`
### 3.10 目录约定
| 目录 | 约定 |
| --------------- | ------------------------------------------ |
| `src/server/` | 后端代码,不能 import `src/web/` |
| `src/web/` | 前端代码,不能 import `src/server/` |
| `src/shared/` | 前后端共享类型,双向可引用 |
| `scripts/` | 独立运行脚本,可 import 项目源码 |
| `tests/` | 测试目录,结构镜像 src 目录 |
| `dist/` | 构建产物gitignore |
| `.build/` | 构建临时文件gitignore |
| `openspec/` | OpenSpec 变更管理与规格文档 |
| `data/` | 默认数据目录gitignore运行期生成 SQLite|
---
## 代码质量
```bash
bun run lint # ESLint 检查
bun run format:check # Prettier 格式检查
bun run format # Prettier 自动格式化
bun run typecheck # TypeScript 类型检查
bun test # 运行所有测试
bun run check # 一键运行 typecheck + lint + format:check + test
```
`check` 是日常开发推荐的质量检查命令。
## 测试
```bash
bun run check # 日常开发(类型检查 + lint + 格式 + 单元测试)
bun run verify # 完整验证check + 构建 + smoke test
```
## 已知限制
当前不做告警通知、数据自动清理、拨测目标动态增删、认证鉴权和分布式部署。Command 类型拨测不支持 Windows 环境。