×
广告

【译】对比GraphQL与REST——两种HTTP API的差异

96
iamfanny
2017.07.11 22:36* 字数 3643

原文标题:GraphQL vs. REST Two ways to send data over HTTP: What’s the difference?
原文地址:https://dev-blog.apollodata.com/graphql-vs-rest-5d425123e34b
作者:Sashko Stubailo
翻译:Fanny

GraphQL目前被认为是革命性的API工具,因为它可以让客户端在请求中指定希望得到的数据,而不像传统的REST那样只能呆板地在服务端进行预定义。这样它就让前、后端团队的协作变得比以往更加的通畅,从而能够让组织更好地运作。而实际上,GraphQL与REST都是基于HTTP进行数据的请求与接收,而且GraphQL也内置了很多REST模型的元素在里面。

那么在技术层面上,GraphQL和REST这两种API模型到底有什么异同呢?我的观点是,他们归根到底其实没多大区别,只不过GraphQL做了一些小改进,使得开发体验产生了较大的改变。

我会从API的各个组件分别来讨论GraphQL和REST都分别是如何处理的。

资源(Resources)

REST的核心思想就是资源,每个资源都能用一个URL来表示,你能通过一个GET请求访问该URL从而获取该资源。根据当今大多数API的定义,你很有可能会得到一份JSON格式的数据响应,整个过程大概是这样:

GET /books/1
{
  "title": "Black Hole Blues",
  "author": { 
    "firstName": "Janna",
    "lastName": "Levin"
  }
  // ... more fields here
}

注:上面的例子里的"author"也会作为一个单独的资源在其他REST API中被用到

需要注意的是,在REST中,一个资源的种类与你获取它的方式是耦合的,比如上面这个例子中的API就可以称之为“book端点”(book endpoint)。

在这一点上GraphQL就大为不同,因为在GraphQL里这两个概念是完全分离的。比如说在你的schema定义中,你可能会有BookAuthor两个类型(type):

type Book {
  id: ID
  title: String
  published: Date
  price: String
  author: Author
}
type Author {
  id: ID
  firstName: String
  lastName: String
  books: [Book]
}

注意这里我们虽然定义了数据类型,但却不知道该如何获取这些数据。这是REST与GraphQL的一个核心差异:资源的描述信息与其获取方式相分离。

如果要去访问某个特定的book或者author资源,我们需要在schema中创建一个Query类型:

type Query {
  book(id: ID!): Book
  author(id: ID!): Author
}

然后我们就可以像REST那样发送请求了:

GET /graphql?query={ book(id: "1") { title, author { firstName } } }
{
  "title": "Black Hole Blues",
  "author": {
    "firstName": "Janna",
  }
}

虽然都是通过请求某个URL来得到相同的响应,但这里我们已经看到GraphQL与REST的差异之处了。

首先,我们看到GraphQL的URL请求里面指定了我们所需要的资源以及在该资源中我们所关心的字段。另外,我们是主动请求得到与book相关的author数据的,而不是服务端替我们决定的。

最重要的是,在请求中我们不需要关心资源的主键和资源之间的关系定义,我们可以通过除id以外的其他字段来获取到相同的Book资源。

小结

现在我们知道的异同点有:
相同点:都有资源这个概念,而且都能通过ID去获取资源。
相同点:都可以通过HTTP GET方式来获取资源
相同点:都可以使用JSON作为响应格式
差异点:在REST中,你所访问的路径就是该资源的唯一标识(ID);在GraphQL中,该标识与访问方式并不相关
差异点:在REST中,资源的返回结构与返回数量是由服务端决定;在GraphQL,服务端只负责定义哪些资源是可用的,由客户端自己决定需要得到什么资源

如果你已经用过GraphQL和REST,以上这些对你来说肯定相当简单。如果你之前没有用过GraphQL,你可以在到这里来实际体验一下。

路由(URL Route) vs. GraphQL Schema

一个具有可预见性的API才是好的API。因为你通常会把一个API当做程序的一部分来使用,所以你必须要知道它需要接收什么参数并预期能够获取到什么样的结果。

