1. 背景

团队归属于前方业务撑持部门,组内的我的项目都以pc中后盾利用为主。比照挪动端利用,代码库比拟宏大,业务逻辑也绝对简单。在继续的迭代过程中,咱们发现以后的代码仓库依然有不少能够优化的点:

能够削弱对ui框架的依赖

21年前端平台决定技术栈对立迁徙到React生态,后续平台的根底建设也都围绕React开展,这就使得商家应用Vue生态做开发的零碎面临技术栈迁徙的难题,将业务逻辑和UI框架节藕变得异样重要。

代码格调能够更加对立

随着代码量和团队成员的减少,利用里格调迥异的代码也越来越多。为了可能继续迅速的进行迭代,团队急需一套对立的顶层代码架构设计计划。

能够集成自动化测试用例

随着业务变得越来越简单,在迅速的迭代过程中团队须要频繁地对性能进行回归,因而咱们对于自动化单测用例的诉求也变的越来越强烈。

为了实现以上的优化,四组对现有的利用架构做了一次重构,而重构的外围就是整洁架构。

2. 整洁架构(The Clean Architecture)

整洁架构(The clean architecture)是由 Robert C. Martin (Uncle Bob)在2012年提出的一套代码组织的理念,其外围次要是根据各局部代码作用的不同将其拆分成不同的档次,在各层次间制订了明确的依赖准则,以达到以下目标:

  1. 与框架无关:无论是前端代码还是服务端代码,其逻辑自身都应该是独立的,不应该依赖于某一个第三方框架或工具库。一套独立的代码能够把第三方框架等作为工具应用。
  2. 可测试:代码中的业务逻辑能够在不依赖ui、数据库、服务器的状况下进行测试。
  3. 和ui无关:代码中的业务逻辑不应该和ui做强绑定。比方把一个web利用切换成桌面利用,业务逻辑不应该受到影响。
  4. 和数据库无关:无论数据库用的是mysql还是mongodb,无论其怎么变,都不该影响到业务逻辑。
  5. 和内部服务无关:无论内部服务怎么变,都不影响到应用该服务的业务逻辑。

为了实现以上目标,整洁架构把利用划分成了entities、use cases、interface adapters(MVC、MVP等)、Web/DB等至多四层。这套架构除了分层之外,在层与层之间还有一个十分明确的依赖关系,外层的逻辑依赖内层的逻辑。

Entity
entities封装了企业级的业务逻辑和规定。entities没有什么固定的模式,无论是一个对象也好,是一堆函数的汇合也好,惟一的规范就是可能被企业的各个利用所复用。

Use Case
entities封装了企业里最通用的一部分逻辑,而利用各自的业务逻辑就都封装在use case外面。日常开发中最常见的对于某个模型的crud操作就属于usecase这一层。

Interface Adapter
这一层相似于胶水层,须要负责内圈的entity和use case同外圈的external interfaces之间的数据转化。须要把外层服务的数据转化成内层entity和usecase能够生产的数据,反之亦然。如下面图上画的,这一层有时候可能很简略(一个转化函数), 有时候可能简单到蕴含一整个MVC/MVP的架构。

External Interfaces
咱们须要依赖的内部服务,第三方框架,以及须要糊的页面UI都归属在这一层。这一层齐全不感知内圈的任何逻辑,所以无论这一层怎么变(ui变动),都不应该影响到内圈的应用层逻辑(usecase)和企业级逻辑(entity)。

依赖准则
在整洁架构的原始设计中,并不是强制肯定只能写这么四层,依据业务的须要还能够拆分的更细。不过无论怎么拆,都须要恪守后面提到的从外至内的依赖准则。即entity作为企业级的通用逻辑,不能依赖任何模块。而外层的ui等则能够应用usecase、entity。

3. 重构

后面介绍了以后代码库目前的一些具体问题,而整洁架构的理念正好能够帮忙咱们优化代码可维护性。

作为前端,咱们的业务逻辑不应该依赖视图层(ui框架及其生态),同时该当保障业务逻辑的独立性和可复用性(usecase & entity)。最初,作为数据驱动的端利用,要保障利用视图渲染和业务逻辑等不受数据变动的影响(adapter & entity)。

