GraphQL最佳实践
GraphQL最佳实践
概览
GraphQL是一种用于API的查询语言,它使客户端能够精确请求它们需要的数据,不多也不少,具有强大的类型系统和自文档化能力。
GraphQL包括:
- 查询语言:用于获取数据的查询语言
- 模式优先:模式驱动的开发方法
- 强类型:内置类型系统和验证
- 自省:自文档化API能力
- 实时:内置订阅支持实时更新
- 灵活:客户端可以精确请求它们需要的数据
为什么这很重要
- 减少过度获取:GraphQL可以减少30-50%的过度获取
- 增强性能:减少往返次数和数据传输
- 提升开发者体验:自文档化改善DX
- 减少带宽:通过精确查询减少数据传输
- 提高类型安全性:内置验证提高类型安全性
核心概念
1. 模式定义
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
}
input CreateUserInput {
name: String!
email: String!
password: String!
}
type Query {
user(id: ID!): User
users: [User!]!
}
type Mutation {
createUser(input: CreateUserInput!): User!
}
2. 解析器实现
import DataLoader from 'dataloader';
const userLoader = new DataLoader(async (ids) => {
const users = await db.users.findMany({
where: { id: { in: ids } },
});
const userMap = new Map(users.map(u => [u.id, u]));
return ids.map(id => userMap.get(id));
});
const resolvers = {
Query: {
user: async (_, { id }, { dataSources }) => {
return await dataSources.userAPI.getUserById(id);
},
users: async (_, __, { dataSources }) => {
return await dataSources.userAPI.getUsers();
},
},
User: {
posts: async (user, _, { dataSources }) => {
return await dataSources.postAPI.getPostsByUserId(user.id);
},
},
Post: {
author: async (post, _, { loaders }) => {
return await loaders.userLoader.load(post.authorId);
},
},
};
3. DataLoader解决N+1问题
import DataLoader from 'dataloader';
const createUserLoader = () => {
return new DataLoader(async (userIds) => {
const users = await db.users.findMany({
where: { id: { in: userIds } },
});
const userMap = new Map(users.map(u => [u.id, u]));
return userIds.map(id => userMap.get(id));
});
};
4. 认证
const authMiddleware = (req, res, next) => {
const token = req.headers.authorization?.replace('Bearer ', '');
if (token) {
try {
req.user = jwt.verify(token, process.env.JWT_SECRET);
} catch (error) {
req.user = null;
}
}
next();
};
const server = new ApolloServer({
typeDefs,
resolvers,
context: ({ req }) => ({
user: req.user,
}),
});
5. 字段级授权
const resolvers = {
User: {
email: (user, _, { user: currentUser }) => {
if (!currentUser || (currentUser.id !== user.id && !currentUser.isAdmin)) {
throw new AuthenticationError('Not authorized');
}
return user.email;
},
},
Query: {
adminPanel: (_, __, { user }) => {
if (!user || !user.isAdmin) {
throw new ForbiddenError('Admin access required');
}
return 'Admin panel data';
},
},
};
6. 基于游标的分页
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
type UserEdge {
node: User!
cursor: String!
}
type UserConnection {
edges: [UserEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type Query {
users(first: Int, after: String): UserConnection!
}
7. 实时订阅
import { WebSocketServer } from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import { PubSub } from 'graphql-subscriptions';
const pubsub = new PubSub();
const POST_CREATED = 'POST_CREATED';
const wsServer = new WebSocketServer({
server: httpServer,
path: '/graphql',
});
useServer({ schema }, wsServer);
const resolvers = {
Subscription: {
postCreated: {
subscribe: () => pubsub.asyncIterator([POST_CREATED]),
},
},
Mutation: {
createPost: async (_, { input }, { dataSources }) => {
const post = await dataSources.postAPI.createPost(input);
pubsub.publish(POST_CREATED, { postCreated: post });
return post;
},
},
};
8. 查询复杂度分析
import { createComplexityLimitRule } from 'graphql-validation-complexity';
const complexityLimitRule = createComplexityLimitRule(1000, {
onCost: (cost) => console.log(`Query complexity: ${cost}`),
});
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [complexityLimitRule],
});
快速开始
-
安装Apollo Server:
npm install @apollo/server graphql -
创建服务器:
const server = new ApolloServer({ typeDefs, resolvers, }); -
启动服务器:
const { url } = await startStandaloneServer(server, { listen: { port: 4000 }, }); -
访问GraphQL Playground:
生产清单
- [ ] 使用基于游标的分页处理大型列表
- [ ] 为嵌套查询实现DataLoader
- [ ] 添加查询深度限制
- [ ] 实现查询复杂度分析
- [ ] 使用字段级缓存
- [ ] 添加认证和授权
- [ ] 实现错误处理中间件
- [ ] 设置监控和日志记录
- [ ] 用描述文档化模式
- [ ] 在生产中使用持久化查询
- [ ] 实施速率限制
- [ ] 添加输入验证
- [ ] 使用TypeScript提高类型安全性
- [ ] 用单元测试测试解析器
- [ ] 监控查询性能
反模式
- N+1查询问题:始终对嵌套查询使用DataLoader
- 过度暴露数据:限制暴露的字段和类型
- 忽略缓存:在多个级别实施缓存
- 错误处理不佳:提供有意义的错误消息
- 没有速率限制:实施速率限制以防止滥用
- 复杂的突变:保持突变简单且专注
- 没有监控:监控查询性能和错误
集成点
- Express REST:
03-backend-api/express-rest - Node.js API:
03-backend-api/nodejs-api - 错误处理:
03-backend-api/error-handling - 中间件:
03-backend-api/middleware - 验证:
03-backend-api/validation - 数据库优化:
04-database/database-optimization - 连接池:
04-database/connection-pooling