执行流程与解析器Resolver


执行流程与解析器Resolver

一次查询的完整旅程

当一段GraphQL查询文本到达服务器,它会经历四个阶段:解析、验证、执行、响应组装。理解这个流程对于调试性能问题、编写高效的解析器至关重要。

query { user(id: "1001") { name posts(limit: 3) { title author { name } } } } ​

阶段一:解析(Parsing)

服务器收到查询文本后,第一步是把它解析成抽象语法树(AST)。这一步只检查语法是否合法——括号是否匹配、字段名是否合法标识符——不关心字段是否存在于Schema中。

AST是一棵树状结构,根节点是 OperationDefinition,子节点对应每个字段,嵌套字段形成子树。上面的查询会被解析成类似这样的结构:

Document OperationDefinition (query) SelectionSet Field: user (args: {id: "1001"}) SelectionSet Field: name Field: posts (args: {limit: 3}) SelectionSet Field: title Field: author SelectionSet Field: name ​

阶段二:验证(Validation)

解析通过后,验证器对照Schema检查查询的语义合法性:

  • 字段 user 是否存在于 Query 类型
  • 参数 id 的类型是否匹配(是 ID!)
  • 返回类型 User 是否有 name 和 posts 字段
  • posts 返回的 [Post] 是否有 title 和 author 字段
  • 不存在循环引用、变量类型是否匹配等

验证失败时,服务器返回错误列表,不会执行查询。验证通过后进入执行阶段。

阶段三:执行(Execution)

执行引擎遍历AST,为每个字段调用对应的解析器函数。解析器是纯函数,接收四个参数:(parent, args, context, info)。

  • parent:父字段的解析器返回值。根字段的 parent 通常是 null 或 rootValue
  • args:查询中传递给该字段的参数
  • context:跨解析器共享的上下文对象(通常包含数据库连接、认证信息、DataLoader实例等)
  • info:包含当前字段元信息的对象(Schema信息、AST节点引用等)
const resolvers = { Query: { user: async (parent, { id }, context) => { // 从数据库获取用户 return await context.db.users.findById(id); } }, User: { name: (parent) => parent.name, // 直接从parent取值 posts: async (parent, { limit }, context) => { // 用parent.id(从上一级解析器获得)查询帖子 return await context.db.posts.findByAuthor(parent.id, { limit }); }, }, Post: { title: (parent) => parent.title, author: async (parent, args, context) => { return await context.db.users.findById(parent.authorId); } } }; ​

注意 parent 的传递链:user 解析器返回用户对象 → 这个对象成为 name 和 posts 解析器的 parent → posts 解析器返回帖子数组 → 每个帖子元素成为 title 和 author 解析器的 parent。

阶段四:响应组装

执行引擎把所有解析器的返回值按AST的结构组装成JSON对象。如果某个解析器返回 null,对应位置放 null;如果解析器抛出异常,该位置放 null 并把错误记录到响应的 errors 数组。

{ "data": { "user": { "name": "林远", "posts": [ { "title": "GraphQL深度剖析", "author": { "name": "林远" } } ] } }, "errors": [ { "message": "权限不足:无法访问部分帖子", "locations": [{ "line": 5, "column": 7 }], "path": ["user", "posts", 2, "title"] } ] } ​

关键点:GraphQL的默认错误处理是部分成功的。即使部分字段出错,其他字段的数据仍然会正常返回。这与REST"出错就返回500"的行为完全不同。

解析器的并发执行

GraphQL引擎的一个特性是同级字段的解析器可以并发执行。在上面的例子中,name 和 posts 是 User 的同级字段,它们可以同时执行(互不依赖)。但 posts 内部的 title 和 author 是同级字段,也可以并发——前提是 author 不需要依赖 title 的结果。

如果 author 解析器需要读取数据库,title 解析器只是从 parent.title 取值,那么 title 几乎是即时完成的,不需要等 author。

默认解析器与解析器解析链

如果你没有为一个字段定义解析器,GraphQL引擎会使用默认解析器:直接从 parent 对象上读取同名属性。比如上面的 name: (parent) => parent.name 实际上可以省略——引擎会自动做同样的事。

自定义解析器覆盖默认行为。常见的场景包括:

  • 字段名与数据源属性名不同(displayName vs user_name)
  • 字段需要计算(fullName = firstName + lastName)
  • 字段需要额外查询(posts 需要查数据库)
  • 字段需要权限检查

图:解析器四参数详解

图:解析器四参数详解

解析器是GraphQL架构的关键抽象

解析器的价值不仅在于它"怎么工作",而在于它"把什么分离了":

数据获取与数据形状分离。Schema定义数据形状("User有name和posts"),解析器定义数据获取方式("从MySQL取name,从Redis取posts")。前端只关心形状,不关心来源。

数据源无关性。一个GraphQL服务可以聚合来自MySQL、PostgreSQL、MongoDB、Redis、REST API、gRPC服务、甚至内存缓存的数据。每个解析器独立决定去哪取数据,引擎只负责编排调用。

关注点分离。每个解析器只负责一个字段的逻辑。user 解析器不关心 posts 怎么查,posts 解析器也不关心 author 怎么查。这种分离让代码更容易维护和测试。

可组合性。解析器可以组合——posts 解析器内部可以调用其他函数或服务,返回的数据自动成为子字段解析器的 parent。

这种"一个字段一个函数"的解析器模型,是GraphQL服务端架构中最核心的设计模式。理解它,就理解了GraphQL为什么能优雅地整合异构数据源,以及为什么它天然适合作为微服务架构的聚合层。

info 参数:了解你正在解析什么

info 参数是四参数里最容易被忽略的一个,它在高级场景里很有用。info 里包含当前字段的 Schema 定义、AST 节点、返回类型、以及父类型信息。几个实际用途:

按需取列。解析器可以检查查询里选择了哪些子字段,只查询对应的数据库列。比如 users 解析器查看 info.fieldNodes[0].selectionSet 里有没有 email,没有就不查 email 列,减少不必要的 IO。

动态字段名。当多个 Schema 字段映射到同一个数据来源时,可以用 info.fieldName 判断当前正在解析哪个字段。

指令与权限元数据。info 里可以读取字段上的指令,实现基于指令的字段级授权(详见第五章)。

async function usersResolver(parent, args, ctx, info) { const selections = info.fieldNodes[0].selectionSet.selections .map(s => s.name.value); const columns = ['id', 'name']; if (selections.includes('email')) columns.push('email'); return ctx.db.query(`SELECT ${columns.join(',')} FROM users`); } ​

注意:按需取列优化了 IO,但牺牲了解析器与 Schema 的简单对应关系。团队需要约定"默认不依赖查询细节"的原则,只在确有性能压力的字段上用这个技巧,否则每个解析器都去解析 selectionSet,代码会迅速变得难以维护。

解析器返回值的三种形态

解析器可以返回三种形态的值:普通值(同步返回)、Promise(异步取数,执行引擎会 await)、以及 AsyncIterable(用于 Subscription 的实时流)。理解这一点有助于排查"为什么数据没出来"——如果你在解析器里返回了未 await 的 Promise,执行引擎仍然会正确等待;但如果你在同步解析器里抛错,错误的包装位置与 async 解析器不同。保持"解析器要么返回数据、要么抛业务错误"的简单约定,能让整个服务的错误处理可预测。

作者与出处
原作者:灏天文库
来源:平台策划编纂
整理人: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U