个性化阅读
专注于IT技术分析

GraphQL与REST-GraphQL教程

本文概述

你可能听说过这个街区的新孩子:GraphQL。如果不是这样, 那么简而言之, GraphQL是一种获取API的新方法, 是REST的替代方法。它最初是Facebook的一个内部项目, 自从开源以来, 它已经吸引了很多人。

本文的目的是帮助你轻松地从REST过渡到GraphQL, 无论你是否已经打算使用GraphQL还是愿意尝试一下。无需具备GraphQL的先验知识, 但需要对REST API有所了解才能理解本文。

GraphQL与REST-GraphQL教程

本文的第一部分将首先给出三个我为何认为GraphQL优于REST的原因。第二部分是有关如何在后端上添加GraphQL端点的教程。

Graphql与REST:为什么要删除REST?

如果你仍然对GraphQL是否适合你的需求犹豫不决, 请在此处给出” REST vs. GraphQL”的相当客观的概述。但是, 由于我使用GraphQL的三大原因, 请继续阅读。

原因1:网络性能

假设你在后端有一个用户资源, 其中包含名字, 姓氏, 电子邮件和其他10个字段。在客户端上, 你通常只需要几个。

在/ users端点上进行REST调用将返回用户的所有字段, 而客户端仅使用所需的字段。显然存在一些数据传输浪费, 这可能是移动客户端要考虑的问题。

GraphQL默认情况下会获取最小的数据。如果只需要用户的名字和姓氏, 请在查询中指定。

下面的接口称为GraphiQL, 它类似于GraphQL的API资源管理器。出于本文的目的, 我创建了一个小项目。该代码托管在GitHub上, 我们将在第二部分中深入研究它。

在界面的左窗格中是查询。在这里, 我们正在获取所有用户-我们将使用REST执行GET / users-并且仅获取其名字和姓氏。

询问

query {
  users {
    firstname
    lastname
  }
}

结果

{
  "data": {
    "users": [
      {
        "firstname": "John", "lastname": "Doe"
      }, {
        "firstname": "Alicia", "lastname": "Smith"
      }
    ]
  }
}

如果我们也想获取电子邮件, 则在”姓氏”下方添加”电子邮件”行即可解决问题。

某些REST后端确实提供了/ users?fields = firstname, lastname之类的选项以返回部分资源。物有所值, Google推荐。但是, 默认情况下未实现它, 它使请求几乎不可读, 尤其是当你使用其他查询参数时:

  • &status = active过滤活动用户
  • &sort = createdAat按用户创建日期对用户进行排序
  • &sortDirection = desc, 因为你显然需要它
  • &include = projects包含用户的项目

这些查询参数是添加到REST API的补丁程序, 用于模仿查询语言。 GraphQL首先是一种查询语言, 它使请求从一开始就简洁明了。

原因2:”包含与端点”设计选择

假设我们要构建一个简单的项目管理工具。我们有三个资源:用户, 项目和任务。我们还定义了资源之间的以下关系:

资源之间的关系

以下是我们向世界展示的一些端点:

终点 描述
GET /用户 列出所有用户
GET /用户/:ID 获取ID为:id的单个用户
GET /用户/:ID /项目 获取一个用户的所有项目

端点很简单, 易于阅读并且组织良好。

当我们的请求变得更加复杂时, 事情变得更加棘手。让我们以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与REST”的大多数辩论中, 人们认为他们必须选择两者之一。这是不正确的。

现代应用程序通常使用几种不同的服务, 这些服务公开了几种API。实际上, 我们可以将GraphQL视为所有这些服务的网关或包装。所有客户端都将命中GraphQL端点, 而该端点将命中数据库层, 诸如ElasticSearch或Sendgrid之类的外部服务或其他REST端点。

GraphQL与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中由强类型化的类型表示。你的模型和GraphQL类型之间应该存在一对一的映射。我们的用户类型为:

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中以”用户”类型请求项目字段时, 此联接将附加到数据库查询中。

最后, 是解析器:

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服务器 由Apollo团队创建的多合一GraphQL服务器。
其他平台的实现 Ruby, PHP等
客户端工具 描述
中继 用于将React与GraphQL连接的框架。
阿波罗客户。 一个带有React, Angular 2和其他前端框架绑定的GraphQL客户端。

总之, 我相信GraphQL不仅仅是炒作。它不会在明天取代REST, 但是它确实为真正的问题提供了有效的解决方案。它是相对较新的, 并且最佳实践仍在发展中, 但这绝对是我们将在未来几年听到的技术。

赞(0)
未经允许不得转载:srcmini » GraphQL与REST-GraphQL教程

评论 抢沙发

评论前必须登录!