本文概述
你可能听说过这个街区的新孩子:GraphQL。如果不是这样, 那么简而言之, GraphQL是一种获取API的新方法, 是REST的替代方法。它最初是Facebook的一个内部项目, 自从开源以来, 它已经吸引了很多人。
本文的目的是帮助你轻松地从REST过渡到GraphQL, 无论你是否已经打算使用GraphQL还是愿意尝试一下。无需具备GraphQL的先验知识, 但需要对REST API有所了解才能理解本文。
本文的第一部分将首先给出三个我为何认为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端点。
使用这两种方法的第二种方法是在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, 但是它确实为真正的问题提供了有效的解决方案。它是相对较新的, 并且最佳实践仍在发展中, 但这绝对是我们将在未来几年听到的技术。
评论前必须登录!
注册