杭州 GraphQLParty 第五场-GraphQL 基于 SPA 架构的工程实践文字版

本文为 2018 年 6 月 9 日,宋小菜与 Coding 共同举办的第一届 GraphQLParty ,下午第五场国内某大型电商前端开发专家邓若奇的演讲稿,现场反响效果极好,对于想要尝试 GraphQL 和在公司初步实践的团队有很大的借鉴意义。

image.png | left | 747x421

大家好,我是阿里的邓若奇。我和 Scott 是好友,非常幸运今天站在这里和大家面对面的交流。我是一名前端,听说今天来的前端特别多,非常高兴,压力也很大。

image.png | left | 747x421

我今天分享的主题是基于 SPA 架构的 GraphQL 工程实践。主要从一名前端的视角来看 GraphQL 在整个 web 链路中包括前端和后端协同效率的问题。

image.png | left | 747x421

先介绍一下我自己,我叫邓若奇,做前端之前做过几年的后端开发,现在在阿里 CBU 体验技术部,主要 B2B 前端工程体系基础建设,同时是集团 nodejs 中间件客户端维护者(之一)及 devops 接口人。

image.png | left | 747x421

    今天分享大概分为五个部分:

    1、谈谈我对 GraphQL 哲学的一些理解。

    2、基于前后端分离一些架构设计与技术选型。

    3、详细介绍基于 GraphQL 构建 BFF 这一层,我的一些分层设计和思考。

    4、介绍一下前后端协作一些效率方面的问题。

    5、讲一下引入 GraphQL 之后需要解决的问题。

image.png | left | 747x421

image.png | left | 747x421

前面其实很多讲师已经讲到了 GraphQL 一些东西,GraphQL 其实就是通过一套 schema 做领域模型的定义,官方称之为 SDL,同时引入一套类型系统。通过这套类型系统来对模型进行约束,就像 PPT 展示的这三个类型一样。

image.png | left | 747x421

在实际使用过程中客户端通过把想要获取的字段通过 schema 文本发送给服务端,服务端经过处理之后以 json 格式返回。这是最常见的一种使用方式。

image.png | left | 747x421

通过以上的描述大概知道 GraphQL 有以下几个特点:

第一,它提供一套统一的模型定义。第二,相比 REST 提供了灵活的按需查询的能力。第三点其实也是容易被大家忽略的一点,就是它通过这套类型系统提供了模型和模型之间关系的描述。这就引入了一个不争的事实,

image.png | left | 747x421

application data graph,虽然服务器是以 json 数据返回的,但我们的应用数据是一个图或者说是一个网。这也是 GraphQL 成为描述应用数据的一个极佳选择。我认为也是它之所以叫 GraphQL 而不是叫 TreeQL 的原因。

image.png | left | 747x421

架构设计与技术选型,前后端分离,说起前后端分离是一个老生常谈的问题,自从我开始做前端一直到现在,我认为前后端分离大致分为四个阶段:

image.png | left | 747x421

第一阶段前端异步去请求数据接口,然后刷新局部的 UI;第二阶段前端接管 view 层,这个时候基于 spa 框架开始涌现,并且一直流行到今天;第三和第四阶段随着 nodejs 技术的兴起,我们开始思考与后端的协同效率问题,通过引入 BFF 这一层实现,可以让前端进行快速的业务迭代,同时后端下沉为服务或者微服务,能够变得更加稳定和高效。

image.png | left | 747x421

这个架构图相信很多人已经看到过,就不多说了。

这是技术选型,显然它不是唯一的,因为前面很多讲师有他们自己的选型。前端选择 react 和 relay,relay 其实是一种基于 react 和 GraphQL 的一种数据整合方案,前面有讲师有提到relay的一些痛点,其实在我看来并不是完全的痛点,relay 最大痛点是文档太少了(笑)。BFF 这一层引入 eggjs,eggjs 是阿里开源的一个面向企业级开发的一个外部框架,可以理解成它就是一个 koa 或者是一个 express,亦或者是一个 mvc 的框架就行了。

image.png | left | 747x421

如何设计BFF。

image.png | left | 747x421

