Schema first 开发Graphql服务器的问题

“Schema优先”GraphQL服务器开发的问题

翻译:hanagm

原文链接

GraphQL服务器开发的工具在过去两年中爆炸式增长。我们认为对大多数工具的需求来自流行的Schema优先方法 - 并且可以通过替代方案解决:代码优先。

img

本文是由三部分组成的系列文章的第一部分:

在Twitter上关注我们,以便在即将发布的文章时收到通知。


本文概述了GraphQL服务器开发的当前状态。以下是所涵盖内容的快速概述:

  1. 本文中“schema优先”的含义是什么?
  2. GraphQL服务器开发的演变
  3. 分析SDL优先发展的问题
  4. 结论:SDL-first可能有用,但需要大量工具
  5. 代码优先:GraphQL服务器开发的一种语言惯用方式

虽然本文主要提供JavaScript生态系统的示例,但其中大部分也适用于其他语言生态系统中的GraphQL服务器开发。


schema优先这个术语非常模糊,总的来说传达了一个非常积极的想法:使schema设计成为开发过程中的优先事项。

在实现schema之前考虑模式(以及API)通常会产生更好的API设计。如果schema设计不尽如人意,那么最终会遇到API的风险,这是后端实现方式的结果,忽略了业务领域的原语和API使用者的需求。

在本文中,我们将讨论开发过程的缺点,其中GraphQL架构首先在SDL中手动定义,之后实现解析器。在这种方法中,SDL是API 的真实来源为了阐明schema优先设计与这种特定实现方法之间的区别,我们将从此处首先将其称为SDL。

相比之下,代码优先(有时也称为解析器优先)是一个以编程方式实现GraphQL架构的过程,而架构的SDL版本是生成的工件。使用代码优先,您仍然可以非常注意前期schema设计!


img

当GraphQL于2015年发布时,工具生态系统很少。在JavaScript中只有官方规范及其参考实现:graphql-js。直到今天,graphql-js也是用于最流行的GraphQL服务器,如apollo-serverexpress-graphqlgraphql-yoga

使用graphql-js构建GraphQL服务器时,GraphQL架构被定义为纯JavaScript对象:

Hello Wrold

const { 
  GraphQLSchema, 
  GraphQLObjectType, 
  GraphQLString 
} = require('graphql')

const schema = new GraphQLSchema({
  query: new GraphQLObjectType({
    name: 'Query',
    fields: {
      hello: {
        type: GraphQLString,
        args: {
          name: { type: GraphQLString }
        },
        resolve: (_, args) => `Hello ${args.name || 'World!'}`
      }
    }
  })
})

User Example

const { 
  GraphQLSchema, 
  GraphQLObjectType, 
  GraphQLString,
  GraphQLID
} = require('graphql')

const UserType = new GraphQLObjectType({
  name: 'User',
  fields: {
    id: { type: GraphQLID },
    name: { type: GraphQLString }
  }
})

const schema = new GraphQLSchema({
  query: new GraphQLObjectType({
    name: 'Query',
    fields: {
      user: {
        type: UserType,
        args: {
          id: { type: GraphQLID }
        },
        resolve: (_, { id }) => {
          return fetchUserById(id) // fetch the user, e.g. from a database
        }
      }
    }
  }),
  mutation: new GraphQLObjectType({
    name: 'Mutation',
    fields: {
      createUser: {
        type: UserType,
        args: {
          name: { type: GraphQLString }
        },
        resolve: (_, { name }) => {
          return createUser(name) // create a user, e.g. in a database
        }
      }
    }
  }),
})

从这些示例中可以看出,用于创建GraphQL模式的API graphql-js非常详细。模式的SDL表示更简洁,更容易掌握:

Hello World (SDL)

type Query {
  hello(name: String): String
}

User Example (SDL)

type User {
  id: ID
  name: String
}
type Query {
  user(id: ID): User
}
type Mutation {
  createUser(name: String): User
}

详细了解如何构建GraphQL模式与graphql-js文章。

为了简化开发并提高对实际API定义的可见性,Apollo graphql-tools于2016年3月开始构建库(是第一次提交)。

目标是将模式定义与实际实现分开,这导致了当前流行的模式驱动模式优先 / SDL优先开发过程:

  1. 在GraphQL SDL中手动编写GraphQL架构定义
  2. 实现所需的解析器功能

通过这种方法,上面的示例现在看起来像这样

Hello World

const { makeExecutableSchema } = require('graphql-tools')

const typeDefs = `
type Query {
  hello(name: String): String
}
`

const resolvers = {
  Query: {
    hello: (_, args) => `Hello ${args.name || 'World!'}`
  }
}

