美文网首页
GraphQL 概念入门

GraphQL 概念入门

作者: 阿兵 | 来源:发表于2021-07-06 15:15 被阅读0次

    GraphQL 概念入门

    Restful is Great! But GraphQL is Better. -- My Humble Opinion.

    GraphQL will do to REST what JSON did to XML. -- Samer Buna from Quora

    GraphQL 作为 Facebook 的前端三架马车之一(另外两架是 Relay 和 React, 三者可以无缝结合),提出也有一段时间了,但真正用过的人却出奇的少,
    截止到 2018 年底,根据 Stateofjs 的数据看,
    虽然只有 1/5 的开发人员用过,但却有高达 62.5% 的人表示听过并想试试😂 果然咸鱼才是主流呀;
    接下来我会就 GraphQL 的前后端应用出个系列文章介绍 GraphQL 及其使用感受,顺便填坑。

    本系列将以 Python + Django + Graphene 作为后端服务端。Apollo 作为前端客户端。

    1. 什么是 GraphQL

    官方是这么说的:

    GraphQL 既是一种用于 API 的查询语言也是一个满足你数据查询的运行时。
    GraphQL 对你的 API 中的数据提供了一套易于理解的完整描述,使得客户端能够准确地获得它需要的数据,而且没有任何冗余,
    也让 API 更容易地随着时间推移而演进,还能用于构建强大的开发者工具。

    我总结一下:

    首先 GraphQL 它是一种跟 Restful 标准类似的查询语言。它们的功能层级类似(如下图),但从实际应用、理念上说的话,它们是完全不一样的两种形态。

    后端架构层级 - graphql.org

    Restful 主张的是 “一切数据皆视为资源”,
    在理想情况下:不同的请求内容对应着一个唯一的 ID. 不同的数据动作对应到不同的 HTTP 方法上。

    GraphQL 主张的是 “一切数据皆视为数据(树状的)”,
    不同的请求内容都会对应到同一个 HTTP 地址,也会对应到同一个 HTTP 方法(POST)。
    不同的动作由操作符决定。它的总体体验会更像是一个 SQL Shell, 你需要什么就请求什么,你不需要的内容它也不会硬塞给你。

    与 Restful 类似,他们都只是个标准,不同产品它们的具体实现也是有所出入的,没有完全实现所有特性也是正常的。
    但主要的功能及特性是在所有实现中都有的。另外两者的不同语言支持基本差不多,常见语言都能有对应的实现。

    下面我会就两者的差异进行相对详细的比较,
    由于 Restful 算是个比较通用的标准了,下面我不会详细介绍 Restful 的具体内容,
    如果你不熟悉 Restful 标准的话最好先了解一下后再继续,以便更好地理解。

    2. GraphQL 与 Restful 的数据操作对比

    2.1 查询

    Restful 主张一切数据皆视为资源,这意味着不同的数据你会需要多次请求,
    比如一个像我这样的 SPA 类型的个人博客,你大概会在进入主页时请求这些内容(纯假设,非实际情况):

    // 一些配置类信息:比如配置项、站点标题等
    https://example.com/config/
    
    // 侧边的标签栏
    https://example.com/tags/
    
    // 侧边的归档栏
    https://example.com/archives/
    
    // 公告信息栏
    https://example.com/billboard/
    
    // 文章的列表
    https://example.com/articles/
    

    像这样一个单页博客一进主页时必然要请求的信息就要这么多,想象一下如果是一个高并发的、内容结构复杂的网站那请求数量会有多大。

    但这些在 GraphQL 中的话,你只需要请求一次,比如:

    query {
      config: {
        key,
        value,
      }
    
      tags: {
        name,
      }
    
      archives: {
        name,
      }
    
      billboard
    
      articles {
        id,
        name,
        description,
        tags: {
          name,
        },
      }
    }
    

    然后,接口的返回内容会类似这样:

    data: {
      config: [
        {
          key: 'title',
          value: 'xxx',
        },
        {
          key: 'slogan',
          value: 'xxx',
        },
      ],
    
      tags: ['Go', 'Java', 'Javascript'],
    
      archives: ['2011', '2012', '2013'],
    
      billboard: 'XXX XXX XXX',
    
      articles: [
        {
          id: 1,
          name: 'some name',
          description: 'XX XX XXX',
          tags: ['tag1', 'tag2'],
        },
        {
          id: 2,
          name: 'some name',
          description: 'XX XX XXX',
          tags: ['tag1', 'tag2'],
        },
        {
          id: 3,
          name: 'some name',
          description: 'XX XX XXX',
          tags: ['tag1', 'tag2'],
        },
      ],
    }
    

    可以看到接口的返回内容是一个按照你所请求的内容及字段准确返回的一个 JSON 数据。
    并保留着你请求的结构体,你需要一个长成什么样的树状结构完全可以由你自己构造,
    后端也不用为你的每个特定的需求去写一些奇奇怪怪的接口以填补业务与开发之间的实现冲突。

    再有一个就是在某些情景下,以 Restful 的方式请求的话会需要将请求分开,即本来可以并行请求的事件因为 Restful 标准而需要改为串行的。

    比如说我们要获取当前这篇文章所关联的分类下的其他类似文章,然后在边上显示。

    你可能会先请求:

    https://example.com/article/1/
    

    得到对应的文章分类后,再请求:

    https://example.com/article/?category=graphql&is_recommend=true
    

    然后你才可以渲染完整的页面内容。这些在 GraphQL 中都是可以避免的。

    对于数据间的关联性,普通的 SQL 外键及关联关系它也都能直接映射成子项返回,
    比如上面的例子一中 articles 里面的 tags 一般就是以数据外键的方式连接到不同的表中的,在 GraphQL 中它能以子树的方式直接取用。

    所有的查询操作在 GraphQL 中以操作符 query 执行。里面可以写无数多个不同的请求内容,不用分开请求多次。
    在你需要对同一个内容请求多次的情况下,目前前端GraphQL客户端都有自动缓存处理,
    可以做到当前网页实例只对某一个内容请求一次后后面再次请求该内容时不会再发出网络请求而直接使用缓存信息,当然你要再次发出也是可以做到的。

    2.2 变更

    在 Restful 中,我们习惯用不同的 HTTP 方法描述行为,但在 GraphQL 中,我们用操作符指示行为。

    上面的 query 指代的是查询操作,相对应于 SQL 中的 select 操作。那如果要修改呢?
    在 GraphQL 中我们用 mutation, 对应到 SQL 中是 update, insertdelete. 乍看之下有种乱了的感觉,但实际使用时并不会有这种感觉。
    虽然这部分我实在是吹不起来,因为这部分我个人会觉得它更像是将本来在后端做的内容放到了前端。它的使用方式非常像是给了前端一个“后端函数”。

    *下面所有例子只是演示,并不是实际写法,实际写法会更严谨

    如果我们要删除一篇文章,可以这样:

    mutation {
      deleteArticle(id: "some ID") {}
    }
    

    如果要新增一篇文章:

    mutation {
      createArticle(
        title: "some title",
        content: "some content",
      ) {
        id,
        title,
        content,
      }
    }
    

    修改操作与新增类似,不再赘述。

    虽然看起来相对稀疏了点,好像很随意的感觉,但其实这些操作是给了开发者更大的自由度。方便订制行为。
    另外有一点值得一吹的,在真正做 Mutation 前,调用了一个 Mutation 操作后,它会在前端代码里面自动做传入的参数类型校验,
    类似静态语言的编译,如果传入参数有错会自动报错并提示出错位置给出错误信息,如果没问题才会发出 HTTP 请求变更操作。
    传到后端时会由后端再次做校验。这点我个人相当喜欢。

    2.3 订阅

    订阅算是个比较突出的特性,传统业务上也常常会有类似的需求,但 Restful 并没有管到。
    在 GraphQL 中订阅这个功能目前在大部分实现中好像是不完全支持的。
    其大体意思就是当你在前端中对某个内容进行订阅后,当后端的这个内容有变更时会由服务端主动通知前端,进而引发 UI 逻辑或操作等。
    目前这部分特性好像大部分实现是没有支持或者没有完全支持的。而且好像大部分情景下只能是依赖长链接或轮询去做。
    尽管如此,它还是非常让人期待的,因为它所做的是让你可以对任意数据进行订阅而不是像传统的实现那样让后端单独为某个特殊内容做个专用接口。

    一个经典应用场景比如:
    当你在打开一篇文章时,对此文章的评论区发出一个订阅操作,此时如果有人在你看文章的同时对文章新增了评论或回复评论之类的,
    对应的变化会由后端主动推送给前端,这样就可以实时地看到最新的评论内容了。

    订阅在GraphQL中对应 subscription 操作符。与 mutation 写法类似。

    如果有人比较了解还请不吝赐教,我对这块不太熟悉也没深入了解 。

    2.4 分页

    传统的 Restful 采用间距式分页,即定好一个固定的间距后,可以由此间距大小及当前所在页码去推算任意页的内容。

    GraphQL 相对更推崇指针式分页,即不管你当前在哪一元素上,都以当前所在这个点为指针,往前或往后请求给定数量。
    它的一个劣势是无法像间距式一样在当前位置推算出任意页的内容,但它的优势在于你完全不用关心任何其他信息,
    永远只把当前点作为指针即可任意向前或向后地无缝伸缩。

    Restful 这样的方式会更符合传统桌面端索引式的交互体验习惯。
    而 GraphQL 这样的方式则在更现代的 web 交互中,比如瀑布流、小程序及原生应用中的下滑自动加载更多这样的情景中更为方便。

    PS: GraphQL 中也可以兼容间距式分页,具体实现方式之后会讲,在这篇文章中不细说。

    3. GraphQL 相对 Restful 的优势

    3.1 接口版本管理

    在 Restful 中标准管理方式是以不同的链接入口声明的,如:

    https://v1.example.com/
    https://example.com/v1/
    

    在 GraphQL 的世界中并不存在不同的入口之分,接口版本迭代可以无缝过度,因为本身不同的查询内容就是由前端发出的,前端并不会受到后端限制。

    3.2 更清晰透明的数据结构及更健壮的接口

    像我上面说的,在实际使用时,前后端都会有强类型的参数校验,所以相对而言是更容易感知错误的,并且!它极大地减少了其他隐性出错的可能。
    比如说像后端改了某个接口中的东西忘了告知前端进而导致的逻辑错误。这些极其容易发生的问题在 GraphQL 中都不太可能出现。

    3.3 更好的缓存系统

    如我在上面【查询】一节中提到的,GraphQL 的客户端自带了缓存系统,可以更进一步节省请求消耗,不想要此特性也可以关掉。

    3.4 前端友好的开发体验

    像上面提到的,你需要什么内容可以由前端决定。在需要强关联性的业务中,
    GraphQL 可以无限查找关联关系,在前端就可以省掉大量的异步语法、极大避免了 Promise 或 Callback 地狱的问题。
    除此以外,在一些后端语言中,比如 Python 字段通常约定为下划线命名。
    但在 Javascript 中,更为常用的是驼峰命名法,由于数据是由后端返回的,避免不了的就让前端的代码变得奇丑无比夹杂着各种不同的命名方式;
    也许你会说,我们可以在拿到数据后进行数据命名清洗,但这无疑会增大时间复杂度同时降低了代码的可维护程度,无论怎么说都不太可取。
    但!这个问题在 GraphQL 中(严谨地说是在我所用的这个技术栈中)并不存在。
    因为在 Graphene 中它会对返回数据自动进行命名方式转换,前端用驼峰,后端用下划线与不干扰。

    3.5 严谨的开发体验及更明显的报错机制

    由于 GraphQL 的输入输出都是基于 Schema 定义的,调用方法及查询都是强类型输入输出,能更高效地定位错误原因及位置。
    如果是客户端操作传的参数不对,它甚至不会发出请求直接就在客户端报错并说明原因。
    如果是正确传参但服务端出错了,则在同一个查询中其他内容都能正确返回,只有出错的内容会为空并会提供一个 message 字段说明出错原因及位置。

    下图为 Github 的 GraphQL v4 API
    可以看到用户查询出错了但 search 的数据正常返回。

    查询出错

    3.5 更方便的调试流程

    除了上面非常详细的报错外,你甚至可以直接在浏览器中进行动态 Debug;
    下图是我自己的一个项目的 Debug 查询,可以查看当前查询的 SQL 语句是怎么写的、用了多久时间、是否用时过久、
    带了什么参数、事务ID、事务状态等各种各样的信息。

    (由于安全原因就不展示具体 SQL 语句了)

    Debug

    3.6 代码即文档

    由上面的图片可以看到其实使用 GraphQL 的时候它会提供一个网页内的动态交互窗口,提供 GraphQL 的自动补全、格式化查询语句、历史操作等信息。
    其中最最最强大的就是它的接口文档了。代码即文档式的信息非常有用。

    在这里你可以直接看到整个树状结构是怎样的,也可以看到清晰的类继承关系(图没截到),也可以看到不同查询内容的详细说明及参数类型等内容。

    Doc

    4. GraphQL 的劣势

    无论什么东西,两物相较必有优有劣。对于开发一个现代化的、复杂的 web 服务或者站点而言,GraphQL 有着一堆的优点,但它也有明显的劣势。

    4.1 性能开销

    由于 GraphQL 自带层级查询,所以容易出现多余的查询,引起不必要的查询开销。

    当然这个问题很大程度上也和你使用的后端框架有关系,关于 Django 的我在此推荐一篇写得很不错的文章:
    N+1 问题

    4.2 无提示开发

    虽然它有提供一个功能十分强大的网页内调试窗口。但是在实际开发中时,前端在 IDE 中写各种语句时是不会有任何提示的,也容易写错拼错,
    因为编辑器根本不能识别你写的是什么内容,它最多把它当成纯文本,也不会格式化内容,所以在开发上在这点上会有点不太舒服。

    4.3 接口测试工具相对少

    Restful 因为历史悠久普用性也大,所以基本上市面上所有的接口工具都能支持 Restful 查询,但 GraphQL 却不行。
    目前我知道的只有 Insomnia 支持,但不能很好支持文档部分功能。(当然框架本身已经提供了超棒的调试工具了)

    5. GraphQL 客户端哪家强

    这个只看你功底了,市面上目前差不多是两家独大的情况,一个是 Facebook 自家的 Relay,另外一个是由社区支持的 Apollo.

    Relay 学习成本会更高些,就类似你刚学完 React 的基本知识,会用 React 写一些简单页面了,
    突然被要求去学习 Redux 理念差不多。需要学习一套新的数据管理方式。

    相较而言 Apollo 会更友好一些,支持 JSX 式的块状写法,也支持直接当成 Axios 那样只用它的请求功能,也可以兼容 Relay 式的查询写法,学习门槛相对低了不少。
    自带的本地状态管理可以完完全全替代掉 Redux. 真 · 一个顶俩(Axios + Redux)

    6. 结束

    两者同为标准、设计指南;并不能客观地说谁就一定比谁好。但就我个人而言我能看到 GraphQL 相对 Restful 的相当多优势。
    毕竟 GraphQL 本身年纪就比 Restful 小,他的出现必然是为了解决前代的遗留问题,不是全部也是大部分。

    最重要的是两者是可以完全兼容的。谁也不干扰到谁。

    最后丢多点拓展阅读及学习链接:

    相关文章

      网友评论

          本文标题:GraphQL 概念入门

          本文链接:https://www.haomeiwen.com/subject/jfnbultx.html