先来看一下基于传统的 mvc 模式的 web service 怎样受理一个 rest 请求的,首先请求进入到 middleware,我们叫中间件或者是拦截器,在中间件处理一些通用的逻辑,比如说用户登陆判断和 api 鉴权,然后请求到 router,router 通过 url 把请求分发到不同的 controller,在 controller 这一层调用 model 进行业务处理,然后 model 再调用 service 层进行取数,数据返回了之后在 controller 层完成数据拼装并且返回。

image.png | left | 747x421

在引入 GraphQL 之后发生了一些变化。首先 router 不需要了,因为 GraphQL 并不是基于 endpoint 的,controller 这一层也不需要了,因为 GraphQL 天生的 resolver 会帮我们搞定数据拼装的功能,另外还引入两个模块:第一个是 connector,第二个是 schema loader,之所以有 connector这个模块,主要是因为基于两点考虑:第一点是出于性能考虑,针对 GraphQL 的一些特点需要进行特殊的缓存设计,另外在做前后端协作的时候,需要有一些约定或者规范比如说分页,跟客户端进行约定分页逻辑的时候,需要有这么一些规范,需要在 connector 层实现。

另外,schema loader 就非常好理解,在应用启动的时候需要加载 schema,所以我们需要有一个模块来加载它。

image.png | left | 747x421

下面将我在构建 BFF 层当中体会比较深的点跟大家交流一下。第一,如何构建 schema?

image.png | left | 747x421

这其实是一个开发模式的问题,我在刚开始写 GraphQL 代码的时候,代码是这样的,其实这段代码也是在 graphql-js 的官方 repo 上抄来的,但是我的代码跟它是一样的,但是现在看来我不会这样写。

image.png | left | 747x421

因为它存在两个问题:首先,schema 是一个与语言无关的,只是模型的一个描述,其次,做开发的时候本着设计先行的原则,先确定模型是长什么样的,然后才开始动手写代码。

image.png | left | 747x421

所以比较赞同:SDL First Philosophy。这个不是我提的,这个是老外提的。

image.png | left | 747x421

首先先确定模型的描述,以及它们之间的关系,等确定模型之间的关系之后,我们再来书写 resolver 具体应该如何处理。

image.png | left | 747x421

最后在应用加载的时候 schema loader 把两者绑定。这些都不是问题。

image.png | left | 747x421
    

第二,鉴权与授权。

image.png | left | 747x421

鉴权称之为 authentication,授权称之为 authorization,说实话我一开始总是搞错,并且这两个概念其实在中国人看来其实可以互换或者说是相同的意思,

image.png | left | 747x421

我认为鉴权主要是针对一些通用的逻辑,比如刚才说的用户登陆的一些逻辑,是一些比较粗粒度的。

image.png | left | 747x421

这是刚才的那张图,把用户登陆判断和 API 鉴权可以放在 middleware 里面实现。

image.png | left | 747x421

授权是什么呢?授权其实是具有一些定制的逻辑,它的粒度可能比较细,针对 GraphQL 来说可能是细化到某一个字段。

image.png | left | 747x421

来看一个例子,这个 query 要查询小明的工资,小明的工资只能小明自己看,如果服务端给它放出来就出现水平权限的问题。

image.png | left | 747x421

所以我们在 resolver 里面需要加入授权逻辑,来保证一定是小明自己(才能看),这里抽象了一个 User.isSelf 接口,这里的设计,我们把授权的逻辑把它封装在 model,让它在不同的 resolver 里面都可以复用。User.isSelf 不光在 salary,可能在 login password 这种地方都可以用到。

image.png | left | 747x421

    第三,缓存设计,

image.png | left | 747x421

这是数据库里面两条用户记录,用户 1 和用户 2 他们互为 friend。

image.png | left | 747x421

然后程序里面有这么两段代码:第一段代码先去查询用户 1,查询回来之后再查询它的 friend,也就是用户 2,第二段代码正好反过来。那么,

image.png | left | 747x421

它的请求时序图是这样的,可以看到一共发了四个请求,并且我们最终查到的数据只有两条,可以看到这是非常浪费的,所以考虑优化一下,