const schema = makeExecutableSchema({
  typeDefs,
  resolvers
})

User Example

const { makeExecutableSchema } = require('graphql-tools')

const typeDefs = `
type User {
  id: ID
  name: String
}
type Query {
  user(id: ID): User
}
type Mutation {
  createUser(name: String): User
}
`

const resolvers = {
  Query: {
    user: (_, args) => fetchUserById(args.id) // fetch the user, e.g. from a database
  },
  Mutation: {
    createUser: (_, args) => createUser(args.name) // create a user, e.g. in a database
  }
}

const schema = makeExecutableSchema({
  typeDefs,
  resolvers
})

这些代码片段与上面使用的代码完全等效graphql-js,除了它们更易读和更容易理解。

可读性不是SDL优先的唯一优势:

  • 这种方法易于理解非常适合快速构建
  • 由于每个新的API操作首先需要在模式定义中体现出来,因此GraphQL模式设计是经过深思熟虑的
  • 架构定义可以用作API文档
  • 模式定义可以作为前端和后端团队之间通信工具 - 前端开发人员获得授权并更多地参与API设计
  • 模式定义可以快速模拟API

虽然SDL-first具有许多优势,但过去两年表明将其扩展到更大的项目是一项挑战。在更复杂的环境中会出现许多问题(我们将在下一节中详细讨论这些问题)。

这些问题本身确实是可以解决的 - 实际问题是解决这些问题需要使用(并学习)许多其他工具。在过去的两年中,已经发布了大量的工具,这些工具正在尝试改进围绕SDL第一次开发的工作流程:从编辑器插件到CLI,再到语言库。

学习,管理和集成所有这些工具的开销减慢了开发人员的速度,并且很难跟上GraphQL生态系统的步伐。


现在让我们深入探讨SDL-first开发的问题领域。请注意,大多数这些问题特别适用于当前的JavaScript生态系统。

使用SDL优先,模式定义必须与解析器实现的确切结构相匹配。这意味着开发人员需要确保模式定义始终与解析器同步!

虽然这对于小型模式来说已经是一个挑战,但是随着模式增长到数百或数千行,实际上变得不可能(作为参考,GitHub GraphQL schema有超过10k行)。

工具/解决方案:有一些工具可以帮助保持模式定义和解析器同步。例如,通过使用像graphqlgen或等库来生成代码graphql-code-generator

不要以为这是一个问题?从graphqlgen 文章中获取我们的测验。

编写大型GraphQL模式时,通常不希望所有GraphQL类型定义都驻留在同一文件中。相反,您希望将它们分成更小的部分(例如,根据功能产品)。

image-20190308140805155

工具/解决方案:graphql-import或最近的graphql-modules这样的工具可以帮助解决。graphql-import使用自定义导入语法编写为SDL注释。graphql-modules是一个工具集,可以帮助解决模式分离解析程序组合以及GraphQL服务器的可伸缩结构的实现。

另一个问题是如何重用 SDL定义。此问题的一个常见示例是中继式连接。虽然提供了一种实现分页的强大方法,但它们需要大量的样板和重复的代码。

目前没有工具可以帮助解决这个问题。开发人员可以编写自定义工具来减少重复代码的需要,但此问题目前缺乏通用解决方案。

GraphQL架构基于强类型系统,在开发过程中可以带来巨大的好处,因为它允许对代码进行静态分析。遗憾的是,SDL通常在程序中表示为纯字符串,这意味着工具无法识别其中的任何结构。

接下来的问题是如何在编辑器工作流中利用GraphQL类型,以便从SDL代码的自动完成和构建时错误检查等功能中受益。

工具/解决方案:graphql-tag库暴露gql,有将一个GraphQL字符串转换成一个AST的功能,因此能够静态分析和更多特点。除此之外,还有各种编辑器插件,例如用于VS Code 的GraphQLApollo GraphQL插件。

模块化架构的想法也引出了另一个问题:如何集合许多现有的(分布式)架构成一个模式。

工具/解决方案:最常用的模式组合方法是模式拼接,它也是上述graphql-tools库的一部分。为了更好地控制模式的组成方式,您还可以直接使用模式委派(它是模式拼接的子集)。

在探索了解决它们的问题领域和各种工具之后,似乎SDL第一次开发最终可以发挥作用 - 而且它还要求开发人员学习和使用大量其他工具。

image-20190308141238294

在Prisma,我们在推动GraphQL生态系统发展方面发挥了重要作用。许多提到的工具都是由我们的工程师和社区成员构建的。

img

