阶段三:GraphQL
GraphQL是Facebook于2015年开源的查询语言和运行时。与REST的"服务端决定返回什么"不同,GraphQL让客户端精确声明需要什么数据——不多不少。本课从核心概念出发,对比REST与GraphQL,搭建第一个GraphQL服务。
| 痛点 | REST场景 | GraphQL解法 |
|---|---|---|
| 过度获取(Over-fetching) | GET /users 返回20个字段,只需3个 | 客户端精确选择字段 |
| 获取不足(Under-fetching) | 获取用户→再获取文章→再获取评论,3次请求 | 一次查询嵌套关联数据 |
| 接口膨胀 | /users, /users/brief, /users/with-posts, /users/with-everything | 一个端点,灵活组合 |
// REST: 获取用户及其文章评论 → 3次请求
GET /api/users/42 // 用户信息
GET /api/users/42/posts // 用户文章
GET /api/posts/1/comments // 文章1的评论
GET /api/posts/2/comments // 文章2的评论
// N+1问题!
// GraphQL: 一次查询搞定
query {
user(id: 42) {
name
email
posts {
title
comments {
body
author { name }
}
}
}
}
# GraphQL Schema语言(SDL)
# 标量类型
scalar DateTime
# 对象类型
type User {
id: ID!
name: String!
email: String!
role: Role!
posts: [Post!]! # 关联字段,由resolver解析
createdAt: DateTime!
}
type Post {
id: ID!
title: String!
content: String!
author: User! # 反向关联
comments: [Comment!]!
tags: [String!]!
createdAt: DateTime!
}
type Comment {
id: ID!
body: String!
author: User!
post: Post!
createdAt: DateTime!
}
# 枚举类型
enum Role {
ADMIN
EDITOR
VIEWER
}
# 查询入口
type Query {
user(id: ID!): User
users(limit: Int, offset: Int): [User!]!
post(id: ID!): Post
posts(authorId: ID, tag: String): [Post!]!
search(query: String!): [SearchResult!]!
}
# 变更入口
type Mutation {
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User!
deleteUser(id: ID!): Boolean!
createPost(input: CreatePostInput!): Post!
addComment(postId: ID!, body: String!): Comment!
}
# 订阅入口
type Subscription {
postCreated(authorId: ID): Post!
commentAdded(postId: ID!): Comment!
}
# 输入类型
input CreateUserInput {
name: String!
email: String!
role: Role = VIEWER
}
input UpdateUserInput {
name: String
email: String
role: Role
}
input CreatePostInput {
title: String!
content: String!
tags: [String!]!
}
# 联合类型(搜索结果可以是多种类型)
union SearchResult = User | Post | Comment
// graphql-server.js - GraphQL服务实现
const express = require('express');
const { createHandler } = require('graphql-http/lib/use/express');
const { buildSchema, GraphQLSchema, GraphQLObjectType, GraphQLString, GraphQLInt, GraphQLList, GraphQLNonNull, GraphQLID } = require('graphql');
const app = express();
// ===== 数据 =====
const users = [
{ id: '1', name: '张三', email: 'zhang@example.com', role: 'ADMIN' },
{ id: '2', name: '李四', email: 'li@example.com', role: 'EDITOR' },
{ id: '3', name: '王五', email: 'wang@example.com', role: 'VIEWER' },
];
const posts = [
{ id: '1', title: 'GraphQL入门', content: 'GraphQL是一种查询语言...', authorId: '1', tags: ['GraphQL', 'API'], createdAt: '2025-01-10' },
{ id: '2', title: 'REST vs GraphQL', content: '两种API风格的对比...', authorId: '1', tags: ['REST', 'GraphQL'], createdAt: '2025-01-11' },
{ id: '3', title: 'Schema设计实践', content: '如何设计好的Schema...', authorId: '2', tags: ['GraphQL', '设计'], createdAt: '2025-01-12' },
];
const comments = [
{ id: '1', body: '非常实用的入门教程!', authorId: '2', postId: '1', createdAt: '2025-01-10' },
{ id: '2', body: '期待更多深入内容', authorId: '3', postId: '1', createdAt: '2025-01-10' },
{ id: '3', body: '对比很客观', authorId: '3', postId: '2', createdAt: '2025-01-11' },
];
// ===== Schema(SDL方式)=====
const schemaStr = `
scalar DateTime
enum Role {
ADMIN
EDITOR
VIEWER
}
type User {
id: ID!
name: String!
email: String!
role: Role!
posts: [Post!]!
postCount: Int!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
comments: [Comment!]!
tags: [String!]!
commentCount: Int!
createdAt: DateTime!
}
type Comment {
id: ID!
body: String!
author: User!
post: Post!
createdAt: DateTime!
}
input CreateUserInput {
name: String!
email: String!
role: Role = VIEWER
}
input CreatePostInput {
title: String!
content: String!
tags: [String!]! = []
}
type Query {
user(id: ID!): User
users(limit: Int = 20, offset: Int = 0): [User!]!
post(id: ID!): Post
posts(authorId: ID, tag: String, limit: Int = 20): [Post!]!
}
type Mutation {
createUser(input: CreateUserInput!): User!
createPost(input: CreatePostInput!): Post!
addComment(postId: ID!, body: String!): Comment!
}
`;
const schema = buildSchema(schemaStr);
// ===== Resolvers =====
const root = {
// Query resolvers
user: ({ id }) => users.find(u => u.id === id),
users: ({ limit, offset }) => users.slice(offset, offset + limit),
post: ({ id }) => posts.find(p => p.id === id),
posts: ({ authorId, tag, limit }) => {
let result = [...posts];
if (authorId) result = result.filter(p => p.authorId === authorId);
if (tag) result = result.filter(p => p.tags.includes(tag));
return result.slice(0, limit);
},
// Type resolvers(字段级解析)
User: {
posts: (user) => posts.filter(p => p.authorId === user.id),
postCount: (user) => posts.filter(p => p.authorId === user.id).length,
},
Post: {
author: (post) => users.find(u => u.id === post.authorId),
comments: (post) => comments.filter(c => c.postId === post.id),
commentCount: (post) => comments.filter(c => c.postId === post.id).length,
},
Comment: {
author: (comment) => users.find(u => u.id === comment.authorId),
post: (comment) => posts.find(p => p.id === comment.postId),
},
// Mutation resolvers
createUser: ({ input }) => {
const id = String(users.length + 1);
const user = { id, ...input };
users.push(user);
return user;
},
createPost: ({ input }, context) => {
const id = String(posts.length + 1);
const post = { id, ...input, authorId: context?.userId || '1', createdAt: new Date().toISOString() };
posts.push(post);
return post;
},
addComment: ({ postId, body }, context) => {
const id = String(comments.length + 1);
const comment = { id, body, authorId: context?.userId || '2', postId, createdAt: new Date().toISOString() };
comments.push(comment);
return comment;
},
};
// ===== 路由 =====
app.all('/graphql', createHandler({
schema,
rootValue: root,
context: (req) => ({ userId: req.headers['x-user-id'] || '1' }),
}));
// GraphiQL IDE
app.get('/graphiql', (req, res) => {
res.send(`GraphiQL
`);
});
app.listen(3000, () => console.log('🔮 GraphQL服务运行在 http://localhost:3000/graphql'));
# 查询1: 获取用户基本信息
curl -s -X POST http://localhost:3000/graphql \
-H "Content-Type: application/json" \
-d '{"query":"{ user(id: \"1\") { name email role } }"}' | jq .
{
"data": {
"user": {
"name": "张三",
"email": "zhang@example.com",
"role": "ADMIN"
}
}
}
# 查询2: 嵌套关联数据(一次查询解决REST的N+1问题)
curl -s -X POST http://localhost:3000/graphql \
-H "Content-Type: application/json" \
-d '{"query":"{ user(id: \"1\") { name posts { title commentCount comments { body author { name } } } } }"}' | jq .
{
"data": {
"user": {
"name": "张三",
"posts": [
{
"title": "GraphQL入门",
"commentCount": 2,
"comments": [
{"body":"非常实用的入门教程!","author":{"name":"李四"}},
{"body":"期待更多深入内容","author":{"name":"王五"}}
]
}
]
}
}
}
# 查询3: 过滤文章
curl -s -X POST http://localhost:3000/graphql \
-H "Content-Type: application/json" \
-d '{"query":"{ posts(tag: \"GraphQL\") { title author { name } } }"}' | jq .
# 变更1: 创建用户
curl -s -X POST http://localhost:3000/graphql \
-H "Content-Type: application/json" \
-d '{"query":"mutation { createUser(input: { name: \"赵六\", email: \"zhao@example.com\" }) { id name email role } }"}' | jq .
# 变更2: 添加评论
curl -s -X POST http://localhost:3000/graphql \
-H "Content-Type: application/json" \
-d '{"query":"mutation { addComment(postId: \"1\", body: \"太棒了!\") { id body author { name } } }"}' | jq .
| 维度 | REST | GraphQL |
|---|---|---|
| 适用场景 | 资源导向、CRUD为主 | 复杂关联、前端驱动 |
| 学习曲线 | 低 | 中等 |
| 缓存 | ✅ HTTP原生支持 | ⚠️ 需额外方案 |
| 文件上传 | ✅ multipart原生 | ⚠️ 需规范扩展 |
| 调试工具 | curl/Postman | GraphiQL/Apollo Explorer |
| 性能 | 多请求开销 | 单请求复杂解析 |
| 版本管理 | URL版本化 | Schema演进 |
| 生态 | 成熟完善 | 快速发展中 |
为电商系统设计Schema,包含:商品、分类、购物车、订单、支付。注意输入类型和关联设计。
分别用REST和GraphQL实现"获取用户的所有订单及其商品详情",对比请求次数和数据量。