image.png | left | 747x421

先引入缓存,在引入缓存之后第二轮的查询在第一轮缓存结果中找到,非常幸运的是第二轮不需要发新的请求了。

image.png | left | 747x421

再进一步深挖一下,如果第一轮请求能够合并成一个请求,这样就太棒了。

image.png | left | 747x421

所以总结一下,我们为了达成这个目的可能需要缓存,这是毫无疑问的。然后我们需要一个请求队列,请求队列什么意思?我们在同一个队列当中,node 的 event loop 是分为一个周期一个周期的,同一个周期里把所有请求全部放在一个队列里,在下一个周期合并成一个请求发出。最后,需要批量处理的能力。一个请求带着批量的 key 过来的时候应该怎么表现,

image.png | left | 747x421

索性的是 Facebook 提供这样的解决方案,data loader,

image.png | left | 747x421

data loader 接收,这个就是面对批量 Key 请求过来的时候应该如何处理,并且每个 data loader 的实例下面都有一个 cache。

image.png | left | 747x421

所以,刚才的需求引入 data loader 之后实现起来就变成这样。

image.png | left | 747x421

这段代码最终的效果把三个请求合并成一个请求,在我们的后端我们其实是执行一条指令把它搞定的。

image.png | left | 747x421

但是,在实际使用的时候感觉结合关系型数据库使用起来还是有一点复杂。

image.png | left | 747x421

首先,一张关系型数据库表大家会设置一些 key,除了有 primary key 之外,还会设置 unique key,我们经常会根据 pk 进行查询,或者根据 uk 进行查询,像这段代码里面会根据 id 去查询用户,也可能会根据 mobile 去查询用户,我们的代码根据 data loader 的哲学不得不初始化两个实例。

image.png | left | 747x421

这种方式带来的最大问题因为是不同的 data loader 实例,缓存是不同的缓存,即使记录是完全一样的,但是你没法利用到,导致缓存的利用率并不高。

image.png | left | 747x421

为了解决这个问题,我书写 rdb-dataloader 这个模块,

image.png | left | 747x421

这个模块其实它的作用很简单,就是我无论查询PK还是查询 UK,在同一个实例里面搞定,让它复用缓存,看红框中的代码,先通过“name”来查询一条记录,然后把这条记录通过它的ID来进行第二次查询,第二次查询显然是不会发出去,它会用缓存。

image.png | left | 747x421

所以要实现这样的功能其实这里面有一个问题,你缓存记录的时候是缓存每条记录的全部字段,让这些字段覆盖你的 UK 和 PK,你可能会担心数据量的问题,但是它其实真的不是问题,数据量控制应该由你的分页逻辑来关心的。

image.png | left | 747x421

这里抛出一个思考 data loader 这种形式是请求级别的缓存,当一个请求进来的时候初始化一个 data loader 的实例,当请求结束之后它就销毁,这个和我们平时用的基于 redis 中心化缓存有什么不同,可不可以切成 redis 的方式,留给大家思考。

image.png | left | 747x421

前后端如何协作,

image.png | left | 747x421

作名一个前端其实在用了 GraphQL 之后必须要思考,对于浏览器性能怎么样,这是进一步挖掘 relay 的原因,下面给大家简单分享一下 relay 的部分特性。

image.png | left | 747x421

这是一个最普通的 react component,一个最普通的诉求就是组件需要异步取数,然后把数据进行渲染,所以在 componentDidMount 里面去把异步取数逻辑加上。

image.png | left | 747x421

所以现实中随着组件数越来越深,页面加载时间也就越来越长,因为子组件必须要等到父组件加载完它的数据之后才开始渲染,这个问题我想优化一下,应该怎么优化,

image.png | left | 747x421

最简单的方式把所有组件所需要的数据全部放在首个请求里面,这个项目交付了之后很不错,后面产品经理找我要搞一个需求,

image.png | left | 747x421

结果我搞了一个 bug,因为我已经搞不清楚这个 query 里的哪些字段是对应哪些组件的。

image.png | left | 747x421