依据以上的思考,咱们对“整洁架构”做了如下落地。

Entities
对于前端利用来说,在entity层咱们只须要将服务端的生数据做一层简略的形象,生成一个贫血对象给后续的渲染和交互逻辑应用。


以上是商家后盾订单模型的entity工厂函数,工厂次要负责对服务端返回的生数据进行加工解决,让其满足渲染层和逻辑层的要求。除了形象数据之外,能够看到在entity工厂还对数据进行了校验,将脏数据、不合乎预期的数据全副解决掉或者进行兜底(具体操作要看业务场景)。

有一点须要留神的是,在设计entity的时候(尤其是根底entity)须要思考复用性。举个例子,在下面orderEntity的根底上,咱们通过简略的组合就能够生成一个虚构商品订单entity:

如此一来,咱们就通过entity层达到了2个目标:

  1. 把前端的逻辑和服务端接口数据隔离开,无论服务端怎么变,前端后续的渲染、业务代码不须要变,咱们只须要变更entitiy工厂函数;并且通过entity层解决过后,所有流入后续渲染&交互逻辑的数据都是牢靠的;对于局部异样数据,前端利用能够第一工夫发现并报警。
  2. 通过对业务模型进行形象,实现了模块间的组合、复用。另外,形象出的entity对代码的维护性也有十分大的帮忙,开发者能够十分直观的晓得所应用的entity所蕴含的所有字段。

Usecase
usecase这一层即是围绕entity开展的一系列crud操作,以及为了页面渲染做的一些联动(通过ui store实现)。因为以后架构的起因(没有bff层),usecase还可能承当局部微服务串联的工作。

举个例子,商家后盾订单页面在渲染前有一堆筹备逻辑:

  1. 依据route的query参数以及一些商家类型参数来决定默认选中哪个tab
  2. 依据是国内商家还是境外商家,调用对应的供应商接口来更新供应商下拉框
    当初大抵的实现是:

咱们能看到7-15、24-125行对this.subType进行了赋值。但因为咱们无奈确定20行的函数是否也对this.subType进行了赋值,所以光凭mounted函数的代码咱们并不能齐全确定subType的值到底是什么,须要跳转到getAllLogisticsCarrier函数确认。这段代码在这里曾经做了简化,理论的代码像getAllLogisticsCarrier这样的调用还有好几个,要想搞清楚逻辑就得把所有函数全看一遍,代码的可读性个别。同时,因为函数都封装在ui组件里,因而要想给函数笼罩单测的话也须要一些革新。
为了解决问题,咱们将这部分逻辑都拆分到usecase层:


首先,能够看到所有usecase肯定是一个纯函数,不会存在副作用的问题。

其次,prepareOrderPage usecase专门为订单页定制,拆分后一眼就能看进去订单页的筹备工作须要干决定选中的tab和拉取供应商列表两件事件。而另一个拆分进去的queryLogisticsCarriers则是封装了商家后盾跨境、国内两种逻辑,后续无论跨境还是国内的逻辑如何变更,其影响范畴被限度在了queryLogisticsCarriers函数,咱们须要对其进行性能回归;而对于prepareOrderPage来说,queryLogisticsCarriers只是() => Promise<{ carriers: ICarrires }>的一个实现而已,其外部调用queryLogisticsCarriers的逻辑齐全不受影响,不须要进行回归。

最初,而因为咱们做了依赖倒置,咱们能够非常容易的给usecase笼罩单测:

单测除了进行性能回归之外,它的形容(demo里应用了Given-When-Then的格局,因为篇幅的起因,对于单测的细节在后续的文章再进行介绍)对于理解代码的逻辑十分十分十分有帮忙。因为单测和代码逻辑强行绑定的缘故,咱们甚至能够将单测形容当成一份实时更新的业务文档。

除了不便写单测之外,在通过usecase拆分实现之后,ui组件真正成为了只负责“ui”和监听用户交互行为的组件,这为咱们后续的React技术栈迁徙奠定了根底;通过usecase咱们也实现了很不错的模块化,对于应用比拟多的一些entity,他的crud操作能够通过独立的usecase具备了在多个页面甚至利用间复用的能力。

