查看原文
其他

解读GraphQL|洞见

2017-03-23 王亦凡 思特沃克

今天我们解读一下2016年11月期技术雷达中的GraphQL,它位于语言象限,处于评估阶段,编号整100,非常方便查找……这项技术比较有意思。对我来说,技术雷达中通常有两种典型技术:

  • 第一种,像Apache Kafka这样的,一看就感觉很牛,然后哇地赞叹一下,但因为离项目场景太远,大概看看热闹就过去了。

  • 第二种,典型就是ECMAScript 2017这种,早晚要用并且应用广泛,但说真的,好像也没啥可介绍的……

GraphQL和这两种都不太一样——它用来构建我们Web前端/移动客户端的API,这个覆盖面就广泛了。不管你是前端/后端/还是移动端开发,都跑不了和API打交道。然而,GraphQL本身非常激进,和我们现在的API形式大相径庭,足够搞个大新闻了。废话不多说,我们进入正题吧。

 1

   


GraphQL是什么? 

GraphQL是Facebook推出的一个查询语言,可能和听起来不同,它并不是一个数据库查询语言,而是你可以用任何其他语言实现的一个用于查询的抽象层

(图片来自:http://t.cn/R6q5Bol)

通常你可以通过GraphQL让你的客户端请求有权决定获取的数据结构,也可以通过GraphQL获得更好的多版本API兼容性。并且与大多数查询语言不同的是,GraphQL是一个静态类型的查询语言,这意味着你可以通过GraphQL获得更强大更安全的开发体验。

Facebook自2012年就已经在移动端上使用GraphQL了,只是去年才将其开源。除了GraphQL之外,市面上也有许多类似的方案:比如Netflix的Falcor,以及ClojureScript编写的om.next,还有om.next的灵感来源Datomic等等。

让我们先来看个例子:

{
 user(id: 3500401)
 {
   id,
   name,
   isViewerFriend,
   profilePicture(size: 50)
   {
     uri,
     width,
     height
   }
 }
}

这条查询很直接——它请求了某个ID下面的id、name以及isViewer状态,同时还请求了她特定尺寸的头像和头像的信息。GraphQL的query很像JSON,而JSON也是我们实现GraphQL的返回值:

{
 "user": 3500401,
 "name": "Jing Chen",
 "isViewerFriend": true,
 "profilePicture":
 {
   "url": "http://someurl.cdn/pic.jpg",
   "width": 50,
   "height": 50
 }
}

这很符合直觉:你发出去的请求中的结构基本就是你将要获得的JSON,并且你也可以像函数一样传参来影响返回值。此外GraphQL并不关心传输协议,你可以将GraphQL放在Get请求的URL里,或者任何你能想到的方式:查询实质上只是普通字符串

 2

   


为什么要选择GraphQL? 

没听说过GraphQL的话可能看到上面的解释还是不清楚,虽然这东西看起来很神奇,但是它是干什么用的?用在哪?好吧,那现在告诉你,GraphQL的核心目标就是取代RESTful API

REST是一种古老的面向服务端和客户端(CS)的架构风格,并不是一项特定的技术。它定义了一系列严格的构建API的原则,用简单的方式描述资源,并认为大部分时候违背这些原则会让软件的扩展性受限。

(图片来自:http://t.cn/R6q5Bol)

随着服务端SOA和客户端Ajax的崛起,通信扩展问题变得越来越重要,REST得以广泛被运用。在MicroService逐渐流行的今天,RESTful API已经成为主流。如今,随着前端和移动端的迅猛发展,REST也面临严峻挑战。

 3

   


REST有什么问题? 

REST本身作为一种对资源的建模,它的扩展性其实并无特殊问题。


我们对于REST的指责经常并不来源于它本身,而来自于它不能解决的问题:诸如性能优化和页面展示的资源分类等等。



我们可以列举REST问题的几个表现——之所以用“表现”来形容,是因为它们都指向同一个问题——在为客户端实现RESTful API过程中性能、页面等等导致的折中设计和REST本身可扩展性之间不可调和的矛盾。注意,以下问题主要针对Web前端/移动端的API,并非Service之间的API——问题来自视图和网络速度。

 3.1 

   

资源分类导致性能受限

在前端我们很少遇到运行效率问题,效率问题主要来自网络请求——一次HTTP请求的代价非常高昂,特别是对移动端来说。如果我们遵循REST的风格,我们就要将各种资源分门别类用不同的API来表示。

而在客户端中我们经常需要一次请求多种资源。这时候我们就要编写许多API来为不同的页面合并这些API。很多时候,我们写的这些API并不是一个特定资源,但我们还得用URL来表示它们。

(图片来自:http://t.cn/RGdbRgV)

此外,当我们选择不合并资源时,性能损耗经常比我们想象的严重:如果合并完全无关的资源,不合并时也只是两个并发的请求,返回的时间大多只取决于更慢的API。而更常见的情况是,资源之间有映射关系:比如我们经常要先请求某个user信息,然后等这个API返回之后再渲染这个user名下的articles。

于是我们再去请求不同的article。这时候,我们发现请求之间甚至不是并行的,而是串行的。而我们现实中的前端应用,因为视图设计中常见层叠的资源,所以也经常会有这种多次串行的请求,这会导致我们的网络请求时间成倍增长。

 3.2 

   

在现代场景中难于维护

虽然REST的目标是易于维护和扩展,但在Web前端/客户端领域,它的表现并没有想象得那么好。我们经常说最明显的Code smell就是重复。许多时候,我们要让API适应视图,但我们都知道,这种API仅被客户端消费,与服务端代码耦合是非常不合理的。

随着前端/移动端的兴起,我们经常还要为多种客户端编写多种API。这些API代码既类似又无聊,并且也要在客户端修改时一起修改——仅前端和后端的重复我们可以让IDE查找,然而这种散落在前后端的契约则很容易遗漏。不仅如此,如果这个API是Public API,一点小改动就要修改版本。

 3.3 

   

缺乏约束

RESTful API通过URL表述资源,它本身是无类型的。现在,随着技术的发展,我们已经有许多非常强大的静态类型语言,它们有非常强大的开发工具来帮助我们检查错误。而在我们系统的API边界,这些重量级的强大工具却无能为力。

(图片来自:http://t.cn/R6qtVH1)

随着Micro service越来越流行、系统中的边界越来越多,静态类型能捕获的错误则越来越少。有时候我们用Scala编写的应用在遭遇API错误时不能不说是一种讽刺……

应对这种情形,我们则要花费额外的努力来维护契约测试,还要小心翼翼地对应Service之间的版本依赖,因为对REST来说,不同版本之间的兼容能力非常弱小。

 3.4 

   

严格,抽象,但并不能解决客户端问题

我们经常可以在网上看到互相指责的文章和讨论,基本上都是一方列举出自己使用RESTful API遇到的实际问题,而另一方认为前者实际应用违反了哪条REST原则,因此不是RESTful API。

这种情形十分典型,后者说的也正确,但这并没有实际帮助——付出高昂的性能和开发代价来维护严格的RESTful是不现实的。此外,在实现之前反被要求反复思考资源之间的关系的方式也不够敏捷。

更糟的是,我们都很难简短准确地跟他人解释REST到底是什么,我没看过Roy Fielding的论文,其实我自己也不清楚。这类似Java大行其道时的设计模式:有用,但太玄学,用起来也不自然,最关键的是不能被代码有效地抽象。

(图片来自:http://t.cn/RL0olle)

然而最终我们发现大部分设计模式在某些语言里要么被彻底消灭,要么就变得很自然,让你感觉不到了。而解决REST问题可能也类似:不再纠结教条,彻底换一种思路。

 4

   


GraphQL好在哪? 

比起苍白的讨论而言,直接使用它可能更有说服力。几个月前,Github宣布他们打算拥抱GraphQL:


We’ve often heard that our REST API was an inspiration for other companies; countless tutorials refer to our endpoints. Today, we’re excited to announce our biggest change to the API since we snubbed XML in favor of JSON: we’re making the GitHub API available through GraphQL.



我们可以试用Github放出的GraphQL API,虽然这个API还在Early Access阶段,但我们可以用它直接探索GraphQL和我们熟悉的API查询方式的区别。Github在他们的网页里内嵌了一个GraphiQL——Facebook提供的GraphQL开发工具,它本质上是一个React组件,通过它,我们可以不构建代码直接阅读查询文档,调试我们的查询。(注:使用需要登录Github并授权)

1.需求驱动

我们可以先在GraphiQL里尝试在左边输入下面这段查询,按Ctrl Enter就可以在右边得到自己的名字和公司了:

{
viewer
{
name
company
}
}

如果观察Network请求的话会发现,无论进行什么查询,请求都是指向同一个endpoint的POST请求。你也可以加上其他字段,或者删掉字段试试看结果会怎样。


GraphQL用来构建客户端API,但它并不关心视图,也不关心服务的到底是什么客户端。



至于请求什么数据,数据怎么组织,全都是客户端说了算——这也是为什么要实现一个查询语言的原因:有了查询语言,你就可以精确描述你想要的了,移动端可能只获取文章标题,而Web端则希望可以预览部分内容。这被Facebook描述为Demand Driven。

除了减少构建无聊CRUD API之外,另一个明显的好处是,对于大部分前后端分离的项目,客户端开发人员可以独立修改页面的展现形式。对于经常需要探索创意的创业公司来说,这降低了迭代成本,而对于前后端分离的大型项目而言,则减少了沟通成本。

2.一次请求复杂数据

这次我们一次请求多种资源,大意就是同时查询你前10个followers的名字,和他们前5个repository的名字(要是觉得还不复杂,你可以嵌套地查repository的follower的repository的follower……这样循环下去):

{
 viewer
 {
   followers(first: 10)
   {
     edges
     {
       node
       {
         name
         repositories(first: 5)
         {
           edges
           {
             node
             {
               name
             }
           }
         }
       }
     }
   }
 }
}

上面提到了传统方式会导致串行请求,这对性能的损耗是十分严重的。而GraphQL的意思,顾名思义——就是图查询语言

不同于平常的请求,实现GraphQL的服务端接收到请求后,虽然还是HTTP的一次请求,但是会根据查询的结构递归地根据查询来调用各项资源的Resolver(可以不太恰当地类比为Controller action),最后拼回一张JSON Graph返回给客户端。因此你可以在一次查询中轻松表述诸如“表弟的七大姑的二侄子的小姨子叫啥来着,多大岁数,有没有对象”这种复杂的关系。

3.静态类型

可能你已经注意到了,你输入的查询都有自动补全和类型纠错功能,这归功于GraphQL的静态类型系统。你可以在定义GraphQL Schema时添加更多的类型来描述不同的资源。在GraphiQL的右边有个“Docs”面板,点开可以看到各种类型的签名和描述,每种类型可以继续点击查看详情。你可以在完全没文档的情况下,仅通过它很快理解所有API。

其实这个“Docs”并不用手动编写,它完全根据服务端代码自动生成。而这个面板本身信息的来源,也只不过是GraphQL查询本身,这被Facebook称为自省(Introspection)。你可以打开Network,刷新页面查看GraphiQL是如何查询所有类型信息的。比如我们可以尝试:

{
 __type(name: "Repository")
 {
   description
 }
 __schema
 {
   types
   {
     name
   }
 }
}

这样就会返回Repository这个type到底是个啥,以及我想知道服务端所有的type。

对于编程语言来说,拥有强大的静态类型是很常见的。对于查询语言,却不是很多见。在这点上GraphQL有点像RPC,可以生成GraphQL Schema来作为服务端和客户端间的契约。一般这个过程也会自动生成JSON schema来方便你做其他的契约测试。Intellij IDEA还有一个非常完善的GraphQL插件,当你服务端Schema有Breaking Change时,客户端代码就会报错,有些编译插件也会产生编译时错误。

4.兼容多版本

由于客户端可以决定请求的内容,服务端也可以不删除废弃的字段,而仅仅加入@Deprecated注解,这样客户端查询时只会被Warning。

这样做的结果就是不同客户端和不同Service之间的版本依赖也变得非常宽松了(注:这段代码是用来在服务端定义Schema而不是查询的,所以不能在GraphiQL中用):

type Film
{
  title: String
  episode: Int
  releaseDate: String
 openingCrawl: String
 director: String @deprecated
 directedBy: Person
}

5.Mutation

我们说RESTful API经常说CRUD这几个动作,也就是说光查询不行,还得能向服务端写数据。当然,GraphQL的核心功能之一就是Mutation,也就是实现CUD这些非只读操作。

比如如下的查询可以让我们创建一个新Repository并返回这个新Repository的ID(很可惜,这个API似乎有问题,Github会匹配不到你自己的ownerId):

mutation
{
 createProject(
   input:
   {
     name:"test repository",
     ownerId:"MDQ6VXNlcjEwMTkzNDA1"
   })
   {
     clientMutationId
     project
     {
       id
     }
  }
}

 5

   


GraphQL潜在的问题? 

虽然GraphQL看起来很酷炫,但是也有些地方要注意。

1.服务端优化

由于是查询本身被解析成图,递归地取值,因此可能会存在服务器性能隐患。特别是对SQL来说,现在非常容易大量出现N+1的情形。

因此,现在GraphQL大多还都用在NoSQL上。但是由于整个请求还是在一次HTTP请求中完成的,理论上我们也有Batch为一个查询的能力,就像许多ORM有一些惰性特性,可以将多个查询过滤语句合并成一条查询一样。

(图片来自:http://t.cn/R6qt6dZ)

Facebook正在研究如何让GraphQL更好地Batch问题比如dataloader,社区也有一些不错的实现。这些库不仅在尝试解决这些问题,而且也揭示了GraphQL作为API抽象层本身,可以在普通Web场景下和ORM结合的能力——通过代码可以将这两层抽象到一起,这很可能让我们可以自己轻易搭建一个GraphQL BAAS(Back-end as a service)。现在已经有很多平台提供这种服务了,比如scaphold、reindex、graphcool等,也有Graffiti这种与Mongo ODM直接结合的类库。

2.安全问题

虽然GraphQL给客户端提供了强大的查询能力,但这也意味着有被客户端滥用的风险。如果不使用某些限制过大的查询,反复请求一条Load出所有Github用户的查询可能会让他们的服务器直接挂掉,GraphQL提高了被DDoS的风险。因此在使用GraphQL的过程中,我们要对安全问题更加重视。

3.需要重新思考Cache策略

REST虽然会引起一些性能问题,但它也以HTTP Cache的方式解决了很多性能问题。而对于多变的GraphQL操作来说,Cache就变成一个需要深入讨论的话题了。然而这种Cache策略就要交给客户端来完成了。

 6

   


然后呢?

因此,接下来的文章会深入讨论基于GraphQL的多种类库以及不同客户端。最终我们也可以在这些类库上看到在现代组件化趋于主流之后,我们的通信应该怎么与组件化设计有效结合。与此同时,随着客户端越来越复杂,我们应该如何同步服务端状态,如何管理缓存等等。

其中一个是Meteor团队推出的Apollo Data,它提供了一系列的服务端以及客户端工具来简化GraphQL的开发,容易上手并且支持Android、IOS、React(Native)以及Angular2。而另外一个更复杂但更强大的选择则是Facebook自己推出的Relay,它支持React(Native),提供了类似Virtual DOM的Diff算法来Diff Cache,可以自动精确管理数据来解决Cache问题。

(图片来自:http://t.cn/R6qcOfq)

我们将会从Apollo开始,感受GraphQL究竟是如何工作的,之后我们也会看到这个方案中的一些问题,而这些问题会将我们引向更深层的Web客户端问题——最终我们会探讨Relay,一个更完整的解决方案,从中我们可以看到Facebook对Web客户端的未来有着怎样的思考。

PS:点击左下角“阅读原文”,阅读后续文章。


- 相关阅读 -

使用Enzyme测试React(Native)组件|洞见

RESTful架构风格下的4大常见安全问题|洞见

点击[阅读原文]可到ThoughtWorks官网查看原文和后续文章内容&绿字部分

本文版权属ThoughtWorks公司所有,如需转载请在后台留言联系。

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存