Schema演进与自定义扩展


Schema演进与自定义扩展

Schema的版本管理哲学(下)

GraphQL鼓励无版本演进--通过向后兼容的变更来持续扩展API,避免REST中 /v1 /v2 /v3 的版本爆炸。但实际项目中,有些变更不可避免地需要"破坏性"修改。

GraphQL规范的工具是 @deprecated 指令。标记一个字段为废弃后,现有查询不受影响,但开发工具会发出警告,给予客户端足够的迁移窗口。

type User { id: ID! name: String! displayName: String! # 新字段 fullName: String @deprecated(reason: "请使用 displayName 字段, 将在2026-12-31移除") avatarUrl: String profilePicture: String @deprecated(reason: "请使用 avatarUrl 字段") email: String! phone: String @deprecated(reason: "隐私保护, 改用 ContactInfo 接口") } ​

废弃策略的实际执行:

  1. 添加新字段(如 displayName),不删除旧字段(fullName)
  2. 标记旧字段为 @deprecated,注明替代字段和移除时间
  3. 更新文档和变更日志
  4. 监控旧字段的使用情况(通过Apollo Studio等工具的查询分析)
  5. 当旧字段的查询量降到可接受水平后,在下一个大版本中移除

Apollo Studio的查询分析工具可以告诉你"过去30天有X个客户端使用了 fullName 字段",这为移除决策提供了数据支撑。

自定义标量

自定义标量让你把领域特定的验证逻辑封装成类型系统的一部分,而不是散落在各个解析器中。

常见场景:

DateTime标量:确保输入是合法的日期时间字符串,输出格式化:

import { GraphQLScalarType, Kind } from 'graphql'; const DateTimeScalar = new GraphQLScalarType({ name: 'DateTime', description: 'ISO 8601日期时间字符串', serialize(value: Date) { return value.toISOString(); }, parseValue(value: string) { const date = new Date(value); if (isNaN(date.getTime())) { throw new Error(`无效的日期时间: ${value}`); } return date; }, parseLiteral(ast) { if (ast.kind === Kind.STRING) { return new Date(ast.value); } throw new Error('DateTime必须是字符串'); } }); ​

Email标量:验证邮箱格式:

const EmailScalar = new GraphQLScalarType({ name: 'Email', description: 'RFC 5322邮箱地址', serialize(value: string) { if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) { throw new Error(`无效的邮箱: ${value}`); } return value.toLowerCase(); }, parseValue(value: string) { if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) { throw new Error('无效的邮箱格式'); } return value.toLowerCase(); }, parseLiteral(ast) { if (ast.kind !== Kind.STRING) throw new Error('Email必须是字符串'); return ast.value.toLowerCase(); } }); ​

JSON标量:接受任意JSON值,适合灵活的数据结构:

const JSONScalar = new GraphQLScalarType({ name: 'JSON', description: '任意JSON值', serialize: (value) => JSON.stringify(value), parseValue: (value) => { if (typeof value === 'string') return JSON.parse(value); return value; }, parseLiteral: (ast) => { switch (ast.kind) { case Kind.INT: return parseInt(ast.value); case Kind.FLOAT: return parseFloat(ast.value); case Kind.STRING: return ast.value; case Kind.BOOLEAN: return ast.value; case Kind.OBJECT: return parseObject(ast); case Kind.LIST: return ast.values.map(v => JSONScalar.parseLiteral(v)); default: throw new Error(`JSON不支持类型: ${ast.kind}`); } } }); ​

自定义标量注册到Schema中:

const server = new ApolloServer({ typeDefs, resolvers, resolversComposition: { scalars: { DateTime: DateTimeScalar, Email: EmailScalar, JSON: JSONScalar } }, }); ​

自定义指令

指令是GraphQL的元编程机制--它在不改变字段定义的情况下,给字段的执行附加额外行为。除了内置的 @deprecated、@skip、@include,你可以定义自己的指令。

@auth指令:标记字段需要的权限级别:

directive @auth(requires: Role = USER) on FIELD_DEFINITION enum Role { USER MODERATOR ADMIN } type Query { publicPosts: [Post!]! myDrafts: [Post!]! @auth(requires: USER) allUsers: [User!]! @auth(requires: ADMIN) analytics: Dashboard! @auth(requires: ADMIN) } ​

指令的实现(使用 graphql-tools 的 schema directive visitor):

import { SchemaDirectiveVisitor } from '@graphql-tools/schema'; class AuthDirective extends SchemaDirectiveVisitor { visitFieldDefinition(field, details) { const { resolve } = field; const requiredRole = this.args.requires; field.resolve = async function (...args) { const context = args[2]; if (!context.user) { throw new AuthenticationError('未认证'); } if (context.user.role !== requiredRole) { throw new ForbiddenError(`需要 ${requiredRole} 权限`); } return resolve.apply(this, args); }; } } ​

@cacheControl指令:设置字段的缓存策略:

directive @cacheControl(maxAge: Int, scope: CacheControlScope = PUBLIC) on FIELD_DEFINITION | OBJECT | INTERFACE type Query { products: [Product!]! @cacheControl(maxAge: 300) # 缓存5分钟 user(id: ID!): User @cacheControl(maxAge: 60, scope: PRIVATE) # 缓存1分钟,按用户隔离 } ​

@upper指令:转换输出为大写:

class UpperDirective extends SchemaDirectiveVisitor { visitFieldDefinition(field) { const { resolve } = field; field.resolve = async function (...args) { const result = await resolve.apply(this, args); if (typeof result === 'string') return result.toUpperCase(); return result; }; } } ​

自定义指令让Schema的设计更加声明式--关注点(认证、缓存、格式转换)通过指令标注,而不是硬编码在解析器中。这提高了Schema的可读性和关注点分离。

Schema迁移的最佳实践

  1. 新增字段和类型:直接添加,无需任何额外步骤
  2. 修改字段类型(如 String → Int):先新增一个字段,逐步迁移客户端,再废弃旧字段
  3. 删除字段:先标记 @deprecated,监控使用量,待使用量降低后删除
  4. 重命名字段:新增新名字字段 + @deprecated 旧字段,逐步迁移
  5. 大规模变更:考虑使用联邦架构,在新子图中定义新Schema,逐步迁移

规则总结:永远不要直接删除或修改一个被客户端使用的字段。用“新增+废弃”的渐进迁移代替破坏性变更。

图:Schema渐进迁移路径

图:Schema渐进迁移路径

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