我们在前面的文章中已经讨论过使用 BFF 的必要性和 GraphQL 的基础知识。本文将对 GraphQL 和传统 REST 进行对比,来说明相较于传统 REST,在 BFF 中使用 GraphQL 的优越性。

数据获取

传统的 REST 接口,前端在获取数据的时候,一直存在两个老大难问题,一个是多获取了数据,另一个是少获取了数据。

  • 多获取了数据

在传统 REST 接口中,一个接口返回的信息往往会比页面真实需要的数据要多。比如商品详情接口,接口返回的商品信息是必然比页面上展示的信息多的,除非给每个前端页面都定制接口。这就是多获取了数据的问题。

一般情况下,多获取了数据并没有太大的危害。但是,如果数据模型比较复杂,尤其在这个接口中还要聚合其他模型的信息的时候,接口性能、可用性等有时候会被多余的信息所拖累。

还是以商品详情为例。通常情况下,在商品购买页面,我们需要获取尽可能多的商品信息,传统 REST 可以满足页面需求。但是在订单页面中,我们只需要展示商品的简版信息,比如名称、规格、价格等,其他的信息一概不要。这时候,如果商品详情中聚合了其他信息,需要查询其他服务的,在订单页面中就完全没有必要获取这些信息。如果被依赖的服务稳定性较差,也会导致订单页面的稳定性比较差。

GraphQL 很好了解决了这个问题。我们通过 Query 明确地告知 BFF 需要哪些数据,BFF 在从下游服务获取这些数据返回给前端。不需要的数据,就不会被获取,减少了不必要的系统调用。

  • 少获取了数据

少获取数据,通常表现在列表中。一般在列表接口中,每个列表元素返回的信息都比较简要,而页面上有时候又想展示一些关联的信息。比如商品列表,列表接口中只返回商品的基本信息,如果页面上要展示每个商品的优惠信息的话,需要根据列表返回的信息再调接口获取。

在 GraphQL 中如何解决少获取了数据的问题呢?在 GraphQL 服务端实现中,有个 Resolver 的概念,即每个字段都有一个 Resolver 函数,GraphQL 在执行的时候,会根据 Query 中查询的字段调用每一个 Resolver 函数获取这个字段的值。因此,我们只需要在服务实现时定义好每个字段的 Resolver 函数即可。比如我们可以给每个商品模型定义一个优惠信息的 Resolver,那么在商品列表中的查询中,在 Query 上添加这个优惠信息字段,GraphQL 就会自动查询每个商品的优惠信息。

N+ 1 问题

N + 1 问题说的就是上面商品列表查询优惠信息的问题。在传统 REST 接口中,如果列表返回 10 条数据,前端就需要在通过 10 次请求查询商品的优惠信息,总共需要 11 次请求。N + 1 问题最大的危害是占用数据库资源太多。如果一次返回 100 个商品,那就需要查询数据库 100 次,很容易耗尽数据库的连接资源。

GraphQL 中通过 dataloaderopen in new window 来解决 N + 1 问题。在商品列表的例子中,dataloader 会将这些查询优惠信息的请求合并成一个请求,再将数据库返回的信息分配给每一个商品模型。当然,如果 BFF 不是直接读取数据库,需要后端服务支持批量查询的接口。

开发效率

在传统 REST 模式下,页面变化通常需要后端接口跟着做调整,需要前后端之间重新对接口进行约定、联调、发布。这一串动作都是非常耗时的。

在 GraphQL 中,如果页面变化的内容原先系统中模型之间的关系已经建立,那么只需要前端页面调整查询的内容即可,减少了接口约定、联调等流程,同时纯前端静态资源的发布也比后端服务发布要方便很多。

性能

通常,一个页面的数据请求不止一个。比如商品详情页,需要商品信息、优惠信息、广告信息等各种数据接口。在传统 REST 接口,我们需要分别调用这些接口获取数据。

在 GraphQL 中,我们可以通过一次请求将这些数据全部都拿到。只需要在 Query 中查询多个数据信息即可,GraphQL 会返回多棵 JSON 树。在弱网环境下,GraphQL 可以比传统 REST 获得更好的性能体验。

迭代升级

在传统 REST 接口中,我们升级原有的接口都是通过在路径中增加版本号来实现。比如从 /rest/v1 升级到 /rest/v2。而在 GraphQL 中,我们可以渐进式的实现 API 的升级。

比如在一个影片管理系统中,我们有如下定义:

type Film {
  title: String
  episode: Int
}
1
2
3
4

现在我们可以导演信息,我们只添加了导演名称:

type Film {
  title: String
  episode: Int
  dirctor: String
}
1
2
3
4
5

过段时间,发现需要导演的详细信息:

type File {
  title: String
  episode: Int
  director: String @deprecated
  directedBy: Person
}
type Person {
  name: String
  directed: [Film!]
}
1
2
3
4
5
6
7
8
9
10

通过新添加一个字段 directedBy 同时将原来的 director 标记为 deprecated。GraphQL 的工具会将标记为 deprecated 的字段隐藏掉。这样,我们就可以实现 API 的渐进式升级,同时不需要引入版本号。

细粒度的字段控制

前面说到,GraphQL 可以实现前端页面按需获取数据。在服务端,GraphQL 依然可以对每个字段进行细粒度的数据收集。因为每个字段都有对应的 Resolver 函数,因此我们可以在这些函数中搜集很多信息,比如字段是否还在使用,字段对应的处理性能数据等。

文档能力

在上一篇文章中,我们知道 GraphQL 有一个类型系统,我们通过 SDL 定义 Schema。有了 Schema,我们就有了系统内的所有类型信息。相当于一个字典,我们可以按图索骥查询所需数据。实现了代码即文档的效果。

在 GraphQL 的内省模式中,我们可以查到系统支持的所有 Query 类型、Mutation 类型,以及每个类型的字段说明等信息。避免了 API 文档的维护工作。

关注微信公众号,获取最新推送~

加微信,深入交流~