GraphQL解析器技能Skill graphql-resolvers

GraphQL解析器技能专注于实现高效、可维护的GraphQL服务器解析器函数,包括上下文管理、DataLoader批处理、错误处理、认证和测试策略。关键词:GraphQL, 解析器, 后端开发, 数据加载, 错误处理, 认证, 测试, 缓存, 中间件, 性能优化。

后端开发 0 次安装 0 次浏览 更新于 3/25/2026

名称: graphql-resolvers 用户可调用: false 描述: 在实现GraphQL解析器时使用,包括解析器函数、上下文管理、DataLoader批处理、错误处理、认证和测试策略。 允许工具: []

GraphQL解析器

应用解析器实现模式来创建高效、可维护的GraphQL服务器。此技能涵盖解析器函数签名、执行链、上下文管理、DataLoader模式、异步处理、认证和测试策略。

解析器函数签名

每个解析器函数接收四个参数:父级、参数、上下文和信息。理解这些参数是编写有效解析器的基础。

type ResolverFn = (
  parent: any,
  args: any,
  context: any,
  info: GraphQLResolveInfo
) => any;

const resolvers = {
  Query: {
    // parent: 根值(通常对查询是未定义)
    // args: 传递给查询的参数
    // context: 共享上下文对象
    // info: 执行信息
    user: async (parent, args, context, info) => {
      const { id } = args;
      const { dataSources, user } = context;

      // 使用上下文访问数据源和认证信息
      return dataSources.userAPI.getUserById(id);
    },

    posts: async (parent, args, context, info) => {
      const { limit, offset } = args;

      // 从info中访问请求的字段
      const fields = info.fieldNodes[0].selectionSet.selections
        .map(s => s.name.value);

      return context.dataSources.postAPI.getPosts({
        limit,
        offset,
        fields
      });
    }
  }
};

字段解析器

字段解析器定义如何解析类型上的单个字段。父级参数包含解析后的父对象。

const resolvers = {
  Query: {
    user: async (_, { id }, { dataSources }) => {
      return dataSources.userAPI.getUserById(id);
    }
  },

  User: {
    // 计算字段的字段解析器
    fullName: (parent) => {
      return `${parent.firstName} ${parent.lastName}`;
    },

    // 相关数据的字段解析器
    posts: async (parent, args, { dataSources }) => {
      // parent.id 来自父User对象
      return dataSources.postAPI.getPostsByAuthor(parent.id);
    },

    // 带参数的字段解析器
    friends: async (parent, { limit }, { dataSources }) => {
      return dataSources.userAPI.getFriends(parent.id, limit);
    },

    // 异步计算字段
    postCount: async (parent, _, { dataSources }) => {
      return dataSources.postAPI.countByAuthor(parent.id);
    }
  },

  Post: {
    author: async (parent, _, { dataSources }) => {
      // parent.authorId 来自父Post对象
      return dataSources.userAPI.getUserById(parent.authorId);
    },

    comments: async (parent, _, { dataSources }) => {
      return dataSources.commentAPI.getByPostId(parent.id);
    }
  }
};

上下文对象模式

上下文对象在单个请求中所有解析器之间共享。用于认证、数据源和请求范围的数据。

interface Context {
  user: User | null;
  dataSources: DataSources;
  db: Database;
  req: Request;
  loaders: Loaders;
}

// 上下文创建函数
const createContext = async ({ req }): Promise<Context> => {
  // 提取并验证认证令牌
  const token = req.headers.authorization?.replace('Bearer ', '');
  const user = token ? await verifyToken(token) : null;

  // 初始化数据源
  const dataSources = {
    userAPI: new UserAPI(),
    postAPI: new PostAPI(),
    commentAPI: new CommentAPI()
  };

  // 初始化DataLoaders
  const loaders = {
    userLoader: new DataLoader(ids => batchGetUsers(ids)),
    postLoader: new DataLoader(ids => batchGetPosts(ids))
  };

  return {
    user,
    dataSources,
    db: database,
    req,
    loaders
  };
};