这时候,对API的访问描述信息就显得很重要。通常我们会通过阅读API文档来获取信息,但通过GraphQL的Introspection机制、以及Swagger这样的REST API工具,这些信息就能可以自动获取到。

如今的REST API通常会由一系列的URL端点组成:

GET /books/:id
GET /authors/:id
GET /books/:id/comments
POST /books/:id/comments

你可以把这种API的形态称之为线性结构——因为这就是一个列表嘛。当你要获取数据时,第一个事情就是搞清楚你要访问的是哪个端点。

而在GraphQL中——其实在上一节里你也看到了——可以通过查看GraphQL schema获得相关信息:

type Query {
  book(id: ID!): Book
  author(id: ID!): Author
}
type Mutation {
  addComment(input: AddCommentInput): Comment
}
type Book { ... }
type Author { ... }
type Comment { ... }
input AddCommentInput { ... }

REST会使用类似GET、POST这样的动词去请求相同的URL来表示这到底是一个读操作还是写操作,而GraphQL会使用不同的预定义类型:Mutation和Query。在GraphQL请求中,你可以通过不同的关键字进行不同的操作:

query { ... }
mutation { ... }

如果你想知道更多关于query的用法,请看我之前写的文章“The Anatomy of a GraphQL Query”.

这里的Query类型定义与上面的REST路由是完全契合的,同样表示了数据的访问入口,因此这是GraphQL中最能与REST的URL端点所对应的概念。

如果是对资源的简单查询,GraphQL与REST是类似的,都是通过指定资源的名称以及相关参数来取得,但不同的是,在GraphQL中,你可以根据资源之间的关联关系来发起一个复杂请求,而在REST中你只能定义一些特殊的URL参数来获取到特殊的响应,或者是通过发起多个请求、再自行把响应得到的数据进行组装才行。

小结

REST对数据的描述形式是一连串的URL端点,而GraphQL则是由相互之间有所关联的schema组成。
相同点:REST API的URL端点列表与GraphQL的Query/Mutation中的字段类似,都表示数据的访问入口。
相同点:都能用不同的方式描述一个API请求到底是读操作还是写操作。
差异点:GraphQL让你可以通过一个资源入口访问到关联的其他资源,只要事先在schema中定义好资源之间的关系即可;而REST则提供了多个URL端点来获取相关的资源。
差异点:在GraphQL中,Query类型可以在一个请求的根节点中被访问,除此以外它跟其他类型没有区别,比如你也可以对一个query中的字段添加参数。而在REST中,即使响应结果是嵌套关系,但在请求中并没有嵌套的概念。
差异点:REST使用POST这样的HTTP方法名称来定义写操作,GraphQL则是查询结构中的关键字。

正因为上述的第一个点,人们通常会把Query类型中的字段称为GraphQL中的“端点”或“查询条件”。虽然这是一个合理的解释,但同时也会对其他人造成误导,让人以为Query类型是一个非常特殊的类型。

路由处理器(Route Handlers)vs. 解析器(Resolvers)

想象一下,当你调用一个API的时候,实际上会发生什么事情?嗯,应该是在服务器上面执行了一些代码来处理这个请求,可能是进行了一些计算,可能从数据库中加载了一些数据,也可能是再次调用了一个别的API。虽然总的来说,作为调用方你并不需要知道内部发生了什么事情,不过由于REST和GraphQL都提供了标准的API实现方法,我们可以通过对比来感受一下两者之间的差异。

因为我比较熟悉JavaScript语言,所以在这个章节中我会使用它来做例子,但你也可以使用其他主流编程语言来实现REST或者GraphQL的API。为了突出重点,我会忽略掉一些构建服务用的过程代码。

首先使用Express实现一个hello world:

app.get('/hello', function (req, res) {
  res.send('Hello World!')
})

这里我们得到了一个可以返回“Hello World!”这个字符串的/hello端点。从这个例子我们可以看到一个REST API请求的的生命周期:

  1. 服务器收到请求并提取出HTTP方法名(比如这里就是GET方法)与URL路径
  2. API框架找到提前注册好的、请求路径与请求方法都匹配的代码
  3. 该段代码被执行,并得到相应结果
  4. API框架对结果进行序列化,添加上适当的状态码与响应头后,返回给客户端

