GraphQL 进阶实践
在掌握了GraphQL基础概念和查询语言后,本章将深入探讨Resolver实现、Mutation设计、Subscription实现以及性能优化等进阶主题。
Resolver详解
什么是Resolver
Resolver是GraphQL服务器中负责获取数据的函数。Schema中的每个字段都有一个对应的Resolver函数,当客户端查询该字段时,Resolver会被调用来获取数据。
Resolver函数签名
function resolver(parent, args, context, info) {
return data;
}
参数说明:
| 参数 | 说明 |
|---|---|
| parent | 父字段的返回值,用于处理嵌套查询 |
| args | 字段参数,来自客户端查询 |
| context | 所有Resolver共享的上下文对象,常用于认证信息、数据源等 |
| info | 查询的执行状态信息,包含Schema详情 |
基本Resolver示例
const resolvers = {
Query: {
user: async (parent, { id }, context, info) => {
const user = await context.dataSource.getUserById(id);
return user;
},
users: async (parent, { limit, offset }, context) => {
const users = await context.dataSource.getUsers({ limit, offset });
return users;
},
articles: async (parent, { filter }, context) => {
return await context.dataSource.getArticles(filter);
}
}
};
字段Resolver
对象类型的每个字段都可以有独立的Resolver:
const resolvers = {
Query: {
user: (parent, { id }) => ({ id, name: "张三", email: "[email protected]" })
},
User: {
email: (parent, args, context) => {
if (!context.user || context.user.id !== parent.id) {
return null;
}
return parent.email;
},
articles: async (parent, { limit }, context) => {
return await context.dataSource.getArticlesByAuthor(parent.id, limit);
}
}
};
Resolver链
GraphQL按层级执行Resolver,父Resolver的结果会传递给子Resolver:
query {
user(id: "123") {
name
articles {
title
comments {
content
}
}
}
}
执行顺序:
Query.user(parent=null, args={id: "123"})
↓ 返回 { id: "123", name: "张三", ... }
User.name(parent={id: "123", ...})
User.articles(parent={id: "123", ...})
↓ 返回 [{ id: "1", title: "文章1" }, ...]
Article.title(parent={id: "1", ...})
Article.comments(parent={id: "1", ...})
↓ 返回 [{ content: "评论1" }, ...]
Comment.content(parent={content: "评论1"})
默认Resolver
如果一个字段没有定义Resolver,GraphQL会使用默认Resolver:从parent对象中获取同名字段。
const user = {
id: "123",
name: "张三",
email: "[email protected]"
};
const resolvers = {
Query: {
user: () => user
}
};
query {
user {
name
email
}
}
name和email字段会自动从user对象中获取。
异步Resolver
Resolver可以是异步函数,返回Promise:
const resolvers = {
Query: {
user: async (parent, { id }, context) => {
const response = await fetch(`/api/users/${id}`);
return response.json();
},
articles: async (parent, args, context) => {
const [articles, count] = await Promise.all([
context.db.articles.findMany({ take: args.limit }),
context.db.articles.count()
]);
return { items: articles, total: count };
}
}
};
Context使用
Context对象在所有Resolver之间共享,常用于:
const server = new ApolloServer({
typeDefs,
resolvers,
context: ({ req }) => {
const token = req.headers.authorization || '';
const user = getUserFromToken(token);
return {
user,
dataSources: {
userAPI: new UserAPI(),
articleAPI: new ArticleAPI()
},
db: database
};
}
});
const resolvers = {
Query: {
me: (parent, args, context) => {
if (!context.user) {
throw new AuthenticationError('未认证');
}
return context.user;
}
},
Mutation: {
createArticle: async (parent, { input }, context) => {
if (!context.user) {
throw new AuthenticationError('未认证');
}
return context.dataSources.articleAPI.create({
...input,
authorId: context.user.id
});
}
}
};
Mutation设计
Mutation命名规范
Mutation应该使用动词开头,清晰表达操作意图:
type Mutation {
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User
deleteUser(id: ID!): DeleteResult!
createArticle(input: CreateArticleInput!): Article!
updateArticle(id: ID!, input: UpdateArticleInput!): Article
publishArticle(id: ID!): Article!
archiveArticle(id: ID!): Article!
deleteArticle(id: ID!): DeleteResult!
}
输入类型设计
使用Input类型封装复杂参数:
input CreateArticleInput {
title: String!
content: String!
tags: [String!]
status: ArticleStatus = DRAFT
}
input UpdateArticleInput {
title: String
content: String
tags: [String!]
status: ArticleStatus
}
返回类型设计
Mutation可以返回操作结果或更新后的资源:
返回资源:
type Mutation {
createArticle(input: CreateArticleInput!): Article!
}
mutation {
createArticle(input: { title: "标题", content: "内容" }) {
id
title
content
createdAt
}
}
返回结果对象:
type ArticleMutationResult {
success: Boolean!
message: String
article: Article
errors: [FieldError!]
}
type FieldError {
field: String!
message: String!
}
type Mutation {
createArticle(input: CreateArticleInput!): ArticleMutationResult!
}
mutation {
createArticle(input: { title: "", content: "内容" }) {
success
message
article {
id
title
}
errors {
field
message
}
}
}
事务处理
Mutation中的多个操作应该考虑事务:
const resolvers = {
Mutation: {
transferPoints: async (parent, { fromId, toId, amount }, context) => {
const transaction = await context.db.beginTransaction();
try {
await transaction.users.decrementPoints(fromId, amount);
await transaction.users.incrementPoints(toId, amount);
await transaction.commit();
return { success: true };
} catch (error) {
await transaction.rollback();
throw new Error('转账失败');
}
}
}
};
批量操作
支持批量创建、更新、删除:
type Mutation {
createArticles(inputs: [CreateArticleInput!]!): [Article!]!
deleteArticles(ids: [ID!]!): BatchDeleteResult!
}
type BatchDeleteResult {
success: Boolean!
deletedCount: Int!
deletedIds: [ID!]!
}
Subscription实现
WebSocket连接
Subscription基于WebSocket实现,需要配置WebSocket服务器:
import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@apollo/server/express4';
import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer';
import { makeExecutableSchema } from '@graphql-tools/schema';
import { WebSocketServer } from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
const schema = makeExecutableSchema({ typeDefs, resolvers });
const httpServer = createServer(app);
const wsServer = new WebSocketServer({
server: httpServer,
path: '/graphql'
});
const serverCleanup = useServer({ schema }, wsServer);
const server = new ApolloServer({
schema,
plugins: [
ApolloServerPluginDrainHttpServer({ httpServer }),
{
async serverWillStart() {
return {
async drainServer() {
await serverCleanup.dispose();
}
};
}
}
]
});
Subscription Resolver
const resolvers = {
Subscription: {
onCommentAdded: {
subscribe: (parent, { articleId }, context) => {
return context.pubsub.asyncIterator(`COMMENT_ADDED_${articleId}`);
}
},
onArticleCreated: {
subscribe: (parent, args, context) => {
return context.pubsub.asyncIterator('ARTICLE_CREATED');
}
}
},
Mutation: {
addComment: async (parent, { articleId, input }, context) => {
const comment = await context.dataSource.addComment(articleId, input);
context.pubsub.publish(`COMMENT_ADDED_${articleId}`, {
onCommentAdded: comment
});
return comment;
}
}
};
过滤和转换
使用withFilter进行条件过滤:
import { withFilter } from 'graphql-subscriptions';
const resolvers = {
Subscription: {
onMessageReceived: {
subscribe: withFilter(
(parent, args, context) => context.pubsub.asyncIterator('MESSAGE_RECEIVED'),
(payload, variables, context) => {
return payload.onMessageReceived.recipientId === context.user.id;
}
)
}
}
};
Subscription最佳实践
认证检查:
const resolvers = {
Subscription: {
onNotification: {
subscribe: (parent, args, context) => {
if (!context.user) {
throw new AuthenticationError('未认证');
}
return context.pubsub.asyncIterator(`NOTIFICATION_${context.user.id}`);
}
}
}
};
资源清理:
const resolvers = {
Subscription: {
onCommentAdded: {
subscribe: (parent, { articleId }, context) => {
const iterator = context.pubsub.asyncIterator(`COMMENT_ADDED_${articleId}`);
return {
[Symbol.asyncIterator]() {
return iterator;
},
return() {
iterator.return();
return Promise.resolve({ done: true });
}
};
}
}
}
};
性能优化
N+1问题
N+1问题是GraphQL性能的主要挑战。当查询嵌套列表时,每个项目都会触发一次数据库查询。
问题示例:
query {
articles {
title
author {
name
}
}
}
如果没有优化,会执行:
SELECT * FROM articles;
SELECT * FROM users WHERE id = 1;
SELECT * FROM users WHERE id = 2;
SELECT * FROM users WHERE id = 3;
...
DataLoader解决方案
DataLoader通过批处理和缓存解决N+1问题:
import DataLoader from 'dataloader';
const userLoader = new DataLoader(async (ids) => {
const users = await db.users.findAll({ where: { id: ids } });
const userMap = new Map(users.map(u => [u.id, u]));
return ids.map(id => userMap.get(id));
});
const resolvers = {
Article: {
author: async (parent, args, context) => {
return context.userLoader.load(parent.authorId);
}
}
};
查询复杂度控制
限制查询的复杂度,防止资源耗尽:
import { createComplexityLimitRule } from 'graphql-validation-complexity';
const complexityLimit = createComplexityLimitRule(1000, {
onCost: (cost) => console.log('query cost:', cost),
formatErrorMessage: (cost) => `查询复杂度 ${cost} 超过限制 1000`
});
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [complexityLimit]
});
查询深度限制
限制嵌套查询的深度:
import depthLimit from 'graphql-depth-limit';
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [depthLimit(5)]
});
持久化查询
对于生产环境,使用持久化查询减少网络传输:
import { ApolloServerPluginLandingPageDisabled } from '@apollo/server/plugin/landingPage/disabled';
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [
ApolloServerPluginLandingPageDisabled()
]
});
客户端发送查询ID而不是完整查询:
{
"extensions": {
"persistedQuery": {
"version": 1,
"sha256Hash": "hash-of-query"
}
}
}
缓存策略
响应缓存:
import responseCachePlugin from '@apollo/server-plugin-response-cache';
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [
responseCachePlugin()
]
});
字段级缓存:
type Article @cacheControl(maxAge: 3600) {
id: ID!
title: String!
content: String! @cacheControl(maxAge: 600)
author: User!
}
错误处理
错误类型
GraphQL定义了标准错误格式:
{
"errors": [
{
"message": "用户不存在",
"locations": [{ "line": 2, "column": 3 }],
"path": ["user"],
"extensions": {
"code": "USER_NOT_FOUND",
"statusCode": 404
}
}
],
"data": {
"user": null
}
}
自定义错误
class UserNotFoundError extends Error {
constructor(message) {
super(message);
this.name = 'UserNotFoundError';
this.extensions = {
code: 'USER_NOT_FOUND',
statusCode: 404
};
}
}
const resolvers = {
Query: {
user: async (parent, { id }) => {
const user = await getUserById(id);
if (!user) {
throw new UserNotFoundError(`用户 ${id} 不存在`);
}
return user;
}
}
};
错误屏蔽
生产环境应该屏蔽内部错误:
const server = new ApolloServer({
typeDefs,
resolvers,
formatError: (error) => {
if (process.env.NODE_ENV === 'production') {
if (error.extensions?.code === 'INTERNAL_SERVER_ERROR') {
return new Error('内部服务器错误');
}
}
return error;
}
});
安全考虑
认证授权
const resolvers = {
Query: {
me: (parent, args, context) => {
if (!context.user) {
throw new AuthenticationError('未认证');
}
return context.user;
}
},
Mutation: {
updateArticle: async (parent, { id, input }, context) => {
const article = await context.dataSource.getArticle(id);
if (article.authorId !== context.user.id) {
throw new ForbiddenError('无权限修改此文章');
}
return context.dataSource.updateArticle(id, input);
}
}
};
输入验证
import { GraphQLScalarType, GraphQLError } from 'graphql';
const EmailScalar = new GraphQLScalarType({
name: 'Email',
description: '邮箱地址',
serialize(value) {
return value;
},
parseValue(value) {
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
throw new GraphQLError('邮箱格式不正确');
}
return value;
}
});
查询白名单
生产环境可以使用查询白名单:
const persistedQueries = {
'hash1': 'query { users { name } }',
'hash2': 'query { articles { title } }'
};
const server = new ApolloServer({
typeDefs,
resolvers,
allowOnlyPersistedQueries: process.env.NODE_ENV === 'production'
});
总结
GraphQL进阶实践的关键点:
- Resolver:理解Resolver链、Context使用、异步处理
- Mutation:良好的命名、输入输出设计、事务处理
- Subscription:WebSocket实现、过滤、认证
- 性能优化:DataLoader解决N+1、复杂度控制、缓存
- 安全:认证授权、输入验证、查询限制
掌握这些进阶知识,可以构建高性能、安全、可维护的GraphQL API。