// 在解析器中使用上下文
const resolvers = {
  Query: {
    me: (_, __, { user }) => {
      if (!user) {
        throw new Error('未认证');
      }
      return user;
    },

    post: async (_, { id }, { loaders }) => {
      return loaders.postLoader.load(id);
    }
  }
};

解析器链和执行

解析器以链式执行,父解析器在子解析器开始前完成。理解执行顺序对于优化至关重要。

const resolvers = {
  Query: {
    // 步骤1: 根解析器执行
    user: async (_, { id }, { db }) => {
      console.log('1. 获取用户');
      return db.users.findById(id);
    }
  },

  User: {
    // 步骤2: 字段解析器使用父数据执行
    posts: async (parent, _, { db }) => {
      console.log('2. 为用户获取帖子', parent.id);
      return db.posts.findByAuthor(parent.id);
    },

    profile: async (parent, _, { db }) => {
      console.log('2. 为用户获取个人资料', parent.id);
      return db.profiles.findByUserId(parent.id);
    }
  },

  Post: {
    // 步骤3: 嵌套字段解析器执行
    comments: async (parent, _, { db }) => {
      console.log('3. 为帖子获取评论', parent.id);
      return db.comments.findByPostId(parent.id);
    }
  }
};

// 查询执行顺序:
// query {
//   user(id: "1") {        # 1. 用户解析器
//     posts {              # 2. 帖子解析器
//       comments {         # 3. 评论解析器
//         text
//       }
//     }
//     profile {            # 2. 个人资料解析器(并行)
//       bio
//     }
//   }
// }

DataLoader模式用于批处理

DataLoader通过将多个单独加载合并为单个批处理请求并缓存结果来解决N+1问题。

import DataLoader from 'dataloader';

// 批处理函数接收键数组
// 必须以相同顺序返回结果数组
const batchGetUsers = async (userIds: string[]) => {
  console.log('批处理加载用户:', userIds);

  // 为所有ID进行单次数据库查询
  const users = await db.users.findByIds(userIds);

  // 为O(1)查找创建映射
  const userMap = new Map(users.map(u => [u.id, u]));

  // 以与输入ID相同的顺序返回用户
  return userIds.map(id => userMap.get(id) || null);
};

// 在上下文中创建加载器
const userLoader = new DataLoader(batchGetUsers, {
  // 可选配置
  cache: true,            // 缓存结果(默认: true)
  maxBatchSize: 100,      // 最大批处理大小
  batchScheduleFn: cb => setTimeout(cb, 10) // 自定义调度
});

const resolvers = {
  Post: {
    author: async (parent, _, { loaders }) => {
      // 这些调用会自动批处理
      return loaders.userLoader.load(parent.authorId);
    }
  },

  Comment: {
    author: async (parent, _, { loaders }) => {
      // 添加到与Post.author相同的批处理中
      return loaders.userLoader.load(parent.authorId);
    }
  }
};

// 示例: 无DataLoader(N+1问题)
// 查询10个帖子 = 1个帖子查询 + 10个作者查询
//
// 有DataLoader:
// 查询10个帖子 = 1个帖子查询 + 1个批处理查询所有作者

高级DataLoader模式

// 复合键加载器
interface CompositeKey {
  userId: string;
  type: string;
}

const batchGetUserData = async (keys: CompositeKey[]) => {
  // 按类型分组以高效查询
  const byType = keys.reduce((acc, key) => {
    acc[key.type] = acc[key.type] || [];
    acc[key.type].push(key.userId);
    return acc;
  }, {});

  // 按类型获取数据
  const results = await Promise.all(
    Object.entries(byType).map(([type, userIds]) =>
      fetchDataByType(type, userIds)
    )
  );

  // 映射回原始键顺序
  return keys.map(key =>
    results.find(r => r.userId === key.userId && r.type === key.type)
  );
};