Adapter
下面usecase例子中的fetchAllLogisticsCarrier就是一个adapter,这一层起到的作用是将内部零碎返回的数据转化成entity,并以一种对立的数据格式返回回来。

这一层很外围的一点即是能够依赖entity的工厂函数,将接口返回的数据转化成前端本人设计的模型数据,保障流入usecase和ui层的数据都是通过解决的“洁净数据”。除此之外,通常在这一层咱们会用一种固定的数据格式返回数据,比方例子中的 {success: boolean, data?: any}。这样做次要是为了抹平对接多个零碎带来的差异性,同时缩小多人合作时的沟通老本。

通过Adapter + entity的组合,咱们根本造成了前端利用和后端服务之间的防腐层,使得前端能够在齐全不分明接口定义的状况下实现ui渲染、usecase等逻辑的开发。在服务端产出定义后,前端只须要将理论接口返回适配到本人定义的模型(通过entity)即可。这一点对前端的测试周提效十分十分十分重要,因为防腐层的存在,咱们能够在测试周实现需要评审之后依据prd的内容设计出业务模型,并以此实现需要开发,在真正进入研发周后只须要和服务端对接实现adapter这一层的适配即可。

在实际过程中,咱们发现在对接同一个零碎的时候(对商家来说就是stark服务)各个adapter对于异样的解决简直截然不同(上述的11-15行),咱们能够通过Proxy对其进行抽离实现复用。当然,后续咱们也齐全有机会依据接口定义来主动生成adapter。

UI
在通过后面的拆分之后,无论咱们的UI层用React还是Vue来写,要做的工作都很简略了:

  1. 监听交互事件并调用对应的usecase来进行响应
  2. 通过usecase来获取entity数据进行渲染

因为entity曾经做了过滤和适配解决,所以在ui层咱们能够放心大胆的用,不须要再写一堆莫名其妙的判断逻辑。另外因为entity是由前端本人定义的模型,无论开发过程中服务端接口怎么变,受影响的都只有entity工厂函数,ui层不会受到影响。

最初,在ui层咱们还剩下令人头痛的技术栈迁徙问题。整个团队目前应用vue的我的项目有10个,按迭代频率和我的项目规模迁徙的计划能够分为两类:

  • 迭代频繁的大利用:次要包含代码行数较多、逻辑较为简单的几个中大型利用。这些利用想要一把梭间接实现迁徙老本极高,但同时每个迭代又有相当的需要。基于这种状况,对于这三个利用咱们采取了微前端的形式进行迁徙。每个利用别离起一个对应的React利用,对于新页面以及局部逻辑曾经齐全和ui解藕迁徙老本不高的业务,都由React利用来承接,最初通过module federation的形式实现交融。
  • 迭代不频繁的小利用:剩下的利用均是复杂度不高的小利用,这部分利用迭代的需要不多,以保护为主。因而咱们的计划是对现有逻辑进行整洁架构重构,在ui和逻辑分层之后间接对ui层进行替换实现迁徙。

4. 后续

通过整洁架构咱们造成了对立的编码标准,在前端利用标准化的路线上迈下了松软的一步。能够预感的是整个标准化的过程会十分漫长,咱们会陆续往规范中减少新的标准使其更加欠缺,短期内在布局中的有:

  • 单测即文档:下面提到了usecase通过依赖倒置来配合单测落地,后续团队冀望将一些业务逻辑的实现细则通过单测的形容来进行积淀,解决业务文档实时性的问题。
  • 欠缺监控体系:前端常遇到的3种异样包含 代码逻辑异样、性能瓶颈(渲染卡顿、内存不足等)、数据导致异样。对于数据异样,咱们能够在entity层映射的过程中退出对异样数据的埋点上报来填补目前监控的空白。(代码逻辑异样通过sentry曾经监控,性能监控对于中后盾利用不须要)

后续在规范逐步稳固之后,咱们也冀望基于稳固的标准进行一些工程化的实际(比方依据mooncake文档主动生成adapter层、基于usecase实现性能开关等),敬请期待。

参考链接:
The Clean Architecture:https://blog.cleancoder.com/u...
Module Federation:https://webpack.js.org/concep...
Anti-corruption Layer pattern:https://docs.microsoft.com/en...

*文/陈子煜
@得物技术公众号