🔄 REST API vs GraphQL
📖 定义
REST API是使用HTTP方法以资源为中心设计的传统API架构。GraphQL是Facebook开发的查询语言,是一种灵活的API方式,允许客户端精确请求所需的数据。REST有多个端点,而GraphQL通过单个端点处理所有数据。
🎯 通过比喻理解
餐厅 vs 自助餐
REST API = 餐厅菜单
├─ 只能订购预定义的菜单
├─ 每个菜单都有固定的组成
├─ "请给我一份披萨" → 得到整个披萨
├─ 难以去除或添加配料
└─ 简单且可预测
GraphQL = 自助餐
├─ 只挑选想要的食物
├─ 只选择需要 的量
├─ "只要奶酪" → 只得到奶酪
├─ 可以自由组合
└─ 灵活但可能复杂
图书馆借书
REST API = 传统借阅
图书管理员:"您需要什么书?"
我:"请给我计算机相关的书"
图书管理员:"这些全部给您"(10本书)
我:"我只需要第1章..."(其余不需要)
GraphQL = 智能借阅
我:"我只需要计算机书的第3章"
图书管理员:"只给您复印第3章"(正好是所需的)
我:"完美!"
⚙️ 工作原理
1. 数据请求方式比较
REST API: 多个端点
GET /users/1 → 用户信息
GET /users/1/posts → 用户的帖子
GET /posts/1/comments → 帖子的评论
总共需要3次请求!
GraphQL: 单个端点
POST /graphql
{
user(id: 1) {
name
posts {
title
comments {
text
}
}
}
}
1次请求获取所有数据!
2. Over-fetching vs Under-fetching
REST API问题
Over-fetching(接收不必要的数据)
GET /users/1
{
"id": 1,
"name": "张三",
"email": "zhang@example.com",
"phone": "138-1234-5678",
"address": "北京市...",
"createdAt": "2024-01-01",
// 只需要名字却接收了所有信息!
}
Under-fetching(数据不足)
GET /users/1 → 用户信息
GET /users/1/posts → 需要额外请求
GET /users/1/friends → 又需要额外请求
// 需要多次请求!
GraphQL解决方案
精确所需
{
user(id: 1) {
name // 只请求名字!
}
}
→ { "name": "张三" }
一次性获取所有
{
user(id: 1) {
name
posts { title }
friends { name }
}
}
→ 所有数据一次性获取!
3. API设计哲学
REST: 以资源为中心
┌─────────────────┐
│ /users │ → 用户列表
│ /users/1 │ → 特定用户
│ /posts │ → 帖子列表
│ /posts/1 │ → 特定帖子
└─────────────────┘
每个资源一个端点
GraphQL: 以查询为中心
┌─────────────────┐
│ /graphql │ → 所有请求
└─────────────────┘
↓
┌────────┐
│ Query │ → 读取数据
│Mutation│ → 修改数据
│Subscribe│ → 实时订阅
└────────┘
💡 实际示例
REST API示例 (Express.js)
// Express.js REST API实现
const express = require('express');
const app = express();
app.use(express.json());
// 数据(实际中使用数据库)
const users = [
{
id: 1,
name: '张三',
email: 'zhang@example.com',
age: 25
},
{
id: 2,
name: '李四',
email: 'li@example.com',
age: 30
}
];
const posts = [
{
id: 1,
userId: 1,
title: 'REST API介绍',
content: 'REST是...'
},
{
id: 2,
userId: 1,
title: 'GraphQL介绍',
content: 'GraphQL是...'
}
];
// ========== GET: 获取所有用户 ==========
app.get('/api/users', (req, res) => {
res.json(users);
});
// ========== GET: 获取特定用户 ==========
app.get('/api/users/:id', (req, res) => {
const user = users.find(u => u.id === parseInt(req.params.id));
if (!user) {
return res.status(404).json({ error: '找不到用户' });
}
res.json(user);
});
// ========== POST: 创建用户 ==========
app.post('/api/users', (req, res) => {
const newUser = {
id: users.length + 1,
name: req.body.name,
email: req.body.email,
age: req.body.age
};
users.push(newUser);
res.status(201).json(newUser);
});
// ========== PUT: 更新用户 ==========
app.put('/api/users/:id', (req, res) => {
const user = users.find(u => u.id === parseInt(req.params.id));
if (!user) {
return res.status(404).json({ error: '找不到用户' });
}
user.name = req.body.name || user.name;
user.email = req.body.email || user.email;
user.age = req.body.age || user.age;
res.json(user);
});
// ========== DELETE: 删除用户 ==========
app.delete('/api/users/:id', (req, res) => {
const index = users.findIndex(u => u.id === parseInt(req.params.id));
if (index === -1) {
return res.status(404).json({ error: '找不到用户' });
}
users.splice(index, 1);
res.status(204).send();
});
// ========== GET: 获取用户的帖子 ==========
app.get('/api/users/:id/posts', (req, res) => {
const userId = parseInt(req.params.id);
const userPosts = posts.filter(p => p.userId === userId);
res.json(userPosts);
});
// ========== GET: 获取特定帖子 ==========
app.get('/api/posts/:id', (req, res) => {
const post = posts.find(p => p.id === parseInt(req.params.id));
if (!post) {
return res.status(404).json({ error: '找不到帖子' });
}
res.json(post);
});
app.listen(3000, () => {
console.log('REST API服务器运行中: http://localhost:3000');
});
REST API客户端使用
// ========== REST API使用 ==========
// 1. 获取用户列表
const response1 = await fetch('http://localhost:3000/api/users');
const users = await response1.json();
console.log(users);
// 2. 获取特定用户
const response2 = await fetch('http://localhost:3000/api/users/1');
const user = await response2.json();
console.log(user);
// 3. 创建用户
const response3 = await fetch('http://localhost:3000/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: '王五',
email: 'wang@example.com',
age: 28
})
});
const newUser = await response3.json();
// 4. 更新用户
const response4 = await fetch('http://localhost:3000/api/users/1', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
age: 26
})
});
// 5. 删除用户
await fetch('http://localhost:3000/api/users/1', {
method: 'DELETE'
});
// ========== 问题: 需要多次请求 ==========
// 如何同时获取用户和帖子?
const userResponse = await fetch('http://localhost:3000/api/users/1');
const user = await userResponse.json();
const postsResponse = await fetch('http://localhost:3000/api/users/1/posts');
const posts = await postsResponse.json();
console.log({ user, posts }); // 2次请求!
GraphQL示例 (Apollo Server)
// GraphQL服务器实现
const { ApolloServer, gql } = require('apollo-server');
// 数据(实际中使用数据库)
const users = [
{ id: 1, name: '张三', email: 'zhang@example.com', age: 25 },
{ id: 2, name: '李四', email: 'li@example.com', age: 30 }
];
const posts = [
{ id: 1, userId: 1, title: 'REST API介绍', content: 'REST是...' },
{ id: 2, userId: 1, title: 'GraphQL介绍', content: 'GraphQL是...' },
{ id: 3, userId: 2, title: 'Node.js教程', content: 'Node是...' }
];
// ========== 模式定义 (Type Definitions) ==========
const typeDefs = gql`
# 用户类型
type User {
id: Int!
name: String!
email: String!
age: Int
posts: [Post!]! # 用户的帖子(关系)
}
# 帖子类型
type Post {
id: Int!
title: String!
content: String!
author: User! # 帖子作者(关系)
}
# 查询(读取数据)
type Query {
# 所有用户
users: [User!]!
# 特定用户
user(id: Int!): User
# 所有帖子
posts: [Post!]!
# 特定帖子
post(id: Int!): Post
}
# 变更(修改数据)
type Mutation {
# 创建用户
createUser(name: String!, email: String!, age: Int): User!
# 更新用户
updateUser(id: Int!, name: String, email: String, age: Int): User
# 删除用户
deleteUser(id: Int!): Boolean!
# 创建帖子
createPost(userId: Int!, title: String!, content: String!): Post!
}
`;
// ========== 解析器(数据获取方法) ==========
const resolvers = {
Query: {
// 获取所有用户
users: () => users,
// 获取特定用户
user: (parent, args) => {
return users.find(u => u.id === args.id);
},
// 获取所有帖子
posts: () => posts,
// 获取特定帖子
post: (parent, args) => {
return posts.find(p => p.id === args.id);
}
},
Mutation: {
// 创建用户
createUser: (parent, args) => {
const newUser = {
id: users.length + 1,
name: args.name,
email: args.email,
age: args.age
};
users.push(newUser);
return newUser;
},
// 更新用户
updateUser: (parent, args) => {
const user = users.find(u => u.id === args.id);
if (!user) return null;
if (args.name) user.name = args.name;
if (args.email) user.email = args.email;
if (args.age) user.age = args.age;
return user;
},
// 删除用户
deleteUser: (parent, args) => {
const index = users.findIndex(u => u.id === args.id);
if (index === -1) return false;
users.splice(index, 1);
return true;
},
// 创建帖子
createPost: (parent, args) => {
const newPost = {
id: posts.length + 1,
userId: args.userId,
title: args.title,
content: args.content
};
posts.push(newPost);
return newPost;
}
},
// ========== 关系解析器 ==========
User: {
// 获取用户的帖子
posts: (parent) => {
return posts.filter(p => p.userId === parent.id);
}
},
Post: {
// 获取帖子的作者
author: (parent) => {
return users.find(u => u.id === parent.userId);
}
}
};
// ========== 启动服务器 ==========
const server = new ApolloServer({ typeDefs, resolvers });
server.listen().then(({ url }) => {
console.log(`GraphQL服务器运行中: ${url}`);
});
GraphQL客户端使用
// ========== GraphQL查询示例 ==========
// 1. 只查询名字(防止Over-fetching)
query {
user(id: 1) {
name
}
}
// 响应: { "data": { "user": { "name": "张三" } } }
// 2. 一次性查询用户和帖子(防止Under-fetching)
query {
user(id: 1) {
name
email
posts {
title
content
}
}
}
// 响应:
{
"data": {
"user": {
"name": "张三",
"email": "zhang@example.com",
"posts": [
{ "title": "REST API介绍", "content": "REST是..." },
{ "title": "GraphQL介绍", "content": "GraphQL是..." }
]
}
}
}
// 3. 同时查询多个资源
query {
users {
name
}
posts {
title
}
}
// 4. 使用变量
query GetUser($userId: Int!) {
user(id: $userId) {
name
email
}
}
// 变量: { "userId": 1 }
// ========== Mutation(修改数据) ==========
// 1. 创建用户
mutation {
createUser(name: "王五", email: "wang@example.com", age: 28) {
id
name
email
}
}
// 2. 更新用户
mutation {
updateUser(id: 1, age: 26) {
id
name
age
}
}
// 3. 删除用户
mutation {
deleteUser(id: 1)
}
// ========== JavaScript客户端 ==========
async function fetchGraphQL(query, variables = {}) {
const response = await fetch('http://localhost:4000/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query,
variables
})
});
return response.json();
}
// 查询用户和帖子
const query = `
query GetUserWithPosts($userId: Int!) {
user(id: $userId) {
name
email
posts {
title
}
}
}
`;
const data = await fetchGraphQL(query, { userId: 1 });
console.log(data);
使用Apollo Client集成React
// ========== Apollo Client设置 ==========
import { ApolloClient, InMemoryCache, ApolloProvider, useQuery, useMutation, gql } from '@apollo/client';
// 创建客户端
const client = new ApolloClient({
uri: 'http://localhost:4000/graphql',
cache: new InMemoryCache()
});
// 将Provider应用到应用
function App() {
return (
<ApolloProvider client={client}>
<UserList />
</ApolloProvider>
);
}
// ========== 使用查询 ==========
const GET_USERS = gql`
query GetUsers {
users {
id
name
email
posts {
title
}
}
}
`;
function UserList() {
const { loading, error, data } = useQuery(GET_USERS);
if (loading) return <p>加载中...</p>;
if (error) return <p>错误: {error.message}</p>;
return (
<div>
{data.users.map(user => (
<div key={user.id}>
<h3>{user.name}</h3>
<p>{user.email}</p>
<ul>
{user.posts.map(post => (
<li key={post.title}>{post.title}</li>
))}
</ul>
</div>
))}
</div>
);
}
// ========== 使用Mutation ==========
const CREATE_USER = gql`
mutation CreateUser($name: String!, $email: String!, $age: Int) {
createUser(name: $name, email: $email, age: $age) {
id
name
email
}
}
`;
function CreateUserForm() {
const [createUser, { data, loading, error }] = useMutation(CREATE_USER);
const handleSubmit = (e) => {
e.preventDefault();
const formData = new FormData(e.target);
createUser({
variables: {
name: formData.get('name'),
email: formData.get('email'),
age: parseInt(formData.get('age'))
}
});
};
if (loading) return <p>创建中...</p>;
if (error) return <p>错误: {error.message}</p>;
if (data) return <p>创建完成: {data.createUser.name}</p>;
return (
<form onSubmit={handleSubmit}>
<input name="name" placeholder="姓名" required />
<input name="email" type="email" placeholder="邮箱" required />
<input name="age" type="number" placeholder="年龄" />
<button type="submit">创建用户</button>
</form>
);
}
// ========== 缓存和优化 ==========
const GET_USER = gql`
query GetUser($id: Int!) {
user(id: $id) {
id
name
email
}
}
`;
function UserProfile({ userId }) {
const { loading, error, data, refetch } = useQuery(GET_USER, {
variables: { id: userId },
// 缓存策略
fetchPolicy: 'cache-first', // 缓存优先
// fetchPolicy: 'network-only', // 总是请求服务器
// fetchPolicy: 'cache-and-network', // 显示缓存并更新
});
// 手动刷新
const handleRefresh = () => {
refetch();
};
if (loading) return <p>加载中...</p>;
if (error) return <p>错误: {error.message}</p>;
return (
<div>
<h2>{data.user.name}</h2>
<p>{data.user.email}</p>
<button onClick={handleRefresh}>刷新</button>
</div>
);
}
🤔 常见问题
Q1. 应该选择REST API还是GraphQL?
A: 根据项目特点选择:
✅ 选择REST API的情况:
├─ 简单的CRUD应用
├─ 资源结构清晰
├─ 缓存很重要(利用HTTP缓存)
├─ 团队熟悉REST
├─ 文件上传/下载较多
└─ 例如: 博客、电商基本功能
✅ 选择GraphQL的情况:
├─ 复杂的数据关系
├─ 移动应用(数据节省很重要)
├─ 客户端多样(网页、应用、平板)
├─ 希望快速前端开发
├─ 需要实时数据订阅
└─ 例如: 社交媒体、仪表板、实时应用
🤝 也可以一起使用:
├─ 主要API使用GraphQL
├─ 文件上传使用REST
└─ 发挥各自优势
Q2. 性能差异如何?
A:
// REST API性能特性
优点:
- 可利用HTTP缓存
GET /api/users/1
Cache-Control: max-age=3600
- CDN缓存简单
- 简单端点快速
缺点:
- Over-fetching传输不必要的数据
GET /api/users/1
→ 返回所有字段(100KB)
- Under-fetching需要多次请求
GET /api/users/1 // 第1次请求
GET /api/users/1/posts // 第2次请求
GET /api/posts/1/comments // 第3次请求
// 总共3次请求!
// GraphQL性能特性
优点:
- 只有精确需要的数据(10KB)
query {
user(id: 1) {
name
email
}
}
- 一次请求获取所有数据
query {
user(id: 1) {
name
posts {
title
comments { text }
}
}
}
// 1次请求完成!
缺点:
- HTTP缓存困难(使用POST请求)
- 复杂查询增加服务器负担
- N+1问题(使用DataLoader解决)
实际基准测试:
REST: 3次请求, 总共200KB, 300ms
GraphQL: 1次请求, 总共50KB, 150ms
→ 移动环境中GraphQL更有优势
Q3. GraphQL的N+1问题是什么?
A: 可以用DataLoader解决:
// ========== N+1问题 ==========
// 查询10个用户和各自的帖子
// 问题: 低效查询
const resolvers = {
Query: {
users: () => {
return db.users.findAll(); // 1次查询
}
},
User: {
posts: (user) => {
return db.posts.findByUserId(user.id); // 每个用户1次!
}
}
};
// 总查询: 1 + 10 = 11次
// 1次: 查询用户
// 10次: 查询每个用户的帖子
// ========== 使用DataLoader解决 ==========
const DataLoader = require('dataloader');
// 批量获取帖子
const postLoader = new DataLoader(async (userIds) => {
// 一次性获取所有帖子
const posts = await db.posts.findByUserIds(userIds);
// 按用户分组
const postsByUser = {};
posts.forEach(post => {
if (!postsByUser[post.userId]) {
postsByUser[post.userId] = [];
}
postsByUser[post.userId].push(post);
});
// 返回每个用户的帖子
return userIds.map(id => postsByUser[id] || []);
});
const resolvers = {
User: {
posts: (user) => {
return postLoader.load(user.id); // 批量处理!
}
}
};
// 总查询: 1 + 1 = 2次
// 1次: 查询用户
// 1次: 查询所有帖子(批量)
// → 性能大幅提升!
// ========== 实际示例 ==========
const { ApolloServer } = require('apollo-server');
const DataLoader = require('dataloader');
const server = new ApolloServer({
typeDefs,
resolvers,
context: () => ({
// 每个请求创建新的DataLoader
loaders: {
postLoader: new DataLoader(batchGetPosts),
userLoader: new DataLoader(batchGetUsers)
}
})
});
// 在解析器中使用
const resolvers = {
User: {
posts: (user, args, context) => {
return context.loaders.postLoader.load(user.id);
}
}
};
Q4. REST API版本管理 vs GraphQL模式演化?
A:
// ========== REST API版本管理 ==========
// 方法1: URL中包含版本
GET /api/v1/users
GET /api/v2/users // 新版本
// 方法2: 在头部指定版本
GET /api/users
Accept: application/vnd.myapp.v1+json
// 问题:
// - 维护多个版本
// - 客户端需要更新
// - 旧版本支持负担
// 示例: 字段名更改
// v1
{
"name": "张三"
}
// v2
{
"fullName": "张三" // name → fullName
}
// ========== GraphQL模式演化 ==========
// 添加很容易
type User {
id: Int!
name: String!
email: String! # 现有
phone: String # 添加(新字段)
address: Address # 添加(新类型)
}
// 更改: 使用@deprecated
type User {
id: Int!
name: String! @deprecated(reason: "Use fullName instead")
fullName: String! # 新字段
email: String!
}
// 客户端逐步更新
query {
user(id: 1) {
name # 仍然有效(deprecated)
fullName # 使用新字段
}
}
// 优点:
// - 单个端点
// - 无需版本管理
// - 渐进式迁移
// - 客户端可以在希望的时候更新
Q5. 如何处理实时数据?
A:
// ========== REST API: 轮询或SSE ==========
// 1. 轮询
setInterval(async () => {
const response = await fetch('/api/messages');
const messages = await response.json();
updateUI(messages);
}, 3000); // 每3秒请求一次
// 缺点: 低效,服务器负担大
// 2. Server-Sent Events (SSE)
const eventSource = new EventSource('/api/messages/stream');
eventSource.onmessage = (event) => {
const message = JSON.parse(event.data);
updateUI(message);
};
// 缺点: 只能单向通信
// ========== GraphQL: Subscription ==========
// 服务器设置
const { PubSub } = require('graphql-subscriptions');
const pubsub = new PubSub();
const typeDefs = gql`
type Message {
id: Int!
text: String!
userId: Int!
createdAt: String!
}
type Subscription {
messageAdded: Message!
}
type Mutation {
addMessage(text: String!): Message!
}
`;
const resolvers = {
Mutation: {
addMessage: (parent, args) => {
const message = {
id: messages.length + 1,
text: args.text,
userId: 1,
createdAt: new Date().toISOString()
};
messages.push(message);
// 通知订阅者
pubsub.publish('MESSAGE_ADDED', {
messageAdded: message
});
return message;
}
},
Subscription: {
messageAdded: {
subscribe: () => pubsub.asyncIterator(['MESSAGE_ADDED'])
}
}
};
// 客户端(React)
import { useSubscription, gql } from '@apollo/client';
const MESSAGE_SUBSCRIPTION = gql`
subscription OnMessageAdded {
messageAdded {
id
text
userId
createdAt
}
}
`;
function ChatRoom() {
const { data, loading } = useSubscription(MESSAGE_SUBSCRIPTION);
useEffect(() => {
if (data) {
console.log('新消息:', data.messageAdded);
addMessageToUI(data.messageAdded);
}
}, [data]);
return <div>{/* UI */}</div>;
}
// 优点:
// - 实时双向通信
// - 基于WebSocket
// - 高效
🎓 下一步
了解了REST API和GraphQL后,可以学习:
- 什么是API? (文档编写中) - API基本概念
- JWT令牌 (文档编写中) - API认证
- 什么是WebSocket? (文档编写中) - 实时通信
实践练习
# ========== REST API实践(Express) ==========
mkdir rest-api-demo
cd rest-api-demo
npm init -y
npm install express
# 编写server.js后
node server.js
# 测试
curl http://localhost:3000/api/users
curl -X POST http://localhost:3000/api/users \
-H "Content-Type: application/json" \
-d '{"name":"张三","email":"zhang@example.com"}'
# ========== GraphQL实践(Apollo Server) ==========
mkdir graphql-demo
cd graphql-demo
npm init -y
npm install apollo-server graphql
# 编写server.js后
node server.js
# 打开GraphQL Playground
# http://localhost:4000
# 可以在浏览器中执行查询!
# ========== React + GraphQL ==========
npx create-react-app my-app
cd my-app
npm install @apollo/client graphql
# 配置Apollo Client后运行
npm start
🎬 总结
REST API和GraphQL各有优缺点:
- REST API: 简单、熟悉、缓存容易
- GraphQL: 灵活、高效、支持实时
- 选择标准: 项目复杂度、团队经验、性能需求
- 混合使用: 根据需要同时使用两种方式
用正确的API设计构建优秀的应用程序! 🔄