这篇文章简单来总结一下,这些天调研GraphQL的感受,以及它跟RESTful的优缺点对比,顺便征求一下社区大佬们的意见,你们给自己家的项目上GraphQL了吗?

契机

近期公司的业务需要扩张,准备要做APP,另外就是ActiveAdmin所写的后台已经让自家人多少有些不适,也是有重写的打算。跟前端小伙伴讨论是否需要前后端分离的时候,前端小伙伴强烈要求分离,理由如下

  1. Ruby不好招人。
  2. 分离之后彼此之间的工作不会有太多的冗余,可以分仓库管理。

我记得社区里面有人针对这个事情回答过

自己项目的话不分离,公司的项目果断分离,因为自己没时间写。

既然如此,那还是分离吧。不过分离之余又面临另一个抉择。我们是要沿用Rails官方拥抱的RESTful模式的API,还是尝试一下近几年兴起的GraphQL?这是一个问题。抛开后台不谈,如果要在APP上尝试GraphQL的话,那么之前RESTful写给小程序的东西可能得重新写一遍。这....还是得多想想,下面来看看GraphQL所带来的好处是否值得我们花这个时间去折腾。

GraphQL说它啥都比RESTful要好?

似乎每一个新技术的出现都会在文档里面鼓吹自己有多好,别人有多不好。先来看看GraphQL有多好吧。

1. 减少请求量

相比起RESTful,即便要获取多个不同资源的数据,前端只需要调整自己的查询字符串就能只通过一个请求获取对应的数据。假设我一个页面只需要文章Post的数据,以及数据库里面所有的分类Category的数据,还有当前用户的数据那么我们可以直接这样去查询

query-result.png

前端拿到数据之后可以自己去解析,原来需要三个请求去完成的工作现在一个请求就搞定了,这咋一看还是蛮吸引的。前端的同学估计已经受够了一个页面(特别是首页)要调用十几个请求,并且还要写十几个回调函数去解析数据的日子了。

GraphQL这套查询方式正好解决了他们这个难处,只要后端定义好相关的动作,他们只需要适当组装一下请求字符串,就能够获取自己需要的所有资源数据,并且只需要向服务器请求一次。比起要请求服务器多次的RESTful模式,这种方式固然节省了不少的请求量,请求量少了,页面加载速度自然要快一些,这点无可厚非。咱们暂不反驳,先继续往下看。

2. 自定义数据的返回,节省数据传输量

官方说,利用GraphQL前端可以自定义数据的返回,后端只需要前期做好定义,后期不用做任何改动。一般来说前端通过接口能获得什么数据都是由后端来决定的,而后端工程师为了减少任务的反复,一般都会返回比前端所需更多的数据。比如:在某个页面前端只需要用户的idemail,如果是RESTful接口的话,为了接口能够更加通用,它可能还会携带别的信息

{
  "id": 1,
  "email": "lanzhihengrj@gmail.com",
  "gender": "male",
  "role": "specialist"
}

哪怕genderrole在这个页面完全就用不上。而GraphQL的做法是,前端可以以查询字符串的方式告诉后端我们只需要某个资源的XX,YY,ZZ字段,麻烦给我们返回一下,其他的就别给我了。像下面的文章资源,我们通过查询字符串只获取了idtitle其他就不需要了

just-id-and-title.png

想要更多,则自己往里面加字段就行

id-title-and-excerpt.png

这么做的好处是,后端只需要返回前端所必要的数据,减少请求响应的数据量。

3. 强类型内省系统

一开始看GraphQL的时候,对那个内省(Introspection)系统是有些懵逼的,在编写RESTful接口的时候,我们一般都会通过Swagger之类的工具来给前端文档,前端可以根据这个文档来进行接口对接,并且可以在Swagger的页面上进行调试。

而在GraphQL里面似乎就不用这样子了,我个人感觉它有点像Redux,后端定义好所有的动作,前端可以通过内省系统查询到这些动作,然后告诉后端系统,自己要执行哪个动作,后端系统就会调度对应的动作,完成前端交代的任务,并返回结果。

在GraphQL里面,动作主要分两种类型

  1. 查询用(queryType)
  2. 修改用(mutationType)

可以这样查询出他们两的名字跟描述