我们看一下 relay 的方式,relay 这里有一个 create Fragment Container这个方法,通过这个方法传入一个react组件,然后传入 GraphQL schema 来返回一个 relay container,其实这是一个高阶组件,通过这种方式,我们实现了依赖注入,也没有打破数据封装性。

image.png | left | 747x421

这个 fragment 在首个最初始的 query 里面把它内联进去,这样就知道这个玩意是哪个组件发出来的。

image.png | left | 747x421

 relay 有一个非常 smart,非常智能的特点,应用的数据是一个图状的,

image.png | left | 747x421

是怎样去存取应用的一个数据的?这是一个伪代码,但是表示 relay 底层的一种协作方式,从上面的例子可以看到存储三个对象,第一个对象是博客,有内容,也有作者,但是这个作者是一个 user 类型,博客不会直接存储 user 的全部数据,而是通过引用的方式引用到第二个对象,同理,在给博客里面下面添加评论,评论的作者和它属于哪个博客,同样是用引用的方式,这个有什么好处,

image.png | left | 747x421

当我比如说作者改头像,比如说 github 你们经常改头像吗,反正我是不经常,但是我发现只要改了头像任何地方都会修改,提的 issue,代码的提交者全部都会改掉。但是我可以确定 github 不是用的 relay(笑),只是说表达这个意思,只要你改了这个对象,在界面任何引用它的地方全部都更新,这就是视图一致性。

image.png | left | 747x421

返回来看一下在 cache 里面 123 是什么东西,是缓存 key,它的缓存 key 是一个全局唯一的,因为把所有的实体全部都塞到缓存里面去了,我不能用数据库的 ID,否则用 ID 为 1 的 user 和 ID 为 1 的博客它们之间怎么搞,

image.png | left | 747x421

所以需要实现 relay 的规范,Global Identifier,我们要保证每个对象它的 ID 是唯一的。 

image.png | left | 747x421

这是一种简单的方式,可以定义任何一种算法或者方式来确定你的 global ID。这里我只是把简单的类型加上它数据库的 ID 进行 base64 了,

image.png | left | 747x421

所以在 relay store 里面我看到都是这些东西。

image.png | left | 747x421

刚才说了是 global ID 是需要后端来进行配合,我们需要在后端定义两个方法,fromGlobalId 就是在 relay 发请求的时候会把 ID 带过来,然后我在后端我只识别数据库 ID,所以要把它解包,把它解成数据库的 ID,在吐出去的时候,需要给每个数据库 ID 给装包有一个 toGlobalId,这两个方法,如果使用刚才我的方法,graphql-relay 这个包已经提供。

image.png | left | 747x421

image.png | left | 747x421

当客户端把文本发送到服务端,服务端经过处理的时候,我们往往发现这种文本是非常大的,特别是对于网络环境不好的一些无线终端它的体验是非常差的。

image.png | left | 747x421

并且它们是一种静态的文本,能不能把它优化一下,比如说传一个 ID 过去,给每一个 query 赋予一个 ID,服务端根据 ID 反查成为 query,然后再把它处理出来。

image.png | left | 747x421

所幸 relay 也提供这样的方式。它给出的答案是在构建时期做,构建 relay 脚本的时候,本质上它是一个模块,会给这个模块做一个 hash,标识当前的 schema 给它加一个指纹,

image.png | left | 747x421

这个 hash 在后端可以去 load 到内存里,在前端也可以把它通过打包打进去,这个时候前后端就对应起来了。

image.png | left | 747x421

所以在真实的场景中是通过 hash,而不是通过 schema text。

image.png | left | 747x421

需要解决的问题,

image.png | left | 747x421

前面的讲师都已经有提到过 DOS Attack。

image.png | left | 747x421

说白了就是这种嵌套攻击,请注意这并不是死循环,这只是一个攻击者故意通过你的 query 无限写的非常复杂的嵌套,让你的服务器消耗殆尽。

image.png | left | 747x421

最简单的你设置 query 文本大小来做预防肯定不行,而设置白名单那么我们使用 GraphQL 的意义又在何处,所以我们还是对 query 的深度进行控制,

image.png | left | 747x421

这里给出一个例子,后面大家其实可以看到。

image.png | left | 747x421

rate limiting 限流,

