从 REST 到 GraphQL:真实项目迁移实录
我们花了 3 个月把 API 从 REST 迁移到 GraphQL。以下是完整记录。
迁移前:REST 的问题
TYPESCRIPT
// 获取用户 + 订单 + 商品信息需要 3 个请求
const user = await fetch('/api/users/1')
const orders = await fetch('/api/users/1/orders')
const products = await Promise.all(
orders.map(o => fetch(`/api/products/${o.productId}`))
)迁移方案:渐进式
不是一次性重写,而是逐步添加 GraphQL 层:
CODE
客户端 ──→ GraphQL Gateway ──→ REST API (原有)
──→ 新 GraphQL Resolver第 1 周:搭建 GraphQL 网关
TYPESCRIPT
// server/src/graphql/schema.ts
import { builder } from './builder'
const UserRef = builder.objectRef<User>('User')
builder.queryFields(t => ({
user: t.field({
type: UserRef,
args: { id: t.arg.string({ required: true }) },
resolve: async (_, { id }) => {
// 调用原有 REST API
const res = await fetch(`http://localhost:3000/api/users/${id}`)
return res.json()
}
})
}))第 2 周:添加关联查询
TYPESCRIPT
builder.objectType(UserRef, {
fields: t => ({
id: t.exposeString('id'),
name: t.exposeString('name'),
orders: t.field({
type: [OrderRef],
resolve: async (user) => {
const res = await fetch(`http://localhost:3000/api/users/${user.id}/orders`)
return res.json()
}
})
})
})第 3 周:解决 N+1 问题
TYPESCRIPT
import DataLoader from 'dataloader'
const orderLoader = new DataLoader(async (userIds: string[]) => {
const orders = await db.orders.findMany({
where: { userId: { in: userIds } }
})
return userIds.map(id => orders.filter(o => o.userId === id))
})
builder.objectType(UserRef, {
fields: t => ({
orders: t.field({
type: [OrderRef],
resolve: async (user) => orderLoader.load(user.id)
})
})
})迁移成果
| 指标 | REST | GraphQL |
|------|------|---------|
| 首页请求数 | 12 | 3 |
| 首页传输量 | 45KB | 18KB |
| 前端代码量 | ~2000 行 | ~800 行 |
踩坑记录
1. 不要一次性重写,渐进式迁移
2. 必须有 DataLoader,否则 N+1 问题严重
3. 做好缓存,GraphQL 默认 POST 不利于 HTTP 缓存
4. 限制查询深度,防止恶意嵌套查询
读者评论 5