Schema与解析器设计模式


Schema与解析器设计模式

Schema设计规范

命名规范。字段名用camelCase,类型名用PascalCase,枚举值用SCREAMING_SNAKE_CASE。这是GraphQL社区的约定,所有工具都遵循这个约定。

# 正确 type UserAccount { id: ID! firstName: String! lastName: String! emailVerified: Boolean! lastLoginAt: DateTime } enum AccountStatus { ACTIVE SUSPENDED PENDING_VERIFICATION } # 错误(不要这样做) type user_account { Id: ID! first_name: String! } ​

分页模式。基于游标的分页(Cursor-based Pagination)是GraphQL的推荐方式。定义Connection和Edge类型来标准化分页接口。

interface Connection { pageInfo: PageInfo! } type PageInfo { hasNextPage: Boolean! hasPreviousPage: Boolean! startCursor: String endCursor: String } type PostConnection implements Connection { edges: [PostEdge!]! pageInfo: PageInfo! totalCount: Int! } type PostEdge { cursor: String! node: Post! } type Query { posts(first: Int = 20, after: String, last: Int, before: String): PostConnection! } ​

first/after 正向分页,last/before 反向分页。totalCount 是可选的(有些场景下计算总数开销大)。

Mutation Payload模式。每个Mutation返回一个Payload类型,包含操作结果、可能的错误信息、以及被修改的实体。

type CreatePostPayload { post: Post success: Boolean! errors: [MutationError!] clientMutationId: String } type MutationError { code: String! message: String! field: String } type Mutation { createPost(input: CreatePostInput!, clientMutationId: String): CreatePostPayload! } ​

clientMutationId 是Relay要求的字段,用于客户端关联请求和响应。即使不用Relay,保留它也没有坏处。

Node接口。全局唯一标识接口,让所有实体类型都实现统一的 id 字段。

interface Node { id: ID! } type Query { node(id: ID!): Node # 按全局ID获取任何实体 nodes(ids: [ID!]!): [Node]! # 批量获取 } type User implements Node { id: ID! name: String! } type Post implements Node { id: ID! title: String! } ​

node(id: ID!) 查询让客户端可以用一个统一的方式获取任何实体,这在缓存失效后重新获取数据时很方便。

解析器设计模式

DataLoader模式。每个请求创建新的DataLoader实例,同类型的数据获取通过DataLoader批处理。这是避免N+1查询的标准模式。

DataSource模式。用Apollo DataSource封装外部API调用,自动处理缓存、去重、重试。

class ProductAPI extends RESTDataSource { override baseURL = 'https://products.internal/'; async getProduct(id: string): Promise<Product> { return this.get(`products/${id}`); } async searchProducts(query: string, limit = 10): Promise<Product[]> { return this.get('products/search', { args: { q: query, limit } }); } } ​

中间件模式。用解析器中间件实现横切关注点(日志、权限检查、性能计时):

function withTiming<T>(resolver: Resolver<T>): Resolver<T> { return async (parent, args, ctx, info) => { const start = performance.now(); try { const result = await resolver(parent, args, ctx, info); const duration = performance.now() - start; ctx.logger.debug(`Resolver ${info.path.key} took ${duration.toFixed(2)}ms`); return result; } catch (error) { const duration = performance.now() - start; ctx.logger.error(`Resolver ${info.path.key} failed after ${duration.toFixed(2)}ms: ${error.message}`); throw error; } }; } ​

错误边界模式。在解析器中捕获特定错误并转换为GraphQL错误:

const resolvers = { Query: { user: async (_, { id }, ctx) => { try { return await ctx.userRepo.findById(id); } catch (error) { if (error.code === 'NOT_FOUND') { return null; // 解析器返回null是合法的 } throw new GraphQLError('用户查询失败', { extensions: { code: 'INTERNAL_ERROR' } }); } } } }; ​

性能考量:

  • 避免在解析器中做大量计算——应该放在服务层
  • 避免在解析器中同步等待——用async/await
  • 避免在解析器中创建新连接——连接池放在context中
  • 避免在解析器中传递大对象——只传递必要的字段

这些设计模式不是教条,而是在大量生产实践中总结出来的经验。每个模式解决一个具体问题——DataLoader解决N+1,DataSource解决外部API调用,中间件解决横切关注点,错误边界解决异常处理。根据你的项目需要选择和应用。

接口与联合类型的选择

接口表达"一组类型共享的契约",联合表达"这个位置可能是几种类型之一"。选择原则:需要跨类型访问共享字段时用接口,只需要区分类型分支时用联合。比如"可搜索的实体"用联合,因为每个分支的字段完全不同;"所有可被收藏的内容"用接口,因为收藏列表要统一展示 id 和 title。

# 接口:收藏列表需要统一展示标题和类型 interface Bookmarkable { id: ID! title: String! type: BookmarkType! } # 联合:搜索结果每个分支展示不同字段 union SearchResult = Article | User | Product | Category ​

误用接口的代价是:把不共享的字段硬塞进接口,每个实现类型都要写无意义的占位字段;误用联合的代价是:客户端处理每个分支都要写重复的判断逻辑。多花几分钟想清楚"这两个类型到底共享什么",比写完后重构便宜得多。

空值设计与错误上报

GraphQL 里 null 既可能是"合法业务空值",也可能是"解析器出错的结果",客户端无法区分。设计建议:业务空值用可空字段返回 null 并保证语义清晰;真正出错时抛 GraphQLError 并带错误码,让客户端按错误码决定展示策略,而不是把错误当成数据。

// 推荐:出错时抛带码的错误,而不是返回 null throw new GraphQLError('商品库存不足', { extensions: { code: 'INSUFFICIENT_STOCK', field: 'stock' } }); ​

同时,Mutation 的 Payload 里显式带 errors 数组(见上文)比依赖顶层 errors 更利于客户端处理——它让"操作失败但响应完整"成为常态,而不是异常。

图:GraphQL Schema设计规范速查

图:GraphQL Schema设计规范速查

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