const dataLoader = new DataLoader(
  batchGetUserData,
  {
    cacheKeyFn: (key: CompositeKey) => `${key.userId}:${key.type}`
  }
);

// 预加载缓存
await dataLoader.prime({ userId: '1', type: 'profile' }, userData);

// 清除特定键
dataLoader.clear({ userId: '1', type: 'profile' });

// 清除所有缓存
dataLoader.clearAll();

异步错误处理

解析器中的适当错误处理确保有意义的错误传递到客户端,同时保护敏感信息。

import { GraphQLError } from 'graphql';
import { ApolloServerErrorCode } from '@apollo/server/errors';

const resolvers = {
  Query: {
    user: async (_, { id }, { dataSources }) => {
      try {
        const user = await dataSources.userAPI.getUserById(id);

        if (!user) {
          throw new GraphQLError('用户未找到', {
            extensions: {
              code: 'USER_NOT_FOUND',
              http: { status: 404 }
            }
          });
        }

        return user;
      } catch (error) {
        // 记录完整错误以调试
        console.error('获取用户时出错:', error);

        // 抛出安全错误给客户端
        if (error instanceof GraphQLError) {
          throw error;
        }

        throw new GraphQLError('获取用户失败', {
          extensions: {
            code: 'INTERNAL_SERVER_ERROR'
          }
        });
      }
    }
  },

  Mutation: {
    createPost: async (_, { input }, { user, dataSources }) => {
      // 验证错误
      if (!input.title || input.title.length < 3) {
        throw new GraphQLError('标题必须至少3个字符', {
          extensions: {
            code: 'BAD_USER_INPUT',
            argumentName: 'title'
          }
        });
      }

      // 认证错误
      if (!user) {
        throw new GraphQLError('必须认证', {
          extensions: {
            code: ApolloServerErrorCode.UNAUTHENTICATED
          }
        });
      }

      try {
        return await dataSources.postAPI.create(input);
      } catch (error) {
        throw new GraphQLError('创建帖子失败', {
          extensions: {
            code: 'INTERNAL_SERVER_ERROR'
          },
          originalError: error
        });
      }
    }
  }
};

认证和授权

在解析器和上下文中实现认证和授权模式。

// 认证中间件
const requireAuth = (resolver) => {
  return (parent, args, context, info) => {
    if (!context.user) {
      throw new GraphQLError('未认证', {
        extensions: { code: 'UNAUTHENTICATED' }
      });
    }
    return resolver(parent, args, context, info);
  };
};

// 授权中间件
const requireRole = (role: string) => (resolver) => {
  return (parent, args, context, info) => {
    if (!context.user) {
      throw new GraphQLError('未认证', {
        extensions: { code: 'UNAUTHENTICATED' }
      });
    }

    if (!context.user.roles.includes(role)) {
      throw new GraphQLError('权限不足', {
        extensions: { code: 'FORBIDDEN' }
      });
    }

    return resolver(parent, args, context, info);
  };
};

const resolvers = {
  Query: {
    me: requireAuth((_, __, { user }) => user),

    adminPanel: requireRole('ADMIN')(
      async (_, __, { dataSources }) => {
        return dataSources.adminAPI.getDashboard();
      }
    ),

    // 基于资源的授权
    post: async (_, { id }, { user, dataSources }) => {
      const post = await dataSources.postAPI.getById(id);

      if (!post) {
        throw new GraphQLError('帖子未找到');
      }

      // 检查用户是否可以查看此帖子
      if (post.status === 'DRAFT' && post.authorId !== user?.id) {
        throw new GraphQLError('无法查看草稿帖子', {
          extensions: { code: 'FORBIDDEN' }
        });
      }

      return post;
    }
  },

  Mutation: {
    updatePost: requireAuth(
      async (_, { id, input }, { user, dataSources }) => {
        const post = await dataSources.postAPI.getById(id);

        // 检查所有权
        if (post.authorId !== user.id && !user.roles.includes('ADMIN')) {
          throw new GraphQLError('未授权更新此帖子', {
            extensions: { code: 'FORBIDDEN' }
          });
        }

        return dataSources.postAPI.update(id, input);
      }
    )
  }
};