GraphQL差不多也是这样工作的,我们来看下这个对应的hello world例子

const resolvers = {
  Query: {
    hello: () => {
      return 'Hello world!';
    },
  },
};

我们看到,这里并没有针对某个URL路径提供函数,而是把Query类型中的hello字段映射到一个函数上了。在GraphQL中这样的函数我们称之为解析器(Resolver)

然后我们就可以这样发起一个查询:

query {
  hello
}

至此,总结一下服务器对一个GraphQL请求的执行过程:

  1. 服务器收到HTTP请求,取出其中的GraphQL查询
  2. 遍历查询语句,调用里面每个字段所对应的Resolver。在这个例子里,只有Query这个类型中的一个字段hello
  3. Resolver函数被执行并返回相应结果
  4. GraphQL框架把结果根据查询语句的要求进行组装

因此我们将会得到如下响应:

{ "hello": "Hello, world!" }

这里有个小技巧:我们其实可以多次调用同一个Resolver:

query {
  hello
  secondHello: hello
}

在这个例子中的生命周期跟上面的是类似的,但因为我们通过别名来两次请求了同一个字段,所以对应Resolver函数hello也会被执行两次。虽然这个例子举得不是很好,不过这里主要想表达的是在一个请求中可以解析多个字段,即使是相同的字段也可以在查询的不同地方被多次访问。

再来看下“嵌套”解析器是怎样的:

{
  Query: {
    author: (root, { id }) => find(authors, { id: id }),
  },
  Author: {
    posts: (author) => filter(posts, { authorId: author.id }),
  },
}

这样的解析器可以处理如下查询请求:

query {
  author(id: 1) {
    firstName
    posts {
      title
    }
  }
}

即使解析器的结构是扁平的,但由于它们被不同的类型所引用,所以你还是可以利用它们来实现嵌套查询。想知道GraphQL如何执行请求,请进一步阅读这篇文章:“GraphQL Explained”

点击这里可以查看完整的例子并体验不同的查询效果


上图形象地说明了使用REST和GraphQL进行多种资源获取的方式的差异

小结

总的来说,REST和GraphQL都提供了很好的API调用方式。如果你对如何构建一个REST API足够熟悉,使用GraphQL来实现同样的API功能对你来说并不是一件难事。但GraphQL的一大优势是让你可以在不需要发起多次请求的情况下调用多个函数来获取资源数据。

相同点:REST的端点与GraphQL查询字段都在服务端调起函数执行。
相同点:REST和GraphQL都使用框架和类库来进行一些通用的网络协议处理。
差异点:一个REST请求对应一个路由处理器(Route Handler),而一个GraphQL的请求可以唤起多个解析器(Resolver)在一次响应中访问多种资源。
差异点:REST需要你自己构建整个请求的响应,而GraphQL的请求响应是由查询方指定结构、并由GraphQL进行构建组装的。

你可以把GraphQL理解为一个可以在一次请求中进行多个端点调用的系统,差不多算是REST的多路复用版。

综上所述

GraphQL里面还有很多东西由于篇幅限制这里并没有涉及,像对象识别、超媒体,以及缓存。这些话题以后有机会我们再来介绍。但我希望你通过本文对GraphQL有一个基本认识,知道它跟REST实际上是有很多概念上的相通。

我个人认为,GraphQL是有一些独特的优势的。特别是使用一系列小的解析器函数来构建一个完整的API这一点,实在是非常酷。这精简了不同场景下形态各异的API数量,并避免让API消费者取到对它来说并没有用的冗余数据。

但在另一方面,GraphQL还并不像REST那样有那么丰富的工具体系。比方说,你就不能像REST那样轻易地对HTTP结果进行缓存。不过目前GraphQL社区正在努力地丰富和完善这些工具和基础建设,就缓存这个例子,其实你也可以通过Apollo ClientRelay这样的工具去缓存GraphQL结果。

如果你对REST和GraphQL有更多的想法,请通过评论来告诉我。

声明:
本译文仅供个人研习、欣赏语言之用,谢绝任何转载及用于任何商业用途。本译文所涉法律后果均由本人承担。本人同意简书平台在接获有关著作权人的通知后,删除文章。

好文翻译
Web note ad 1