query-type-and-mutation-type.png

如果我们想进一步知道,后端所允许的查询动作,可以进一步去查找。从前面的结果可以知道,查询调度器的名字是Happy,看看它旗下有哪些“艺人”。

query-type.png

可以看到它支持的查询有Post, allPosts, Category, allCategories等等,还能查看到他们分别接收的参数,这些都是后台定义好的。只要运用熟练,这其实就相当于一份API文档,还是蛮方便的。

RESTful的反击

我不确定大家是否喜欢上面这种模式,反正我本人不是很喜欢,对我来说,似乎有点过早优化了。前端许多社区似乎都有这样一个问题,Demo看起来很无敌,实际应用的时候要你命

1. 你真的减少了往来的数据量?

按GraphQL这种说法,前端能够定义自己需要的数据,在一定程度上能够减少响应的数据量。然而,请求的数据量你有没有算进去?用GraphQL获取一个人的基本信息的时候大概是这个样子

// Post /graphql

query {
  user(id: 1) {
    name
    gender
    avatar {
      url
      signed_id
    }
    ....
  }
}

不过RESTful的话,直接/users/1就好了吧。要是我真的需要一个用户很多数据的话其实RESTful更方便。发送请求要容易许多,而GraphQL需要自己去定制前端所需要的每一个字段。用我前同事Hugo的说法是

这个地方有一个临界点,当RESTful一个接口返回很多前端用不着的字段时,才会节省数据量。

其实真的出现这种极端的情况,或许我们

  1. 直接针对这个接口做优化就好。
  2. 不然就另外提供一个/users/1/simple的接口,只返回精简的数据。
  3. 只针对这个接口的功能用GraphQL的模式来实现(没有洁癖的话)?

Why you shouldn’t use GraphQL这篇文章提到了一个我挺认可的说法

GraphQL is an alternative to REST for developing APIs, not a replacement.

2. 减少了请求量,然而....

试想要是一个页面(比如落地页)需要调用十几二十个接口,你用GraphQL的话,确实是可以一个接口拿到所有的数据。举个例子,假设要填充首页需要有10个数组,那么如果用RESTful的话我们调10个接口,而GraphQL的话大概这样就能搞定

// Post /graphql

query {
  products1 {
    name
  }
  products2 {
    title
    sku
  }
  ..... // 还有8个类似的东西
}

不过兄弟,这还没完,如果这10个数组是需要分页的呢,那是不是有趣一些了?这种时候如果用RESTful,各个组件会分别调动指定的接口,获取更多对应资源的数据。如果你用GraphQL,那么你几乎不可能沿用上面的查询字符串,因为你预先并不知道哪个资源要获取更多数据。所以针对分页的情况你可能还要针对不同组件维护对应的查询字符串。大概是这样

// Post /graphql

query getProducts1($page: Int!){
  products1(page: $page) {
    name
  }
}
// query variables
{
  "page": 2 // 取决于第几页
}

以上模板还要写9个类似的,当然你也可以用工厂函数,然而这种情况用GraphQL似乎也没省多少事,就首次加载的时候RESTful要调10个接口,用GraphQL一个接口就搞定了,能节省掉一些请求。Emmmmmm,我针对这种情况用RESTful写一个专门的首页加载接口似乎也行?或者只针对首页加载的情况使用GraphQL技术,其他地方依旧用RESTful?

3. 全都是POST请求,而且Endpoint都一个样...什么鬼

GraphQL有个特点,就是请求链接都是一样的,而且全是POST请求,另外,正常来说所有请求不管成功失败都会返回200。如果不点开请求详情,你根本不知道这个请求干了什么,下面这张图就比较生动了

all-post.png

链接都一样,只通过定义不同的动作,以及相关的参数来做不同的事情,或者说获取不同的数据。这样后端只需要定义好能用的那些动作,从此“高枕无忧”。反正我玩了好几天,越搞越纠结。RESTful有个比较舒服的地方就在于,增删查改,弄得清清楚楚,利用了GETPOSTPUTDELETE这些语义化的动词,找问题要方便得多。并且对不同资源的操作会有不同的endpoint(可理解为不同的url),这样辨识度会更高。而GraphQL所有请求都是是统一入口的(假设是/graphql),只是利用不同的查询字符串来做不同的事情。