缓存策略

在解析器级别实现缓存以提高性能。

import { createHash } from 'crypto';

// 内存缓存
const cache = new Map<string, { data: any; expiry: number }>();

const getCacheKey = (prefix: string, args: any): string => {
  const hash = createHash('md5')
    .update(JSON.stringify(args))
    .digest('hex');
  return `${prefix}:${hash}`;
};

const cacheResolver = (
  resolver,
  { ttl = 300, prefix = 'cache' } = {}
) => {
  return async (parent, args, context, info) => {
    const key = getCacheKey(prefix, args);
    const cached = cache.get(key);

    if (cached && cached.expiry > Date.now()) {
      console.log('缓存命中:', key);
      return cached.data;
    }

    const result = await resolver(parent, args, context, info);

    cache.set(key, {
      data: result,
      expiry: Date.now() + (ttl * 1000)
    });

    return result;
  };
};

const resolvers = {
  Query: {
    // 缓存5分钟
    popularPosts: cacheResolver(
      async (_, { limit }, { dataSources }) => {
        return dataSources.postAPI.getPopular(limit);
      },
      { ttl: 300, prefix: 'popular-posts' }
    ),

    // Redis缓存
    user: async (_, { id }, { redis, dataSources }) => {
      const cacheKey = `user:${id}`;

      // 先尝试缓存
      const cached = await redis.get(cacheKey);
      if (cached) {
        return JSON.parse(cached);
      }

      // 获取并缓存
      const user = await dataSources.userAPI.getUserById(id);
      await redis.setex(cacheKey, 3600, JSON.stringify(user));

      return user;
    }
  }
};

解析器中间件和插件

创建可重用的中间件模式以处理横切关注点。

// 日志中间件
const logResolver = (resolver) => {
  return async (parent, args, context, info) => {
    const start = Date.now();
    const fieldName = info.fieldName;

    try {
      const result = await resolver(parent, args, context, info);
      const duration = Date.now() - start;
      console.log(`${fieldName} 解析在 ${duration}毫秒内`);
      return result;
    } catch (error) {
      console.error(`${fieldName} 失败:`, error);
      throw error;
    }
  };
};

// 计时中间件
const timeResolver = (resolver) => {
  return async (parent, args, context, info) => {
    const start = performance.now();
    const result = await resolver(parent, args, context, info);
    const duration = performance.now() - start;

    // 添加计时到扩展
    info.operation.extensions = info.operation.extensions || {};
    info.operation.extensions.timing =
      info.operation.extensions.timing || {};
    info.operation.extensions.timing[info.fieldName] = duration;

    return result;
  };
};

// 组合中间件
const compose = (...middlewares) => (resolver) => {
  return middlewares.reduceRight(
    (acc, middleware) => middleware(acc),
    resolver
  );
};

const resolvers = {
  Query: {
    user: compose(
      logResolver,
      timeResolver,
      requireAuth
    )(async (_, { id }, { dataSources }) => {
      return dataSources.userAPI.getUserById(id);
    })
  }
};

测试解析器

使用模拟上下文和数据源编写解析器的全面测试。

import { describe, it, expect, vi } from 'vitest';