image.png | left | 747x421

因为 GraphQL 它不是基于 rest 的,所以肯定不能对于 /graphql 这个路由你去限制它每分钟只能调用多少次,你真正要限制是读写和操作。

image.png | left | 747x421

这是一个例子,这个例子表示每分钟最多只能添加 20 个评论,通过 directive来做,

image.png | left | 747x421

但是限流实现成本比较大,如果你是专门实现限流这个功能,它需要依赖第三方的一些服务,比如说你在计算限流次数的时候,还有计算时间窗口的时候,因为你的应用是一个集群,你不可能做在应用里面。所以我在代码里面还没有实现。


更多专业前端知识,请上 【猿2048】www.mk2048.com

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/366461.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

使用sikuli和Arquillian测试HTML5 canvas应用程序

HTML5引入了一个很棒的新元素,可以用来在窗格上绘制任意内容: canvas元素。 数十年来,胖客户端应用程序的标准功能现已引入Web应用程序领域。 Web开发人员不再需要使用专有插件在其应用程序中绘制图像或图表。 但是,在进行测试时…

浅谈一下我了解的PWA

Progressive Web Apps,简称PWA,中文翻译过来就是渐进式网页应用,个人觉得它可以算是Web应用的下一个进化方向之一, 毕竟技术的发展有时候是不可预估的,谁也无法预料技术的走向,但至少它代表了一种可能性,下…

质性研究工具_质性研究【001】

袁长蓉 复旦大学 博士研究生导师课程 人类是追求意义的物种,会不断地从生活细节当中不断的归纳提炼,形成精神。形成理论抽象的概念,然后再利用这些概念,这些理论反过来,解释生活。 质性研究是由地到天的研究。质性研究…

项目开发中发布更新文档备注

项目开发中 经常会遇到 某一个团队成员更新了,配置文件但是没有及时沟通导致项目发布后出现异常的情况。 这时就需要一个项目更新发布的规范说明文件,比如每次更新更改了哪些,进行文档备注。 更新的脚本文件也要随更新文档一起给到,配置文件也要将最新的配置信息给到。 转载于:…

bind函数polyfill源码解析

准备知识 使用new来调用函数会自动执行下面的操作: 创建一个全新的对象这个新对象会被执行原型连接这个新对象会绑定到函数调用的this如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象 注意this绑定规则,new操作具…

使用JUnit和Repeat注​​释编写有效的负载测试

EasyTest最近推出了一套新的注释,可帮助其用户编写有效的测试用例。 进入EasyTest的两个主要注释是: 重复 持续时间 今天,我们将讨论重复标注。 一种新的方法级别注释 重复已添加到EasyTest框架。 此批注可用于重复相同的测试多次。 在您…

css线性渐变

此方式可以实现背景色由上往下渐变,这里加上-webkit-考虑兼容问题,若要改变渐变方向,改变top即可,如right、left、bottom效果图: 代码如下: background: -webkit-linear-gradient(top,red,black); 不带前缀&#xff0c…

scroll-view组件bindscroll实例应用:自定义滚动条

我们知道scroll-view组件作为滑动控件非常好用,而有时候我们想放置一个跟随滚动位置来跟进的滚动条,但又不想用滚动条api该怎么办呢?(当然是自己写一个呗还能怎么办[自黑冷漠脸]) 嗯,没错。自己写一个就好了…

C# -- HttpWebRequest 和 HttpWebResponse 的使用

C# -- HttpWebRequest 和 HttpWebResponse 的使用 结合使用HttpWebRequest 和 HttpWebResponse,来判断一个网页地址是否可以正常访问。 1.举例 class Program{static void Main(string[] args){string strUrl "https://www.baidu.com";HttpWebRequest wr…

mysql数据库存储引擎和索引的描述_Mysql InnoDB引擎的索引与存储结构详解

前言在Oracle 和SQL Server等数据库中只有一种存储引擎,所有数据存储管理机制都是一样的。而MySql数据库提供了多种存储引擎。用户可以根据不同的需求为数据表选择不同的存储引擎,用户也可以根据自己的需要编写自己的存储引擎。MySQL主要存储引擎的区别M…

