1
0

refactor: 迁移 Bun fullstack 架构

This commit is contained in:
2026-05-14 00:23:37 +08:00
parent bcfac52112
commit 6e485cc991
36 changed files with 403 additions and 1081 deletions

View File

@@ -20,16 +20,16 @@
```text
src/
server/
app.ts Bun HTTP 路由入口(路由分发 + API 汇聚、StaticAssets 接口定义)
bootstrap.ts 后端统一启动引导loadConfig → store → engine → startServer → shutdown
config.ts CLI 参数解析(仅提取配置文件路径)
dev.ts 开发模式启动入口
server.ts HTTP server 启动工厂(接收 StartServerOptions
dev.ts 开发模式启动入口mode: "development"HMR 自动注入)
main.ts 生产模式启动入口mode: "production",安全头启用
server.ts HTTP server 启动工厂Bun.serve routes 声明式路由 + HTML import
helpers.ts 共享响应格式化工具(见下方函数清单)
middleware.ts API 参数校验中间件(guardGetHead、validateTargetId、validateTimeRange、validatePagination
static.ts 静态资源服务与 SPA fallback
routes/ API 路由 handler按端点拆分签名因端点而异
middleware.ts API 参数校验中间件validateTargetId、validateTimeRange、validatePagination
routes/ API 路由 handler按端点拆分
health.ts GET /health无 store 参数)
meta.ts GET /api/meta
summary.ts GET /api/summary
targets.ts GET /api/targets
history.ts GET /api/targets/:id/history
@@ -61,7 +61,7 @@ src/
command/ Command Checker自包含模块含 types/schema/execute/expect/validate/text
shared/
api.ts 前后端共享 TypeScript 类型
web/ Vite + React 前端 Dashboard
web/ React 前端 Dashboard(通过 Bun HTML import 集成)
app.tsx 根组件(编排全局状态与布局)
main.tsx 入口QueryClient 挂载 + ErrorBoundary + ReactQueryDevtools
styles.css 全局样式与自定义 CSS 变量
@@ -78,7 +78,7 @@ src/
utils/ 前端工具函数
time.ts 时间处理subtractHours
stats.ts 趋势统计计算computeTrendStats
scripts/ 开发、构建、schema 生成和 smoke test 脚本
scripts/ 构建、schema 生成和清理脚本
tests/ Bun test 测试(结构镜像 src 目录)
openspec/ OpenSpec 变更与规格文档
probe-config.schema.json 用户配置 JSON Schema 导出物(用于 IDE 自动补全和校验)
@@ -98,12 +98,12 @@ probe-config.schema.json 用户配置 JSON Schema 导出物(用于 IDE 自动
```
启动流程:
dev.ts / build entry → readRuntimeConfig(cli args, 仅提取 configPath)
→ bootstrap({ configPath, mode, staticAssets? })
dev.ts / main.ts → readRuntimeConfig(cli args, 仅提取 configPath)
→ bootstrap({ configPath, mode })
→ loadConfig(yaml) → ResolvedConfig{ host, port, dataDir, maxConcurrentChecks, retentionMs, targets }
→ ProbeStore(db) → store.syncTargets(targets)
→ ProbeEngine(store, targets, maxConcurrentChecks, retentionMs) → engine.start()
→ startServer({ config, mode, store, staticAssets? })
→ startServer({ config, mode, store })
→ 注册 SIGINT/SIGTERM shutdownengine.stop + store.close
运行时:
@@ -113,8 +113,9 @@ probe-config.schema.json 用户配置 JSON Schema 导出物(用于 IDE 自动
数据清理: 定时 prune(retentionMs),每小时执行一次
HTTP 请求:
Request → app.ts(路由分发) → routes/*.ts(handler)
Request → Bun.serve routes 声明式匹配 → routes/*.ts(handler)
→ middleware.ts(参数校验) → helpers.ts(响应格式化) → Response
前端: "/*": homepage (HTML import) → SPA fallback + HMR(开发模式)
```
### 1.2 库使用优先级
@@ -133,50 +134,63 @@ HTTP 请求:
### 1.3 API 路由开发
路由文件位于 `src/server/routes/`,每个端点一个文件。handler 函数签名因端点而异
路由文件位于 `src/server/routes/`,每个端点一个文件。路由通过 `server.ts``Bun.serve({ routes })` 声明式注册,使用 per-method handler 对象
```typescript
// 无 store 的路由(健康检查不依赖数据库)
export function handleHealth(method: string, mode: RuntimeMode): Response;
// server.ts 中的路由注册
routes: {
"/*": homepage, // HTML importSPA fallback
"/api/*": () => jsonResponse(createApiError("API route not found", 404), { mode, status: 404 }),
"/api/meta": { GET: () => handleMeta(mode) },
"/api/summary": { GET: () => handleSummary(store, mode) },
"/api/targets": { GET: () => handleTargets(store, mode) },
"/api/targets/:id/history": { GET: (req) => handleHistory(req.params.id, new URL(req.url), store, mode) },
"/api/targets/:id/trend": { GET: (req) => handleTrend(req.params.id, new URL(req.url), store, mode) },
"/health": { GET: () => handleHealth(mode) },
}
```
Handler 函数签名因端点而异:
```typescript
// 无 store 的路由
export function handleHealth(mode: RuntimeMode): Response;
export function handleMeta(mode: RuntimeMode): Response;
// 仅有 store 的路由
export function handleSummary(store: ProbeStore, method: string, mode: RuntimeMode): Response;
export function handleTargets(store: ProbeStore, method: string, mode: RuntimeMode): Response;
export function handleSummary(store: ProbeStore, mode: RuntimeMode): Response;
export function handleTargets(store: ProbeStore, mode: RuntimeMode): Response;
// 带 target ID 和查询参数的路由
export function handleHistory(idStr: string, url: URL, method: string, store: ProbeStore, mode: RuntimeMode): Response;
export function handleTrend(idStr: string, url: URL, method: string, store: ProbeStore, mode: RuntimeMode): Response;
export function handleHistory(idStr: string, url: URL, store: ProbeStore, mode: RuntimeMode): Response;
export function handleTrend(idStr: string, url: URL, store: ProbeStore, mode: RuntimeMode): Response;
```
**请求处理流程**
1. `app.ts``createFetchHandler` 作为总入口,根据 URL pattern 匹配路由
2. `/health` 路由独立处理,不经过 `guardGetHead`(使用 `helpers.ts``allowsGetHead` 自行校验方法
3. `/api/*` 路由统一经过 `guardGetHead` 做方法检查(仅允许 GET/HEAD返回 `null` 表示通过
4. 各 handler 内部通过 `middleware.ts` 提供的 `validateTargetId``validateTimeRange``validatePagination` 做参数校验,`pageSize` 最大值为 `200`
5. 校验函数返回 `Response` 实例表示校验失败(直接返回),返回数据对象表示通过
6. 业务逻辑通过 `store` 查询数据,用 `helpers.ts``jsonResponse``mapCheckResult``formatDuration` 等格式化输出
1. `Bun.serve``routes` 对象按路径 + HTTP 方法匹配请求
2. 未匹配方法的请求落入 `/api/*` 通配符(返回 404
3. 各 handler 内部通过 `middleware.ts` 提供的 `validateTargetId``validateTimeRange``validatePagination` 做参数校验,`pageSize` 最大值为 `200`
4. 校验函数返回 `Response` 实例表示校验失败(直接返回),返回数据对象表示通过
5. 业务逻辑通过 `store` 查询数据,用 `helpers.ts``jsonResponse``mapCheckResult``formatDuration` 等格式化输出
**新增路由步骤**
1.`src/server/routes/` 下创建 `<name>.ts`
2. 实现 handler 函数并 export
3.`app.ts``createFetchHandler` 中注册路径匹配和调用
3.`server.ts``routes` 对象中注册路径和 method handler
4.`tests/server/app.test.ts` 中添加对应测试
### 1.4 共享工具
- **`helpers.ts`**:跨路由共用的响应工具函数
- `allowsGetHead(method)` — 判断是否为 GET/HEAD 方法
- `createApiError(error, status)` — 构造 API 错误体
- `createHeaders(mode, init)` — 创建响应 Headers生产模式附加安全头
- `createHealthResponse()` — 构造健康检查响应
- `formatDuration(ms)` — 毫秒转为可读时长字符串
- `jsonResponse(body, options)` — JSON 响应构造(自动处理 HEAD 空体)
- `jsonResponse(body, options)` — JSON 响应构造
- `mapCheckResult(row)` — 数据库行转 API CheckResult
- `methodNotAllowedResponse(allow, mode)` — 构造 405 响应
- **`middleware.ts`**API 参数校验函数(`guardGetHead``validateTargetId``validateTimeRange``validatePagination`,其中 `pageSize` 上限为 `200`
- **`static.ts`**:生产模式下的静态资源服务与 SPA fallback
- **`middleware.ts`**API 参数校验函数(`validateTargetId``validateTimeRange``validatePagination`,其中 `pageSize` 上限为 `200`
### 1.5 类型定义规范
@@ -403,7 +417,7 @@ TcpChecker implements Checker
□ probes.example.yaml — 配置示例
□ bun run schema + bun run schema:check — Schema 导出同步
□ bun run check — 全量质量检查通过
□ bun run verify — 完整验证(含 build + smoke test
□ bun run verify — 完整验证(check + build
□ README.md — 用户文档
□ DEVELOPMENT.md — 项目结构目录树
```
@@ -496,7 +510,7 @@ CommandChecker.execute → 收集观测(exitCode/stdout/stderr/durationMs)
### 1.11 错误模式
- **API 错误**`{ error: "描述", status: <code> }`,状态码 400/404/405/503
- **API 错误**`{ error: "描述", status: <code> }`,状态码 400/404/503
- **CheckFailure**`{ kind: "error"|"mismatch", phase, path, expected?, actual?, message }`
- **错误处理**expect 校验失败记录首个失败原因;网络/超时/进程崩溃统一为 `kind:"error"`,请求/TLS/timeout 错误归属 `phase:"request"`body 超限/解码/解析错误归属 `phase:"body"`
- **日志**:解析失败等非致命异常用 `console.warn`,启动失败用 `console.error` + `process.exit(1)`
@@ -522,14 +536,14 @@ CommandChecker.execute → 收集观测(exitCode/stdout/stderr/durationMs)
| 层面 | 技术 | 用途 |
| ------ | --------------------------------------------------- | ---------------------------- |
| 框架 | React 19 | UI 组件开发 |
| 构建 | Vite 8 | 开发服务与生产构建 |
| 构建 | Bun HTML importfullstack 模式) | 开发服务与生产构建 |
| 语言 | TypeScript 6 | 类型安全 |
| UI 库 | TDesign React + tdesign-icons-react | UI 组件与图标 |
| 数据层 | TanStack Query (React Query) + React Query Devtools | 服务端状态管理与自动轮询 |
| 图表 | Recharts | 拨测趋势折线图与状态环状图 |
| 路由 | 无(单页面 Dashboard | 仅需 Drawer/Tab 做页面内导航 |
**不引入的依赖**React Router单页面场景不需要、状态管理库TanStack Query 即服务端状态层,组件内用 `useState` 足够)
**不引入的依赖**React Router单页面场景不需要、状态管理库TanStack Query 即服务端状态层,组件内用 `useState` 足够)、Vite已由 Bun 原生 fullstack 替代)
### 2.2 组件树与数据流
@@ -729,81 +743,47 @@ export function TargetGroup({ name, targets, onTargetClick }: TargetGroupProps)
### 3.1 开发期运行
#### 同时启动前后端
```bash
bun run dev probes.yaml
```
`scripts/dev.ts` 通过 `Bun.spawn` 同时启动两个子进程
`bun --watch src/server/dev.ts` 启动单进程 fullstack 开发服务器
```
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
```
- 后端 API + 前端 SPA 在同一端口(默认 3000
- `development` 模式自动注入 HMR前端修改即时热更新
- `--watch` 监听后端文件变更自动重启
- 访问 `http://127.0.0.1:3000` 即可使用完整应用
### 3.2 前后端集成方式
#### 开发期代理
#### 统一进程架构
Vite 配置了开发代理(`vite.config.ts`)和代码分割策略
前后端通过 Bun 的 HTML import 机制集成为单进程应用
```typescript
// 开发代理
server: {
proxy: {
"/api": {
target: `http://127.0.0.1:${backendPort}`,
changeOrigin: true,
},
// server.ts
import homepage from "../web/index.html";
const server = Bun.serve({
development: mode === "development" ? { hmr: true, console: true } : false,
routes: {
"/*": homepage, // SPA fallback开发模式自动注入 HMR
"/api/*": () => ..., // API 通配符(未匹配路由返回 404
"/api/summary": { GET: () => handleSummary(store, mode) },
"/health": { GET: () => handleHealth(mode) },
// ...
},
}
// 生产代码分割rolldownOptions.output.codeSplitting.groups
// vendor-react: react/react-dom/scheduler
// vendor-tdesign: tdesign
// vendor-chart: recharts/d3-*
});
```
前端访问 `/api/*`Vite 开发服务器自动转发到后端 `http://127.0.0.1:${backendPort}`,无需 CORS 配置。
- 开发模式(`development: { hmr: true, console: true }`Bun 自动为 HTML import 注入 HMR client前端修改无需手动刷新并将浏览器 console 回显到终端
- 生产模式HTML 及其引用的 JS/CSS 资源在 `bun build --compile` 时自动打包进可执行文件
前端开发地址为 `http://127.0.0.1:5173`(严格端口 `strictPort: true`)。
#### 路由优先级
后端在开发模式下不提供静态资源服务,访问 `http://127.0.0.1:3000` 会提示"请通过 Vite 前端地址访问"
Bun routes 的匹配规则:具体路径 > 通配符。`/api/summary` 优先于 `/api/*``/health` 优先于 `/*`
#### 生产期集成
生产可执行文件是单体应用:前端静态资源嵌入 binary通过 `StaticAssets` 接口:`files: Record<string, Blob>` + `indexHtml: Blob`),后端同时提供 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 路由)
未匹配 method 的请求(如 POST /api/summary会落入 `/api/*` 通配符返回 404。
### 3.3 构建打包
@@ -813,33 +793,28 @@ SPA fallback 逻辑(`src/server/static.ts`
bun run build
```
#### 构建流程详解
#### 构建流程
`scripts/build.ts` 执行以下步骤
`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 bootstrap + staticAssets调用 production bootstrap作为 Bun.build 入口
4. Bun.build({ compile, minify, sourcemap: "linked" })
└── 输出dist/dial-server单文件可执行 binary
Bun.build({
compile: { outfile: "dist/dial-server" },
entrypoints: ["src/server/main.ts"],
minify: true,
sourcemap: "linked",
})
```
- 入口为 `src/server/main.ts``mode: "production"`,启用安全头)
- HTML import 的前端资源自动打包进可执行文件Bun 自动生成 manifest
- 无需中间产物目录,一步生成最终 binary
#### 产物
| 产物 | 用途 |
| ------------------ | -------------------------- |
| `dist/dial-server` | 生产可执行文件 |
| `dist/web/` | Vite 构建产物(中间产物) |
| `.build/` | 临时生成文件(构建后清理) |
| 产物 | 用途 |
| ------------------ | ---------------------------------------- |
| `dist/dial-server` | 生产可执行文件(含前端资源,单文件部署) |
#### 构建参数
@@ -853,11 +828,17 @@ bun run build
./dist/dial-server probes.yaml
```
启动后:
- 访问 `http://127.0.0.1:3000/` → 返回前端 SPA
- 访问 `http://127.0.0.1:3000/api/*` → 返回后端 API
- 访问 `/dashboard` 等前端路由 → SPA fallback 到 index.html
#### 清理
```bash
bun run clean
# 清理 dist/ 构建产物、.build/ 缓存和 *.bun-build 临时文件
# 清理 dist/ 构建产物和 *.bun-build 临时文件
```
### 3.4 开发工作流
@@ -865,8 +846,8 @@ bun run clean
#### 日常开发循环
```bash
bun run dev probes.yaml # 启动开发环境
# 修改代码 → Vite HMR前端/ bun --watch(后端自动重启
bun run dev probes.yaml # 启动开发环境(单进程,含 HMR
# 修改前端代码 → HMR 热更新 / 修改后端代码 → --watch 自动重启
bun run check # 提交前运行完整质量检查
```
@@ -874,44 +855,30 @@ bun run check # 提交前运行完整质量检查
```bash
bun run verify
# = bun run check + bun run build + bun run test:smoke
# = bun run check + bun run build
```
`verify` 适合 CI 或正式提交前会完整验证类型检查、lint、格式、单元测试、构建、smoke test
`verify` 适合 CI 或正式提交前会完整验证类型检查、lint、格式、单元测试和生产构建
### 3.5 Smoke Test
### 3.5 Executable/E2E 验证
```bash
bun run test:smoke
```
`scripts/smoke.ts` 构建后验证流程:
1. 动态分配空闲端口
2. 用临时配置文件启动 `dist/dial-server`
3. 等待健康检查通过
4. 验证所有 API 端点返回正确数据
5. 验证静态资源服务(含 SPA fallback 和 404 处理)
6. 验证安全 headers
7. 测试结束清理临时目录和进程
`scripts/smoke.ts` 覆盖过薄,已从当前工作流移除。后续如需验证 production executable 的 API、HTML import manifest、SPA fallback 和静态资源行为,应重新设计独立的 executable/E2E 测试。
### 3.6 脚本说明
| 脚本 | 文件 | 说明 |
| ---------------------- | ----------------------------------- | ------------------------------- |
| `bun run dev` | `scripts/dev.ts` | 同时启动前后端开发服务 |
| `bun run build` | `scripts/build.ts` | Vite 构建 + Bun 编译可执行文件 |
| `bun run schema` | `scripts/generate-config-schema.ts` | 生成 `probe-config.schema.json` |
| `bun run schema:check` | `scripts/generate-config-schema.ts` | 检查配置 schema 导出物是否同步 |
| `bun run test:smoke` | `scripts/smoke.ts` | 构建后的端到端验证 |
| `bun run clean` | `scripts/clean.ts` | 清理构建缓存与临时文件 |
| 脚本 | 文件 | 说明 |
| ---------------------- | ----------------------------------- | ----------------------------------- |
| `bun run dev` | `src/server/dev.ts` | 单进程 fullstack 开发服务(含 HMR |
| `bun run build` | `scripts/build.ts` | Bun 编译可执行文件(含前端资源) |
| `bun run schema` | `scripts/generate-config-schema.ts` | 生成 `probe-config.schema.json` |
| `bun run schema:check` | `scripts/generate-config-schema.ts` | 检查配置 schema 导出物是否同步 |
| `bun run clean` | `scripts/clean.ts` | 清理构建缓存与临时文件 |
### 3.7 环境变量
| 变量 | 用途 | 默认值 |
| --------------------------- | ---------------------------------------------------- | -------- |
| `PORT`/`BACKEND_PORT` | 后端监听端口(开发期 Vite 代理目标、生产期监听端口) | `3000` |
| `BUN_TARGET`/`BUILD_TARGET` | 交叉编译目标平台(仅在 `bun run build` 时有效) | 当前平台 |
| 变量 | 用途 | 默认值 |
| --------------------------- | ----------------------------------------------- | -------- |
| `BUN_TARGET`/`BUILD_TARGET` | 交叉编译目标平台(仅在 `bun run build` 时有效) | 当前平台 |
### 3.8 项目配置文件
@@ -919,7 +886,6 @@ bun run test:smoke
| ---------------------- | ---------------------------------------------- |
| `package.json` | 项目信息、脚本、依赖声明 |
| `tsconfig.json` | TypeScript 配置ESNext 模块、严格模式) |
| `vite.config.ts` | Vite 开发代理与构建配置(含代码分割策略) |
| `eslint.config.js` | ESLint 规则(含前端不得 import server 的检查) |
| `commitlint.config.js` | commitlint 提交信息格式校验 |
| `.prettierrc.json` | Prettier 格式化规则(`printWidth: 120` |
@@ -936,17 +902,16 @@ bun run test:smoke
### 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 |
| 目录 | 约定 |
| ------------- | ---------------------------------------------------- |
| `src/server/` | 后端代码,不能 import `src/web/`HTML import 除外) |
| `src/web/` | 前端代码,不能 import `src/server/` |
| `src/shared/` | 前后端共享类型,双向可引用 |
| `scripts/` | 独立运行脚本,可 import 项目源码 |
| `tests/` | 测试目录,结构镜像 src 目录 |
| `dist/` | 构建产物gitignore |
| `openspec/` | OpenSpec 变更管理与规格文档 |
| `data/` | 默认数据目录gitignore运行期生成 SQLite |
---
@@ -986,15 +951,15 @@ bun run check # 一键运行 schema:check + typecheck + lint + test
### TypeScript 严格标志
| 标志 | 值 | 说明 |
| ------------------------------------ | ----- | -------------------------------------------------------------------------- |
| `strict` | true | 全局严格模式 |
| `noUnusedLocals` | true | 未使用局部变量视为错误 |
| `noUnusedParameters` | false | 保留关闭(路由 handler 统一签名需要,如 `handleXxx(store, method, mode)` |
| `noPropertyAccessFromIndexSignature` | true | 禁止通过点号访问索引签名属性,强制使用括号语法 |
| `noUncheckedIndexedAccess` | true | 数组/Map 访问必须运行时真值检查 |
| `noImplicitOverride` | true | 子类覆盖父类方法时必须显式使用 `override` 关键字 |
| `verbatimModuleSyntax` | true | 强制 `import type` 纯类型导入 |
| 标志 | 值 | 说明 |
| ------------------------------------ | ----- | ------------------------------------------------ |
| `strict` | true | 全局严格模式 |
| `noUnusedLocals` | true | 未使用局部变量视为错误 |
| `noUnusedParameters` | false | 保留关闭(路由 handler 统一签名需要 |
| `noPropertyAccessFromIndexSignature` | true | 禁止通过点号访问索引签名属性,强制使用括号语法 |
| `noUncheckedIndexedAccess` | true | 数组/Map 访问必须运行时真值检查 |
| `noImplicitOverride` | true | 子类覆盖父类方法时必须显式使用 `override` 关键字 |
| `verbatimModuleSyntax` | true | 强制 `import type` 纯类型导入 |
### Git Hooks
@@ -1013,7 +978,7 @@ bun run check # 一键运行 schema:check + typecheck + lint + test
```bash
bun run check # 日常开发(类型检查 + lint含格式 + 单元测试)
bun run verify # 完整验证check + 构建 + smoke test
bun run verify # 完整验证check + 构建)
```
## 已知限制