首页 / 后端开发 / Apollo Server实战:从零

Apollo Server实战:从零设计GraphQL接口,把REST聚合查询从6次压到1次

Roxi
Roxi 加速器 — 稳定·快速·安全
全球节点覆盖,支持所有主流平台,一键连接无需配置。新用户免费试用。
立即体验 →

Chapter 1:OK so,先把GraphQL项目跑起来

SEO 基础优化内容策略规划外链体系建设技术架构升级转化漏斗分析

兄弟们开录!今天 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变成垃圾桶

85%转化提升2.5s响应速度100+功能模块365天持续更新

屏幕看这里!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,我下期直接挑一个现场优化!