Vue结合HTML5拖放API 实现目录拖拽~

拖放事件 dom被拖拽--->经过一些dom--->到达指定dom 被拖拽的dom:(源对象) dragstart 源对象被拖拽 drag 源对象拖拽过程中 dragend 源对象拖拽结束(drop事件后执行) 拖拽过程中经过的dom:&#xf…

jQuery(一)初识

jQuery 的功能概括1、html 的元素选取2、html的元素操作3、html dom遍历和修改4、js特效和动画效果5、css操作6、html事件操作7、ajax异步请求方式 selector: 操作(DOM)/$(selector).action(): <!DOCTYPE html> <html> <head> <meta charset"utf-8&q…

第16章-使用Spring MVC创建REST API

1 了解REST 1.1 REST的基础知识 REST与RPC几乎没有任何关系。RPC是面向服务的&#xff0c;并关注于行为和动作&#xff1b;而REST是面向资源的&#xff0c;强调描述应用程序的事物和名词。 为了理解REST是什么&#xff0c;我们将它的首字母缩写拆分为不同的构成部分&#xf…

使用Apache Mahout创建在线推荐系统

最近&#xff0c; 我们一直在为Yap.TV实施推荐系统&#xff1a;在安装应用程序并转到“ Just for you”选项卡后&#xff0c;您可以看到它的运行情况。 我们以Apache Mahout为基础进行建议。 Mahout是一个“可扩展的机器学习库”&#xff0c;其中包含使用协作过滤算法的基于用户…

linux mono mysql_LJMM平台( Linux +Jexus+MySQL+mono) 上使用MySQL的简单总结

近准备把PDF.NET框架的开源项目“超市管理系统”移植到Linux上跑(演示地址&#xff1a;http://221.123.142.196)&#xff0c;使用Jexus服务器和MySQL数据库&#xff0c;相对使用SQLite而言&#xff0c;用MySQL问题比较多&#xff0c;但最后还是一一解决了&#xff0c;先总结如下…

node中的缓存机制

缓存是node开发中一个很重要的概念&#xff0c;它应用在很多地方&#xff0c;例如&#xff1a;浏览器有缓存、DNS有缓存、包括服务器也有缓存。 一、缓存作用 那缓存是为了做什么呢&#xff1f; 1.为了提高速度&#xff0c;提高效率。 2.减少数据传输&#xff0c;节省网费。 …

《H5 移动营销设计指南》 读书笔记整理

一个前端工程师最近迷上了营销类的H5页面&#xff0c;被五花八门的H5页面迷的眼花缭乱&#xff0c;兴趣使然&#xff0c;于是买了一本《H5 营销设计指南》&#xff0c;看完以后对营销类的H5页面有了更深的理解&#xff0c;感觉很实在&#xff0c;所以参考读书笔记整理成PPT分享…

mysql-plus多数据库_IDEA项目搭建九——MybatisPlus多数据库实现

一、简介MybatisPlus中引用多数据库时&#xff0c;传统的配置就失效了&#xff0c;需要单独写配置来实现&#xff0c;下面就说一下具体应该如何操作二、引入MybatisPlus多数据源配置还是先看一下我的项目结构&#xff0c;Model是单独的模块&#xff0c;请自行创建1、创建一个Ma…

数字逻辑基础篇1

1. 双阈值准则在模拟条件下&#xff0c;假设点亮灯泡需要1.7V以上电压。抽象为数字电路&#xff0c;可以认为&#xff1a; U>1.7V U1 U<1.7V U0 这种条件称之为单阈值&#xff08;1.7&#xff09;&#xff0c;但是单阈值导致的问题是&#xff1a; 电压在1.7V附近…

Neo4j:在Neo4j浏览器的帮助下探索新数据集

当我查看一个新的Neo4j数据库时&#xff0c;发现困难之一是确定其中包含的数据的结构。 我习惯于关系数据库&#xff0c;在该数据库中您可以轻松地获取表列表和外键&#xff0c;从而使它们彼此连接。 传统上&#xff0c;使用Neo4j时很难做到这一点&#xff0c;但是随着Neo4j浏…