- app.ts 单体路由拆分为 routes/ + helpers + middleware + static 独立模块 - 类型去重:CheckFailure/CheckResult 以 shared/api.ts 为唯一源头,收紧 phase 联合类型 - es-toolkit 替换:isPlainObject/isNil/isEmptyObject/isEqual/isError/Semaphore/groupBy - Bun 内置 API:Object.fromEntries 替代手写 headersToRecord - bun:sqlite 规范:prepare() → query() 利用内置缓存,避免 N+1 查询 - 新增 getLatestChecksMap/allGetTargetStats 批量查询方法 - 新增 backend-code-quality/api-route-separation/batch-data-queries 规范 - 补充 openspec/config.yaml 后端开发规范与 DEVELOPMENT.md 后端开发指引
12 KiB
12 KiB
DiAL 开发文档
本文档面向 DiAL 项目的开发者,介绍项目结构、构建流程、测试、代码规范等内容。
用户使用说明请参阅 README.md。
项目结构
src/
server/
app.ts Bun HTTP 路由入口(路由分发 + API 汇聚)
config.ts CLI 参数解析
dev.ts 开发期启动入口
server.ts HTTP server 启动
helpers.ts 共享响应格式化工具(jsonResponse、createHeaders 等)
middleware.ts API 参数校验中间件(guardGetHead、validateTargetId 等)
static.ts 静态资源服务 与 SPA fallback
routes/ API 路由 handler(按端点拆分)
health.ts GET /health
summary.ts GET /api/summary
targets.ts GET /api/targets
history.ts GET /api/targets/:id/history
trend.ts GET /api/targets/:id/trend
checker/
types.ts 类型定义
config-loader.ts YAML 配置解析与校验
store.ts SQLite 数据存储
fetcher.ts HTTP 拨测执行
command-runner.ts 命令行拨测执行
size.ts 大小单位解析
engine.ts 调度引擎(按 interval 分组的 es-toolkit groupBy + Semaphore 并发控制)
expect/
http.ts HTTP 响应断言
command.ts 命令行输出断言
body.ts HTTP body 断言(JSONPath/XPath/CSS,类型判断使用 es-toolkit)
failure.ts 失败信息类型
shared/
api.ts 前后端共享 TypeScript 类型
web/ Vite + React 前端 Dashboard
components/ UI 组件(表格、分组、Drawer、状态条等)
constants/ 常量定义(列配置、类型映射、排序/筛选/颜色阈值函数)
hooks/ TanStack Query 数据层(useTargetDetail 集成轮询/条件查询)
utils/ 前端工具函数
scripts/ 开发、构建和 smoke test 脚本
tests/ Bun test 测试
openspec/ OpenSpec 变更与规格文档
构建 executable
bun run build
构建流程:
- 运行
vite build,输出前端资源到dist/web - 生成临时
.build/static-assets.ts,嵌入 Vite 产物 - 生成临时
.build/server-entry.ts,作为生产入口 - 运行
Bun.build({ compile }),输出dist/dial-server
运行 executable:
./dist/dial-server probes.yaml
代码质量
bun run lint
bun run format:check
bun run format
bun run check
check依次运行typecheck、lint、format:check和单元测试。
测试
bun run check
bun run verify
check适合日常开发,包含类型检查、lint、格式检查和单元测试。verify先运行check,再重新构建生产 executable 并运行 smoke test。
前后端边界
前端只通过 HTTP 调用后端,API 路径为 /api/*。共享类型放在 src/shared,前端不得 import src/server 的运行时实现。
后端开发指引
架构概览
启动流程:
dev.ts → readRuntimeConfig(cli args) → loadConfig(yaml)
→ ProbeStore(db) → ProbeEngine(store, targets) → startServer(store)
运行时:
定时器(tick) → ProbeEngine.probeGroup()
→ HTTP: fetcher.ts / Command: command-runner.ts
→ expect/*.ts 校验 → store.insertCheckResult()
HTTP 请求:
Request → app.ts(路由分发) → routes/*.ts(handler)
→ middleware.ts(参数校验) → helpers.ts(响应格式化) → Response
库使用优先级
后端代码开发遵循严格的库选择顺序:
| 优先级 | 来源 | 典型用途 |
|---|---|---|
| 1 | Bun 内置 API | Bun.serve、bun:sqlite、Bun.spawn、Bun.file、Bun.YAML |
| 2 | es-toolkit | 类型判断(isPlainObject/isNil/isEmptyObject)、深度比较(isEqual)、错误判断(isError)、并发控制(Semaphore)、集合操作(groupBy) |
| 3 | 标准 Web API | Object.fromEntries、Headers、fetch、AbortController |
| 4 | 主流三方库 | cheerio(HTML 解析)、xpath + @xmldom/xmldom(XML 解析) |
| 5 | 自行实现 | 仅在以上都无法满足时(如 parseDuration、parseSize、evaluateJsonPath 等专项逻辑) |
API 路由开发
路由文件位于 src/server/routes/,每个端点一个文件。handler 函数签名统一为:
export function handleXxx(params, store: ProbeStore, method: string, mode: RuntimeMode): Response;
请求处理流程:
app.ts的createFetchHandler作为总入口,根据 URL pattern 匹配路由- API 路由统一经过
guardGetHead做方法检查(仅允许 GET/HEAD) - 各 handler 内部通过
middleware.ts提供的validateTargetId、validateTimeRange、validatePagination做参数校验 - 校验函数返回
Response表示校验失败(直接返回),返回数据对象表示通过 - 业务逻辑通过
store查询数据,用helpers.ts的jsonResponse、mapCheckResult、formatDuration等格式化输出
新增路由步骤:
- 在
src/server/routes/下创建<name>.ts - 实现 handler 函数并 export
- 在
app.ts的handleApiRoute中注册路径匹配和调用 - 在
tests/server/app.test.ts中添加对应测试
共享工具
helpers.ts:跨路由共用的响应工具函数(jsonResponse、createHeaders、createApiError、mapCheckResult、formatDuration、createHealthResponse)middleware.ts:API 参数校验函数(guardGetHead、validateTargetId、validateTimeRange、validatePagination)static.ts:生产模式下的静态资源服务与 SPA fallback
类型定义规范
- 共享类型以
src/shared/api.ts为唯一源头,前后端共同引用 - 前端不得
import src/server/下的任何文件 - 严格联合类型优先于宽类型:如
phase: "status" | "duration" | ...而非phase: string - 后端内部扩展:
checker/types.ts中CheckResult通过extends共享版本的ApiCheckResult增加targetName等内部字段 - 存储层类型(
StoredTarget、StoredCheckResult)独立定义,与 API 类型分离
数据存储规范
基于 bun:sqlite,WAL 模式运行,数据库文件位于配置的 dataDir 下。
Statement 使用规范:
| 场景 | 方式 | 原因 |
|---|---|---|
| 单次读/写 | this.db.query(sql).get()/all()/run() |
bun:sqlite 内置 statement 缓存,自动复用 |
| 事务内多次复用 | this.db.prepare(sql) 缓存为局部变量 |
事务闭包中需要持有引用 |
查询优化:
- 避免 N+1 查询:批量场景优先用单次 SQL 聚合(GROUP BY、子查询 JOIN)+ 内存组装
- 新增批量查询方法时必须编写对应单元测试
getSummary()和GET /api/targets的响应组装已通过getLatestChecksMap+getAllTargetStats实现批量查询
Schema:
targets表:name(UNIQUE)、type、target(展示摘要)、config(JSON)、interval_ms、timeout_ms、expect(JSON)、grpcheck_results表:target_id(FK CASCADE)、timestamp、matched(0/1)、duration_ms、status_detail、failure(JSON)- 复合索引:
(target_id, timestamp)
拨测引擎
- 调度:
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()写入 SQLite,engine 通过targetNameToId缓存 name→id 映射
expect 断言系统
两层模型:观测值收集 → 规则校验。
HTTP 校验流程:
runHttpCheck → 收集观测(statusCode/headers/body/durationMs)
→ checkHttpExpect → status → duration → headers → body(可选)
→ 首个失败即停止,返回 CheckFailure
Command 校验流程:
runCommandCheck → 收集观测(exitCode/stdout/stderr/durationMs)
→ checkCommandExpect → exitCode → duration → stdout → stderr
→ 首个失败即停止
Body 规则类型:
contains:文本包含匹配regex:正则表达式匹配json:JSONPath 提取 + 操作符比较(使用es-toolkit/isPlainObject区分纯值和操作符)css:cheerio CSS 选择器 + 操作符比较xpath:XPath 节点提取 + 操作符比较
操作符:equals(深度比较,es-toolkit/isEqual)、contains、match(正则)、empty(isNil+isEmptyObject)、exists、gte/lte/gt/lt
错误模式
- 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)
测试规范
- 测试文件与源文件对应:
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,样式开发遵循以下优先级(从高到低):
- 使用 TDesign 组件:布局、间距、排版优先使用 TDesign 组件(如 Space、Divider、Typography)
- 使用 TDesign 组件 props:通过组件的 props 参数控制外观(如
theme、variant、size) - 使用 TDesign CSS tokens:颜色、间距、字体等使用
--td-*CSS 变量(如--td-success-color、--td-comp-margin-xxl) - 在 styles.css 中定义 CSS 类:无法通过上述方式满足的样式需求,集中定义在
styles.css中 - 自行开发组件:仅在 TDesign 无法满足需求时自行开发
红线:
- 严禁在组件中使用
style属性内联调整样式 - 严禁通过 CSS 覆盖 TDesign 组件内部类名(如
.t-tab-panel),如需定制使用组件的classNameprop - 严禁使用
!important - 颜色统一使用 TDesign CSS tokens(
--td-success-color、--td-error-color、--td-warning-color等),不使用硬编码色值
已知限制
当前不做告警通知、数据自动清理、拨测目标动态增删、认证鉴权和分布式部署。Command 类型拨测不支持 Windows 环境。