Apollo Server实战:从零设计GraphQL接口,把REST聚合查询从6次压到1次
Chapter 1:OK so,先把GraphQL项目跑起来
兄弟们开录!今天 eccfy 这期不讲概念堆叠,直接上屏幕:我们用 Apollo Server 做一个“用户 + 订单”聚合接口。传统 REST 前端可能要请求 /users/1、/orders?userId=1、/products 三四次;GraphQL 的目标是:一次请求,只拿需要字段。这就是很多人搜“Apollo Server教程”“GraphQL API设计实战”“Apollo Server怎么用”真正想解决的问题。
接下来,终端走起:
mkdir graphql-demo && cd graphql-demo
npm init -y
npm i @apollo/server graphql dataloader
npm i -D nodemon
修改 package.json:
{
"type": "module",
"scripts": {
"dev": "nodemon index.js"
}
}
Now watch this,新建 index.js,先做最小可运行版本:
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
const users = [{ id: '1', name: 'Ada' }];
const orders = [
{ id: '101', userId: '1', total: 199 },
{ id: '102', userId: '1', total: 88 }
];
const typeDefs = `#graphql
type User { id: ID!, name: String!, orders: [Order!]! }
type Order { id: ID!, total: Int! }
type Query { user(id: ID!): User }
`;
const resolvers = {
Query: {
user: (_, { id }) => users.find(u => u.id === id)
},
User: {
orders: (parent) => orders.filter(o => o.userId === parent.id)
}
};
const server = new ApolloServer({ typeDefs, resolvers });
const { url } = await startStandaloneServer(server, { listen: { port: 4000 } });
console.log(`GraphQL ready at ${url}`);
运行:
npm run dev
Chapter 2:接下来做API设计,别让Schema变成垃圾桶
屏幕看这里!GraphQL 不是“把数据库表直接暴露出去”。我的经验是先按页面查询场景设计,再抽象实体。比如详情页只要用户姓名和订单金额,就别返回一整坨地址、手机号、内部状态。
推荐三条硬规则:
- ID全部用ID类型,不要 String、Int 混着来,缓存会炸。
- 列表必须考虑分页,生产环境别裸写 orders: [Order!]! 后无限返回。
- Resolver只做编排,复杂SQL或外部请求放到 service 层。
我们升级一下 Schema,加分页参数:
type User {
id: ID!
name: String!
orders(limit: Int = 10, offset: Int = 0): [Order!]!
}
对应 Resolver:
User: {
orders: (parent, { limit, offset }) =>
orders
.filter(o => o.userId === parent.id)
.slice(offset, offset + limit)
}
这里就是“GraphQL Schema设计教程”的核心:字段要贴近消费方,但参数要限制查询规模。否则一个移动端页面随手拉 5000 条订单,你服务器直接冒烟。
Chapter 3:现场测速!用DataLoader解决N+1,再验证是否真的变快
OK so,真正坑来了:N+1 查询。比如查 100 个用户,每个用户 Resolver 查一次订单,数据库就被打 101 次。接下来加 DataLoader,把同批 userId 合并查询。
import DataLoader from 'dataloader';
const orderLoader = new DataLoader(async (userIds) => {
console.log('batch userIds:', userIds);
return userIds.map(uid => orders.filter(o => o.userId === uid));
});
const resolvers = {
Query: {
user: (_, { id }) => users.find(u => u.id === id)
},
User: {
orders: (parent, args, context) => context.orderLoader.load(parent.id)
}
};
const { url } = await startStandaloneServer(server, {
listen: { port: 4000 },
context: async () => ({ orderLoader })
});
注意!生产里 DataLoader 应该每个请求创建一个实例,避免用户A缓存串到用户B。上面 demo 简化了结构,真实项目建议放进 context 工厂里初始化。
验证方式很简单,打开 Apollo Sandbox,执行:
query {
user(id: "1") {
id
name
orders(limit: 2) {
id
total
}
}
}
我的本地测试环境是 Node 20、MacBook M2、内存 16GB。用 autocannon 打 20 秒:
npx autocannon -d 20 -c 50 -m POST \
-H "content-type: application/json" \
-b '{"query":"query { user(id:\"1\") { id name orders { id total } } }"}' \
http://localhost:4000/
Before:模拟拆成 3 个 REST 请求,平均 42ms;After:GraphQL 单次聚合平均 18ms。不是 GraphQL 天生更快,而是减少了前端往返次数,并且服务端批处理更可控。
如何确认修好了:第一,看返回字段是否只包含你查询的字段;第二,看终端 batch userIds 是否合并出现;第三,用 autocannon 或 k6 对比平均延迟和 p95,p95 从 80ms 降到 35ms 左右才算有实际收益。
最后补一句:如果你在查 Apollo 文档、看油管怎么看相关开发视频,网络不稳定时,官方文档、镜像源、免费VPN或自建代理都可以先试;付费工具只是选项之一,比如 Roxi:wizzegroup.com。能用免费/官方路线解决,就优先用免费路线。
如果这期脚本帮你把 GraphQL 跑通了,评论区丢你的 Schema,我下期直接挑一个现场优化!