api
尊敬的各位来宾,软件同行们,大家下午好。我叫吕靖,大家也可以叫我 Jimmy,我呢?之前在 ThoughtWorks 思特沃克和阿里巴巴工作过,目前在 Parabol 创业,专注于敏捷会议工具,致力于让未来的工作更有意义。
非常高兴来参加本次创新峰会,与各位分享「API 的未来:graphql 如何为敏捷组织铺平道路」,让我们翻开目录。今天我会给大家从 4 个方面来介绍全面介绍 GraphQL,结构非常简单,第一部分是 What & How: API 的未来跟敏捷,也就是未来组织,会碰撞出什么样的火花。第二部分是 How:我介绍如何实践 GraphQL,直接分享 4 个工程实践的经验。以及第三部分,Where,在什么样的真实的应用场景下,结合具体的软件功能和代码实践,来介绍 4 个具体的真实案例。最后,我会总结一下 graphql 是怎么帮助我们公司进行异步协作的,作为一个敏捷组织,我们自身,就一直在探索未来的研发模式,和未来的工作方式。
好,那么让我们进入第一部分, API 的未来与敏捷组织。首先什么是 graphql API,GraphQL 后面的 QL 就是 query language,它是以一种声明式的语言描述 API 对数据的需求。比方说这里有一个简单的图例,第一步定义 Schema 也就是数据的需求,第二步,你就可以去请求任意你想要的数据的结构,第三步,API 返回可预测的数据的结果。因为 GraphQL 是基于类型的,所以所有的数据结构和数据类型都是可预测的。
简单的介绍之后,有朋友就会问了,为什么我们要用 graphql,现在的 REST API 难道还不够好吗?那么,让我们先来看一个简单的案例,大家是否有购买汽车的体验呢?想象一下,如果切换一下视角,当你作为销售人员,为买车的客户去办理汽车贷款。在这个场景下,我们来思考一下,总共会有几种不同的对象呢?
左边是客户要买的汽车,它会有不同的类型,车的型号、车的大小,以及否是新能源等等一些属性。以及右边,可以看到我们需要向银行和车管所去请求客户的信息,比方说客户姓名、信用情况、是否有驾照等一些基本的信息。所以在这样一个数据聚合的场景下,我们可以发现汽车、客户、银行这三种不同的对象,分别需要去三种不同的渠道都需要去请求对应的数据。
像以往,如果是使用 rest API,就分别需要去请求不同的 resource 也就是资源,每个资源对应不同的数据结构。而 graphql 就是一种 rest API 的替代,可以通过 schema 所定义的数据类型和数据结构,直接得到我们想要的一种聚合的结果,也就是所有想要的数据一次性就返回回来了。
这会有什么好处呢?解决了什么问题呢?GraphQL 解决的第一个问题是请求不足,也就是 under-fetching,这意味着,当我们去请求数据时,比如在先前的例子当中关于客户的个人信息的时候,发现 /API/user 就只能拿到 user 的名字,比方说我叫吕靖,但是当需要请求我住在哪里时,我的 address 地址是什么的时候,就会发现我们不得不去借助于第二个 API 也就是 /API/address,通过 user ID 再一次请求 address API 才能够拿到我的家庭住址。
所以,这里的问题在于,客户端无法一次性获得所有对数据的需求,而是在后边通过两个 API 共同去获取到所有的数据。可想而知,这只是一个非常简单的用例。当你需要请求更多的数据,比方说还需要个人的身份证、驾驶证等信息,银行的信用信息、汽车所对应的型号信息,你是不是在脑海中就想到了非常多的 API 的路径需要去请求?而且还可能出现请求顺序的各种复杂情况呢?
而我们再看右边,这是 graphql 的 API,它只需要 /graphql 这样一个端口,就可以获取到先前提到的所有的数据。比方说这里的 user 用户的名字以及住址、居住城市等等就一次性被返回回来了。
结构是不是非常的简单?
我们再来看下一个,跟 Under-Fetching 请求不足相对的,GraphQL 解决的第二个问题是 Over-Fetching 请求过度,当我们去请求 API 的时候,因为后端无法预知前端页面所需要的到底有哪些数据,所以一般情况下 API 都只能一次性把所有的数据都返回回来。比方说,右下角这张图是一个 API 返回的所有数据,当我们需要从这个数据结构里面,去找个某个人的名字的时候,是不是需要去找很多很多的层级,才能发现它具体的位置。如果说每次对数据的请求只需要那么一个字段,但是后端 API 却会返回非常多的其他字段。这种超额的数据获取,就导致了信息的冗余,浪费服务器和客户端之间的网络资源,这还只是一方面,让我们拆分来看。
第一点是,GraphQL 能够满足按需获取数据,减少网络请求的开销。第二点是,当不同的页面需要不同的数据的时候,可想而知,后端 API 无法同时对应多个页面,不同的数据差异化。那这个时候,难道我们的后端 API 要为前端每个页面去定制化不同的 API 吗?如果这样的话,会造成了更大的浪费。第三个场景是,随着业务需求的不断变化,API 本身也有渐进式的改进迭代的需求,因此 API 在不同版本之间可能需要返回不同的数据。比方说,从客户的老身份证到新身份证这样一个字段,需要迁移。那以前的话,就需要在 API 层面上直接做两个字段的区分,或者通过加参数字段等等,而这样反而导致了另外一个层面的复杂度。而在 graphql 里面,你就可以简单的用标识符直接标识,该字段已经被新字段替代了。
好,刚刚从 2 个 API 的问题出发,介绍了什么是 GraphQL 以及 GraphQL 是如何解决问题的,我来总结一下,为什么 GraphQL 可以助力于敏捷组织,我们得点题,对吧?首先呢,我们得思考,为什么我们的产品需求会经常发生变化?这里的大背景肯定就是因为,在 21 世纪 VUCA 的市场大环境下,VUCA 也就是易变性、不确定性、复杂性和模糊性的英文缩写哈,在不断变化的时代大背景下,越来越多的企业组织,就是在主动或被迫的放弃长期而精密的预先计划,转而使用快速迭代的方式,以满足市场的需求。不得不说打造敏捷组织是时代背景下企业的应对之策。
所以,graphql 在这样的背景下,从 API 层面,非常漂亮地解决了 3 个重要问题。首先是按需返回数据:在填充页面数据的时候,需要多次返回网络请求,GraphQL 一次性就把客户端页面所需要的数据一次性返回回来了,显然就优化了网络请求的速度,也能够更快的为我们的用户呈现具体的页面。
而 GraphQL 解决的第 2 个问题在于,客户端与服务端相互依赖,没办法解耦,因为这里的客户端不只是前端,比方说小程序端、网页端、移动端,甚至未来随着智能汽车的普及,车机系统也会发展越来越迅速,而车机上面显示的内容跟手机端、 web 端可能都有所不同。以前,后端需要为不同的客户端去定制不同的 API,每个客户端也需要适配不同的后端 API,从而导致了客户端持续地依赖服务端,根本没办法解耦。而 GraphQL 就非常漂亮地解决了这个问题,可以按照前端所需要的数据需求,按需地返回数据,并且不同的客户端需要哪些数据,是由它自己来定义的。
GraphQL 解决的第 3 个问题,则是开发效率层面的,GraphQL 从 API 层面解决了糟糕的前端开发体验。就像前面提到的,市场在变化,用户需求的变化也是非常频繁的,而 graphql 能够帮助前端开发在这样极其频繁的数据变化情况下轻松应对。
总结下来,这 3 个问题的核心点都可以归结于「耦合」。而「解耦合」对于「应对变化」是至关重要的,尤其是对咱们的敏捷组织而言:首先用户体验很重要,GraphQL 在网络层面直接优化了网络请求的次数。第 2 个是在客户端非常多的情况下,我们需要在不同的应用场景下应对用户的需求,使用一个 /graphql API 就在不同的客户端直接返回不同的数据。第 3 个是因为用户界面往往是发生变化最多的地方,我们不应该是长期而精密地去预先计划页面长什么样子,这是不显示的,而是随着市场需求的变化,产品需要持续的优化和迭代我们的用户界面,此时快速应对页面变化的效率也就至关重要啦。
不知道刚刚对 GraphQL 的介绍,有没有给大家带来一些新的灵感呢?这个时候你可能不由得好奇,咦,我该如何去使用 graphql 呢?所以让我们进入第二部分,介绍 graphql 的工程实践经验,我会从以下 4 个方面来介绍 graphq 的真实实践。首先是使用 relay 作为 graphql 客户端,来请求 graphql 的后端 API,当然在 graphql 查询当中会用到 dataloader 来进行缓存优化,以及 graphql 和 dataloader 是如何解决 N+1 查询问题的。最后呢,我们 Parabol 的产品其实是 realtime 多人协同的,所以我们会用 GraphQL 的订阅能力,来满足实时协同的需求。
接下来关于 How 的部分,可能会比较干货,我尽可能使用一些案例和代码来作为解释。首先,我们来看 GraphQL 如何进行数据请求?类比一下,graphql 相当于“图数据库”的单一入口,大家可以简单这样理解,严谨一点,我加了一个引号啦。只需要访问这样一个入口,发送你想要的数据的查询语句,就能直接拿到对应的数据了。非常简单的单一端点 /graphql,再加上多样化的查询方式。
总的来说, graphql 的运行环境当中,有以下 3 种操作:首先是 query 用于读取数据以及 mutation 用于写入数据,也就意味着 graphql 它既可以是查询、读操作,也可以是修改、写操作,以及 subscription 订阅操作,可以用来自动地接收实时数据。Realtime 实时性在疫情大背景这样的一个环境下是非常有必要的,用于满足在线协同的产品需求,接下来我会逐一介绍 GraphQL 的 query 查询、mutation 修改和 Subscription 订阅的实践经验。
而这里提到的 hook,则是因为 React hook 是 React 社区主推的一种代码复用的方式。 hook 简单理解就是 functions 函数,用于跟 React 状态和生命周期“打交道”。那么 hook 直译过来就是钩子,你可以想象把 graphql 的请求数据从 state 状态里面直接勾出来,并且自动化地处理生命周期相关的一些脏活。
接下来我会 4 个实践案例,首先进入到第 1 个实践,使用 useFragment 数据片段,前面提到,GraphQL 是一门查询语言,并且支持 Fragments,也就是我们可以「分片分段」地去查询,也就意味着:第一,声明片段,每个组件可以只声明自己的数据需求就可以了。比方说,我们可以看到右边,有一个简单的组件图例,最上面会有头像和名称,以及下方的内容简介,接下来是一些额外的 Agenda 日程安排信息。
那么在左边的组件树代码结构当中,每个组件都直接在内部定义自己的数据需求就可以了。而在最顶层的组件可以一次性把所有的 graphql 数据都请求回来。那么这也就意味着每个组件都在独立 query 自己的数据,最终会由 relay 自动化地去合并发送一个网络请求,从而组件就只需要关心自己所定义的数据就可以了。另外呢,最后一点是“代码共享”,如果我们在组件级别已经把数据片段定义完毕,那在其他地方如果有复用该代码片段的时候,就可以直接拿来复用即可,所以它也就能够实现数据片段代码的共享。
前面也已经提到了 relay 最大的特点就是有 compiler 编译器,它可以自动解析重复的字段。比方说前面提到的头像组件和展现内容的组件,这两个组件它都用到了 name 这个字段对吧?那么这样一个字段,会在最终合成的 query 请求当中自动被合并成一个字段了。
前面我们提到 fragment 就是用来定义数据片段的需求,然后通过 ... 三个点的方式来复用数据片段,而最终由 GraphQL 自动化生成这样一样 query,右边的这个 query 是支持将所有子组件的片段,合并到一个大型的整体的 query,而合成操作并不需要我们自己去操作,而是 relay 自动化的就帮我们做到了。
相应地,当我们有列表数据需要分页查询的时候,也非常简单,我们只需要用这个 usePaginationFragment 就可以了。在 fragment 里面增加一个 directive,也就是在列表字段这儿加上一个 connection 的标识符,然后在括号里给它一个名字,代表着这个 fragment 是一个分页的 query 从而下一次当我们请求更多的数据时,就会自动生成请求下一页数据的查询语句。那么,该如何触发这个分页查询的请求呢,我们可以看 usePaginationFragment 的返回值,能够直接拿到一个 loadNext 也就是请求下一页,这样一个方法,从而我们可以放在这样的一个按钮的回调函数里,让用户通过点击去请求更多数据。
好,让我们回顾一下,前端的页面都可以看作是由组件树所构成。比如说这里的 Facebook 主页,我们就可以看到最上方会有导航栏,左边会有个侧边栏,中间会有 banner 里面的轮播图和右边的联系人列表,以及中间部分的内容信息流。
这样一个组件树就可以由 relay 自动地去获取到每个组件所需要的数据片段,并且在顶层也就是 home 组件级别进行一次性的数据请求。数据被请回来之后,Relay 还会自动化的将每个组件 fragment 所需要的 data 数据片段,直接分发给每一个组件,从而每个组件就都拿到了自己想要的数据。
比如说,这里的 post 组件,它只需要定义 useFragment 的参数,也就是 graphql 的 fragment 模板字符串,从而就可以拿到对应的 data, 我们就可以直接在下面通过 data.title 或者 data.body 取到对应的 fragment 里面所定义的数据结构了。上面的 fragment 这里定义了 title 和 body,所以接下来在组件当中就可以通过 data 点巴拉巴拉,直接去使用这些字段了。
接下来,我们一起看第二个实践,大家可能会好奇这些 fragment 是怎么发布到怎么样请求出去的呢?请求的时机该怎么处理呢?这里有一个成双成对的一对 Hook,分别是 useQueryLoader 和 usePreloadedQuery,我们可以看这里有一个 UserPost 组件对吧,它依赖了右边的 PostDisplay 组件,用来显示 Post 推文的用户名和内容。直接看代码示例可能会有一点难度,我们在这里只需要关心,在上层组件使用了 loader 定义好一个 query 之后,query 查询的数据包括 fragment 片段和 name 字段(从而这个 hook 就可以拿到两个参数,一个是 query reference,另外一个是 load query 方法,可以直接通过 load query 来直接请求对应的用户的推文。)而在这个 PostDisplay 组件中,则使用了 usePreloadedQuery (加上前面所定义的 graphql 的查询语句以及查询语句的引用)可以拿到对应的 data 和 data 里面的 post 引用。需要注意的是,这个子组件是被 Suspense 包裹起来的,为什么需要这样做呢?
接下来,我就会用一个例子来为大家仔细讲解,使用 GraphQL 和 React 请求数据的细节,解释这样做对用户体验会有什么好处。首先,我们以「显示一条推文」作为示例,通常来说,为了用户体验我们会在页面上显示一个 loading 表示数据加载中的效果,比如这张动图,当我们加载一整个页面的时候,可以看到最开始有一个 loading,表明是在请求数据,然后会加载更多的组件,因为我们请求数据之后才决定要渲染哪些组件,而这些组件它又分别有自己的数据请求的需求,所以说我们就会持续不断得先看到两个 loading,然后再看到无数的 loading,所以这个用户体验其实是非常之差的。
让我们回顾一下,这种数据加载的模式,可以称之为 fetch on render,也就是在渲染之后再请求数据。 让我们仔细看一下这个 loading 的加载序列,我们需要先下载代码渲染 home 组件,再去请求后端的数据。此时在请求数据回来之前,我们就不得不去 render fallback 也就是 loading 的那个转圈圈的效果。在所有的数据完成之后,我们才能够看到 home 组件被成功渲染出来。最终,我们来看一下页面实际渲染的示例,这里会有长时间的空白。加载数据,加载数据加载到一半,发现中间组件还需要继续加载更多的数据,于是才能显示出来。
与“渲染后再请求数据”相对应的,我们使用的是 render as you fetch 也就是边渲染边获取数据,加上 React 的 suspense 「翻译成悬停」之后,loading 加载序列就变成了,我们首先请求数据,请求数据的同时,显示 loading 的效果,重点是数据准备好之后,我们就可以看到整个页面就被显示完毕了。
那么我们可以看一下实际的页面渲染过程,就是先看到空白,然后直接整个组件树全部一次性地就被显示出来了。可以看下先是显示 loading 的效果,再显示所有的内容,体验会比刚刚再加上 loading 的过程要好得多。
所以,我们再回过来看这个代码示例,这里的 userQueryLoader 返回一个 load query 它就可以用来手动地触发对应的数据请求,以及在数据还没有被请求回来之前,userQueryLoader 保留的是一个 query 的引用,queryReference。从而下面使用的 Suspense 会用这个 queryReference 标识当前是否请求完毕,如果还没有就会默认去使用 loading fallback 表示当前是一个 loading 的状态。当然在数据请求回来之后,被 Suspense 悬停的这个组件,就可以通过 usePreloadedQuery 直接拿到所对应组件所需要的数据,因为这两个 Hook 是成双成对的,所以 usePreloadedQuery 就使用了上层由 useQueryLoader 生成的,并传下来的这个 query 引用。
总结一下 GraphQL query 的实践经验,首先第一个 Hook 函数,user query loader 用来加载和保留一个 query,从而持续维持一个状态当中的查询引用。而第二个 Hook 函数,use preloaded query 就接收这个查询引用,从而在数据返回来之后,自动返回对应的数据 data。当然,在 Relay 的上下文中,当组件卸载之后, user query loader 它就会自动处理所对应的所有加载后的查询。最后,我还没有介绍的 Hook 函数还包括,useLazyLoaderQuery 和 loadQuery API 在这里就不做展开了,它们都有自己特定的一些使用场景。
那么,时间有限,我们就进入第 3 个实践部分,也就是 graphql 如何解决 N 加 1 查询问题?我们需要切换成使用 graphql 的方式来思考问题,对比来看,左边是 rest API 进行的 N 加 1 请求,我们先拿到所有的 stories 即所有的故事卡片,再根据这些故事卡片逐一地去获取对应的细节。比方说这里拿到每一个 story 之后,再根据 story 的超链接去获取 story 的具体细节,拿到它的 id 和具体描述文本是什么。而在 graphql 里面,我们只需要一次性请求,就通过 query 花括号 stories 里面去定义 stories 所对应的 ID 和 text 就可以一次性直接返回 story item 的列表数据。
而在这样的一个列表数据查询的使用场景下,我们会用 dataloader 来对这个查询进行缓存。简单来说,dataloader 是一个与内存缓存配对的一个批处理算法。我们可以看到这张图,当我们的 GraphQL query 请求没有 data loader 的时候,当我们去查询这里的任务 1、任务 2。那么通过 graphql 访问后端数据库时,每一个任务的查询都需要去 database 去查询一遍。比方说它属于同一个 ownerA 对吧,都属于任务 1 和任务 2 都属于这个 owner A 而此时如果访问如果没有缓存的话,每一次请求 owner 的时候都需要再次去请求数据库。
所以,我们来看加上 dataloader 缓存之后的效果,还是请求任务 1 和任务 2 的数据,dataloader 会自动把任务 1 和任务 2 两个请求进行一个合并,并且在查询 owner A 的时候,就一次性就把 task 和 owner 的信息都返回回来了。并且还有个额外好处是,如果下一个请求,它也查询了这个任务 1,它就会直接进入到 dataloader 刚刚所缓存的这个字典里面,直接返回了任务一所对应的所有数据啦。
总结来看, data loader 它能够批处理数据请求,从而减少了对数据库的一个访问。另外,dataloader 将结果直接缓存在内存中,从而就可以完全消除请求,不再对数据库产生压力。
接下来进入具体的代码环节,让我们分别来看几个小示例。这里的代码是使用 dataloader 为 SQL 数据库建立外键。我们可以看到这个是一个 user by team ID 的一个查询场景。首先我们通过 user by ID 的方式拿到所有的 user ,在我们访问 user 的时候,就可以看到,每一个 user 都在这里的 dataloader 字典过了一遍,通过它的 userID 或获取数据,并同时将数据进行一个填装,最后再一次直接返回回去。每次只需要用这样一个简单的 get clear prime 的方式,就完成了一次数据的缓存。
(当然这里也需要特别注意,还是要注意查询深度的一个限制。因为 graphql 支持关联查询,当你有个树结构的时候,你需要去考虑限制查询的深度,避免过度递归带来的问题。)当然,在一般的场景下,dataloader 字典这样的方式就已经能够应对大多数情况了。
在实际的 graphql 请求里,会遇到这么一个问题:一个 query 可能会包含了数百种不同的类型,该怎么缓存呢?比方说一个请求,它既请求了刚刚提到的任务,也需要请求用户的信息,还需要请求团队的信息。但是一个 dataloader 数据加载器,只能用于一个实体类型。如果说,明明只请求了一种类型的 dataloader,但是却需要实例化其他所有类型的 dataloader。所以,我们该使用什么样的方式来优化它?我们就用一个 lazy 懒实例化的机制实现了一个 data loader 的字典,data loader 字典实现一个 get 方法,接受一个 loader 的名字,只有当第一次访问该数据类型的时候,才会创建对应的 data loader 也就是说不会再造成一个请求。也就是说,当只有一种类型的 query 数据请求时,只有一种 data loader 被访问时,它才会被调用,从而实现一个懒实例化。
最后一个,也就是第 4 个实践,则是给大家介绍 GraphQL 的 subscription 订阅。演讲最开始的时候,我提到过我们的产品 Parabol 专注于「在线敏捷会议工具」,能够支持实时的多人协同。 比方说,Retro 也就是敏捷回顾会议,就支持这样的多人协同。比方说很多人可以一起填写 Reflection 进行反思的卡片,然后再对这些卡片进行分组,分组的时候可以支持多人同时进行拖拽。当然,在分组之后对每一张卡片进行投票,得到一个按票数排序的分组顺序,再然后从优先级最高的主题开始讨论,讨论的时候也会支持新建下一步行动和支持 emoji 表情等等这样的反馈功能。重点是,所有的这些操作都是实时显示在每一个人的会议界面中的。
所以说,我们来看 dataloader 如何在订阅场景下对数据进行缓存的。这里的问题在于,subscription 订阅其实是长时间存在的,于是我们没有办法直接去使用 dataloader 的缓存功能。所以我们创建了一个叫 dataloader warehouse 也就是一个字典的仓库。首先,我们刚提到每一个人都可以实时地对数据进行一个修改,也就是说每一个 mutation 它在修改完数据之后,会立马返回对应的一个执行结果,包括所有的数据,通知给所有订阅者。那么在这个时间点,就可以通过 warehouse 也就是这个仓库,为每一个 mutation 都存储一个新的 dataloader 字典,当这个 mutation 返回订阅结果给订阅者的时候,相应的把这个字典 ID 给返回回去,从而其他的订阅者。除了做修改用户 A 以外,其他的用户 BCDE 每一个订阅者就可以直接通过 ID 的方式直接拿到对应的 data loader 的 ID 的字典,从而通过这个字典就直接进行 data loader 缓存查询,直接非常快速的就返回了所有需要更新的数据。
好,这就是关于 graphql 工程实践经验的分享,我通过 4 个实践案例,分别讲到了 relay 的 hook,通过 useFragment 查询数据片段;通过 useQueryLoader 结合 Suspense 优化页面的加载效果,介绍了边获取边渲染提高用户体验的一个方式;然后解释了 N+1 请求查询问题,在 graphql 里面可以使用 dataloader 进行优化,以及刚刚讲到 subscription 订阅,允许多个查询即时地返回 dataloader id 给每一个订阅者,从而每个订阅者可以直接使用它快速的返回对应的数据。
好,讲完了 How 的部分,那么接下来,就进入到 Where 也就是在哪些场景 GraphQL 的具体实践,讲一下 graphql 的真实应用案例,我还会从集成 rest API 的角度,来给大家介绍如何去做身份验证和嵌套认证,以及我们去集成 github 的 GraphQL API,因为我们作为一款敏捷会议软件产品,需要跟第三方的像是 JIRA 或者是 github GitLab 第三方 API 打交道。在最后我还会分享一个彩蛋,接下来会给大家介绍。
我们来看敏捷 5 大会议的另一个场景,Sprint Poker 计划扑克。这样的会议形态,大家可能在之前的会议中实践过,就是敏捷团队在迭代开始之前,需要给每一张卡片估点的时候,采用扑克牌点数的方式来对一张卡进行投票,表示完成这个用户故事的工作量。所以说我们就相当于把会议形态搬到了线上,并且以一种实时的方式允许每个团队成员对其进行估点,从而自动计算出投票结果。
好,那么在这样一个场景中,第一个案例就来讲一下它是怎么集成第三方 API 的。比方说这里会有一个添加故事卡片的界面,用来筛选本次迭代需要计划的用户故事。这个界面你可以看到已经集成了 github、jira 以及 Parabol 自己的任务管理器,当然还包括 GitLab 以及最近上线的微软 team 等平台,所以说 Sprint Poker 就需要去集成第三方。
从而我分享的第 1 个案例,需要解决的问题就是集成 Jira 的 REST API,这里我给了一个代码示例,集成 JIRA 的授权认证以及怎么样去刷新认证。这里的代码大家可以看到,我们也是新建了一个 data loader 的字典。在这个字典里面我们可以看到我们去拿到 JIRA 的授权信息。拿到 assets token 之后,可以给它设定失效时长,并且在失效时间内,我们就直接去直接返回了 token 那如果说它是在失效时间之外,也就是已经超时失效了,那么我们就会去请求新的 token,当然这里的 token 可以根据之前的 refresh token 直接去向 rest API 请求新的 token,从而我们就可以使用这个授权信息,去请求 JIRA 里面所有的故事点啦。
紧接着,第 2 个案例是一个嵌套查询,在嵌套查询 REST APIs 时,怎么样去复用授权信息,因为 rest API 它是有前面我们说到的缺点的。比方说这里我们同时需要去取到 JIRA 所返回回来的一些用户信息、团队信息和任务信息。那么每一个 API 是不是都需要这样 rest API 的授权信息呢?所以说我们就直接在这里新建一个 dataloader,从而在查询 JIRA 的 issue 的时候,直接拿到对应的授权信息给每一个 ServerManager,再去请求对应的 issue 也就是 JIRA 里面的故事卡片。拿回所有数据之后,再做 map 进行统一的数据处理。
好,前面 2 个案例都讲的是关于 REST API 的查询。那么第 3 个用例我们就来看一下 Mutation 修改。比如说,我们每一次更新完这个故事卡的点数之后,会需要写回到 JIRA 或者是 GitHub 对吧。因为我们会把这里所得出来的一个平均值,也就是大家给出的一个故事点的平均值填充到返回去。如果右边还会有对应的这个 comments 也就是评论信息的话,也会相应地同步回到 JIRA 或者 GitHub,所以这里就涉及到一个 mutation 操作,我们来看下这种连环的 mutation 操作是怎么样实现的。
这一次,我们来拿 GitHub 的 GraphQL API 来举例,这里可以看到,当我们使用一个 mutation 为当前会议添加成员,重点是,这个 mutation 里面会有一个 githubAPI mutation 对吧,这个子 mutation 里面也接收了我所需要添加的这个团队成员,并且去拿到对应的这个成员的头像。这样的话我们就可以在右边的更新故事点的同时显示所有人的头像了。那这个时候就可以通过这样的方式一次性就把应用的数据更新了,也同时把 github 的 API 数据也更新了。
除了嵌套修改,GraphQL API 的 query 查询也是可以嵌套的,直接通过 github 的 schema 就能够查询到 github API 里面的 query 里面的 user 的相关信息,上方是我们自己的应用程序的 Schema,内嵌的是 GitHub 的 GraphQL Schema。
你可以看到它非常简单,我们只需要输入 user 的当前 query 的 githubAPI 数据,就可以去拿到用户下面的所有子任务,并且它也是可以复用 user 的头像信息的 Fragment 数据片段的,所以说可以一次性把内部的和外部的第三方数据全部返回来。
那么,这里是怎么做到的呢?GraphQL Schema 的集成,相当于做了数据聚合的大一统。而这个大一统是由 Parabol 的开源项目 nest-graphql-endpoint 来实现的,从而能够在自己的 graphql 的 API 里面内嵌其他的 graphql 的 API。使用这样一个开源库之后,我们可以通过 nestGitHubEndpoint 集成 GitHub 的 Endpoint,得到一个 merge 的 schema,这个合并的 Schema 就可以同时显示在我们的 API 文档里面,我们可以还通过这个活文档直接查询到 github 里面有哪些 API 字段。当然,也可以查询我们自己的应用的 API 字段,在 merge 的 GitHubAPI schema 里面,还可以直接使用 GitHub 的 query 和 mutation 操作,是不是变得非常简单?如果大家感兴趣的话,可以通过地址点到我们的 github 的开源项目地址。如果能帮忙点上一个 star 就更好了。除此之外,顺便提一句我们公司的所有代码,包括主应用的代码都是开源的。
分享到这里,就进入到我们最后的总结部分啦。graphql 和异步协作,前面也提到我们所有的代码都是开源的,另外我们的所有的团队也是全球分布式的,这张图里面是非常有代表性的(但这张图里面没有我),每个人都拿着自己的护照,也就意味着我们所有的开发人员、产品人员都是来自于不同的地区,来自于不同的洲。
所以,这带来了潜在的两个客观条件,第一个是跨地区,大家都是完全地远程办公,我们公司没有实体的办公室。第二个是跨时区,大家在沟通的时候只能采取异步协作的方式,就是说你发了消息,我不一定今天就能够看到回复你。相应地,这给我们的开发带来了很多好处,比如说你可以专心写代码,深度工作,同时也能够享受生活。
顺着我今天演讲的话题,我想给大家讲一下 graphql 是怎么样对异步协作有所帮助的。协作需要沟通,开发工作的沟通需要文档,比方说我们这里会有一个 graphql 的活文档,左边是 explorer 就是我们可以通过字段的 schema 定义来找到所有的字段的名称和它所对应的数据结构,中间则是 query 输入查询语句的地方,可以通过你所定义的 schema 获取到对应的返回数据,而且数据都是可预期的结构,直接返回回来啦。
那么,我们可以看到这样的一个活文档,它在实践的时候会有什么样的好处呢?首先它是减少了前后端来回沟通的成本。因为所有的 API 我们都可以从前端直接定义它的数据结构,并且直接返回回来。从而从根本上就解决了这样的情况,前端说今天需要开发某个页面,需要等待后端 API 的完成,从而前端就可以直接按需地请求自己所需要的数据。并且,在整个页面层级只需要访问一次 API 即可, 除此之外,每个组件都可以去定义自己的数据需求片段,片段也是可以自由组合的,也就是意味着每个开发同学只需要去定义自己所需要的组件的数据就可以了,尽可能减少了不必要的沟通。而这些 fragment 数据片段的自由组合和聚合以及数据的分发,那都是由 relay 自动化的去实现和处理。
第二点在于 API 文档的更新,大家应该都深有体会,文档如果需要额外去写的话,那么更新就一定会不及时甚至干脆就不写文档了。这就是困扰我们大多数前后端协同开发时,令人头痛的地方。明明已经定义了 API 数据结构,那数据结构发生变化之后,每次又得反复不断地去通知前端同学:诶,我这儿有一个文档上的更新,并且很多时候更新并不可怕,可怕的是你更新了哪个字段以及哪个字段,它的数据类型发生了变化,从而导致了很多不必要的 bug。因为在传统的 rest API 当中,每个字段它是没有数据类型的定义的。这也就意味着,如果我把一个 number 从原有的 int 类型直接改成了 string 类型,那这个时候,前端因为没有做适当的格式化,导致不必要的 bug。所以呢,如果说文档更新不及时,或者更新不准确,那还不如不写文档对吧。
那么在 graphql 这样场景下,当我们把 graphql 的 schema 定义完成之后,我们就相当于有了一个现成的活文档了。而活文档可以直接有一个直观的好处 —— 你不再需要去写文档,而文档对于所有人都是可访问的。我举个小例子,比方说我们的销售同学也是可以直接用文档来做一些权限的控制处理的。比方说对于一些企业用户,他需要有一些更新的 Future 特性,比方说我们最近开发了新的 standup 站立会议的功能。在做市场推广的时候,销售同学可以手动通过 API 的方式去给客户开启对应的权限,他们不需要有任何开发人员的帮助,就能够在文档上面完成他所需要的工作。所以说文档的使用成本是非常之低的,我们团队内部的所有人都是可访问可操作的,当然也是可以给操作人设置权限的。
最后一点,我想强调一个概念,叫做使用代码直接来沟通,在 DevOps 领域经常说的,Infrastructure as Code 基础设施即代码,其实本质上也是「使用代码直接沟通」。当我们减少了不必要的很多冗余的沟通以外,我们真正需要沟通的地方就会变得少了很多。在 GraphQL API 的场景下,我们直接定义好 schema 数据结构以及这些 mutation 操作的接口具体参数是什么样之后,都可以直接在 schema 上就体现出来了。而我们正常地使用代码进行提交时,会产生对应的 pull request,此时所有的开发人员就基于 pull request 所提交的代码,进行直接的反馈评论,这样其实大大加速了我们开发的过程。Schema 优先一定是未来的研发模式之一。
总结下来,「软件开发是一场社会活动」,而社会活动需要大量的沟通,沟通是可以同步的、也可以是异步的。而我们这样分布式的团队,在沟通时采用 schema 优先的这种异步方式,会大大改进我们的沟通效率,减少不必要的浪费。并且,异步带来的其他好处大家已经不言而喻了,因为作为开发人员或管理者,相信你们最讨厌的也就是开会。如果有同感,还可以去听明天早上李小波老师的演讲,分享他在全球分布式团队对于异步协作实践的探索。
那么今天呢,这就是我本次分享的所有内容啦,我为大家介绍了什么是 GraphQL,为什么要使用 GraphQL,GraphQL 如何解决按需加载数据的问题,如何助力敏捷组织快速响应变化;以及,我介绍了 4 个关于 GraphQL 的具体工程实践经验,4 个真实场景下的 GraphQL 应用案例。
相对来说,GraphQL 在国内的使用成频率还不是那么高,所以说如果大家对 GraphQL, 对 API 的未来以及对敏捷组织如何快速迭代感兴趣的话,可以加我的微信进一步交流,左右两边的二维码都可以扫,😁。
感谢大家的聆听,同时也特别感谢 K+ 创新峰会的组织和安排,我也从这次峰会当中学到了非常多的东西。最后呢,祝大家都能写出没有 Bug 的代码,我的分享就到这里,谢谢大家!
最后更新于