经过几个月的开发和与GraphQL社区的密切互动,我们逐渐意识到我们只是在修复症状。这就像攻打九头蛇Boss - 解决一个问题导致一些新的问题。

我们非常感谢Apollo的朋友们的工作,他们不断致力于改进围绕SDL开发的开发工作流程。

另一个以SDL优先构建GraphQL服务器的流行示例是AWS AppSync。它与Apollo模型略有不同,因为解析器(通常)不是以编程方式实现,而是从模式定义自动生成。

虽然社区从这么多工具中获益匪浅,但是当开发人员需要对某个组织的工具链进行全面赌注时,存在生态系统锁定的风险。

SDL优先的另一个问题是,无论使用哪种编程语言,它都会通过强加类似的原则来忽略编程语言的各个特征。

代码优先方法在其他语言中运行得非常好:Scala库sangria-graphql利用Scala强大的类型系统来优雅地构建GraphQL模式,graphlq-ruby使用Ruby语言的许多令人敬畏的DSL功能。


大多数SDL优先问题来自于我们需要将手动编写的SDL模式映射到编程语言。此映射是导致需要其他工具的原因。如果我们遵循SDL优先路径,则需要为每个语言生态系统重新创建所需的工具,并且每个语言生态系统的外观也不同。

我们应该努力寻求一种更简单的开发模型,而不是使用更多工具来增加GraphQL服务器开发的复杂性。理想情况下,它允许开发人员利用他们已经使用的编程语言 - 这就是代码优先的想法。

还记得定义架构的最初示例graphql-js吗?这是代码优先意味着的本质。没有手动维护的模式定义版本,而是从实现模式的代码生成 SDL 。

虽然 graphql-jsAPI非常冗长,但是其他语言中有许多流行的框架基于代码优先方法工作,例如已经提到的,以及Python或Elixir。graphlq-ruby sangria-graphqlgrapheneabsinthe-graphql

image-20190308141926516

虽然本文主要是关于首先理解SDL的问题,但是对于构建具有代码优先框架的GraphQL架构的内容来说,这是一个小小的片段:

Hello World

const Query = objectType('Query', t => {
  t.string('hello', {
    args: { name: stringArg() },
    resolve: (_, { name }) => `Hello ${name || `World`}!`,
  })
})

const schema = makeSchema({
  types: [Query],
  outputs: {
    schema: './schema.graphql'),
    typegen: './typegen.ts',
  },
})

User Example

const User = objectType('User', t => {
  t.id('id')
  t.string('name', { nullable: true })
})

const Query = objectType('Query', t => {
  t.field('user', 'User', {
    args: { id: idArg() },
    resolve: (_, { id }) => fetchUserById(id) // fetch the user, e.g. from a database
  })
})

const Mutation = objectType('Query', t => {
  t.field('createUser', 'User', {
    args: { name: stringArg() },
    resolve: (_, { name }) => createUser(name) // create a user, e.g. in a database
  })
})

const schema = makeSchema({
  types: [User, Query, Mutation],
  outputs: {
    schema: './schema.graphql'),
    typegen: './typegen.ts',
  },
})

通过这种方法,您可以直接在TypeScript / JavaScript中定义GraphQL类型。通过正确的设置并且由于智能代码补全,您的编辑人员将能够在您定义时建议可用的GraphQL类型,字段和参数。

典型的编辑器工作流程包括在后台运行的开发服务器,可在保存文件时重新生成类型。

一旦定义了所有GraphQL类型,就会将它们传递给函数以创建GraphQLSchema可在GraphQL服务器中使用的实例。通过指定ouputs,您可以定义生成的SDL和打字所在的位置。

本系列文章的下一部分将更详细地讨论代码优先开发。

之前我们列举了SDL优先开发的好处。事实上,在使用代码优先方法时,没有必要在大多数方面妥协。

使用GraphQL架构作为前端和后端团队的关键通信工具的最重要的好处仍然存在。

以GitHub GraphQL API为例:GitHub使用Ruby和代码优先的方法来实现他们的API。SDL架构定义是基于实现API的代码生成的。但是,架构定义仍会接入版本控制。这使得在开发过程中跟踪API的更改非常容易,并且可以改善各个团队之间的通信。

API文档或授权前端开发人员等其他好处也不会因代码优先方法而丢失。

这篇文章相当理论化,并没有包含太多代码 - 我们仍然希望能引起您对代码优先开发的兴趣。要查看更多实际示例并了解有关代码优先开发体验的更多信息,请继续关注并在接下来的几天内关注Prisma Twitter帐户 👀

你觉得这篇文章怎么样?加入Prisma Wechat Group,与其他GraphQL爱好者一起讨论SDL优先和代码优先开发。

返回