实践:技术栈迁移
本实践演示如何使用 SpecForge 规划和执行技术迁移。我们将以将 Node.js API 从 Express 4 迁移到 Fastify 为例 — 这是一个涉及路由、中间件、插件和错误处理的常见迁移。
同样的工作流适用于任何技术迁移:框架升级、数据库引擎更换、ORM 替换或语言升级。
开始之前
迁移风险较高,因为它们涉及多个文件,可能以微妙的方式破坏行为。SpecForge 通过以下方式降低风险:
- 在任何代码变更之前创建功能一致性规格
- 生成验证前后行为是否匹配的测试
- 提供分阶段迁移计划,每步都有回滚点
- 随迁移进行检测漂移
第 1 步 — 分析源代码库
提示词:
逆向工程项目 proj_abc123 中 /Users/me/my-app/src 的代码库这会从当前实现创建规格 — 记录应用实际做了什么。这些规格成为功能一致性基准。
第 2 步 — 分析迁移范围
提示词:
为项目 proj_abc123 规划从 Express 到 Fastify 的迁移预期结果:
## 迁移分析:Express 4 → Fastify 5
### 范围
- 23 个路由文件
- 8 个中间件(身份验证、日志、限速、cors、压缩、错误、验证、缓存)
- 4 个错误处理器
- 2 个 WebSocket 集成(socket.io → @fastify/websocket)
- 12 个模拟 Express 内部的测试文件
### 等效映射
Express → Fastify
─────────────────────────────────────────
app.use(middleware) → fastify.addHook / fastify.register(plugin)
app.get('/path', handler) → fastify.get('/path', { schema }, handler)
req.body → request.body(相同)
res.json(data) → reply.send(data)
res.status(201).json(data) → reply.code(201).send(data)
next(err) → throw err(或 return reply.send(err))
express-validator → @fastify/ajv-compiler + JSON Schema
multer → @fastify/multipart
cors(包) → @fastify/cors
helmet → @fastify/helmet
express-rate-limit → @fastify/rate-limit
socket.io → @fastify/websocket + ws 直接使用
### 风险区域
- 高:socket.io 迁移(没有直接等效 — 行为变更)
- 高:中间件顺序(Fastify 生命周期与 Express 不同)
- 中:错误处理(Fastify 使用 setErrorHandler,而非错误中间件)
- 低:路由参数(兼容语法)
### 估算工作量:8–14 天
### 推荐方式:绞杀榕(逐路由迁移)第 3 步 — 创建迁移规格
提示词:
为项目 proj_abc123 中从 Express 迁移到 Fastify 创建规格。
方式:绞杀榕 — 通过反向代理逐组迁移路由。规格将包含的验收标准:
- 所有 23 个路由组在迁移前后表现完全一致(一致性测试套件)
- 响应模式使用 Fastify 内置的 AJV 集成进行验证
- 身份验证中间件迁移到 Fastify 生命周期钩子(onRequest)
- 错误响应保持与 Express 实现相同的 JSON 结构
- 保留 WebSocket 功能(从 socket.io 迁移到 @fastify/websocket)
- 响应时间无回归 — Fastify P99 延迟 ≤ Express P99 × 1.05
- 所有现有集成测试在 Fastify 服务器上通过
- 零停机迁移 — 过渡期间 Express 和 Fastify 并发运行
第 4 步 — 挑战迁移计划
提示词:
挑战项目 proj_abc123 的迁移规格SpecForge 会探查边缘情况:
- Express → Fastify 切换期间进行中的请求会怎样?
- 过渡期间会话 cookie 如何处理?
- 如果功能标志失败,一些用户命中 Express,另一些命中 Fastify 怎么办?
- 是否有没有 Fastify 等效的 Express 特定中间件?
第 5 步 — 生成功能一致性测试
提示词:
为项目 proj_abc123 中的迁移规格生成测试这些测试针对 Express 和 Fastify 服务器同时运行 — 确保行为完全一致:
typescript
// parity.test.ts
describe.each([
['Express', expressApp],
['Fastify', fastifyApp],
])('%s — POST /api/orders', (_, app) => {
it('returns 201 with order ID on valid payload', async () => {
const res = await request(app)
.post('/api/orders')
.send({ productId: 'prod_1', quantity: 2 })
expect(res.status).toBe(201)
expect(res.body).toMatchObject({ orderId: expect.any(String) })
})
it('returns 422 on missing productId', async () => {
const res = await request(app)
.post('/api/orders')
.send({ quantity: 2 })
expect(res.status).toBe(422)
})
})第 6 步 — 生成执行计划
提示词:
为项目 proj_abc123 中的迁移规格生成执行计划绞杀榕计划看起来像这样:
markdown
## 第 1 阶段:基础
- [ ] 安装 Fastify + 插件(fastify-cors、fastify-helmet 等)
- [ ] 与 Express 并行设置 Fastify 服务器(不同端口)
- [ ] 配置带功能标志路由的反向代理(nginx/Caddy)
- [ ] 编写针对两个服务器运行的一致性测试框架
## 第 2 阶段:迁移路由(按组 — 可并行化各组)
- [ ] 组 A:/api/auth 路由(低风险,无 WebSocket)
- [ ] 组 B:/api/users 路由(中等风险,验证变更)
- [ ] 组 C:/api/orders 路由(高风险,支付集成)
- [ ] 组 D:/api/admin 路由(低风险,仅内部)
## 第 3 阶段:迁移中间件
- [ ] 身份验证钩子(Express 中间件 → Fastify onRequest)
- [ ] 错误处理器(next(err) → setErrorHandler)
- [ ] 限速(@fastify/rate-limit)
- [ ] CORS + Helmet(@fastify/cors、@fastify/helmet)
## 第 4 阶段:WebSocket 迁移
- [ ] 用 @fastify/websocket + ws 替换 socket.io
- [ ] 将客户端 socket.io 迁移到原生 WebSocket
- [ ] 一致性测试:WebSocket 消息流
## 第 5 阶段:切换
- [ ] 将反向代理切换到 100% Fastify
- [ ] 保持 Express 运行 48 小时(回滚窗口)
- [ ] 无事故后 48 小时删除 Express
- [ ] 更新 PLAN.md 并将迁移规格标记为完成第 7 步 — 执行迁移
每个阶段:
- 实现变更
- 针对两个服务器运行一致性测试
- 使用
validate验证功能一致性 - 检测迁移规格与实现之间的漂移
每个阶段后的提示词:
根据 /Users/me/my-app/src 的代码验证项目 proj_abc123 中的迁移规格第 8 步 — 性能验证
提示词:
审计项目 proj_abc123 中 /Users/me/my-app/src 的代码检查 Fastify 实现在架构合规性和代码质量上至少与 Express 基准一样高分。
对于延迟验证,针对两个服务器运行你的负载测试工具(k6、Artillery、wrk)并比较 P99 延迟 — 规格标准说 Fastify P99 ≤ Express P99 × 1.05。
第 9 步 — 记录架构决策
提示词:
为项目 proj_abc123 中的迁移规格生成 ADR这会生成:
- ADR-001:为什么选择绞杀榕而非大爆炸式迁移
- ADR-002:Fastify 上下文中的 @fastify/websocket 相对于 socket.io
- ADR-003:用于请求验证的 AJV JSON Schema 相对于 express-validator
第 10 步 — 捕获经验教训
提示词:
捕获经验教训:从 Express 迁移到 Fastify 时,中间件顺序更重要 —
Fastify 生命周期钩子(onRequest、preHandler、onSend)必须在迁移开始前规划,
而非在实现过程中发现回滚计划
SpecForge 的绞杀榕方法意味着回滚始终可行:
| 阶段 | 回滚操作 |
|---|---|
| 迁移期间 | 将反向代理切换回 100% Express |
| 切换后(< 48 小时) | Express 仍在运行 — 切换代理 |
| Express 删除后 | 从 git 标签 pre-migration 恢复 |
开始之前始终标记最后稳定的 Express 提交:git tag pre-fastify-migration。
将本实践应用于其他迁移
| 迁移 | 关键关注点 | 推荐方式 |
|---|---|---|
| Django → FastAPI | 异步模式、ORM 差异 | 绞杀榕 |
| Mongoose → Prisma | 模式迁移、查询 API | 逐模块 |
| React 类组件 → Hooks | 行为一致性、生命周期 | 逐组件 |
| PostgreSQL → CockroachDB | SQL 方言差异 | 先只读副本 |
| REST → GraphQL | 客户端破坏性变更 | 增量式(保留 REST) |
| Node 18 → Node 22 | API 弃用 | 先运行 detect_deprecations |