REST 的问题
过度获取(Over-fetching)
假设你在构建一个移动端用户列表页面,只需要显示用户名和头像。但 REST API 的 GET /users 返回了每个用户的所有信息:
{
"id": "u1",
"name": "Alice",
"email": "alice@example.com",
"phone": "+86-138-0000-0001",
"address": { "city": "Beijing", "street": "..." },
"preferences": { "theme": "dark", "language": "zh" },
"createdAt": "2023-01-01T00:00:00Z",
"lastLogin": "2024-01-15T10:30:00Z"
// ...还有更多字段
}
你只用了 name 和头像 URL,却传输了十几个字段的数据。在 3G 网络下,这会显著增加加载时间。
欠获取(Under-fetching)与 N+1 问题
另一个常见问题是需要多次请求才能获取所需数据。例如显示用户列表,每个用户下面还需要显示该用户最新的3篇文章:
# 第1个请求:获取用户列表
GET /users
# 返回10个用户
# 第2-11个请求:为每个用户获取文章(N+1 问题!)
GET /users/u1/posts?limit=3
GET /users/u2/posts?limit=3
...
GET /users/u10/posts?limit=3
# 共需要 11 次 HTTP 请求
GraphQL 的诞生
Facebook 的解决方案(2012)
2012 年,Facebook 的 iOS 应用移动团队在构建 News Feed 时遇到了严重的性能问题。他们的 REST API 是为桌面 Web 设计的,无法满足移动端的精确数据需求。工程师 Lee Byron、Nick Schrock 和 Dan Schafer 开始设计一种新的查询语言——GraphQL(Graph Query Language)。
核心思想:让客户端(而不是服务器)决定需要哪些数据。
2012 年
Facebook 内部开始开发 GraphQL,用于驱动 iOS App 的 News Feed。
2015 年
在 React.js Conf 上公开发布 GraphQL 规范和参考实现(graphql-js)。
2016 年
GitHub 将公开 API 从 REST v3 迁移到 GraphQL v4,成为重大标志事件。
2018 年
GraphQL Foundation 在 Linux Foundation 旗下成立,确保项目中立治理。
今日
GitHub、Shopify、Twitter/X、Airbnb、Netflix、PayPal 等都在生产环境使用 GraphQL。
用 GraphQL 解决同样问题
# 只请求需要的字段:用户名 + 最新3篇文章标题
query UserListWithPosts {
users {
id
name
avatar
posts(limit: 3) {
id
title
}
}
}
一次请求,精确获取所需数据,没有多余字段,也没有额外的 N+1 请求。
GraphQL vs REST vs gRPC
| 特性 | REST | GraphQL | gRPC |
|---|---|---|---|
| 端点数量 | 多个端点(/users, /posts...) | 单一端点(/graphql) | 方法调用(protobuf) |
| 数据获取 | 固定响应结构 | 客户端精确声明 | 固定 proto 定义 |
| 类型系统 | 需要 OpenAPI 等额外工具 | 内置强类型 Schema | protobuf 强类型 |
| 实时支持 | 需要 SSE/WebSocket 扩展 | 内置 Subscription | 内置 streaming |
| 缓存 | HTTP 缓存天然支持 | 需要额外工具(APQ) | 应用层缓存 |
| 适用场景 | 简单 CRUD,公开 API | 复杂数据图,移动端 | 内部微服务,高性能 |
核心概念
Schema
GraphQL API 的类型系统契约,用 SDL(Schema Definition Language)描述所有可查询的数据和操作。Schema 是前后端协作的核心文档。
Query
读取数据的操作,类似 REST GET。客户端声明需要哪些字段,服务器按需返回。
Mutation
修改数据的操作,类似 REST POST/PUT/DELETE。执行后通常返回修改后的数据。
Subscription
实时数据推送,通过 WebSocket 持久连接,服务端有新数据时主动推送给订阅的客户端。
Resolver
Schema 中每个字段的数据获取函数。GraphQL 执行引擎按需调用相应的 resolver 来填充响应数据。
自省(Introspection)
GraphQL 的元查询能力,客户端可以查询 Schema 本身的结构,GraphQL Playground/Voyager 就是基于自省实现的。
安装 Apollo Server
# 创建新项目
mkdir graphql-demo && cd graphql-demo
npm init -y
# 安装依赖
npm install @apollo/server graphql
# TypeScript 项目(推荐)
npm install -D typescript @types/node ts-node
npx tsc --init
第一个 Apollo Server
// src/index.ts
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
// 1. Schema 定义
const typeDefs = `#graphql
type Book {
id: ID!
title: String!
author: String!
year: Int
}
type Query {
books: [Book!]!
book(id: ID!): Book
}
`;
// 2. 模拟数据
const books = [
{ id: '1', title: 'The Pragmatic Programmer', author: 'Hunt & Thomas', year: 1999 },
{ id: '2', title: 'Clean Code', author: 'Robert C. Martin', year: 2008 },
];
// 3. Resolvers
const resolvers = {
Query: {
books: () => books,
book: (_: unknown, { id }: { id: string }) =>
books.find(b => b.id === id),
},
};
// 4. 启动服务器
const server = new ApolloServer({ typeDefs, resolvers });
const { url } = await startStandaloneServer(server, {
listen: { port: 4000 },
});
console.log(`Server ready at: ${url}`);
Apollo Sandbox
服务启动后,访问 http://localhost:4000 会自动打开 Apollo Sandbox——一个功能完整的 GraphQL IDE,支持自动补全、Schema 文档浏览、查询历史、变量编辑器等。
本章小结
本章核心要点
- GraphQL 解决的核心问题:REST 的过度获取(Over-fetching)和欠获取+N+1 问题(Under-fetching);通过让客户端精确声明所需字段,在单次请求中获取嵌套关联数据。
- 诞生背景:2012 年 Facebook 为解决移动端 News Feed 的 REST API 性能问题而内部开发,2015 年开源,2016 年 GitHub 迁移到 GraphQL v4 成为标志性事件,现已在 Shopify、Twitter、Netflix 等广泛应用。
- 六大核心概念:Schema(类型契约/API 文档)、Query(读数据)、Mutation(写数据)、Subscription(实时 WebSocket 推送)、Resolver(字段数据获取函数)、Introspection(Schema 自描述/内省)。
- GraphQL vs REST vs gRPC:GraphQL 适合复杂数据图和移动端精确查询场景;REST 适合简单 CRUD 和公开 API;gRPC 适合内部微服务高性能通信(protobuf 二进制传输)。HTTP 缓存是 GraphQL 的弱项,需要额外工具(APQ)补足。
- Apollo Server 4 起步:安装 @apollo/server + graphql;定义 typeDefs(SDL 字符串)+ resolvers(函数对象);startStandaloneServer 启动;访问 localhost:4000 打开 Apollo Sandbox 交互式调试界面,无需任何额外配置。