• home > webfront > engineer > Architecture >

    前端代码复用学习笔记:整洁架构与清晰架构

    Author:zhoulujun Date:

    基础代码的复用往往比较简单,但是业务代码的复用通常是困难的,如果没有特殊的手段去治理项目会逐渐发展为难以维护的巨石应用,按照维基百

    基础代码的复用往往比较简单,但是业务代码的复用通常是困难的,如果没有特殊的手段去治理项目会逐渐发展为难以维护的巨石应用,按照维基百科记载,代码的复用形式主要有三种,程序库,应用框架,设计模式

    程序库

    前端业务代码在程序库的体现主要是通过业务组件,稍微大点的团队都有自己的业务组件库,但是我去过的很多团队都有落地难的问题,其中有些是技术层面的,但是更多的是出现在跨职责协作上,其中,我认为影响最大的是 UI 设计师和前端开发之间的协作关系

    UI 组件资产

    UI 设计师作为前端的直接上游,生产前端工程师所需的设计资源,如果生产的设计资源本身就是不规范的,必然会极大影响前端团队组件化的发展

    UI 设计师作为设计人员,和工程师的思维方式不同,对组件设计规范的意识并不强烈,大部分设计人员对规范的定义仅仅在主题色和主观意识强烈的 风格 上,UI 设计师的最终产出由少部分 视觉要素约定 +设计师个人 主观设计倾向 决定,也就是说,设计师的心情好坏和人员更替会直接影响每次产出设计稿的呈现,这对设计师来说很正常,但对前端工程师来说是致命的

    基于此,我们需要在工具上对 UI 设计师的产出加以约束,使其在前端工程师可接受的范围内自由发挥,例如 figma,figma 的 component 概念与前端组件化概念高度吻合,我们可以要求设计稿中可复用的独立单元必须是一个 figma component,从而倒逼设计师建立业务的组件资产,进而确保组件设计的一致性

    组件代码设计

    按照软件设计原则,找到程序中的变化内容并将其与不变的内容区分开,而在前端,按照代码改动频率依次排列,视图代码>视图逻辑>业务逻辑,相比业务逻辑,UI 代码属于高频变动的部分,尤其是是由 html+css 组成的视图代码

    例如电商的商品卡片,不同节日下的商品卡片很可能有不同的 UI 样式,但是他们的业务逻辑是一样的,他们使用同样的业务数据和业务行为

    在这种情况下大部分开发会选择两条路

    • 要么为每个节日商品卡片单独封装一个组件,

    • 要么封装一个包含所有节日样式的巨石组件,然后由外部的一个 prop 参数控制组件的渲染

    前者会导致代码的大量冗余,后者违背了单一职责的设计原则,组件内部根据外部传入的节日类型写了一堆 if else,时间久了没人看得懂一堆 if else 里写的是什么

    为了减少代码冗余和提升可维护性,我们可以按照职责将组件划为四部分

    • 业务上下文组件

    • 交互上下文组件

    • 无状态视图组件

    • 入口组件

    可以看到和 Flux 架构很像,但是又有些区别,更像是对 Flux 架构的补充完善

    • 多了一层交互状态组件,在 Flux 架构里只是简单区分了无状态组件和有状态组件,但是对 UI 的状态管理很模糊,究竟是由视图组件自己生产自己消费,还是由状态组件统一管理。不过 Flux 架构都是五年前的东西了,当时对组件的状态管理还处于探索阶段,也没有 hooks 这种能够最小化抽取状态和逻辑的利器,对代码的粒度按照职责做了进一步的细分,自然就有了更多复用的可能

    • 使用 context + hook 代替 props 传参,这个其实很好理解,在父组的 render 函数里声明子组件并对子组件传参,从而达到控制子组件渲染的目的,这在耦合定义里叫控制耦合,而使用 context 可以做到无需在父组件内声明子组件也依旧可以传递数据,这种基于消费数据结构而进行耦合的方式叫标记耦合,在对各种耦合的定义中,标记耦合就是要比控制耦合的耦合度更低,耦合度低复用率也就上去了

    • 减少全局依赖,把全局依赖下沉到某种上下文里,之前做过几次老代码到新项目的迁移,迁移过去的组件有大量直接依赖全局环境的代码,request,localstorage,甚至是挂在 window 下的全局变量,为了不影响新项目的运行,迁移过去的老代码做了大量修改,如果一开始就把这些全局依赖封装在 context 里,平移过去的代码几乎不需要修改,或者只是修改 context 这一小块就够了

    应用框架

    一条业务线往往会存在多个项目,不同项目之间通用业务代码的复用一直是个问题,在若干个项目上往往存在着多次复制黏贴的代码,经过项目的多次迭代,他们可能还会在各自的项目上衍生出不同的版本,长此以往维护成本会让开发人员痛苦不堪,我们当然知道抽离到 npm 私库是最佳实践,但是抽离模块的繁琐过程很大程度上提高了业务线上开发人员的心智成本,从而降低了将代码抽离到 npm 的意愿,在这种情况下,Monorepo 也许是更好的解决方案

    lerna 提供了一个简易的 Monorepo 方案,利用 lerna 软链接项目作为依赖的特性,使得项目间的代码共享变得更加容易


    v2-0fbb65969609ae961a39390803027edf_1440w.webp


    上面我们用 lerna 管理了三个项目,其中 shared 属于其他两个项目的共享代码,wap-app 和 web-app 可以通过依赖的方式直接消费 shared 项目中的组件,工具函数和业务服务,极大优化我们的工作流程

    设计模式

    很多时候我们提到代码复用第一时间想到的组件,但是组件有组件的局限性,它是和 UI 框架 强耦合 的,而有一些场景需要需要实现 ==跨平台跨框架的代码复用== ,例如一个产品它既有 PC 网页,也有 RN 开发的 App,还有小程序

    他们存在完全迥异的视图层或者框架体系,与之强耦合的组件没办法实现复用,但它们背后的业务逻辑都是一样的,所以为了复用我们的业务逻辑,需要实现业务逻辑的 ==框架无关,环境无关==

    设计本质上就是以一种可以将它们重新组合在一起的方式将事物拆开…… 将事物拆分成可以重新组合的事物,这就是设计。 — Rich Hickey《设计、重构和性能》

    系统设计其实就是系统的拆分,最重要的是我们可以在不耗费太多时间的情况下重新把它们组起来。

    我同意上面这个观点,但我认为系统架构的另一个主要目标是系统的可扩展性。我们应用的需求是不断变化的。我们希望我们的程序可以非常易于更新和修改以满足持续变化的新需求。干净的架构就可以帮助我们实现这一目标。

    了解干净架构,可以阅读《前端领域的 “干净架构” https://juejin.cn/post/7054888223830441991

    整洁架构Clean Architecture

    《架构整洁之道》中提出的整洁架构就是解决这个问题的,腾讯文档团队也有对整洁架构的相关实践 让 JS 摆脱框架的束缚

    我们可以将 react 的代码快速的迁移到一个类 react 框架,但是我们很难将他迁移到 Vue 框架和 Angular 框架。可能在代码迁移合并升级的时候,我们能做的可能只有重构。

    我们可能会想到使用 Web Components 或者是微服务,但是带来的可能会是,更多的浏览器限定,要看更多的框架文档去遵循更多的规范。

    无论框架如何的迭代,我们 JS 是永远不会太大的变化的,所有框架都是基于 JS 去写的,如何让我们通用 JS 代码被不同的框架去应用,让我们的通用代码去框架化,是我们应该思考的问题。

    既然我们要去框架化,那架构就不去用了,直接用 JS 就好了呀,那就是一种技术的倒退,我们要做的不是不用框架,而是用一种更巧妙的方式去可以适配更多的框架。

    例如,目前前端的类 MVVM 架构,ViewModel 层可以取出 Model 的数据同时帮忙处理 View 中由于需要展示内容而涉及的业务逻辑。我们要去用我们不变的 JS 代码去适配更多框架的 VM

    Clean Architecture 是由 Robert C. Martin 在 2012 年提出的,最早只是在 Android,Android 应用有很重的 View 层。今天,前端应用走向了 MV* 的架构方案,也有了一层很重的 View 层。

    《DDD中常提到的应用架构总结(六边形、洋葱、整洁、清晰) https://blog.csdn.net/luo15242208310/article/details/124708008

    架构随时间的演进可参见下图:

    架构随时间的演进

    Robert C. Martin 总结了六边形架构(即端口与适配器架构):DCI (Data-Context-Interactions,数据-场景-交互)架构BCI(Boundary Control Entity,Boundary Control Entity)架构等多种架构,归纳出了这些架构的基本特点:

    • 框架无关性。系统不依赖于框架中的某个函数,框架只是一个工具,系统不能适应于框架

    • 可被测试。业务逻辑脱离于 UI、数据库等外部元素进行测试。

    • UI 无关性。不需要修改系统的其它部分,就可以变更 UI,诸如由 Web 界面替换成 CLI。

    • 数据库无关性。业务逻辑与数据库之间需要进行解耦,我们可以随意切换 LocalStroage、IndexedDB、Web SQL。

    • 外部机构(agency)无关性。系统的业务逻辑,不需要知道其它外部接口,诸如安全、调度、代理等。

    如你所见,作为一个普通(不分前后端)的开发人员,我们关注于业务逻辑的抽离,让业务逻辑独立于框架。而在前端的实化,则是让前端的业务逻辑,可以独立于框架,只让 UI(即表现层)与框架绑定。一旦,我们更换框架的时候,只需要替换这部分的业务逻辑即可。


    在这里插入图片描述整洁架构Clean Architecture.webp

    这种架构使得,框架和一切外部相关的实现细节被隔离在框架与驱动层,业务逻辑层只负责业务本身,与外部的联系由接口适配器层控制,假设我们需要从 Vue 迁移到 React,或者从 Vue2 升级到 Vue3,我们只需要变动接口适配器层和框架与驱动层,业务逻辑层可以继续复用

    如图所示 Clean Architecture 一共分为四个环,四个层级。环与环之间,存在一个依赖关系原则:源代码中的依赖关系,必须只指向同心圆的内层,即由低层机制指向高级策略。其类似于 SOLID 中的依赖倒置原则:

    • 高层模块不应该依赖低层模块,两者都应该依赖其抽象

    • 抽象不应该依赖细节,细节应该依赖抽象

    与此同时,四个环都存在各自核心的概念:

    实体 Entities (又称领域对象或业务对象,实体用于封装企业范围的业务规则)

    • 用例 Use Cases(交互器,用例是特定于应用的业务逻辑)

    • 接口适配器 Interface Adapters (接口适配器层的主要作用是转换数据)

    • 框架和驱动(Frameworks and Drivers),最外层由各种框架和工具组成,比如 Web 框架、数据库访问工具等

    这个介绍可能有些简单,让我复制/粘贴一下更详细的解释:

    • 实体(Entities)实体用于封装企业范围的业务规则。实体可以是拥有方法的对象,也可以是数据结构和函数的集合。如果没有企业,只是单个应用,那么实体就是应用里的业务对象。这些对象封装了最通用和高层的业务规则,极少会受到外部变化的影响。任何操作层面的改动都不会影响到这一层

    • 用例(Use Cases),用例是特定于应用的业务逻辑,一般用来完成用户的某个操作。用例协调数据流向或者流出实体层,并且在此过程中通过执行实体的业务规则来达成用例的目标。用例层的改动不会影响到内部的实体层,同时也不会受外层的改动影响,比如数据库、UI 和框架的变动。只有而且应当应用的操作发生变化的时候,用例层的代码才随之修改

    • 接口适配器(Interface Adapters)接口适配器层的主要作用是转换数据,数据从最适合内部用例层和实体层的结构转换成适合外层(比如数据持久化框架)的结构。反之,来自于外部服务的数据也会在这层转换为内层需要的结构。

    • 框架和驱动(Frameworks and Drivers)。最外层由各种框架和工具组成,比如 Web 框架、数据库访问工具等。通常在这层不需要写太多代码,大多是一些用来跟内层通信的胶水代码。这一层包含了所有实现细节,把实现细节锁定在这一层能够减少它们的改动对整个系统造成的伤害。

    从某种意义上来说,Clean Architectute 是一种规范化模板化的实施方案。与此同时,它在数据层的三层机制,使得它存在两层防腐层,usecase 可以作为业务的缓冲层,repository 层可以隔离后端服务与模型。

    清晰架构Explicit Architecture

    2017年Herberto Graca在其《软件架构编年》史系列文章中提出清晰架构Explicit Architecture,即将DDD, Hexagonal, Onion, Clean, CQRS等进行融合后的架构。

    在这里插入图片描述

    • 最中心的红色多边形Application Core即表示业务逻辑实现,即应用核心

    • 红色多边形的边界即表示端口Port,即应用核心的入口/出口定义

    • Application Layer - 应用层,包括:

      • Application Services,业务用例的编排服务即及其interface定义,应用服务的作用通常如下:

        • 使用 Repostitory 查找一个或多个实体;

        • 让这些实体执行一些领域逻辑;

        • 再次使用 Repostitory 让这些实体持久化,有效地保存数据变化;

        • 触发应用事件(如发送邮件、调用第三方API、发送MQ消息等)。

      • CQRS命令/查询处理器

      • Event Listener事件监听器

      • Port端口定义,如ORM 接口Repostitory、搜索引擎接口、消息接口等等

    • Domain Layer - 领域层,这一层含了数据和操作数据的逻辑,它们只和领域本身有关,独立于调用这些逻辑的业务过程。它们完全独立,对应用层完全无感知。

      • Domain Services - 领域服务,封装涉及多实体(相同或不同实体类型)的领域逻辑,且领域服务间可以相互调用。

      • Domain Models - 领域模型,在架构的正中心,完全不依赖外部任何层次的领域模型。它包含了那些表示领域中某个概念的业务对象,如实体、值对象、枚举以及其它领域模型种用到的任何对象(如领域事件Domain Events,简单理解为MQ消息)。

    • 红色多边形的外侧左半圆部分即为主/主动适配器(用户界面User Interface实现)

      • 如Spring MVC中的Controller实现

      • Command Query Bus 命令查询总线

    • 红色多边形的外侧右半圆部分即次/被动适配器(基础设置Infrastructure实现)

      • 如数据持久化实现Mysql、短信通知实现、MQ通知、搜索引擎ES实现等

      • Event Bus 事件总线

    • 依赖方向由外到内,且内层不知道外层(参见之前洋葱架构)

    采用按组件分包Package By Component,即整合传统按层分包Package By Layer和按特性分包Package By Feature,组件Component可以理解为专属于特定一个领域内的业务逻辑服务和数据访问逻辑的组合,也可以理解为特定领域,如账单、用户、评论或帐号等。

    在清晰架构中可以理解为:

    • 先按照层次进行分包

      • 表现层Presentation

      • 业务核心层Application Core

      • 基础设施层Infrastructure)

    • 之后每一层次再按照特性分包




    参考文章:

    前端业务代码如何复用 https://zhuanlan.zhihu.com/p/406182932

    让 JS 摆脱框架的束缚 https://mp.weixin.qq.com/s/n65x3duoeQAtQU_fH_dJDw

    DDD中常提到的应用架构总结(六边形、洋葱、整洁、清晰) https://blog.csdn.net/luo15242208310/article/details/124708008

    Clean Frontend Architecture:整洁前端架构 https://phodal.github.io/clean-frontend/

    微模块-前端业务模块化探索# https://eluxjs.com/designed/micro-module.html

    “整洁架构” 和商家前端的重构之路  https://my.oschina.net/u/5783135/blog/5562550



    转载本站文章《前端代码复用学习笔记:整洁架构与清晰架构》,
    请注明出处:https://www.zhoulujun.cn/html/webfront/engineer/Architecture/8910.html