GraphQL 与 REST - GraphQL 教程
已发表: 2022-03-11你可能听说过这个街区的新孩子:GraphQL。 如果不是,GraphQL 是一种获取 API 的新方法,是 REST 的替代方案。 它最初是 Facebook 的一个内部项目,由于它是开源的,因此获得了很大的关注。
本文的目的是帮助您轻松地从 REST 过渡到 GraphQL,无论您是否已经决定使用 GraphQL,或者您只是愿意尝试一下。 不需要 GraphQL 的先验知识,但需要熟悉 REST API 才能理解本文。
文章的第一部分将首先给出我个人认为 GraphQL 优于 REST 的三个原因。 第二部分是关于如何在后端添加 GraphQL 端点的教程。
Graphql 与 REST:为什么要放弃 REST?
如果您仍然对 GraphQL 是否适合您的需求犹豫不决,这里给出了“REST 与 GraphQL”的相当广泛和客观的概述。 但是,对于我使用 GraphQL 的三大理由,请继续阅读。
原因一:网络性能
假设您在后端有一个用户资源,其中包含名字、姓氏、电子邮件和其他 10 个字段。 在客户端上,您通常只需要其中的几个。
在/users
端点上进行 REST 调用会返回用户的所有字段,客户端只使用它需要的字段。 显然存在一些数据传输浪费,这可能是移动客户端的考虑因素。
默认情况下,GraphQL 会获取尽可能少的数据。 如果您只需要用户的名字和姓氏,请在查询中指定。
下面的接口叫做 GraphiQL,它就像一个 GraphQL 的 API 浏览器。 为了本文的目的,我创建了一个小项目。 代码托管在 GitHub 上,我们将在第二部分深入研究。
在界面的左侧窗格中是查询。 在这里,我们正在获取所有用户——我们将使用 REST GET /users
users——并且只获取他们的名字和姓氏。
询问
query { users { firstname lastname } }
结果
{ "data": { "users": [ { "firstname": "John", "lastname": "Doe" }, { "firstname": "Alicia", "lastname": "Smith" } ] } }
如果我们也想获取电子邮件,在“姓氏”下方添加“电子邮件”行就可以了。
一些 REST 后端确实提供像/users?fields=firstname,lastname
这样的选项来返回部分资源。 对于它的价值,谷歌推荐它。 但是,默认情况下它没有实现,并且它使请求几乎不可读,尤其是当您折腾其他查询参数时:
-
&status=active
过滤活跃用户 &sort=createdAat
按创建日期对用户进行排序&sortDirection=desc
因为你显然需要它&include=projects
包含用户的项目
这些查询参数是添加到 REST API 以模仿查询语言的补丁。 GraphQL 首先是一种查询语言,它使请求从一开始就简洁准确。
原因 2:“包含与端点”设计选择
假设我们要构建一个简单的项目管理工具。 我们拥有三种资源:用户、项目和任务。 我们还定义了资源之间的以下关系:
以下是我们向世界公开的一些端点:
端点 | 描述 |
---|---|
GET /users | 列出所有用户 |
GET /users/:id | 获取具有 id 的单个用户:id |
GET /users/:id/projects | 获取一个用户的所有项目 |
端点简单、易读且组织良好。
当我们的请求变得更加复杂时,事情就会变得更加棘手。 让我们以GET /users/:id/projects
端点为例:假设我只想在主页上显示项目的标题,但在仪表板上显示项目+任务,而不进行多次 REST 调用。 我会打电话给:
-
GET /users/:id/projects
。 - 在仪表板页面上
GET /users/:id/projects?include=tasks
(例如),以便后端附加所有相关任务。
通常的做法是添加查询参数?include=...
以使其工作,甚至被 JSON API 规范推荐。 像?include=tasks
这样的查询参数仍然是可读的,但不久之后,我们将得到?include=tasks,tasks.owner,tasks.comments,tasks.comments.author
。
在这种情况下,创建/projects
端点来执行此操作是否更明智? /projects?userId=:id&include=tasks
之类的东西,因为我们会少包含一层关系? 或者,实际上, /tasks?userId=:id
端点也可以工作。 这可能是一个困难的设计选择,如果我们有一个多对多的关系,那就更复杂了。
GraphQL 在任何地方都使用include
方法。 这使得获取关系的语法强大且一致。
这是一个从 id 为 1 的用户那里获取所有项目和任务的示例。
询问
{ user(id: 1) { projects { name tasks { description } } } }
结果
{ "data": { "user": { "projects": [ { "name": "Migrate from REST to GraphQL", "tasks": [ { "description": "Read tutorial" }, { "description": "Start coding" } ] }, { "name": "Create a blog", "tasks": [ { "description": "Write draft of article" }, { "description": "Set up blog platform" } ] } ] } } }
如您所见,查询语法很容易阅读。 如果我们想更深入地包括任务、评论、图片和作者,我们不会三思而后行如何组织我们的 API。 GraphQL 使获取复杂对象变得容易。
原因3:管理不同类型的客户
在构建后端时,我们总是首先尝试使 API 尽可能地被所有客户端广泛使用。 然而,客户总是希望少打电话,多取钱。 通过深度包含、部分资源和过滤,Web 和移动客户端发出的请求可能彼此之间存在很大差异。
对于 REST,有几个解决方案。 我们可以创建自定义端点(即别名端点,例如/mobile_user
)、自定义表示( Content-Type: application/vnd.rest-app-example.com+v1+mobile+json
),甚至是客户端- 特定的 API(就像 Netflix 曾经做过的那样)。 这三个都需要后端开发团队付出额外的努力。
GraphQL 为客户端提供了更多的权力。 如果客户端需要复杂的请求,它会自己构建相应的查询。 每个客户端可以以不同的方式使用相同的 API。
如何开始使用 GraphQL
在当今大多数关于“GraphQL vs. REST”的辩论中,人们认为他们必须选择两者之一。 这是不正确的。
现代应用程序通常使用几种不同的服务,这些服务公开了几个 API。 我们实际上可以将 GraphQL 视为所有这些服务的网关或包装器。 所有客户端都将访问 GraphQL 端点,而该端点将访问数据库层、外部服务(如 ElasticSearch 或 Sendgrid)或其他 REST 端点。
使用两者的第二种方法是在您的 REST API 上有一个单独的/graphql
端点。 如果您已经有大量客户端访问您的 REST API,但您想在不损害现有基础设施的情况下尝试 GraphQL,这将特别有用。 这就是我们今天正在探索的解决方案。
如前所述,我将通过 GitHub 上提供的一个小示例项目来说明本教程。 它是一个简化的项目管理工具,包含用户、项目和任务。
该项目使用的技术是用于 Web 服务器的 Node.js 和 Express,作为关系数据库的 SQLite,以及作为 ORM 的 Sequelize。 三个模型(用户、项目和任务)在models
文件夹中定义。 REST 端点/api/users
、 /api/projects
和/api/tasks
向世界公开,并在rest
文件夹中定义。
请注意,GraphQL 可以使用任何编程语言安装在任何类型的后端和数据库上。 选择这里使用的技术是为了简单和可读性。
我们的目标是在不删除 REST 端点的情况下创建/graphql
端点。 GraphQL 端点将直接访问数据库 ORM 以获取数据,因此它完全独立于 REST 逻辑。
类型
数据模型在 GraphQL 中由types表示,它们是强类型的。 您的模型和 GraphQL 类型之间应该存在一对一的映射。 我们的User
类型将是:
type User { id: ID! # The "!" means required firstname: String lastname: String email: String projects: [Project] # Project is another GraphQL type }
查询
查询定义了可以在 GraphQL API 上运行的查询。 按照惯例,应该有一个RootQuery
,其中包含所有现有的查询。 我还指出了每个查询的 REST 等价物:

type RootQuery { user(id: ID): User # Corresponds to GET /api/users/:id users: [User] # Corresponds to GET /api/users project(id: ID!): Project # Corresponds to GET /api/projects/:id projects: [Project] # Corresponds to GET /api/projects task(id: ID!): Task # Corresponds to GET /api/tasks/:id tasks: [Task] # Corresponds to GET /api/tasks }
突变
如果查询是GET
请求,突变可以被视为POST
/ PATCH
/ PUT
/ DELETE
请求(尽管它们实际上是查询的同步版本)。
按照惯例,我们将所有的突变放在一个RootMutation
中:
type RootMutation { createUser(input: UserInput!): User # Corresponds to POST /api/users updateUser(id: ID!, input: UserInput!): User # Corresponds to PATCH /api/users removeUser(id: ID!): User # Corresponds to DELETE /api/users createProject(input: ProjectInput!): Project updateProject(id: ID!, input: ProjectInput!): Project removeProject(id: ID!): Project createTask(input: TaskInput!): Task updateTask(id: ID!, input: TaskInput!): Task removeTask(id: ID!): Task }
请注意,我们在这里引入了新类型,称为UserInput
、 ProjectInput
和TaskInput
。 这也是 REST 的常见做法,用于创建用于创建和更新资源的输入数据模型。 在这里,我们的UserInput
类型是没有id
和projects
字段的User
类型,请注意关键字input
而不是type
:
input UserInput { firstname: String lastname: String email: String }
架构
通过类型、查询和突变,我们定义了GraphQL 模式,这是 GraphQL 端点向世界公开的内容:
schema { query: RootQuery mutation: RootMutation }
这个模式是强类型的,这让我们能够在 GraphiQL 中拥有那些方便的自动完成功能。
解析器
现在我们有了公共模式,是时候告诉 GraphQL 在请求每个查询/突变时要做什么了。 解决者做艰苦的工作; 例如,他们可以:
- 命中内部 REST 端点
- 调用微服务
- 命中数据库层进行 CRUD 操作
我们在示例应用程序中选择第三个选项。 让我们看看我们的解析器文件:
const models = sequelize.models; RootQuery: { user (root, { id }, context) { return models.User.findById(id, context); }, users (root, args, context) { return models.User.findAll({}, context); }, // Resolvers for Project and Task go here }, /* For reminder, our RootQuery type was: type RootQuery { user(id: ID): User users: [User] # Other queries }
这意味着,如果在 GraphQL 上请求user(id: ID!)
查询,那么我们从数据库返回User.findById()
,这是一个 Sequelize ORM 函数。
在请求中加入其他模型怎么样? 好吧,我们需要定义更多的解析器:
User: { projects (user) { return user.getProjects(); // getProjects is a function managed by Sequelize ORM } }, /* For reminder, our User type was: type User { projects: [Project] # We defined a resolver above for this field # ...other fields } */
因此,当我们在 GraphQL 中请求User
类型中的projects
字段时,此连接将附加到数据库查询中。
最后,突变解析器:
RootMutation: { createUser (root, { input }, context) { return models.User.create(input, context); }, updateUser (root, { id, input }, context) { return models.User.update(input, { ...context, where: { id } }); }, removeUser (root, { id }, context) { return models.User.destroy(input, { ...context, where: { id } }); }, // ... Resolvers for Project and Task go here }
你可以在这里玩这个。 为了保持服务器上的数据干净,我禁用了突变解析器,这意味着突变不会在数据库中执行任何创建、更新或删除操作(因此在界面上返回null
)。
询问
query getUserWithProjects { user(id: 2) { firstname lastname projects { name tasks { description } } } } mutation createProject { createProject(input: {name: "New Project", UserId: 2}) { id name } }
结果
{ "data": { "user": { "firstname": "Alicia", "lastname": "Smith", "projects": [ { "name": "Email Marketing Campaign", "tasks": [ { "description": "Get list of users" }, { "description": "Write email template" } ] }, { "name": "Hire new developer", "tasks": [ { "description": "Find candidates" }, { "description": "Prepare interview" } ] } ] } } }
为现有应用重写所有类型、查询和解析器可能需要一些时间。 但是,有很多工具可以帮助您。 例如,有一些工具可以将 SQL 模式转换为 GraphQL 模式,包括解析器!
把所有东西放在一起
使用定义明确的模式和解析器对模式的每个查询执行什么操作,我们可以在后端挂载一个/graphql
端点:
// Mount GraphQL on /graphql const schema = makeExecutableSchema({ typeDefs, // Our RootQuery and RootMutation schema resolvers: resolvers() // Our resolvers }); app.use('/graphql', graphqlExpress({ schema }));
我们可以在后端拥有一个漂亮的 GraphiQL 界面。 要在没有 GraphiQL 的情况下发出请求,只需复制请求的 URL,然后使用 cURL、AJAX 或直接在浏览器中运行它。 当然,有一些 GraphQL 客户端可以帮助您构建这些查询。 请参阅下面的一些示例。
下一步是什么?
本文的目的是让您了解 GraphQL 的样子,并向您展示在不丢弃 REST 基础架构的情况下尝试 GraphQL 绝对是可能的。 了解 GraphQL 是否适合您的需求的最佳方法是亲自尝试。 我希望这篇文章能让你潜水。
有很多我们没有在本文中讨论的特性,例如实时更新、服务器端批处理、身份验证、授权、客户端缓存、文件上传等。了解这些特性的绝佳资源是如何使用 GraphQL。
以下是一些其他有用的资源:
服务器端工具 | 描述 |
---|---|
graphql-js | GraphQL 的参考实现。 您可以将它与express-graphql 一起使用来创建服务器。 |
graphql-server | 由 Apollo 团队创建的一体化 GraphQL 服务器。 |
其他平台的实现 | 红宝石、PHP 等 |
客户端工具 | 描述 |
---|---|
中继 | 用于连接 React 和 GraphQL 的框架。 |
阿波罗客户端。 | 一个 GraphQL 客户端,绑定了 React、Angular 2 和其他前端框架。 |
总之,我相信 GraphQL 不仅仅是炒作。 它明天还不会取代 REST,但它确实为真正的问题提供了一个高性能的解决方案。 它相对较新,最佳实践仍在发展中,但它绝对是我们将在未来几年内听到的一项技术。