GraphQL 使用笔记
[!abstract] 笔记目标 本笔记旨在提供 GraphQL 的实战使用指南,涵盖从 Schema 设计到客户端集成的完整流程,帮助开发者快速上手并避免常见陷阱。
一、核心概念速览
GraphQL 的核心是 Schema-first 的设计哲学。所有操作都围绕 Schema 展开:
graph LR
A[客户端] -->|发送 Query/Mutation| B[GraphQL 服务器]
B -->|解析 Schema| C[类型系统]
B -->|调用| D[解析器 Resolvers]
D -->|访问| E[数据源 DB/API]
E -->|返回数据| D
D -->|组装数据| B
B -->|返回 JSON| A
关键术语:
- Schema:API 的契约,定义所有可查询的数据和操作
- Query:读取数据(幂等)
- Mutation:写入数据(可能产生副作用)
- Resolver:为每个字段提供数据获取逻辑的函数
- Type:数据结构的定义(对象类型、标量类型、枚举等)
二、Schema 设计最佳实践
1. 类型定义原则
# 好的设计:使用对象类型而不是标量类型
type User {
id: ID!
name: String!
email: String!
posts: [Post!]! # 关联关系
createdAt: DateTime!
}
# 避免:使用 JSON 标量类型(失去类型安全)
type User {
id: ID!
data: JSON # 不推荐
}
2. 查询与变更设计
type Query {
# 单个资源查询
user(id: ID!): User
# 列表查询(带分页)
users(first: Int, after: String): UserConnection!
# 搜索查询
searchUsers(query: String!): [User!]!
}
type Mutation {
# 创建
createUser(input: CreateUserInput!): CreateUserPayload!
# 更新
updateUser(id: ID!, input: UpdateUserInput!): UpdateUserPayload!
# 删除
deleteUser(id: ID!): DeleteUserPayload!
}
# 使用 Input 类型封装输入参数
input CreateUserInput {
name: String!
email: String!
}
# 使用 Payload 类型封装返回结果
type CreateUserPayload {
user: User
errors: [Error!]
}
3. 分页设计(Relay 风格)
type UserConnection {
edges: [UserEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type UserEdge {
node: User!
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
三、查询语法详解
1. 基本查询
# 获取用户及其文章
query GetUserWithPosts {
user(id: "1") {
name
email
posts {
title
content
createdAt
}
}
}
2. 使用变量
query GetUser($userId: ID!) {
user(id: $userId) {
name
email
}
}
# 变量传递
{
"userId": "1"
}
3. 片段(Fragments)复用
fragment UserBasicInfo on User {
id
name
email
}
fragment PostInfo on Post {
id
title
content
createdAt
}
query GetUserWithPosts {
user(id: "1") {
...UserBasicInfo
posts {
...PostInfo
}
}
}
4. 指令(Directives)
query GetUser($includePosts: Boolean!) {
user(id: "1") {
name
email
posts @include(if: $includePosts) {
title
}
}
}
四、服务端实现要点
1. 解析器(Resolver)结构
const resolvers = {
Query: {
user: async (parent, { id }, context, info) => {
// parent: 父解析器的结果
// args: 查询参数
// context: 请求上下文(数据库连接、用户信息等)
// info: 查询字段信息
return context.db.users.findById(id);
},
},
User: {
// 字段级解析器
posts: async (parent, args, context) => {
return context.db.posts.findByUserId(parent.id);
},
},
Mutation: {
createUser: async (parent, { input }, context) => {
try {
const user = await context.db.users.create(input);
return { user, errors: [] };
} catch (error) {
return { user: null, errors: [error.message] };
}
},
},
};
2. 上下文(Context)管理
const server = new ApolloServer({
typeDefs,
resolvers,
context: ({ req }) => {
// 从请求头获取 token
const token = req.headers.authorization || '';
// 验证用户
const user = getUserFromToken(token);
// 返回上下文对象
return {
user,
db: database,
loaders: createLoaders(), // DataLoader 实例
};
},
});
3. 数据源(Data Sources)集成
// 使用 REST API 作为数据源
class UserAPI extends RESTDataSource {
constructor() {
super();
this.baseURL = 'https://api.example.com/';
}
async getUser(id) {
return this.get(`users/${id}`);
}
async createUser(userData) {
return this.post('users', userData);
}
}
// 配置数据源
const server = new ApolloServer({
typeDefs,
resolvers,
dataSources: () => ({
userAPI: new UserAPI(),
}),
});
五、客户端使用
1. 主流客户端库对比
| 库 | 特点 | 适用场景 |
|---|---|---|
| Apollo Client | 功能全面,缓存强大,社区活跃 | 中大型项目,需要复杂缓存逻辑 |
| Relay | Facebook 出品,性能优化极致,学习曲线陡 | 大型应用,对性能要求极高 |
| urql | 轻量级,可扩展,插件系统 | 中小型项目,需要灵活定制 |
| graphql-request | 极简,无缓存,仅发送请求 | 简单场景,脚本,SSR |
2. Apollo Client 基本使用
import { ApolloClient, InMemoryCache, gql } from '@apollo/client';
// 初始化客户端
const client = new ApolloClient({
uri: 'https://api.example.com/graphql',
cache: new InMemoryCache(),
});
// 查询
const GET_USER = gql`
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
}
}
`;
client.query({
query: GET_USER,
variables: { id: '1' },
}).then(result => console.log(result.data));
// 变更
const CREATE_USER = gql`
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
user {
id
name
}
errors
}
}
`;
client.mutate({
mutation: CREATE_USER,
variables: { input: { name: 'John', email: 'john@example.com' } },
});
3. React 集成(Apollo Client)
import { useQuery, useMutation, gql } from '@apollo/client';
const GET_USERS = gql`
query GetUsers {
users {
id
name
}
}
`;
function UserList() {
const { loading, error, data } = useQuery(GET_USERS);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<ul>
{data.users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
六、性能优化
1. DataLoader 解决 N+1 问题
import DataLoader from 'dataloader';
// 创建 DataLoader 实例
const createLoaders = () => ({
userLoader: new DataLoader(async (userIds) => {
// 批量查询用户
const users = await db.users.findByIds(userIds);
// 按 ID 顺序返回结果
return userIds.map(id => users.find(user => user.id === id));
}),
});
// 在解析器中使用
const resolvers = {
Post: {
author: async (parent, args, context) => {
// 使用 DataLoader 自动批量加载
return context.loaders.userLoader.load(parent.authorId);
},
},
};
2. 查询复杂度分析
import { createComplexityRule } from 'graphql-query-complexity';
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [
createComplexityRule({
maximumComplexity: 1000,
estimators: [
fieldExtensionsEstimator(),
simpleEstimator({ defaultComplexity: 1 }),
],
onComplete: (complexity) => {
console.log('Query complexity:', complexity);
},
}),
],
});
3. 缓存策略
// Apollo Client 缓存配置
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
users: {
// 合并分页数据
keyArgs: false,
merge(existing = { edges: [] }, incoming) {
return {
...incoming,
edges: [...existing.edges, ...incoming.edges],
};
},
},
},
},
},
});
七、安全考虑
1. 认证与授权
// 在上下文中验证用户
const context = ({ req }) => {
const token = req.headers.authorization;
const user = verifyToken(token);
if (!user) {
throw new AuthenticationError('Not authenticated');
}
return { user };
};
// 在解析器中检查权限
const resolvers = {
Mutation: {
deleteUser: async (parent, { id }, { user }) => {
if (!user.isAdmin) {
throw new ForbiddenError('Not authorized');
}
return db.users.delete(id);
},
},
};
2. 查询深度限制
import depthLimit from 'graphql-depth-limit';
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [depthLimit(10)], // 限制查询深度为 10
});
3. 速率限制
import rateLimit from 'express-rate-limit';
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 分钟
max: 100, // 每个 IP 最多 100 个请求
});
app.use('/graphql', limiter);
八、常用工具与生态
1. 开发工具
- GraphQL Playground:交互式查询 IDE
- GraphiQL:浏览器内 IDE
- Apollo DevTools:Chrome 扩展,调试缓存和查询
- GraphQL Code Generator:自动生成 TypeScript 类型
2. 服务端框架
- Apollo Server:最流行,功能全面
- GraphQL Yoga:轻量级,基于 Express
- Mercurius:Fastify 插件,高性能
- Strapi:Headless CMS,内置 GraphQL
3. 客户端库
- Apollo Client:React/Vue/Angular 支持
- Relay:React 专用
- urql:轻量级,框架无关
- graphql-request:极简请求库
九、与 REST 迁移策略
1. 渐进式迁移
graph TD
A[现有 REST API] --> B[添加 GraphQL 层]
B --> C[新功能使用 GraphQL]
C --> D[逐步迁移旧功能]
D --> E[完全迁移]
2. 混合架构
// REST 和 GraphQL 共存
app.use('/api', restRouter); // 旧 REST 端点
app.use('/graphql', graphqlMiddleware); // 新 GraphQL 端点
// GraphQL 解析器调用 REST API
const resolvers = {
Query: {
user: async (parent, { id }) => {
const response = await fetch(`/api/users/${id}`);
return response.json();
},
},
};
十、考试相关考点
1. GraphQL vs REST 对比
| 维度 | GraphQL | REST |
|---|---|---|
| 数据获取 | 精确获取,一次请求 | 固定结构,多次请求 |
| 版本控制 | 演进式,无需版本号 | URL 版本控制 |
| 类型系统 | 强类型 Schema | 无强制类型 |
| 工具链 | 自动生成文档 | 需要 Swagger 等 |
| 学习曲线 | 较陡 | 平缓 |
| 缓存 | 客户端缓存复杂 | HTTP 缓存简单 |
2. 常见面试题
-
GraphQL 解决了 REST 的什么问题?
- 过度获取(Over-fetching)
- 不足获取(Under-fetching)
- 多次请求
-
GraphQL 的缺点是什么?
- N+1 查询问题
- 缓存复杂
- 学习曲线陡峭
- 文件上传复杂
-
如何优化 GraphQL 性能?
- DataLoader 批量加载
- 查询复杂度分析
- 深度限制
- 缓存策略
-
GraphQL 适合什么场景?
- 移动应用(带宽敏感)
- 复杂数据关系
- 快速迭代前端
- API 聚合层
十一、实战检查清单
Schema 设计检查
- 使用强类型,避免 JSON 标量
- 输入参数使用 Input 类型
- 返回结果使用 Payload 类型
- 实现 Relay 风格分页
- 定义错误类型
性能优化检查
- 实现 DataLoader 解决 N+1
- 设置查询深度限制
- 配置查询复杂度分析
- 实现适当的缓存策略
安全检查
- 实现认证(Authentication)
- 实现授权(Authorization)
- 设置速率限制
- 验证输入参数
- 记录查询日志
开发体验检查
- 启用 GraphQL Playground
- 配置 Code Generator
- 设置 Apollo DevTools
- 编写单元测试
十二、常见问题与解决方案
1. N+1 查询问题
问题:查询用户列表时,每个用户的文章触发单独查询。
解决:使用 DataLoader 批量加载。
2. 循环引用
问题:用户引用文章,文章引用用户,导致无限循环。
解决:在 Schema 中明确指定深度限制,或使用惰性加载。
3. 文件上传
问题:GraphQL 原生不支持文件上传。
解决:使用 graphql-upload 包或单独的 REST 端点处理文件。
4. 实时数据
问题:如何实现实时更新?
解决:使用 GraphQL 订阅(Subscriptions)配合 WebSocket。
关联笔记
前置知识:REST API · 强类型系统 · JSON 同族概念:gRPC · API 网关 · OData 应用场景:前后端分离 · 微服务 · 移动应用开发 相关工具:Apollo Server · Apollo Client · Relay · DataLoader