写在开头的部分,本文的契机是最近我们组同事在客户端实现了一套 Redux,对某个业务域的功能进行重构设计,iOS、Android 都遵循这套规则,即 Redux。为什么需要客户端去实现一套 Redux?商品模块业务逻辑非常负责,商品基础信息非常多,比如多规格、多单位、价格、库存等信息,还有对应的门店、网店模型,还有各种行业能力开关控制,早期的实现有 Rx 的角色,导致代码逻辑较为复杂,数据流动比较乱。架构设计、代码维护各方面来看都不是很优雅,加上最近有大的业务调整,中台的同学用 Redux + 单向数据流的方式重构了业务。
另一个下线经常请求网关数据,iOS、Android 各自去声明 DTO Model,然后解析数据,生成客户端需要的数据,这样“重复”的行为经常发生,所以索性用 TS + 脚本,统一做掉了网关数据模型自动生成 iOS、Android 模型的能力。但是在讨论框架设计的时候回类比 React Redux、Flutter Fish Redux、Vuex 等,还会聊到单向数据流、双向数据流,但是有些同学的理解就是错误的。所以本文第一部分「纠错题」部分就是讲清楚前端几个关键概念。
后面的部分按照逻辑顺序讲一下:微前端 -> BFF/网关 -> 微服务。
一、纠错题
1. Vue 是双向数据流吗?
如果回答“是”,那么你应该没有搞清楚“双向数据流”与“双向绑定”这2个概念。其实,准确来说两者不是一个维度的东西,单向数据流也可以实现双向绑定。
其实,你要是仔细看过 Vue 的官方文档,那么官方就已经说明 Vue 其实是 One-Way Data Flow。下面这段话来自官方
首先明确下,我们说的数据流就是组件之间的数据流动。Vue 中所有的 props 都使得其父子 props 之间形成一个单向下行绑定:父级 props 的更新回向下流动到子组件中,但反过来不行,这样防止从子组件意外更改其父组件的状态,从而导致你的应用数据流难以理解。
此外,每次父级组件发生变更时,子组件中所有的 props 都将会被刷新为最新的值,这意味着你不应该在子组件内去修改 prop,假如你这么做了,浏览器会在控制台中输出警告。Vue 和 React 修改 props 报错如下:
2. React 是 MVVM 吗?
不直接回答这个问题,我们先来聊几个概念,这几个概念弄清楚了,问题的答案也就呼之欲出了。
2.1 单向绑定、双向绑定的区别?
讲 MVVM 一定要讲 binder 这个角色。也就是单向绑定和双向绑定。
Vue 支持单向绑定和双向绑定。单向绑定其实就是 Model 到 View 的关联,比如 v-bind。双向绑定就是 Model 的更新会同步到 View,View 上数据的变化会自动同步到 Model,比如 v-model.
v-model 其实是语法糖,因为 Vue 是 tamplate,所以在经过 webpack ast 解析之后,tamplate 会变为 render,v-model 变为 v-bind 和 v-on
可以用单向绑定很简单地可以实现双向绑定的效果,就是 one-way binding + auto event binding。如下
其实看了源码就会发现(Vue3 Proxy),单向绑定和双向绑定的区别在于,双向绑定把数据变更的操作部分,由框架内部实现了,调用者无须感知。
2.2 Redux
使用 React Redux 一个典型的流程图如下
假如我们把 action 和 dispatcher 的实现隐藏在框架内部,这个图可以简化为
假如进一步,我们将互相手动通知的机制再隐藏起来,可以简化为
你仔细看,是不是就是 MVVM?那问题的答案很明显了,React 不是 MVVM。
3. Vue 是 MVVM 吗
官方说了“虽然没有完全遵循 MVVM 的思想,但是受到了 MVVM 的启发。所以 Debug 的时候经常看到 VM” Model 的变动会触发 View 的更新,这一点体现在 V-bind 上 而 View 的变更即使同步到 Model 上,体现在 V-model 上 上述都遵循 MVVM,但是 Vue 提供了 Ref 属性,允许使用 ref 拿到 Dom,从而直接操作 Dom 的一些样式或者属性,这一点打破了 MVVM 的规范
4. 单向绑定和双向绑定使用场景
我们再来思考一个问题,为什么 Vue 要移除 .sync, Angular 要增加 < 来实现单向绑定?
当应用变得越来越大,双向数据流会带来很多的不可预期的结果,这让 debug、调试、测试都变得复杂。
所以 Vue、Angular 都意识到这一点,所以移除了 .sync,支持单向绑定。
正面聊聊问题:
单向绑定,带来的单向数据流,带来的好处是所有状态变化都可以被记录、跟踪,状态变化必须通过手动调用通知,源头可追溯,没有“暗箱操作”。同时组件数据只有唯一的入口和出口,使得程序更直观更容易理解,有利于应用的可维护性。缺点是实现同样的需求,代码量会上升,数据的流转过程变长。同时由于对应用状态独立管理的严格要求(单一的全局store),在处理局部状态较多的场景时(如用户输入交互较多的“富表单型”应用)会显得特别繁琐。
双向绑定优点是在表单交互较多的场景下,会简化大量业务无关的代码。缺点就是由于都是“暗箱操作”,无法追踪局部状态的变化,潜在的行为太多也增加了出错时 debug 的难度。同时由于组件数据变化来源入口变得可能不止一个,程序员水平参差不齐,写出的代码很容易将数据流转方向弄得紊乱,整个应用的质量就不可控。
总结:单向绑定跟双向绑定在功能上基本上是互补的,所以我们可以在合适的场景下使用合适的手段。比如在 UI 控件中(通常是类表单操作),可以使用双向的方式绑定数据;而其他场景则使用单向绑定的方式
二、 微前端
上面提到的 Vue、React、单向数据流、双向数据流都是针对单个应用去组织和设计工程的,但是当应用很大的时候就会存在一些问题。引出今天的一个主题,“微前端”
客户端的同学都知道,我们很早就在使用组件化、模块化来拆分代码和逻辑。那么你可以说出到底什么是组件、什么是模块?
组件就是把重复的代码提取出来合并成为一个个组件,组件强调的是重用,位于系统最底层,其他功能都依赖于组件,独立性强
模块就是分属同一功能/业务的代码进行隔离成独立的模块,可以独立运行,以页面、功能等维度划分为不同模块,位于业务架构层。
这些概念在前端也是如此,比如写过 Vue、React 的都知道,对一个页面需要进行组件的拆分,一个大页面由多个 UI 子组件组成。模块也一样,前端利用自执行函数(IIFE)来实现模块化,成熟的 CommonJS、AMD、CMD、UMD 规范等等。比如 iOS 侧利用模块化来实现了商品、库存、开单等业务域的拆分,也实现了路由库、网络库等基础能力模块。而前端更多的是利用模块化来实现命名空间和代码的可重用性
其实,看的出来,前端、客户端实行的模块化、组件化的目标都是分治思想,更多是站在代码层面进行拆分、组织管理。但是随着前端工程的越来越重,传统的前端架构已经很难遵循“敏捷”的思想了。
1. 什么是微前端
关心技术的人听到微前端,立马会想起微服务。微服务是面向服务架构的一种变体,把应用程序设计为一些列松耦合的细粒度服务,并通过轻量级的通信协议组织起来。越老越重的前端工程面临同样的问题,自然而然就将微服务的思想借鉴到了前端领域。
言归正传,微前端(Micro-Frontends)是一种类似于微服务的架构,它将微服务的理念应用于浏览器端,即将 Web 应用由单一的单体应用转变为多个小型前端应用聚合为一的应用。各个前端应用还可以独立运行、独立开发、独立部署。微前端不是单纯的前端框架或者工具,而是一套架构体系。
2. 特点
2.1 技术无关
抛两个场景,大家思考一下:
- 你新入职一家公司,老板扔给你一个 5 年陈的项目,需要你在这个项目上持续迭代加功能。
- 你们起了一个新项目,老板看惯了前端的风起云涌技术更迭,只给了架构上的一个要求:"如何确保这套技术方案在 3~5 年内还葆有生命力,不会在 3、5 年后变成又一个遗产项目?"
第一个场景我们初步一想,可以啊,我只需要把新功能用 React/Vue 开发,反正他们都只是 UI library,给我一个Dom 节点我想怎么渲染怎么渲染。但是你有没有考虑过这只是浮在表层的视图实现,沉在底下的工程设施呢?我要怎么把这个用 React 写的组件打出一个包,并且集成到原来的用 ES5 写的代码里面?或者我怎么让 Webpack 跟 之前的 Grunt 能和谐共存一起友好的产出一个符合预期的 Bundle?
第二个场景,你如何确保技术栈在 3~5 年都葆有生命力?别说跨框架了,就算都是 React,15 跟 16 都是不兼容的,hooks 还不能用于 Class component 呢我说什么了?还有打包方案,现在还好都默认 Webpack,那 Webpack 版本升级呢,每次都跟进吗?别忘了还有 Babel、less、typescript 诸如此类呢?别说 3 年,有几个人敢保证自己的项目一年后把所有依赖包升级到最新还能跑起来?
为什么举这两个场景呢,因为我们去统计一下业界关于”微前端“发过声的公司,会发现 adopt 微前端的公司,基本上都是做 ToB 软件服务的,没有哪家 ToC 公司会有微前端的诉求(有也是内部的中后台系统),为什么会这样?很简单,因为很少有 ToC 软件活得过 3 年以上的。而对于 ToB 应用而言,3~5 年太常见了好吗!去看看阿里云最早的那些产品的控制台,去看看那些电信软件、银行软件,哪个不是 10 年+ 的寿命?企业软件的升级有多痛这个我就不多说了。所以大部分企业应用都会有一个核心的诉求,就是如何确保我的遗产代码能平滑的迁移,以及如何确保我在若干年后还能用上时下热门的技术栈?
如何给遗产项目续命,才是我们对微前端最开始的诉求。很多开发可能感受不深,毕竟那些一年挣不了几百万,没什么价值的项目要么是自己死掉了,要么是扔给外包团队维护了,但是要知道,对很多做 ToB 领域的中小企业而言,这样的系统可能是他们安身立命之本,不是能说扔就扔的,他们承担不了那么高的试错成本。
甚至新的业务,每个技术团队应该可以根据自己团队成员的技术储备、根据业务特点选择合适的技术栈,而不需要特别关心其他团队的情况,也就是 A 团队可以用 React,B 团队可以用 Vue,C 团队甚至可以用 Angular 去实现。
2.2 简单、松耦合的代码库
比起一整块的前端工程来说,微前端架构下的代码库更小更容易开发、维护。此外,更重要的是避免模块间不合理的隐式耦合造成的复杂度上升。通过界定清晰的应用边界来降低意外耦合的可能性,增加子应用间逻辑耦合的成本,促使开发者明确数据和事件在应用程序中的流向
2.3 增量升级
理想的代码自然是模块清晰、依赖明确、易于扩展、便于维护的……然而,实践中出于各式各样的原因:
- 历史项目,祖传代码
- 交付压力,当时求快
- 就近就熟,当时求稳……
总存在一些不那么理想的代码:
- 技术栈落后,甚至强行混用多种技术栈
- 耦合混乱,不敢动,牵一发何止动全身
- 重构不彻底,重构-烂尾,换个姿势重构-又烂尾……
而要对这些代码进行彻底重构的话,最大的问题是很难有充裕的资源去大刀阔斧地一步到位,在逐步重构的同时,既要确保中间版本能够平滑过渡,同时还要持续交付新特性:
所以,为了实施渐进式重构,我们需要一种增量升级的能力,先让新旧代码和谐共存,再逐步转化旧代码,直到整个重构完成
这种增量升级的能力意味着我们能够对产品功能进行低风险的局部替换,包括升级依赖项、更替架构、UI 改版等。另一方面,也带来了技术选型上的灵活性,有助于新技术、新交互模式的实验性试错
2.4 独立部署
独立部署的能力在微前端体系中至关重要,能够缩小变更范围,进而降低相关风险
因此,每个微前端都应具备有自己的持续交付流水线(包括构建、测试并部署到生产环境),并且要能独立部署,不必过多考虑其它代码库和交付流水线的当前状态:
假如 A、B、C 三个 Biz 存在三个系统,三者可以独立部署,最后由主系统去集成发布应用。这样子更加独立灵活。想想以前的单体应用,比如一个复杂的业务应用,很可能在 B 构建的时候没有通过 lint 导致整个应用失败掉,这样既浪费了构建 A 的时间,又让整个应用的构建变为串行,低效。
2.5 团队自治
除了代码和发布周期上的解耦之外,微前端还有助于形成独立的团队,由不同的团队各自负责一块独立的产品功能,最好根据 Biz 去划分,由于代码都是独立的,所以基于 Biz 的前端业务团队,可以设计思考,提供更加合理的接口和能力。甚至可以抽象更多的基础能力,按照业务思考提供更加完备的能力,也就是团队更加自治。
3 如何实现
微前端主要采用的是组合式应用路由方案,该方案的核心思想是“主从”(玩过 Jenkins 的同学是不是很耳熟),即包括一个基座“MainApp “ 应用和若干个微应用(MircroApp),基座大多采用的是一个前端 SPA 项目,主要负责应用注册、路由映射、消息下发等,而微应用是独立的前端项目,这些项目不限于采用的具体技术(React、Vue、Angular、Jquery)等等,每个微应用注册到基座中,由基座进行管理,即使没有基座,这些微应用都可以单独访问,如下:
一个微应用框架需要解决的问题是:
路由切换的分发问题
作为微前端的基座应用,是整个应用的入口,负责承载当前微应用的展示和对其他路由微应用的转发,对于当前微应用的展示,一般是由以下几步构成:
- 作为一个SPA的基座应用,本身是一套纯前端项目,要想展示微应用的页面除了采用iframe之外,要能先拉取到微应用的页面内容, 这就需要远程拉取机制。
- 远程拉取机制通常会采用fetch API来首先获取到微应用的HTML内容,然后通过解析将微应用的JavaScript和CSS进行抽离,采用eval方法来运行JavaScript,并将CSS和HTML内容append到基座应用中留给微应用的展示区域,当微应用切换走时,同步卸载这些内容,这就构成的当前应用的展示流程。
- 当然这个流程里会涉及到CSS样式的污染以及JavaScript对全局对象的污染,这个涉及到隔离问题会在后面讨论,而目前针对远程拉取机制这套流程,已有现成的库来实现,可以参考 import-html-entry 和 system.js。
对于路由分发而言,以采用vue-router开发的基座SPA应用来举例,主要是下面这个流程:
- 当浏览器的路径变化后,vue-router会监听hashchange或者popstate事件,从而获取到路由切换的时机。
- 最先接收到这个变化的是基座的router,通过查询注册信息可以获取到转发到那个微应用,经过一些逻辑处理后,采用修改hash方法或者pushState方法来路由信息推送给微应用的路由,微应用可以是手动监听hashchange或者popstate事件接收,或者采用React-router,vue-router接管路由,后面的逻辑就由微应用自己控制。
主应用、微应用的隔离问题
应用隔离问题主要分为主应用和微应用,微应用和微应用之间的JavaScript执行环境隔离,CSS样式隔离,我们先来说下CSS的隔离。
CSS隔离:当主应用和微应用同屏渲染时,就可能会有一些样式会相互污染,如果要彻底隔离CSS污染,可以采用CSS Module 或者命名空间的方式,给每个微应用模块以特定前缀,即可保证不会互相干扰,可以采用webpack的postcss插件,在打包时添加特定的前缀。
而对于微应用与微应用之间的CSS隔离就非常简单,在每次应用加载时,将该应用所有的link和style 内容进行标记。在应用卸载后,同步卸载页面上对应的link和style即可。
JavaScript隔离:每当微应用的JavaScript被加载并运行时,它的核心实际上是对全局对象Window的修改以及一些全局事件的改变,例如jQuery这个js运行后,会在Window上挂载一个
window.$
对象,对于其他库React,Vue也不例外。为此,需要在加载和卸载每个微应用的同时,尽可能消除这种冲突和影响,最普遍的做法是采用沙箱机制(SandBox)。沙箱机制的核心是让局部的JavaScript运行时,对外部对象的访问和修改处在可控的范围内,即无论内部怎么运行,都不会影响外部的对象。通常在Node.js端可以采用vm模块,而对于浏览器,则需要结合with关键字和window.Proxy对象来实现浏览器端的沙箱。
通信问题
应用间通信有很多种方式,当然,要让多个分离的微应用之间要做到通信,本质上仍离不开中间媒介或者说全局对象。所以对于消息订阅(pub/sub)模式的通信机制是非常适用的,在基座应用中会定义事件中心Event,每个微应用分别来注册事件,当被触发事件时再有事件中心统一分发,这就构成了基本的通信机制,流程如下图:
目前开源了很多微前端框架,比如 qiankun ,感兴趣的可以去看看源码
4. 是否采用微前端
为什么要做微前端?或者说微前端解决了什么问题?也就回答了是否需要采用微前端。微前端的鼻祖 Single-spa 最初要解决的问题是,在老项目中使用新的技术栈。现阶段蚂蚁金服微前端框架 Qiankun 所声明的微前端要解决的另一个主要问题是,巨石工程所面临的维护困难喝协作开发困难的问题。如果工程面临这两方面的问题,我觉得微前端就可以试试了。你们觉得呢?
三、微服务平台下基于 GraphQL 构建 BFF 的思考
1. 大前端架构演进
我们来讲一个故事
1. V1
假设早期2011年一家电商公司完成了单体应用的拆分,后端服务已经 SOA 化,此时应用的形态仅有 Web 端,V1架构图如下所示:
2. V2
时间来到了2012年初,国内刮起了一阵无线应用的风,各个大厂都开始了自己的无线应用开发。为了应对该需求,架构调整为 V2,如下所示
这个架构存在一些问题:
- 无线 App 和内部的微服务强耦合,任何一侧的变化都可能对另一侧造成影响
- 无线 App 需要知道内部服务细节
无线 App 端需要做大量的聚合裁剪和适配逻辑
聚合:某一个页面可能同时需要调用好几个后端 API 进行组合,比如商品详情页所需的数据,不能一次调用完成
裁剪:后端提供的基础服务比较通用,返回的 Payload 比较大,字段太多,App 需要根据设备和业务需求,进行字段裁剪用户的客户端是Android还是iOS,是大屏还是小屏,是什么版本。再比如,业务属于哪个行业,产品形态是什么,功能投放在什么场景,面向的用户群体是谁等等。这些因素都会带来面向端的功能逻辑的差异性。后端在微服务和领域驱动设计(不在本文重点讲解范畴)的背景下,各个服务提供了当前 Domain 的基础功能,比如商品域,提供了商品新增、查询、删除功能,列表功能、详情页功能。
但是在商品详情页的情况下,数据需要调用商品域、库存域、订单域的能力。客户端需要做大量的网络接口处理和数据处理。
适配:一些常见的适配常见就是格式转换,比如有些后端服务比较老,提供 SOAP/XML 数据,不支持 JSON,这时候就需要做一些适配代码
- 随着使用类型的增多(iOS Phone/Pad、Android Phone/Pad、Hybrid、小程序),聚合裁剪和适配的逻辑的开发会造成设备端的大量重复劳动
- 前端比如 iOS、Android、小程序、H5 各个 biz 都需要业务数据。最早的设计是后端接口直出。这样的设计会导致出现一个问题,因为 biz1 因为迭代发版,造成后端改接口的实现。 Biz2 的另一个需求又会让服务端改设计实现,造成接口是面向业务开发的。不禁会问,基础服务到底是面向业务接口开发的吗?这和领域驱动的设计相背离
在这样的背景下诞生了 BFF(Backend For Frontend) 层。各个领域提供基础的数据模型,各个 biz 存在自己的 BFF 层,按需取查询(比如商品基础数据200个,但是小程序列表页只需要5个),在比如商品详情页可能需要商品数据、订单数据、评价数据、推荐数据、库存数据等等,最早的设计该详情页可能需要请求6个接口,这对于客户端、网页来说,体验很差,有个新的设计,详情页的 BFF 去动态组装调用接口,一个 BFF 接口就组装好了数据,直接返回给业务方,体验很好
3. V2.1
V2 架构问题太多,没有开发实施。为解决上述问题,在外部设备和内部微服务之间引入一个新的角色 Mobile BFF。BFF 也就是 Backend for Frontend 的简称,可以认为是一种适配服务,将后端的微服务进行适配(主要包括聚合裁剪和格式适配等逻辑),向无线端设备暴露友好和统一的 API,方便无线设备接入访问后端服务。V2.1 服务架构如下
这个架构的优势是:
无线 App 和内部服务不耦合,通过引入 BFF 这层简介,使得两边可以独立变化:
- 后端如果发生变化,通过 BFF 屏蔽,前端设备可以做到不受影响
- 前端如果发生变化,通过 BFF 屏蔽,后端微服务可以暂不变化
- 当无线端有新的需求的时候,通过 BFF 屏蔽,可以减少前后端的沟通协作开销,很多需求由前端团队在 BFF 上就可以自己搞定
- 无线 App 只需要知道 Mobile BFF 的地址,并且服务接口是统一的,不需要知道内部复杂的微服务地址和细节
- 聚合裁剪和适配逻辑在 Mobile BFF 上实现,无线 App 端可以简化瘦身
4. V3
V2.1 架构比较成功,实施后较长一段时间支持了公司早期无线业务的发展,随着业务量暴增,无线研发团队不断增加,V2.1 架构的问题也被暴露了出来:
- Mobile BFF 中不仅有各个业务线的聚合/裁剪/适配和业务逻辑,还引入了很多横跨切面的逻辑,比如安全认证,日志监控,限流熔断等,随着时间的推移,代码变得越来越复杂,技术债越来越多,开发效率不断下降,缺陷数量不断增加
- Mobile BFF 集群是个失败单点,严重代码缺陷将导致流量洪峰可能引起集群宕机,所有无线应用都不可用
为了解决上述伪命题,决定在外部设备和 BFF 之间架构一个新的层,即 Api Gateway,V3 架构如下:
新的架构 V3 有如下调整:
- BFF 按团队或者业务进行解耦拆分,拆分为若干个 BFF 微服务,每个业务线可以并行开发和交付各自负责的 BFF 微服务
官关(一般由独立框架团队负责运维)专注横跨切面功能,包括:
- 路由,将来自无线设备的请求路由到后端的某个微服务 BFF 集群
- 认证,对涉及敏感数据的 Api 进行集中认证鉴权
- 监控,对 Api 调用进行性能监控
- 限流熔断,当出现流量洪峰,或者后端BFF/微服务出现延迟或故障,网关能够主动进行限流熔断,保护后端服务,并保持前端用户体验可以接受。
- 安全防爬,收集访问日志,通过后台分析出恶意行为,并阻断恶意请求。
- 网关在无线设备和BFF之间又引入了一层间接,让两边可以独立变化,特别是当后台BFF在升级或迁移时,可以做到用户端应用不受影响
在新的 V3 架构中,网关承担了重要的角色,它是解耦和后续升级迁移的利器,在网关的配合下,单块 BFF 实现了解耦拆分,各业务团队可以独立开发和交付各自的微服务,研发效率大大提升,另外,把横跨切面逻辑从 BFF 剥离到网关后,BFF 的开发人员可以更加专注于业务逻辑交付,实现了架构上的关注分离。
5 V4
业务在不断发展,技术架构也需要不断的迭代和升级,近年来技术团队又新来了新的业务和技术需求:
- 开放内部的业务能力,建设开发平台。借助第三方社区开发者能力进一步拓宽业务形态
- 废弃传统服务端 Web 应用模式,引入前后端分离架构,前端采用 H5 单页技术给用户提供更好的用户体验
V4 的思路和 V3 差不多,只是拓展了新的接入渠道:
- 引入面向第三方开放 Api 的 BFF 层和配套的网关层,支持第三方开发者在开放平台上开发应用
- 引入面向 H5 应用的 BFF 和配套网关,支持前后分离和 H5 单页应用模式
V4 是一个比较完整的现代微服务架构,从外到内依次是:端用户体验层 -> 网关层 -> BFF 层 -> 微服务层。整个架构层次清晰、职责分明,是一种灵活的能够支持业务不断创新的演进式架构
总结:
- 在微服务架构中,BFF(Backend for Frontend)也称聚合层或者适配层,它主要承接一个适配角色:将内部复杂的微服务,适配成对各种不同用户体验(无线/Web/H5/第三方等)友好和统一的API。聚合裁剪适配是BFF的主要职责。
- 在微服务架构中,网关专注解决跨横切面逻辑,包括路由、安全、监控和限流熔断等。网关一方面是拆分解耦的利器,同时让开发人员可以专注业务逻辑的实现,达成架构上的关注分离。
- 端用户体验层->网关层->BFF层->微服务层,是现代微服务架构的典型分层方式,这个架构能够灵活应对业务需求的变化,是一种支持创新的演化式架构。
- 技术和业务都在不断变化,架构师要不断调整架构应对这些的变化,BFF和网关都是架构演化的产物。
2. BFF & GraphQL
大前端模式下经常会面临下面2个问题
- 频繁变化的 API 是需要向前兼容的
- BFF 中返回的字段不全是客户端需要的
至此 2015 年 GraphQL 被 Facebook 正式开源。它并不是一门语言,而是一种 API 查询风格。本文着重讲解了大前端模式下 BFF 层的设计和演进,需要配合 GraphQL 落地。下面简单介绍下 GraphQL,具体的可以查看 Node 或者某个语言对应的解决方案。
1. 使用 GraphQL
服务端描述数据 - 客户端按需请求 - 服务端返回数据
服务端描述数据
type Project {
name: String
tagline: String
contributors: [User]
}
客户端按需请求
{
Project(name: 'GraphQL') {
tagline
}
}
服务端返回数据
{
"project": {
tagline: "A query language for APIS"
}
}
2. 特点
1.定义数据模型,按需获取
就像写 SQL 一样,描述好需要查询的数据即可
2. 数据分层
数据格式清晰,语义化强
3. 强类型,类型校验
拥有 GraphQL 自己的类型描述,类型检查
4. 协议⽽非存储
看上去和 MongoDB 比较像,但是一个是数据持久化能力,一个是接口查询描述。
5. ⽆须版本化
写过客户端代码的同学会看到 Apple 给 api 废弃的时候都会标明原因,推荐用什么 api 代替等等,这点 GraphQL 也支持,另外废弃的时候也描述了如何解决,如何使用。
3. 什么时候需要 BFF
- 后端被诸多客户端使用,并且每种客户端对于同一类 Api 存在定制化诉求
- 后端微服务较多,并且每个微服务都只关注于自己的领域
- 需要针对前端使用的 Api 做一些个性话的优化
什么时候不推荐使用 BFF
- 后端服务仅被一种客户端使用
- 后端服务比较简单,提供为公共服务的机会不多(DDD、微服务)
4. 总结
在微服务架构中,BFF(Backend for Frontend)也称re聚合层或者适配层,它主要承接一个适配角色:将内部复杂的微服务,适配成对各种不同用户体验(无线/Web/H5/第三方等)友好和统一的API。聚合裁剪适配是BFF的主要职责。
在微服务架构中,网关专注解决跨横切面逻辑,包括路由、安全、监控和限流熔断等。网关一方面是拆分解耦的利器,同时让开发人员可以专注业务逻辑的实现,达成架构上的关注分离。
端用户体验层->网关层->BFF层->微服务层,是现代微服务架构的典型分层方式,这个架构能够灵活应对业务需求的变化,是一种支持创新的演化式架构。
技术和业务都在不断变化,架构师要不断调整架构应对这些的变化,BFF和网关都是架构演化的产物。
四、后端架构演进
1. 一些常见名词解释
云原生( Cloud Native )是一种构建和运行应用程序的方法,是一套技术体系和方法论。 Cloud Native 是一个组合词,Cloud+Native。 Cloud 是适应范围为云平台,Native 表示应用程序从设计之初即考虑到云的环境,原生为云而设计,在云上以最佳姿势运行,充分利用和发挥云平台的弹性+分布式优势。
Iaas:基础设施服务,Infrastructure as a service,如果把软件开发比作厨师做菜,那么IaaS就是他人提供了厨房,炉子,锅等基础东西, 你自己使用这些东西,自己根据不同需要做出不同的菜。由服务商提供服务器,一般为云主机,客户自行搭建环境部署软件。例如阿里云、腾讯云等就是典型的IaaS服务商。
Paas:平台服务,Platform as a service,还是做菜比喻,比如我做一个黄焖鸡米饭,除了提供基础东西外,那么PaaS还给你提供了现成剁好的鸡肉,土豆,辣椒, 你只要把这些东西放在一起,加些调料,用个小锅子在炉子上焖个20分钟就好了。自己只需关心软件本身,至于软件运行的环境由服务商提供。我们常说的云引擎、云容器等就是PaaS。
Faas:函数服务,Function as a Service,同样是做黄焖鸡米饭,这次我只提供酱油,色拉油,盐,醋,味精这些调味料,其他我不提供,你自己根据不同口味是多放点盐, 还是多放点醋,你自己决定。
Saas:软件服务,Software as a service,同样还是做黄焖鸡米饭,这次是直接现成搞好的一个一个小锅的鸡,什么调料都好了,已经是个成品了,你只要贴个牌,直接卖出 去就行了,做多是在炉子上焖个20分钟。这就是SaaS(Software as a Service,软件即服务)的概念,直接购买第三方服务商已经开发好的软件来使用,从而免去了自己去组建一个团队来开发的麻烦。
Baas:你了解到,自己要改的东西,只需要前端改了就可以了,后端部分完全不需要改。这时候你动脑筋,可以招了前端工程师,前端页面自己做,后端部分还是用服务商的。
这就是BaaS(Backend as a Service,后端即服务),自己只需要开发前端部分,剩下的所有都交给了服务商。经常说的“后端云”就是BaaS的意思,例如像LeanCloud、Bomb等就是典型的BaaS服务商。
MicroService vs Severless:MicroService是微服务,是一种专注于单一责任与功能的小型服务,Serverless相当于更加细粒度和碎片化的单一责任与功能小型服务,他们都是一种特定的小型服务, 从这个层次来说,Serverless=MicroService。
MicroService vs Service Mesh:在没有ServiceMesh之前微服务的通信,数据交换同步也存在,也有比较好的解决方案,如Spring Clould,OSS,Double这些,但他们有个最大的特点就是需要你写入代码中,而且需要深度的写 很多逻辑操作代码,这就是侵入式。而ServiceMesh最大的特点是非侵入式,不需要你写特定代码,只是在云服务的层面即可享受微服务之间的通信,数据交换同步等操作, 这里的代表如,docker+K8s,istio,linkerd等。
2. 架构演进
后端整个的演进可以如上图所示,经历了不断迭代,具体的解释这里不做展开。跟 SOA 相提并论的还有一个 ESB(企业服务总线),简单来说ESB就是一根管道,用来连接各个服务节点。为了集成不同系统,不同协议的服务,ESB 可以简单理解为:它做了消息的转化解释和路由工作,让不同的服务互联互通;使用ESB解耦服务间的依赖
SOA 和微服务架构的差别
- 微服务去中心化,去掉 ESB 企业总线。微服务不再强调传统 SOA 架构里面比较重的 ESB 企业服务总线,同时 SOA 的思想进入到单个业务系统内部实现真正的组件化
- Docker 容器技术的出现,为微服务提供了更便利的条件,比如更小的部署单元,每个服务可以通过类似 Node 或者 Spring Boot 等技术跑在自己的进程中。
- SOA 注重的是系统集成方面,而微服务关注的是完全分离
REST API:每个业务逻辑都被分解为一个微服务,微服务之间通过REST API通信。
API Gateway:负责服务路由、负载均衡、缓存、访问控制和鉴权等任务,向终端用户或客户端开发API接口.
前端、BFF、后端一些常见的设计模式
写在开头的部分,本文的契机是最近我们组同事在客户端实现了一套 Redux,对某个业务域的功能进行重构设计,iOS、Android 都遵循这套规则,即 Redux。为什么需要客户端去实现一套 Redux?商品模块业务逻辑非常负责,商品基础信息非常多,比如多规格、多单位、价格、库存等信息,还有对应的门店、网店模型,还有各种行业能力开关控制,早期的实现有 Rx 的角色,导致代码逻辑较为复杂,数据流动比较乱。架构设计、代码维护各方面来看都不是很优雅,加上最近有大的业务调整,中台的同学用 Redux + 单向数据流的方式重构了业务。
另一个下线经常请求网关数据,iOS、Android 各自去声明 DTO Model,然后解析数据,生成客户端需要的数据,这样“重复”的行为经常发生,所以索性用 TS + 脚本,统一做掉了网关数据模型自动生成 iOS、Android 模型的能力。但是在讨论框架设计的时候回类比 React Redux、Flutter Fish Redux、Vuex 等,还会聊到单向数据流、双向数据流,但是有些同学的理解就是错误的。所以本文第一部分「纠错题」部分就是讲清楚前端几个关键概念。
后面的部分按照逻辑顺序讲一下:微前端 -> BFF/网关 -> 微服务。
一、纠错题
1. Vue 是双向数据流吗?
如果回答“是”,那么你应该没有搞清楚“双向数据流”与“双向绑定”这2个概念。其实,准确来说两者不是一个维度的东西,单向数据流也可以实现双向绑定。
其实,你要是仔细看过 Vue 的官方文档,那么官方就已经说明 Vue 其实是 One-Way Data Flow。下面这段话来自官方
首先明确下,我们说的数据流就是组件之间的数据流动。Vue 中所有的 props 都使得其父子 props 之间形成一个单向下行绑定:父级 props 的更新回向下流动到子组件中,但反过来不行,这样防止从子组件意外更改其父组件的状态,从而导致你的应用数据流难以理解。
此外,每次父级组件发生变更时,子组件中所有的 props 都将会被刷新为最新的值,这意味着你不应该在子组件内去修改 prop,假如你这么做了,浏览器会在控制台中输出警告。Vue 和 React 修改 props 报错如下:
2. React 是 MVVM 吗?
不直接回答这个问题,我们先来聊几个概念,这几个概念弄清楚了,问题的答案也就呼之欲出了。
2.1 单向绑定、双向绑定的区别?
讲 MVVM 一定要讲 binder 这个角色。也就是单向绑定和双向绑定。
Vue 支持单向绑定和双向绑定。单向绑定其实就是 Model 到 View 的关联,比如 v-bind。双向绑定就是 Model 的更新会同步到 View,View 上数据的变化会自动同步到 Model,比如 v-model.
v-model 其实是语法糖,因为 Vue 是 tamplate,所以在经过 webpack ast 解析之后,tamplate 会变为 render,v-model 变为 v-bind 和 v-on
可以用单向绑定很简单地可以实现双向绑定的效果,就是 one-way binding + auto event binding。如下
其实看了源码就会发现(Vue3 Proxy),单向绑定和双向绑定的区别在于,双向绑定把数据变更的操作部分,由框架内部实现了,调用者无须感知。
2.2 Redux
使用 React Redux 一个典型的流程图如下
假如我们把 action 和 dispatcher 的实现隐藏在框架内部,这个图可以简化为
假如进一步,我们将互相手动通知的机制再隐藏起来,可以简化为
你仔细看,是不是就是 MVVM?那问题的答案很明显了,React 不是 MVVM。
3. Vue 是 MVVM 吗
官方说了“虽然没有完全遵循 MVVM 的思想,但是受到了 MVVM 的启发。所以 Debug 的时候经常看到 VM” Model 的变动会触发 View 的更新,这一点体现在 V-bind 上 而 View 的变更即使同步到 Model 上,体现在 V-model 上 上述都遵循 MVVM,但是 Vue 提供了 Ref 属性,允许使用 ref 拿到 Dom,从而直接操作 Dom 的一些样式或者属性,这一点打破了 MVVM 的规范
4. 单向绑定和双向绑定使用场景
我们再来思考一个问题,为什么 Vue 要移除 .sync, Angular 要增加 < 来实现单向绑定?
当应用变得越来越大,双向数据流会带来很多的不可预期的结果,这让 debug、调试、测试都变得复杂。
所以 Vue、Angular 都意识到这一点,所以移除了 .sync,支持单向绑定。
正面聊聊问题:
单向绑定,带来的单向数据流,带来的好处是所有状态变化都可以被记录、跟踪,状态变化必须通过手动调用通知,源头可追溯,没有“暗箱操作”。同时组件数据只有唯一的入口和出口,使得程序更直观更容易理解,有利于应用的可维护性。缺点是实现同样的需求,代码量会上升,数据的流转过程变长。同时由于对应用状态独立管理的严格要求(单一的全局store),在处理局部状态较多的场景时(如用户输入交互较多的“富表单型”应用)会显得特别繁琐。
双向绑定优点是在表单交互较多的场景下,会简化大量业务无关的代码。缺点就是由于都是“暗箱操作”,无法追踪局部状态的变化,潜在的行为太多也增加了出错时 debug 的难度。同时由于组件数据变化来源入口变得可能不止一个,程序员水平参差不齐,写出的代码很容易将数据流转方向弄得紊乱,整个应用的质量就不可控。
总结:单向绑定跟双向绑定在功能上基本上是互补的,所以我们可以在合适的场景下使用合适的手段。比如在 UI 控件中(通常是类表单操作),可以使用双向的方式绑定数据;而其他场景则使用单向绑定的方式
二、 微前端
上面提到的 Vue、React、单向数据流、双向数据流都是针对单个应用去组织和设计工程的,但是当应用很大的时候就会存在一些问题。引出今天的一个主题,“微前端”
客户端的同学都知道,我们很早就在使用组件化、模块化来拆分代码和逻辑。那么你可以说出到底什么是组件、什么是模块?
组件就是把重复的代码提取出来合并成为一个个组件,组件强调的是重用,位于系统最底层,其他功能都依赖于组件,独立性强
模块就是分属同一功能/业务的代码进行隔离成独立的模块,可以独立运行,以页面、功能等维度划分为不同模块,位于业务架构层。
这些概念在前端也是如此,比如写过 Vue、React 的都知道,对一个页面需要进行组件的拆分,一个大页面由多个 UI 子组件组成。模块也一样,前端利用自执行函数(IIFE)来实现模块化,成熟的 CommonJS、AMD、CMD、UMD 规范等等。比如 iOS 侧利用模块化来实现了商品、库存、开单等业务域的拆分,也实现了路由库、网络库等基础能力模块。而前端更多的是利用模块化来实现命名空间和代码的可重用性
其实,看的出来,前端、客户端实行的模块化、组件化的目标都是分治思想,更多是站在代码层面进行拆分、组织管理。但是随着前端工程的越来越重,传统的前端架构已经很难遵循“敏捷”的思想了。
1. 什么是微前端
关心技术的人听到微前端,立马会想起微服务。微服务是面向服务架构的一种变体,把应用程序设计为一些列松耦合的细粒度服务,并通过轻量级的通信协议组织起来。越老越重的前端工程面临同样的问题,自然而然就将微服务的思想借鉴到了前端领域。
言归正传,微前端(Micro-Frontends)是一种类似于微服务的架构,它将微服务的理念应用于浏览器端,即将 Web 应用由单一的单体应用转变为多个小型前端应用聚合为一的应用。各个前端应用还可以独立运行、独立开发、独立部署。微前端不是单纯的前端框架或者工具,而是一套架构体系。
2. 特点
2.1 技术无关
抛两个场景,大家思考一下:
- 你新入职一家公司,老板扔给你一个 5 年陈的项目,需要你在这个项目上持续迭代加功能。
- 你们起了一个新项目,老板看惯了前端的风起云涌技术更迭,只给了架构上的一个要求:"如何确保这套技术方案在 3~5 年内还葆有生命力,不会在 3、5 年后变成又一个遗产项目?"
第一个场景我们初步一想,可以啊,我只需要把新功能用 React/Vue 开发,反正他们都只是 UI library,给我一个Dom 节点我想怎么渲染怎么渲染。但是你有没有考虑过这只是浮在表层的视图实现,沉在底下的工程设施呢?我要怎么把这个用 React 写的组件打出一个包,并且集成到原来的用 ES5 写的代码里面?或者我怎么让 Webpack 跟 之前的 Grunt 能和谐共存一起友好的产出一个符合预期的 Bundle?
第二个场景,你如何确保技术栈在 3~5 年都葆有生命力?别说跨框架了,就算都是 React,15 跟 16 都是不兼容的,hooks 还不能用于 Class component 呢我说什么了?还有打包方案,现在还好都默认 Webpack,那 Webpack 版本升级呢,每次都跟进吗?别忘了还有 Babel、less、typescript 诸如此类呢?别说 3 年,有几个人敢保证自己的项目一年后把所有依赖包升级到最新还能跑起来?
为什么举这两个场景呢,因为我们去统计一下业界关于”微前端“发过声的公司,会发现 adopt 微前端的公司,基本上都是做 ToB 软件服务的,没有哪家 ToC 公司会有微前端的诉求(有也是内部的中后台系统),为什么会这样?很简单,因为很少有 ToC 软件活得过 3 年以上的。而对于 ToB 应用而言,3~5 年太常见了好吗!去看看阿里云最早的那些产品的控制台,去看看那些电信软件、银行软件,哪个不是 10 年+ 的寿命?企业软件的升级有多痛这个我就不多说了。所以大部分企业应用都会有一个核心的诉求,就是如何确保我的遗产代码能平滑的迁移,以及如何确保我在若干年后还能用上时下热门的技术栈?
如何给遗产项目续命,才是我们对微前端最开始的诉求。很多开发可能感受不深,毕竟那些一年挣不了几百万,没什么价值的项目要么是自己死掉了,要么是扔给外包团队维护了,但是要知道,对很多做 ToB 领域的中小企业而言,这样的系统可能是他们安身立命之本,不是能说扔就扔的,他们承担不了那么高的试错成本。
甚至新的业务,每个技术团队应该可以根据自己团队成员的技术储备、根据业务特点选择合适的技术栈,而不需要特别关心其他团队的情况,也就是 A 团队可以用 React,B 团队可以用 Vue,C 团队甚至可以用 Angular 去实现。
2.2 简单、松耦合的代码库
比起一整块的前端工程来说,微前端架构下的代码库更小更容易开发、维护。此外,更重要的是避免模块间不合理的隐式耦合造成的复杂度上升。通过界定清晰的应用边界来降低意外耦合的可能性,增加子应用间逻辑耦合的成本,促使开发者明确数据和事件在应用程序中的流向
2.3 增量升级
理想的代码自然是模块清晰、依赖明确、易于扩展、便于维护的……然而,实践中出于各式各样的原因:
- 历史项目,祖传代码
- 交付压力,当时求快
- 就近就熟,当时求稳……
总存在一些不那么理想的代码:
- 技术栈落后,甚至强行混用多种技术栈
- 耦合混乱,不敢动,牵一发何止动全身
- 重构不彻底,重构-烂尾,换个姿势重构-又烂尾……
而要对这些代码进行彻底重构的话,最大的问题是很难有充裕的资源去大刀阔斧地一步到位,在逐步重构的同时,既要确保中间版本能够平滑过渡,同时还要持续交付新特性:
所以,为了实施渐进式重构,我们需要一种增量升级的能力,先让新旧代码和谐共存,再逐步转化旧代码,直到整个重构完成
这种增量升级的能力意味着我们能够对产品功能进行低风险的局部替换,包括升级依赖项、更替架构、UI 改版等。另一方面,也带来了技术选型上的灵活性,有助于新技术、新交互模式的实验性试错
2.4 独立部署
独立部署的能力在微前端体系中至关重要,能够缩小变更范围,进而降低相关风险
因此,每个微前端都应具备有自己的持续交付流水线(包括构建、测试并部署到生产环境),并且要能独立部署,不必过多考虑其它代码库和交付流水线的当前状态:
假如 A、B、C 三个 Biz 存在三个系统,三者可以独立部署,最后由主系统去集成发布应用。这样子更加独立灵活。想想以前的单体应用,比如一个复杂的业务应用,很可能在 B 构建的时候没有通过 lint 导致整个应用失败掉,这样既浪费了构建 A 的时间,又让整个应用的构建变为串行,低效。
2.5 团队自治
除了代码和发布周期上的解耦之外,微前端还有助于形成独立的团队,由不同的团队各自负责一块独立的产品功能,最好根据 Biz 去划分,由于代码都是独立的,所以基于 Biz 的前端业务团队,可以设计思考,提供更加合理的接口和能力。甚至可以抽象更多的基础能力,按照业务思考提供更加完备的能力,也就是团队更加自治。
3 如何实现
微前端主要采用的是组合式应用路由方案,该方案的核心思想是“主从”(玩过 Jenkins 的同学是不是很耳熟),即包括一个基座“MainApp “ 应用和若干个微应用(MircroApp),基座大多采用的是一个前端 SPA 项目,主要负责应用注册、路由映射、消息下发等,而微应用是独立的前端项目,这些项目不限于采用的具体技术(React、Vue、Angular、Jquery)等等,每个微应用注册到基座中,由基座进行管理,即使没有基座,这些微应用都可以单独访问,如下:
一个微应用框架需要解决的问题是:
路由切换的分发问题
作为微前端的基座应用,是整个应用的入口,负责承载当前微应用的展示和对其他路由微应用的转发,对于当前微应用的展示,一般是由以下几步构成:
- 作为一个SPA的基座应用,本身是一套纯前端项目,要想展示微应用的页面除了采用iframe之外,要能先拉取到微应用的页面内容, 这就需要远程拉取机制。
- 远程拉取机制通常会采用fetch API来首先获取到微应用的HTML内容,然后通过解析将微应用的JavaScript和CSS进行抽离,采用eval方法来运行JavaScript,并将CSS和HTML内容append到基座应用中留给微应用的展示区域,当微应用切换走时,同步卸载这些内容,这就构成的当前应用的展示流程。
- 当然这个流程里会涉及到CSS样式的污染以及JavaScript对全局对象的污染,这个涉及到隔离问题会在后面讨论,而目前针对远程拉取机制这套流程,已有现成的库来实现,可以参考 import-html-entry 和 system.js。
对于路由分发而言,以采用vue-router开发的基座SPA应用来举例,主要是下面这个流程:
- 当浏览器的路径变化后,vue-router会监听hashchange或者popstate事件,从而获取到路由切换的时机。
- 最先接收到这个变化的是基座的router,通过查询注册信息可以获取到转发到那个微应用,经过一些逻辑处理后,采用修改hash方法或者pushState方法来路由信息推送给微应用的路由,微应用可以是手动监听hashchange或者popstate事件接收,或者采用React-router,vue-router接管路由,后面的逻辑就由微应用自己控制。
主应用、微应用的隔离问题
应用隔离问题主要分为主应用和微应用,微应用和微应用之间的JavaScript执行环境隔离,CSS样式隔离,我们先来说下CSS的隔离。
CSS隔离:当主应用和微应用同屏渲染时,就可能会有一些样式会相互污染,如果要彻底隔离CSS污染,可以采用CSS Module 或者命名空间的方式,给每个微应用模块以特定前缀,即可保证不会互相干扰,可以采用webpack的postcss插件,在打包时添加特定的前缀。
而对于微应用与微应用之间的CSS隔离就非常简单,在每次应用加载时,将该应用所有的link和style 内容进行标记。在应用卸载后,同步卸载页面上对应的link和style即可。
JavaScript隔离:每当微应用的JavaScript被加载并运行时,它的核心实际上是对全局对象Window的修改以及一些全局事件的改变,例如jQuery这个js运行后,会在Window上挂载一个
window.$
对象,对于其他库React,Vue也不例外。为此,需要在加载和卸载每个微应用的同时,尽可能消除这种冲突和影响,最普遍的做法是采用沙箱机制(SandBox)。沙箱机制的核心是让局部的JavaScript运行时,对外部对象的访问和修改处在可控的范围内,即无论内部怎么运行,都不会影响外部的对象。通常在Node.js端可以采用vm模块,而对于浏览器,则需要结合with关键字和window.Proxy对象来实现浏览器端的沙箱。
通信问题
应用间通信有很多种方式,当然,要让多个分离的微应用之间要做到通信,本质上仍离不开中间媒介或者说全局对象。所以对于消息订阅(pub/sub)模式的通信机制是非常适用的,在基座应用中会定义事件中心Event,每个微应用分别来注册事件,当被触发事件时再有事件中心统一分发,这就构成了基本的通信机制,流程如下图:
目前开源了很多微前端框架,比如 qiankun ,感兴趣的可以去看看源码
4. 是否采用微前端
为什么要做微前端?或者说微前端解决了什么问题?也就回答了是否需要采用微前端。微前端的鼻祖 Single-spa 最初要解决的问题是,在老项目中使用新的技术栈。现阶段蚂蚁金服微前端框架 Qiankun 所声明的微前端要解决的另一个主要问题是,巨石工程所面临的维护困难喝协作开发困难的问题。如果工程面临这两方面的问题,我觉得微前端就可以试试了。你们觉得呢?
三、微服务平台下基于 GraphQL 构建 BFF 的思考
1. 大前端架构演进
我们来讲一个故事
1. V1
假设早期2011年一家电商公司完成了单体应用的拆分,后端服务已经 SOA 化,此时应用的形态仅有 Web 端,V1架构图如下所示:
2. V2
时间来到了2012年初,国内刮起了一阵无线应用的风,各个大厂都开始了自己的无线应用开发。为了应对该需求,架构调整为 V2,如下所示
这个架构存在一些问题:
- 无线 App 和内部的微服务强耦合,任何一侧的变化都可能对另一侧造成影响
- 无线 App 需要知道内部服务细节
无线 App 端需要做大量的聚合裁剪和适配逻辑
聚合:某一个页面可能同时需要调用好几个后端 API 进行组合,比如商品详情页所需的数据,不能一次调用完成
裁剪:后端提供的基础服务比较通用,返回的 Payload 比较大,字段太多,App 需要根据设备和业务需求,进行字段裁剪用户的客户端是Android还是iOS,是大屏还是小屏,是什么版本。再比如,业务属于哪个行业,产品形态是什么,功能投放在什么场景,面向的用户群体是谁等等。这些因素都会带来面向端的功能逻辑的差异性。后端在微服务和领域驱动设计(不在本文重点讲解范畴)的背景下,各个服务提供了当前 Domain 的基础功能,比如商品域,提供了商品新增、查询、删除功能,列表功能、详情页功能。
但是在商品详情页的情况下,数据需要调用商品域、库存域、订单域的能力。客户端需要做大量的网络接口处理和数据处理。
适配:一些常见的适配常见就是格式转换,比如有些后端服务比较老,提供 SOAP/XML 数据,不支持 JSON,这时候就需要做一些适配代码
- 随着使用类型的增多(iOS Phone/Pad、Android Phone/Pad、Hybrid、小程序),聚合裁剪和适配的逻辑的开发会造成设备端的大量重复劳动
- 前端比如 iOS、Android、小程序、H5 各个 biz 都需要业务数据。最早的设计是后端接口直出。这样的设计会导致出现一个问题,因为 biz1 因为迭代发版,造成后端改接口的实现。 Biz2 的另一个需求又会让服务端改设计实现,造成接口是面向业务开发的。不禁会问,基础服务到底是面向业务接口开发的吗?这和领域驱动的设计相背离
在这样的背景下诞生了 BFF(Backend For Frontend) 层。各个领域提供基础的数据模型,各个 biz 存在自己的 BFF 层,按需取查询(比如商品基础数据200个,但是小程序列表页只需要5个),在比如商品详情页可能需要商品数据、订单数据、评价数据、推荐数据、库存数据等等,最早的设计该详情页可能需要请求6个接口,这对于客户端、网页来说,体验很差,有个新的设计,详情页的 BFF 去动态组装调用接口,一个 BFF 接口就组装好了数据,直接返回给业务方,体验很好
3. V2.1
V2 架构问题太多,没有开发实施。为解决上述问题,在外部设备和内部微服务之间引入一个新的角色 Mobile BFF。BFF 也就是 Backend for Frontend 的简称,可以认为是一种适配服务,将后端的微服务进行适配(主要包括聚合裁剪和格式适配等逻辑),向无线端设备暴露友好和统一的 API,方便无线设备接入访问后端服务。V2.1 服务架构如下
这个架构的优势是:
无线 App 和内部服务不耦合,通过引入 BFF 这层简介,使得两边可以独立变化:
- 后端如果发生变化,通过 BFF 屏蔽,前端设备可以做到不受影响
- 前端如果发生变化,通过 BFF 屏蔽,后端微服务可以暂不变化
- 当无线端有新的需求的时候,通过 BFF 屏蔽,可以减少前后端的沟通协作开销,很多需求由前端团队在 BFF 上就可以自己搞定
- 无线 App 只需要知道 Mobile BFF 的地址,并且服务接口是统一的,不需要知道内部复杂的微服务地址和细节
- 聚合裁剪和适配逻辑在 Mobile BFF 上实现,无线 App 端可以简化瘦身
4. V3
V2.1 架构比较成功,实施后较长一段时间支持了公司早期无线业务的发展,随着业务量暴增,无线研发团队不断增加,V2.1 架构的问题也被暴露了出来:
- Mobile BFF 中不仅有各个业务线的聚合/裁剪/适配和业务逻辑,还引入了很多横跨切面的逻辑,比如安全认证,日志监控,限流熔断等,随着时间的推移,代码变得越来越复杂,技术债越来越多,开发效率不断下降,缺陷数量不断增加
- Mobile BFF 集群是个失败单点,严重代码缺陷将导致流量洪峰可能引起集群宕机,所有无线应用都不可用
为了解决上述伪命题,决定在外部设备和 BFF 之间架构一个新的层,即 Api Gateway,V3 架构如下:
新的架构 V3 有如下调整:
- BFF 按团队或者业务进行解耦拆分,拆分为若干个 BFF 微服务,每个业务线可以并行开发和交付各自负责的 BFF 微服务
官关(一般由独立框架团队负责运维)专注横跨切面功能,包括:
- 路由,将来自无线设备的请求路由到后端的某个微服务 BFF 集群
- 认证,对涉及敏感数据的 Api 进行集中认证鉴权
- 监控,对 Api 调用进行性能监控
- 限流熔断,当出现流量洪峰,或者后端BFF/微服务出现延迟或故障,网关能够主动进行限流熔断,保护后端服务,并保持前端用户体验可以接受。
- 安全防爬,收集访问日志,通过后台分析出恶意行为,并阻断恶意请求。
- 网关在无线设备和BFF之间又引入了一层间接,让两边可以独立变化,特别是当后台BFF在升级或迁移时,可以做到用户端应用不受影响
在新的 V3 架构中,网关承担了重要的角色,它是解耦和后续升级迁移的利器,在网关的配合下,单块 BFF 实现了解耦拆分,各业务团队可以独立开发和交付各自的微服务,研发效率大大提升,另外,把横跨切面逻辑从 BFF 剥离到网关后,BFF 的开发人员可以更加专注于业务逻辑交付,实现了架构上的关注分离。
5 V4
业务在不断发展,技术架构也需要不断的迭代和升级,近年来技术团队又新来了新的业务和技术需求:
- 开放内部的业务能力,建设开发平台。借助第三方社区开发者能力进一步拓宽业务形态
- 废弃传统服务端 Web 应用模式,引入前后端分离架构,前端采用 H5 单页技术给用户提供更好的用户体验
V4 的思路和 V3 差不多,只是拓展了新的接入渠道:
- 引入面向第三方开放 Api 的 BFF 层和配套的网关层,支持第三方开发者在开放平台上开发应用
- 引入面向 H5 应用的 BFF 和配套网关,支持前后分离和 H5 单页应用模式
V4 是一个比较完整的现代微服务架构,从外到内依次是:端用户体验层 -> 网关层 -> BFF 层 -> 微服务层。整个架构层次清晰、职责分明,是一种灵活的能够支持业务不断创新的演进式架构
总结:
- 在微服务架构中,BFF(Backend for Frontend)也称聚合层或者适配层,它主要承接一个适配角色:将内部复杂的微服务,适配成对各种不同用户体验(无线/Web/H5/第三方等)友好和统一的API。聚合裁剪适配是BFF的主要职责。
- 在微服务架构中,网关专注解决跨横切面逻辑,包括路由、安全、监控和限流熔断等。网关一方面是拆分解耦的利器,同时让开发人员可以专注业务逻辑的实现,达成架构上的关注分离。
- 端用户体验层->网关层->BFF层->微服务层,是现代微服务架构的典型分层方式,这个架构能够灵活应对业务需求的变化,是一种支持创新的演化式架构。
- 技术和业务都在不断变化,架构师要不断调整架构应对这些的变化,BFF和网关都是架构演化的产物。
2. BFF & GraphQL
大前端模式下经常会面临下面2个问题
- 频繁变化的 API 是需要向前兼容的
- BFF 中返回的字段不全是客户端需要的
至此 2015 年 GraphQL 被 Facebook 正式开源。它并不是一门语言,而是一种 API 查询风格
1. 使用 GraphQL
服务端描述数据 - 客户端按需请求 - 服务端返回数据
服务端描述数据
type Project {
name: String
tagline: String
contributors: [User]
}
客户端按需请求
{
Project(name: 'GraphQL') {
tagline
}
}
服务端返回数据
{
"project": {
tagline: "A query language for APIS"
}
}
2. 特点
1.定义数据模型,按需获取
就像写 SQL 一样,描述好需要查询的数据即可
2. 数据分层
数据格式清晰,语义化强
3. 强类型,类型校验
拥有 GraphQL 自己的类型描述,类型检查
4. 协议⽽非存储
看上去和 MongoDB 比较像,但是一个是数据持久化能力,一个是接口查询描述。
5. ⽆须版本化
写过客户端代码的同学会看到 Apple 给 api 废弃的时候都会标明原因,推荐用什么 api 代替等等,这点 GraphQL 也支持,另外废弃的时候也描述了如何解决,如何使用。
3. 什么时候需要 BFF
- 后端被诸多客户端使用,并且每种客户端对于同一类 Api 存在定制化诉求
- 后端微服务较多,并且每个微服务都只关注于自己的领域
- 需要针对前端使用的 Api 做一些个性话的优化
什么时候不推荐使用 BFF
- 后端服务仅被一种客户端使用
- 后端服务比较简单,提供为公共服务的机会不多(DDD、微服务)
4. 总结
在微服务架构中,BFF(Backend for Frontend)也称re聚合层或者适配层,它主要承接一个适配角色:将内部复杂的微服务,适配成对各种不同用户体验(无线/Web/H5/第三方等)友好和统一的API。聚合裁剪适配是BFF的主要职责。
在微服务架构中,网关专注解决跨横切面逻辑,包括路由、安全、监控和限流熔断等。网关一方面是拆分解耦的利器,同时让开发人员可以专注业务逻辑的实现,达成架构上的关注分离。
端用户体验层->网关层->BFF层->微服务层,是现代微服务架构的典型分层方式,这个架构能够灵活应对业务需求的变化,是一种支持创新的演化式架构。
技术和业务都在不断变化,架构师要不断调整架构应对这些的变化,BFF和网关都是架构演化的产物。
四、后端架构演进
1. 一些常见名词解释
云原生( Cloud Native )是一种构建和运行应用程序的方法,是一套技术体系和方法论。 Cloud Native 是一个组合词,Cloud+Native。 Cloud 是适应范围为云平台,Native 表示应用程序从设计之初即考虑到云的环境,原生为云而设计,在云上以最佳姿势运行,充分利用和发挥云平台的弹性+分布式优势。
Iaas:基础设施服务,Infrastructure as a service,如果把软件开发比作厨师做菜,那么IaaS就是他人提供了厨房,炉子,锅等基础东西, 你自己使用这些东西,自己根据不同需要做出不同的菜。由服务商提供服务器,一般为云主机,客户自行搭建环境部署软件。例如阿里云、腾讯云等就是典型的IaaS服务商。
Paas:平台服务,Platform as a service,还是做菜比喻,比如我做一个黄焖鸡米饭,除了提供基础东西外,那么PaaS还给你提供了现成剁好的鸡肉,土豆,辣椒, 你只要把这些东西放在一起,加些调料,用个小锅子在炉子上焖个20分钟就好了。自己只需关心软件本身,至于软件运行的环境由服务商提供。我们常说的云引擎、云容器等就是PaaS。
Faas:函数服务,Function as a Service,同样是做黄焖鸡米饭,这次我只提供酱油,色拉油,盐,醋,味精这些调味料,其他我不提供,你自己根据不同口味是多放点盐, 还是多放点醋,你自己决定。
Saas:软件服务,Software as a service,同样还是做黄焖鸡米饭,这次是直接现成搞好的一个一个小锅的鸡,什么调料都好了,已经是个成品了,你只要贴个牌,直接卖出 去就行了,做多是在炉子上焖个20分钟。这就是SaaS(Software as a Service,软件即服务)的概念,直接购买第三方服务商已经开发好的软件来使用,从而免去了自己去组建一个团队来开发的麻烦。
Baas:你了解到,自己要改的东西,只需要前端改了就可以了,后端部分完全不需要改。这时候你动脑筋,可以招了前端工程师,前端页面自己做,后端部分还是用服务商的。
这就是BaaS(Backend as a Service,后端即服务),自己只需要开发前端部分,剩下的所有都交给了服务商。经常说的“后端云”就是BaaS的意思,例如像LeanCloud、Bomb等就是典型的BaaS服务商。
MicroService vs Severless:MicroService是微服务,是一种专注于单一责任与功能的小型服务,Serverless相当于更加细粒度和碎片化的单一责任与功能小型服务,他们都是一种特定的小型服务, 从这个层次来说,Serverless=MicroService。
MicroService vs Service Mesh:在没有ServiceMesh之前微服务的通信,数据交换同步也存在,也有比较好的解决方案,如Spring Clould,OSS,Double这些,但他们有个最大的特点就是需要你写入代码中,而且需要深度的写 很多逻辑操作代码,这就是侵入式。而ServiceMesh最大的特点是非侵入式,不需要你写特定代码,只是在云服务的层面即可享受微服务之间的通信,数据交换同步等操作, 这里的代表如,docker+K8s,istio,linkerd 等。
2. 架构演进
1. 演进图
后端整个的演进可以如上图所示,经历了不断迭代,目前到了微服务、Service Mesh 的时代。具体的解释这里不做展开,感兴趣的可以去自行谷歌。跟 SOA 相提并论的还有一个 ESB(企业服务总线),简单来说 ESB 就是一根管道,用来连接各个服务节点,为了集成不同系统,不同协议的服务。
ESB 可以简单理解为:它做了消息的转化解释和路由工作,让不同的服务互联互通,使用ESB解耦服务间的依赖。
2. SOA 和微服务架构的差别
微服务去中心化,去掉 ESB 企业总线。微服务不再强调传统 SOA 架构里面比较重的 ESB 企业服务总线,同时 SOA 的思想进入到单个业务系统内部实现真正的组件化
Docker 容器技术的出现,为微服务提供了更便利的条件,比如更小的部署单元,每个服务可以通过类似 Node 或者 Spring Boot 等技术跑在自己的进程中。
SOA 注重的是系统集成方面,而微服务关注的是完全分离
3. 早期微服务架构
REST API:每个业务逻辑都被分解为一个微服务,微服务之间通过REST API通信。
API Gateway:负责服务路由、负载均衡、缓存、访问控制和鉴权等任务,向终端用户或客户端开发API接口
每个业务逻辑都被分解为一个微服务,微服务之间通过 REST API 通信。一些微服务也会向终端用户或客户端开发API接口。但通常情况下,这些客户端并不能直接访问后台微服务,而是通过 API Gateway 来传递请求。API Gateway 一般负责服务路由、负载均衡、缓存、访问控制和鉴权等任务。
4. 断路器
断路器背后的基本思想非常简单。您将受保护的函数调用包装在断路器对象中,该对象监视故障。一旦故障达到某个阈值,断路器就会跳
闸,并且所有对断路器的进一步调用都会返回错误,而根本不会进行受保护的调用。通常,如果断路器跳闸,您还需要某种监视器警报。
通常,断路器和服务发现等基础实现独立接入。
6. 早期微服务架构的问题及解决方案
- 框架/SDK太多,后续升级维护困难
- 服务治理逻辑嵌入业务应用,占有业务服务资源
- 服务治理策略难以统一
- 额外的服务治理组件(中间件)的维护成本
- 多语言:随着Node、Java以及其他后端服务的兴起,可能需要开发多套基础组件来配合主应用接入,SDK维护成本高
随机引入了 Sidecar 模式:
将这些基础服务和应用程序绑定在一起,但使用独立的进程或容器部署,这能为跨语言的平台服务提供同构接口。SideCare 很多人会很懵逼,这个词怎么理解,下面的配图,旁边那一小坨就比较形象了。
Sidecar模式的优势
- 在运行时环境和编程语言方面独立于它的主应用程序,不需要为每种语言开发一个 Sidecar
- Sidecar可以访问与主应用程序相同的资源
- 因为它靠近主应用程序(部署在一起),所以在它们之间通信时没有明显的延迟
- 即使对于不提供可扩展性机制的应用程序,也可以、将sidecar作为自己的进程附加到主应用程序所在的主机或子容器中进行扩展
7. Service Mesh 的形成
每个服务都将有一个配套的代理 sidecar,鉴于服务仅通过 sidecar 代理相互通信,我们最终会得到类似于下图的部署:
服务网格是用于处理服务到服务通信的专用基础设施层。它负责通过构成现代云原生应用程序的复杂服务拓扑来可靠地交付请求。在实践中,服务网格通常被实现为一系列轻量级网络代理,这些代理与应用程序代码一起部署,应用程序不需要知道。
网格 = 容器网格 + 服务
典型的 Service Mesh 架构
控制层:
- 不直接解析数据包
- 与控制平面中的代理通信,下发策略和配置
- 负责网络行为的可视化
- 通常提供 API 或者命令行工具可用于配置版本化管理,便于持续集成和部署
数据层:
- 通常是按照无状态目标设计的,但实际上为了提高流量转发性能,需要缓存一些数据,因此无状态也是有争议的
- 直接处理入站和出站数据包,转发、路由、健康检查、负载均衡、认证、鉴权、产生监控数据等
- 对应用来说透明,即可以做到无感知部署
Istio 是一个典型主流的 Service Mesh 框架。有关 Istio 的详细介绍可以看这里的电子书。
五、写在最后
为什么客户端工程师需要看这些,看上去和日常工作没啥关系,但是知识都是积累才有用的,你先储备才有机会发挥出来,知识是会复利的,某个领域的知识和解决方案可以为你在解决其他问题的时候提供思路和眼界。另外大前端不可能只和客户端、前端打交道,后端的解决方案、技术趋势也要了解,有助于你站在更宏观的角度解决某个问题,清楚设计。