// 查询产品
query {
  product(id: 1) {
    name
    sku
  }
}
// 删除产品
mutation {
  removeProduct(id: 1) {
    product {
      name
      sku
    }
  }
}

product, removeProduct这些动作都后端定义好的,前端只需要告诉后端自己要干嘛,期望返回什么东西即可。只不过接口调用的结果,你只能自己查看响应内容才知道了。

error.png

可以看到,只要动作被执行了,哪怕是出错,接口都会返回200,返回结果会有errors的字段,附上一堆错误消息。反正个人还是觉得RESTful这种能够通过不同状态码,还有请求类型来判断接口性质的做法比较“人性化”。

4. 不小心的改动容易出“人命”

其实写GraphQL接口的时候我已经忘记了自己在写Ruby了,哪怕Ruby本身是强类型语言,但是我们也不用凡事都去定义类型啊。而GraphQL感觉就是一套类型系统构建起来的东西,你得以声明的方式去定义很多的类。这当然也有它的安全性,起码我接收个参数,外层机制就能告诉前端,你传的类型不对。

不过也有难搞的地方,代码的方式是声明式的,前端能够获取什么数据都是后台来决定。比如,一个用户有name, id, gender这三个字段,如果后端限制你只能访问idname,你就不能访问gender。幻想一种情形,一开始后端给前端暴露了id, name, gender三个字段

module Types
  class PostType < Types::BaseObject
    description "Post Type"
    field :id, ID, null: false
    field :name, String, null: true
    field :gender, String, null: true # 后面会删掉
  end
end

前端也应用了这几个字段

query {
  user(id: 1) {
    id
    name
    gender
  }
}

某天一个不知情的后端把gender给删掉了,那么前端调用接口的时候就会直接报错,对于线上环境来说这还是挺致命的。毕竟它可能一个页面只发送了一次请求,所以请求中某个资源获取失败,会导致页面其他的资源也获取不了,前端页面有可能直接瘫痪。

breaking.png

官方倒是提过这个问题,它称之为Breaking Changes。它也提供了一些解决方案

  1. 把Schema结构dump出来,以便跟踪。
  2. 利用GraphQL::SchemaComparator在跑CI的时候检测新的提交是否有Breaking Changes

当然我相信适当的测试用例也能避免这种情况的发生,只是笔者当时就想:“这么折腾到底我还要不要用它?”

5. Rails原生就比较亲近RESTful

公司的后台用的Rails,原生就比较亲近RESTful的做法,硬要上GraphQL也不是说不行。只是总感觉有那么点“不适应”---好吧,我承认是非常地不适应。

  1. 为什么我要不停地去定义类型?
  2. 为什么我们要把所有东西都放在一个endpoint去解决?
  3. 为什么我要让前端那么费劲去组装各种查询字符串?
  4. 为什么我要舍弃用得挺直观的HTTP请求动作(GET,PUT),还有各种有意义的状态码(500,404)?
  5. 为什么我要放弃Rails提供的在Controller里头十分方便的工具函数?
  6. .....

不知道社区的各位怎样,反正笔者是觉得越写越别扭了。似乎省下的时间都可以慢慢地优化以前的接口了。

尾声

经历过上面的考量,还有近期的实践之后,我觉得GraphQL我已经入门了,可以放弃了。于是乎跟前端小伙伴进行了一次灵魂的对话

我:“我感觉GraphQL有以上的xxxx问题,我们到底要不要用?” 前端小伙伴:“感觉增加了一堆的工作量,好处就.....” 我:“好处只增加了一丢丢?” 前端小伙伴:“不,目前都没感受到有什么好处。”

前几天我还在犹豫是否要在公司项目的新功能里面引入GraphQL,现在基本是可以下定决心不用了。我并不否认,GraphQL减少请求量,自定义数据返回这些好处都十分诱人,或许对于Facebook,Github这种量级的公司来说是个不错的选择,然而对我们这种初创公司来说真的经不起这般折腾,而且个人觉得有点过早优化了,省点时间跟朋友吃吃饭好了。

remove-graphql.png

反正我是放弃了,不知社区的朋友怎么看,你们开始用GraphQL了吗?