著作权归作者所有。商业转载请联系 Scott 获得授权,非商业转载请注明出处[务必保留全文,勿做删减]。
Scott 近两年无论是面试还是线下线上的技术分享,遇到许许多多前端同学,由于团队原因,个人原因,职业成长,技术方向,甚至家庭等等原因,在理想国与现实之间,在放弃与坚守之间,摇摆不停,心酸硬扛,大家可以找我聊聊南聊聊北,对工程师的宿命有更多的了解,有更多的看见与听见,Scott 微信:codingdream。
本系列共 18+ 篇(最开始 15+ 篇,未来会扩充到 30 篇),此为第六篇预热[对旧文章进行了更多观点的更新和补充],大家感兴趣后面文章可以点下关注,再转发下朋友圈我就心满意足了。
正文开始
中国铁路大提速是指由 1997 年至 2007 年期间,中国铁路一共进行了六次大面积的提速,提速前的 1993 年,全国列车平均旅行速度仅有 48.1 公里 / 小时,到今天,中国着眼于建设高速客运专线,使其最高速度达到 350 公里,如今庞大的高铁协调系统复杂程度远超过往,而调度运输效率同样翻天地覆。
技术革命会促使组织结构发生巨大的变化,也会给组织间的协调方式带来新机遇和新挑战,这一点在互联网公司同样成立。我们提到互联网公司,总能联想到敏捷、快、轻、颠覆这些字眼,而事实上除了头部巨头公司的部分部门和部分小而美的中小创业团队,真正能做到敏捷的公司并不如想象中的多,这些公司不仅缺乏敏捷和效率才能持续优化组织升级的思维和意识,更多是缺少自我革命推倒重来的勇气,所以 996 横行,只能靠人力和时间来弥补效率和能力的不足,来实现所谓产品上的快速迭代。
实际上产品上能实现连续正确的快速迭代,影响它的因素是有很多的,比如:业务团队的决策和执行、产品经理的行业功底、项目经理的管理能力、工程师的项目经验、前后端之间的协同等等。从顶层的战略设计,到管理层的理解与推动,到一线的过程设计和执行,任意一个环节掉链子,都会导致产品的迭代出现方向上或者速度上的问题,而工程师的视角容易自前向后自左向右自内向外,往往缺少自上向下的透视,我们重点看前后端协同这块的优化路径。
研发流程的核心人物是 PM
这是小菜技术部我们所遵守大项目研发流程,它往往是线性的:
其中在开发阶段,也是跟前后端工程师密切相关的就是在数据对接这一层有较大的合作成本,我们把这个线性的过程再绘制如下:
来看红色的几个,系统设计,里面涉及到 Java 服务端工程怎么搭建、骨架怎么搭建、服务怎么拆分,最后具象的时候,是服务端同学设计这个数据库和表结构,这几张表上面的字段有哪些。然后接口设计,服务端同学会给出接口的文档,比如说会给你提供五个接口,每个接口 15 个字段,才会进入到前后端对接完之后,再帮你去做一份 Mock 数据,然后前端在页面上把页面样式重构之后,再去调假的 Mock 数据,然后页面交互流程调通之后再切到正式接口,大概是这个套路。这里面有很多工具栈,很多第三方开源的工具可以用,那我们发现这里面前后端堵塞在这个点,而且堵塞很多年也解决不掉,因为接口设计控制权在服务端手里,前端不知道会给到多少接口,然后在接口评审时候,15 分钟或者一个小时,前端基本上很难理解哪个字段背后的业务含义,过后还要和服务同学反复再沟通确认,就因为在这个点的堵塞导致上面三个问题不是很好解决。
往往产品迭代出了问题,就有可能就是上图中某个环节出了问题,所以我们会需要强势的靠谱的 PM 做更严谨的项目管理,同时从技术团队也要给产品经理和运营施加压力,来挤压出最真实的需求,这里对项目经理业务能力和项目能力是重度依赖的,越靠谱的项目经理,这里就有越高效合理的项目迭代,反之可能就坑坑无数。
通过这里引入更靠谱的项目经理,我们可以大大降低前后端以及研发前置和后置流程中的风险点,迭代也会更有节奏感,再往下到数据接口层面的对接和 UI 上的透出效率,项目经理就爱莫能助了。
展示层协同与效率的成本
结合多端展示多样化报表的场景,最初研发成本居高不下,从前端的视角看过去是这样的:
第一个是多端之间的类报表同步,大家现在开发前端,可能开发小程序,APP,PC,会有多端,对我们公司场景来说我们这个端比较杂,可能要在 ERP 要透出报表,在 App 上透出报表,在小程序上透出报表,但是面对不同权限的人报表透出的维度不一样,但本质上下面的数据源是同一份,那我们就可能开发很多个接口去对应不同的 APP,那这个纯靠前端做数据切片组装不太现实。
第二个是多端之间多模块共享,这个模块还不是组件,比如有一个用户模块,一个订单模块、一个物流模块,每个模块里面可能会有一两个组件来组成,但向下所需要的基本数据可能还是同一份或者同两份,只是在不同端上的 UI 呈现不一样。那我们想要让这些模块之间去共享数据很难,我们在不同端上开发一个组件就绑定一个接口,这个组件拿到另外一个里面去,套到那个模块去用的时候发现行不通了,这也是一个很大的成本。
第三个是业务变化快,产品总要升级迭代,UI 重构或者交互改版的时候,加一个字段,减一个字段,或者叠加几个字段合并后的状态值,那么接口又要升级,或者加一个新接口,这个同样导致合作成本很高。
把三个问题再抽象一下,其实就是这三个:
第一个是 API 的设计,服务端同学有的时候会被迫也好,被“强奸”也好,我必须要面向你多变的 UI 去做 API 的设计,面向这些页面服务,页面变化的时候,API 可能也要升级。
第二个是 Mock 职责重合,之前也知道业界很多公司自己做了 Mock 工具和平台,我们一直也是在使用第三方的,有时候是前后端共同维护同一份 Mock 接口,有的时候是服务端去维护,但是总之要存在一个协作成本,到底谁来对它负责,这件事情,到现在也说不清楚。服务端同学给你做完 Mock 之后终于可以沉下心来做底层的业务开发,但发现临时需要调整一个字段,就把接口调整了,但是忘记去更新 Mock 文档,前端不知道这个事情,到后面俩人一对接发现字段对不上,这就是一个典型的工作流协作问题。
最后一个问题,这个对于 toB 公司,或者 toC 也有这样的场景,就是报表,报表可能是一个刚需,对于管理层其实需要看到过去一周一个月公司交易的整个规模、吨位、物流情况、库存情况,不同维度的数据观测。业务打法一变报表的维度也要变,传统的报表开发就前后端各一个,服务端搞定数据库,跨表跨库查询,给出标准的字段结构,前端就是把它套到 Table 表格里面去,这个事情很简单,但是可能要排期,一天两天三天,可能产出报表的速度就很有限了。
GraphQL 作为解法是否可行
我们在协同和效率上,最初尝试的解法就是通过 GraphQL,从 2017 年开始小型测试到 2018 年上半年一定规模的推行使用,在这个过程中也跟业界的童鞋有过不少交流,当时收集了如下这些问题:
针对这些问题,我们结合后端 DDD 的设计,尝试在内部前后接口的协同上找突破点,后来在网关这一层,集成 GraphQL 的一个聚合服务,我们理想中的 GraphQL 接入方式,是跟网关同层嵌入在里面做一个管道,但是最终实现的时候,暂时把它放在网关的下面,主要考虑到跟网关的兼容开发成本,比如鉴权安全这些事情都交给网关去做了,它纯粹的做数据聚合,见下图:
针对刚才的问题,我们先把宋小菜在自己的业务场景下,技术的解决方案拿出来给大家看一下。我们现在解决方案是在网关这一层,集成 GraphQL 的一个聚合服务,第二场架构师会来讲具体架构图,这边单独讲这一个点。我们理想中的 GraphQL 接入方式,是跟网关同层嵌入在里面做一个管道,但是现在我们的实现方式呢,考虑到快速跑通,暂时把它放在网关的下面,是为它的鉴权跟安全不想占太多开发成本,把它交给网关去做了,所以它就只做数据聚合这么一件事情,通过系统改造,我们也获得了一些收益:
那么 2016 年 2017 年差不多 2 年多时间整个公司开发的报表一共才 50 张报表,报表开发速度成为一个瓶颈,当我们通过 GraphQL 在端上透出以后,包括服务端去一些拼装的动作,现在提供了可视化报表编辑的系统,这个系统面向产品经理和运营,面向服务端工程师,他们通过可视化的界面配置一下,报表就生成了,报表开发效率直接解锁,这个系统上线以后四个月就产出 200 多张报表,把整个公司报表需求全部消化掉了。
有一个有意思的案例,产品经理跟业务方开会,业务方说:我需要看一下某个服务站一周的报表数据,我要提需求,我需要这个指标这个指标。然后会还没开完,产品经理就把报表做完直接上线了,现在报表产出就是这么一个节奏。通过做这个系统我们发现 GraphQL 可以给我们带来很大方便,我们就继续往下挖,把 GraphQL 它的价值从 APP 端继续往下沉,沉到服务端,我们就做了大舅子,前面的报表系统是大(搭)表(表)哥(格),是搭 Excel 表格的一个谐音,这边是大舅子,公司的产品啊,他们都是亲戚。
大舅子作为今天我们讨论的一个前后端数据组装方案,通过它一个通常需要 4 天的单人小日常小项目,现在可以缩短到 3 天,参与的前后端人越多,多人对接成本降低就越多,通过这个解法,前后端各自的职能也发生了一些演变,同时对于两个工种都有正面的一些影响:
那关子卖完了,我们聊聊如何搭建这样的一个系统,也会简单给大家科普下 GraphQL 的知识:
最开始先看一下大舅子的这个数据聚合系统,在我们整个系统架构里面到底处于哪一个位置。从图示中可以看出它处于我们的网关和后端数据服务的中间,刚刚 Scott 也解释过,我们网关本身已经存在了,所以它已经把如鉴权和安全一类的事情做掉了,所以我们在做数据聚合服务的时候,将这个服务放到网关后面。为什么取名叫 GPM,它全称叫 GraphQL Pipe Manager,这是对一个对后端服务提供的数据提供数据拼装的这么一个系统。
这是我们整个 GPM 内部架构大概一个结构图,分成两部分:一部分是正式的服务,可以看到正式的服务比较简单,因为要保证提供正式数据服务稳定,所以我们尽量简化它。另一部分稍微复杂一些的,是开发服务,在开发服务里面我们可以对类型进行编辑管理,然后在开发服务上进行测试,最后应用到我们的正式数据服务上。
介绍完我们使用 GraphQL 的大致情况后,鉴于在场的部分同学之前可能没有接触过 GraphQL,所以我先来介绍一下什么是 GraphQL,GraphQL 全称叫 Graph Query Language,官方宣传语是“为你的 API 量身定制的查询语言”,用传统的方式来解释就是:相当于将你所有后端 API 组成的集合看成一个数据库,用户终端发送一个查询语句,你的 GraphQL 服务解析这条语句并通过一系列规则从你的“API 数据库”里面将查询的数据结果返回给终端,而 GraphQL 就相当于这个系统的一个查询语言,像 SQL 之于 MySQL 一样。
使用 GraphQL 能带来哪些便捷
宋小菜经过一个时间不是很长的实践,发现使用 GraphQL 给我们带来五点的比较方便的地方:一个是单一入口,第二个是文档的展示和编写,第三个也是比较有特色一点,就是数据冗余可以使用 GraphQL 来避免,第四点数据聚合是这一个系统本身最主要的职能,还有最后一点就是数据 Mock,Mock 相当于比较棒的附加值。
单一入口
首先说一下单一的入口这一点。传统的 RESTful API 里,不管前端还是后端都要对 API 做管理,一是版本管理,二是路径管理,非常麻烦,增加了工程管理的复杂度。但是如果使用 GraphQL,只需要一个入口就可以了。刚刚也说到 GraphQL 相当于一个数据库,它的入口只有一个,我们只需要访问这个入口,将我们要查询的语句发送给这个入口,就可以拿到相应的数据,所以说它是一个单端点 + 多样化查询方式的这么一个结构。
文档化
第二点是文档,这里文档虽然不能完全替代传统的文档,但是它能在一定程度上方便我们。传统的 RESTful API 文档管理,市面上有很多工具,像 Swagger、阿里开源的 RAP 以及 showdoc 等。但使用这些 API 文档管理工具的时候其实是有一定的学习成本的。像 Swagger,可能对于老手来说使用起来不是很复杂,但是对于刚上手的开发者来说上手还是需要一点时间的。然后还有在使用这些平台的时候都会遇到让人头痛的“API 和文档同步”的问题,很多时候需要自己去做 API 和文档同步的插件来解决。
如果使用 GraphQL 就可以在一定程度上解决 API 文档的一些问题:在做 GraphQL 类型定义的时候我们可以对类型以及类型的属性增加描述 (description) , 这相当于是对类型做注释,当类型被编译以后就可以在相应的工具上面看到我们编辑的类型详情了,像示例的这一个类型 Article,它的描述是“文章”,它的属性有哪些,有什么含义,都会展示在大家面前,只要我们在开发的时候规范编写类型,整个文档的展示就比较规范了。
使用 GraphQL 还有一个比较棒的功能,就是每一个 GraphQL 类型 其实相当于 mongo 里面的一个 collection,或者 mongoose 里面的 Model, 而每一个类型之间关系也可以用工具很形象的表现出来。像系统上用到这么一个模型,它对应到哪些和它有关系的模型都高亮出来了。
链接:https://apis.guru/graphql-voyager/
这里演示一下,可以传送看一下,在 Github API 4.0 开放出的 GraphQL API,它将 Github 所有的对外类型都暴露出来了。可以看到每一个类型对应的定义和解释都在左面有显示出来。每一个类型都有对应的 UML 图展示,这是一个比较大并且比较复杂的 UML 关系图。我们主要平时用到一个核心的类型其实就是仓库 (repository) 类型,我们可以看到这个类型比较复杂,同时它也是比较核心的,和它有所关联的类型就非常的多,仓库类型下还有 issue 这个属性。如果我们参考 Github 开放的 API 4.0,就可以做到在 Github 上面开发相关的插件。
避免数据冗余
使用 GraphQL 的第三点好处就是可以避免数据冗余。我们在传统的 RESTful 处理冗余的数据字段大约有这么三种处理方式:
- 一是前端选择要不要展示这些字段;
- 二是要么做一个中间层(BFF)去筛选这些字段,然后再返回终端来展示出来;
- 三则比较传统也比较麻烦,还不一定能生效,就是前端和后端去做约定,如果说这一个接口这一个字段已经不要,可以和后端商量一下把这个删掉,但是有一种情况可能造成冗余字段删不掉的,那就是后端的同学做这个接口可能是“万能接口”,也就是说这个接口在这个页面会用,在另外一个页面也能用,在这个应用会用,在另外一个应用也可能会用,多端之间存在部分数据共享,后端同学为了方便可能会写这么一个“万能”的接口来应付这种情况,久而久之,发现字段冗余到很多了,但是随便删除又可能会影响到很多地方,导致这个接口大而不能动,所以前后端都不得不忍受它。
但如果使用 GraphQL,就可以避免接口字段冗余这个问题,使用 GraphQL 的话,前端可以自己决定自己想要的返回的数据结构。刚刚我也解释过,GraphQL 实际上是一种查询语言,我们在使用时就像是在数据库里面查询数据一样,查询的某一个数据要哪些字段可以在查询语句里写好,要哪些字段就返回给我们哪些字段。
拿 PPT 上这个作为示例:我们要去拿 id 为 1 的文章,如果我只要 id 和 content,我在 query 里面指定这两个字段,那么返回的就是 id 和 content,如果除了 id 和 content 之外,我还要拿需要作者信息的时候,我只需要在 query 里面指定 author , GraphQL 就将作者的信息给返回回来。这样就能做到前端决定自己想要什么结构的数据返回的就是什么样的数据。
多种数据聚合
最重要一点当然是数据聚合,数据聚合在使用传统的 RESTful 的方式时有多种解决方案:
一种前端发针对这个页面上的多数据源单独发起数据请求,然后一一展示出来,这样可能会出现页面数据加载不同步的情况。
第二种就是开发做数据拼装的中间层(BFF),用于拼装后端提供的数据,然后返回给前端。
还有一种是宋小菜在最前期的使用一种方案,那就是后端同学编写针对页面的 API,即所谓胶水代码,来拼接各个服务的数据,返回给前端。
如果是第三种情况的话,就会有大量的工程需要我们去维护,大量的 API 需要我们去维护。但如果使用 GraphQL 的话,这些问题都不会存在,因为它是天生支持数据拼装的。
为什么它是天生支持数据拼装的呢?我来尝试着从 GraphQL 执行的原理上大概解释一下。这是个 GraphQL 执行的大致流程,第一步我们去验证需要去执行 GraphQL 的标准,同时去验证将要去查询语句的合法性,第二步生成执行的上下文,关键点在第三步和第四步,第三步是获取查询语句所需要查询的字段,这里叫 fields,所有需要查询的字段可以在查询语句里通过算法拿到,这里可以解释刚刚提到的 GraphQL 怎么做到避免返回数据的冗余的。拿到所有需要查询的字段后,第四步针对每一个字段去执行它的 resolver,可以从 resolver 返回数据里面拿到字段对应的数据,最后是格式化结果并返回。
第四步我解释一下,在 GraphQL 里面有一个类型的概念叫类型(type),每一个类型下面对应的是一个或多个字段,每一个字段绑定了一个 resolver,这个 resolver 的作用就是获取字段对应的数据。对应到刚刚举的例子,比如 article 这个类型,它有四个字段:id,author,content,comment。每一个字段都对应的一个 resolver。而这个 resolver 其实是可以被开发者重新定义的,如果说没有定义的话 GraphQL 会给一个默认的 resolver,像 Article 的 author 字段类型是 User , User 可以从用户服务里面去获取,所以我们可以将 author 这个字段 resolver 重新定义一下,通过 UserService 获取用户信息。下面的评论(comment)也一样,我们可以通过 CommentService 获取评论数据,这样可以做到在查询这个文章的时候既获取了文章本身的数据,也通过 UserService 和 CommentService 获取到了作者信息和评论信息,然后经过拼装返回给客户端,这样就达到了使用 GraphQL 进行数据拼接的目的。
方便数据 Mock
第五点是附加的一点,我们可以适当地利用 GraphQL 做数据 mock。那么使用 GraphQL 怎么去做到 mock 呢?
GraphQL 的类型大致可以分为两种类型:
- 一种标量类型,像普通的开发语言一样,提供 Int,Float,String 这种标量类型,这种类型在 GraphQL 中也对用着一个 resolver,我们可以通过重新定义其 resolver 来做到对标量类型的 mock, 像 Int 返回的范围是什么,Float 返回的范围是什么?String 返回的格式是什么样的?等。同时我们在开发中常用到的一些简单但是有一定规则的数据类型像手机号码、图片地址、身份证号码、身份证号码这样的数据我们也可以通过自定义标量类型来做到数据 mock。
- 第二种是普通类型,像刚刚示例中的文章(Article)类型,普通类型下面可能会有多个字段,每个字段对应的数据类型可能是普通类型也可能是标量类型,这种类型也可以做 mock,如果我们对标量类型做了适当的 mock 以后,像 Article 的 mock 数据就会自动生成。
使用 GraphQL 做数据 mock 还有一个方便之处在于经典 mock 数据可以被很方便地复用。如刚刚示例中查询 Article 下面类型为 User 的 字段 author 的时候可以利用这个特性,因为用户信息(User)这个类型不仅仅会用于文章的作者也可能会用于评论的作者,所以我们针对 User 类型做一个 mock 数据,这个 mock 数据可以会在查询文章作者和查询评论作者中同时用到,同时我们也可以在返回 mock 数据时耍一些小花招,例如从几个用户数据中随机返回一个用户信息,或者根据查询条件返回对应的假数据等。
使用 GraphQL 做数据 mock 有多方面的好处:
- 好处之一就是 mock 数据随着类型 (type) 走,当我们修改类型以后,它的 mock 数据也是会被同步修改,不会出现 mock 数据和类型不同步的情况;
- 好处之二就是能很容易地实现 mock 数据的细粒度,原理刚刚也解释过了,这样能够很大提高我们的开发效率。
- 好处之三是 mock 数据可以复用,节约开发时间。
- 最后一点,那就是 mock 数据的职责可以由前后端共同承担。或者说由前端自己来做,因为通常情况下 mock 数据的消费者都是前端自己,为何不自产自销呢,省去大量的交流成本。
GraphQL 数据聚合系统形态
这里简单做一个演示,展示一下 GPM 的最终形态:
在 GPM 中每一个生成类型都会以表单的形式展现出来,当然代码的形式也会有特定的地方呈现,我们只是对每一个类型都进行可视化,如果作为一个新人来使用,只需要点击按纽添加类型,指定类型名字,填写类型描述,根据类型的实际情况设置缓存有效时间,绑定到宋小菜的哪些 APP。然后针对已经添加好的类型可以对它做字段的添加的操作,指定字段的名字、类型、描述、缓存有效时间,以及 mock 数据。
同时这一个系统可以直接在线上测试并发布类型的:我们在编辑好一个类型以后,可以部署到开发环境上,然后在 IDE 里面做调试提前查看返回数据是否正确。像刚刚说到的处理数据字段冗余是怎么做的,这里可以演示一下,在前端不想要这一个字段时,直接在查询语句里面删掉然后执行查询就能拿到不包含这个字段的数据了。我们也可以通过 IDE 获取这个查询语句结果的 mock 数据。
在写查询语句的时候这个 IDE 根据我们已经生产的 schema 自动帮我们提示,就像使用普通的桌面 IDE 一样,而每一个类型的文档可以从右边的弹窗里面看到。GPM 将 IDE 分为了正式服务和测试服务的 IDE, 正式 IDE 时针对线上数据做查询的。我们在测试好了新增或者修改的类型以后就可以部署正式环境上了,不用重新发布 GPM 就可以做到。
这是刚刚提到文档展示,GPM 也集成进来了,可以看到这些类型有哪些,然后这些类型到底有什么含义,类型和类型之间的关系是什么样的,都可以在这里很方便的去查看。
在 GPM 上我们还做了一些附加的功能,因为我们后端提供的微服务大多数是用使用 RSETful 的方式去调用的,所以我们特意做了一个针对 RSETful 请求的追踪,这里可以看到每一个 RSETful 访问的情况。
最重要的其实是对每一次 GraphQL 查询语句的追踪。可以看到,像我们执行这么一个查询语句,拿到的数据结果,执行时间,这一个查询语句的详情都能看到,同时还可以看到每一个字段查询速度如何。又比如说,像这一个接口,它绑定两个服务,一个服务是囤货单的服务,还有一个服务是供应商信息服务,这样子可以看到每一个查询字段它追踪到这种执行效率怎么样的,可以根据这个查询结果来告知后端同学做优化。
还有我们的部分自定义 Mock,这就是整个 GPM 大概的样子。因为时间的关系,我就只稍微说一下我们是怎么去实现在线编辑部署 GraphQL 的。
如何实现 Schema 的可在线编辑
GPM 是使用 Nodejs 搭建的,所以这个方案是针对 nodejs 的,其他语言的解决方案需要大家自己去探索了,实现这个功能有以下几个关键点。
关键点之一是替换 schema,实际上 schema 可以被修改的,只要我们使用特定的方式将每次执行的 schema 修改掉,那就做到了每次执行 graphql 时都会使用到最新的 schema 了。
关键点之二怎么做到修改已经在使用中的 schema:我们将 GraphQL 的 schema 分为两部分:一部分是类型定义,另一部分是 resolver。前面也提到过,每个类型下面有字段,每个字段下面绑定了 resolver,我们其实可以把类型定义和 resolver 分开来,同时对 resolver 进行适当的分层。GPM 的分层结构是这样子,但这是我们自己的这种分层,其实还有其他方案,后面的讲师会讲到。然后我们将 resolver 和 type 定义做好以后,将它使用一些开发工具将它绑定起来,就生成了这么 GraphQL 的 schema。
typeDefs 的本质是 String,关键自傲与如何动态生成 Resolver。
在做查询时候就参考 schema 来做,type 定义本质上是 string,关键的一点就是怎么去动态生成 resolver,也就是第三个关键点,这里稍微简单讲一下。
实现这个生成工作需要这么几个关键环节:
- 替换 schema
- 将 typeDefs 和 resolver 抽象为数据模型
- 充分利用 context,简化 resolver,便于动态生成
- 进行参数提取
结合上图,我们首先需要去简化 resolver,resolver 本身它的形式是固定的,函数签名其实就是这样,类型下面字段名字,字段名字下面有四个参数,然后返回结果:
- 第一个参数是父类型的查询结果,我们有可能会使用到它类型下面的一些查询的数据
- 第二个是指定的查询参数
- 第三个最就是我们刚刚提到的执行上下文(Context),我们可以在执行上下文 (Context) 里面去调用绑定的各种服务。
这就是是 GPM 中 resolver 大致形式,第一步拼装参数,第二步使用执行上下文调用服务,就可以动态拿到数据,这样就可以做到动态生成 resolver。
使用 GraphQL 数据聚合面临的问题
我们在使用 GraphQL 的时候有一些无法避免的问题是需要去解决的,这里有两个绕不开的问题:
- 第一是安全问题
- 第二是慢查询的问题
针对慢查询,在终端已经有很多用缓存去解决这个问题的方案了,像 apollo、relay。还有就是在 GraphQL 服务里面去做缓存,apollo 提供的 apollo-engine 就是这种方式,但这个要翻墙才能用,所以只能用来它作为参考。还有一种方案是宋小菜在 GPM 里面用到的:合理地利用 GraphQL 提供的指令 (directives),去置换 resolver,这样做到 GQ 服务数据的缓存。还有使用 dataloader 来批量处理多次重复查询。
如何做数据搜集
最后有一个让人眼前一亮的玩意,就在做 GraphQL 的时候有一个数据收集,在 GQ 生态里面有 GraphQL-extension,用起来非常好用,我们可以参考它来做一个自己的 extension 追踪 GraphQL 查询语句的执行情况,我们也可以使用第三方工具来做,如 apollo 的 trace。
最后来一起开个脑洞:
GraphQL 本身其实是一个标准,我们没有必要一定要使用官方 提供的 GraphQL 引擎,我们可以根据自己的实际情况去实现自己的 GraphQL,重新回到 GraphQL 的执行的一个流程,我们在实现自己的 GraphQL 引擎时可以做到以下优化:
- 相同的 query 是不是每次都需要 Vlidate
- 相同的 query,它们的 fields 是否也不用重复 collect
- execteFields 是否还有优化空间
相同的查询语句其实没必要每次都去做验证,这里可以节约一点点查询时间。既然是相同的查询语句它的这种字段收集其实没有必要再去做收集,可以用一些比较简单的方式去避免重复 collect fields。还有比较提升性能一点,官方的 graphql-js 去执行每一个字段的 resolver 时是循环串行执行的,有没有可能做到针对实际情况适当地并行执行 resolver。
小结
最后做一个总结,当宋小菜在使用 GraphQL 尝试去优化前后端效率的时候,概括起来有以下六个特点:
- 单一入口,单端的入口方便前端做工程管理,避免后端做烦琐的 API 版本管理
- 文档,这个文档可能在一定程度上能够解决文档同步的问题和前端开发阅读的问题
- 数据冗余比较方便,减少前后端交流成本
- 数据聚合。GraphQL 天生支持数据聚合。因为每一个类型绑定 resolver,所以定义不同的 resolver,就可以拿到不同服务上的数据,可以做到不同类型数据源的数据拼装
- MOCK,适当将 MOCK 的职责交到前端,或者前后端一起维护,而且维护起来比较简单。所以说 MOCK 方便我们开发
- 动态编辑做到实时部署,敏捷开发。实时部署很快做到线上数据的响应
而对于宋小菜的前后端合作工作流,直观上可以看到这几个变化:
- 前端从接口设计环节,向前介入到服务端的系统设计中的库表结构评审环节,此时不仅能了解到库表的字段分布和业务含义,也能在库表设计上就提出一些建议,帮助服务端输出更友好的字段类型和结构给前端,比如 精度和维度,这两个是分开存,还是用逗号隔开,存一个 String,是有分别的
- 服务端省去 Mock,省去胶水 API 的设计和维护,省去 Mock,节约的时间可以专心做底层基于业务的系统拆分,提供更稳定的数据服务,构建更健壮兼容的底层架构
- 前端在接口评审之前,就可以在 GraphQL 的自定义类型 Mock 上抽象大部分的字段出来(服务端一但确定库表结构,后续改动的可能性就会很小了),此时就可以把 DOM 页面实现后,把占位符的字段就填进去了大部分,最终结构上在接口评审环节双方针对接口特殊性,再核对调整一遍就好了
- 前端由于有服务端领域边界的支撑,可以针对特定领域及领域的组合,来封装更有弹性的组件,组件的扩展性可以由配置决定,而不是某一个 API 决定,这个配置向下就是 GraphQL 的聚合能力
关于第 3 点,是需要前后端不断磨合的,关于第 4 点,我们仍然在探索尝试,最终想要表达下我们前端团队的做事的一些理念,我个人认为这一点很重要,尤其对于初创团队。团队里面,无论事情看上去是属于谁的,最终事情一定是公司的,无论一个技术推广影响到谁或者撼动了谁的所谓原来立场所代表的利益,只要对公司研发团队效率有利,有利于技术演进,有利于推动业务更快的走,那么就要果断尝试。最终,为我们所有人的行动买单的是公司,但最最终,依然是我们自己。
这是 2018 年 6 月份我们在 GraphQLParty 上的分享,时至今日我们依然在不少的业务场景中应用,但也遇到了实际的困难,最大的困难是团队人手的紧缺,导致我们始终无法更深度的去优化和重构它,所以它的价值一直未得到最大化,但我们心里一直有准备继续做这件事,如果你对 Node 比较精通,对前后端协同感兴趣,欢迎来我们团队一起在这个方向上继续沉淀。
最最后,本文作为预热篇,旨在针对如下话题为大家输出:
把团队蛮荒到自动化运维的从 0 到 1
成长历程总结输出给社区,帮助更多的小团队少走弯路
以一种可被量化的方式汇聚小菜前端的困惑、沉淀与方法路径,给团队带来更多创作成就感
从更多视角侧切进入团队管理 / 技术演进 / 个人成长的过程中,探讨工程师团队的价值最大化
如果大家感兴趣,我们小菜前端团队,会集体智慧共同凝聚,一起撰写并推出一本偏前端职业生涯、技术成长和团队成长的小册,回馈给大家,大家在文后记得留言评论和提需求哦,还有别忘了加 Scott 微信哈:codingdream。