describe('用户解析器', () => {
  it('应通过ID获取用户', async () => {
    const mockUser = { id: '1', username: 'test' };

    const mockContext = {
      dataSources: {
        userAPI: {
          getUserById: vi.fn().mockResolvedValue(mockUser)
        }
      }
    };

    const result = await resolvers.Query.user(
      null,
      { id: '1' },
      mockContext,
      {} as any
    );

    expect(result).toEqual(mockUser);
    expect(mockContext.dataSources.userAPI.getUserById)
      .toHaveBeenCalledWith('1');
  });

  it('用户未找到时应抛出错误', async () => {
    const mockContext = {
      dataSources: {
        userAPI: {
          getUserById: vi.fn().mockResolvedValue(null)
        }
      }
    };

    await expect(
      resolvers.Query.user(null, { id: '999' }, mockContext, {} as any)
    ).rejects.toThrow('用户未找到');
  });

  it('应要求认证', async () => {
    const mockContext = {
      user: null,
      dataSources: {}
    };

    await expect(
      resolvers.Query.me(null, {}, mockContext, {} as any)
    ).rejects.toThrow('未认证');
  });

  it('应使用DataLoader进行批处理', async () => {
    const mockUsers = [
      { id: '1', username: 'user1' },
      { id: '2', username: 'user2' }
    ];

    const batchFn = vi.fn().mockResolvedValue(mockUsers);
    const loader = new DataLoader(batchFn);

    const mockContext = {
      loaders: { userLoader: loader }
    };

    // 进行多次调用
    const [user1, user2] = await Promise.all([
      resolvers.Post.author(
        { authorId: '1' },
        {},
        mockContext,
        {} as any
      ),
      resolvers.Post.author(
        { authorId: '2' },
        {},
        mockContext,
        {} as any
      )
    ]);

    expect(user1).toEqual(mockUsers[0]);
    expect(user2).toEqual(mockUsers[1]);
    expect(batchFn).toHaveBeenCalledTimes(1);
    expect(batchFn).toHaveBeenCalledWith(['1', '2']);
  });
});

最佳实践

  1. 保持解析器薄: 将业务逻辑委托给服务层,仅使用解析器进行数据获取和转换
  2. 使用DataLoader: 为任何获取相关数据的解析器实现DataLoader以避免N+1查询
  3. 利用上下文: 将所有解析器的共享资源(数据库、认证、数据源)存储在上下文中
  4. 优雅处理错误: 捕获错误并抛出有意义的GraphQLError实例,带有适当的代码
  5. 实现适当认证: 在解析器或中间件中一致检查认证和授权
  6. 策略性缓存: 使用内存或分布式缓存在解析器级别缓存昂贵操作
  7. 使用类型化解析器: 为解析器函数定义TypeScript类型以在编译时捕获错误
  8. 全面测试: 为解析器编写单元测试,带有模拟依赖和边缘情况
  9. 避免阻塞操作: 尽可能使用async/await和并行执行以防止阻塞
  10. 监控性能: 记录解析器执行时间并识别慢速解析器进行优化

常见陷阱

  1. N+1查询: 在没有批处理的情况下循环中获取相关数据,导致过多数据库查询
  2. 阻塞操作: 在解析器中使用同步操作阻塞事件循环
  3. 内存泄漏: 在闭包或模块范围中存储无限制增长的数据
  4. 不一致错误处理: 抛出原始错误而没有适当的GraphQLError包装和代码
  5. 解析器中过度获取: 当只需要特定字段时获取整个对象
  6. 上下文突变: 在解析器执行期间修改上下文对象,导致副作用
  7. 缺失认证检查: 在敏感解析器中忘记验证认证
  8. 不当DataLoader使用: 为每个解析器创建新DataLoader实例而不是每个请求
  9. 循环解析器链: 创建导致无限循环的解析器依赖
  10. 未使用info参数: 忽略包含优化所需请求字段的info参数

何时使用此技能

在以下情况下使用GraphQL解析器技能:

  • 实现新的GraphQL服务器
  • 优化现有解析器性能
  • 调试N+1查询问题
  • 添加认证和授权
  • 实现数据批处理和缓存
  • 编写解析器单元测试
  • 为更好维护性重构解析器
  • 向解析器添加日志和监控
  • 实现自定义中间件或插件
  • 从REST API迁移到GraphQL

资源