在2202年的今天,前端应用走向了 MV* 的架构方案,有了一层很重的 View。随着业务场景的越来越专业化和复杂化,大型 SPA 应用的流行,前端承担的职责也越来越多。即使在精心设计过的架构,也很容易导致迭代着迭代着发现代码改不动了

经过一年多前端团队在使用 Vue 实现众多业务的过程中,经历了前期少量探索,中期大量应用,后期架构和性能优化的三个阶段。

在该技术栈积累了一定经验之后,在 App 火车票查询列表页的相关业务模块,基于 Clean Architecture 整洁架构之道的思想,进行了一次技术大重构。

What is Clean?

通俗的讲:《Clean Architecture》里184页里有一句话 系统应该分层设计,低层的设计应该依赖于高层,因为低层容易变化,而高层不容易变化

用我的理解通俗易懂的解释一下,我们来看一下这些概念的 level,从高层到低层,举个例子:

  • 某APP要增加用户粘性

  • 根据1,PM判断要增加Plus member特有的服务是正确的增加用户粘性的方法

  • APP开了只有Plus member可以使用的plus购物优惠服务

  • 为了让服务受欢迎,我们需要一个科学模型来计算受欢迎的的产品是什么

  • 科学模型需要使用大量数据

  • 根据技术调研,来选取合适的底层技术来存取大数据

这里dependency是从低级到6->5->4->3->2->1 (6依赖5依赖4....)

考虑可能的几种变化:

  • 如果APP下一步目标不是增加用户粘性,而是可劲儿挣钱让利润翻番,那么就根本不会有2,那么也就不会有3,从而不会有4,5,6,而会有支持这个目标的另外的x,y,z....

  • 如果有一种更好的批量存储大数据的技术可以取代S3,那么6会变,5也不需要变。

这个其实是在讲依赖倒置原则,高层不应该被“底层自己的细节变化”所影响。而底层应该应高层的要求而实现。

所以说,在一个合理分层的系统设计里,高层相比低层不容易变,是因为低层需要变的时候(设可能性为X), 高层不需要变(当然这也跟领导和PM水平有关,领导和PM把握不准产品定位,不知道用轻量的主意实验和用数据引导决策,那么上层就天天变)而高层需要变的时候(可能性为Y),低层的实现选择/支持一般都需要变。所以低层的变化可能性为(X+Y), 而高层的变化可能性为Y。

*这也就是DDD中所说的,业务决定技术实现, 而不是相反。

一、项目初期回顾

我们有一个 根据用户已维护的证件号码进行展示 功能点:

如果没有证件类型返回,默认展示身份证号码 优先展示身份证号 如果证件类型为 'B' 展示护照号 其余证件类型展示其它证件号码 唔,不难,先来一串流水线的代码。

1.Heavy View

在某个 JSX 中:

return (<div class="info">if (!psg.cardType || psg.cardType == '1') {numberStr = (<span class="idNo">`身份证:${psg.idNo}`</span>)} else if (psg.cardType == 'B') {numberStr = (<span class="pass">`护照:${psg.passport}`</span>)} else if (psg.cardType) {numberStr = (`其它证件:${psg.otherNumber}`)}</div>
)

存在的问题:这里的视图层其实我只需要一串号码就可以了,但这里却承担了各种逻辑判断、数据筛选等“杂活”,视图代码与逻辑代码比例已经接近 1 : 1。

导致的后果:

  • 难以直观地理解视图结构,并且在视图层写大段的注释显然是很不优雅。

  • 或许这些业务逻辑判断可以放在生命周期中统一解决,但是带来的后果会是,复杂组件变得难以理解。

  • 随着组件复杂度提高,生命周期中被逻辑不相关的副作用充斥,这很容易产生缺陷。

优化思路:视图层好单一,数据展示到视图层之前,做好数据的筛选、转换,判断逻辑抽象层公用函数放入 util 中。

2.Duplicated

上面的展示证件代码,在不同的产品都有同样的功能点,机票、火车票、酒店都有相同的展示。

存在的问题:同样的逻辑在两个视图层中重复出现,这是团队协作经常会遇到的问题,假设例子中的逻辑较假设非常复杂的,各成员实现方式不一致,在后期维护将会造成许多问题。

导致的后果:

  • 违反了代码重复原则,后期需要统一修改时,涉及文件多成本大。

  • 团队中各成员形成“知识不同步”,同样的功能 A B 都实现了,但是互相却不知道

  • 并且容易出现因实现方案不同导致的结果不一致的问题。

优化思路:试图将某个实体抽象成一个类,比如将出行人抽象成 Traveler 类,类中有一个方法为 getIDNO 用来判断用户是否为签约客户,之后视图层只需要调用 Traveler.getIDNO() 便能够复用这块逻辑,并且容易理解其含义。

3.Translators

在用户列表中:

  • 手机号码的展示,只需要展示前三位和后四位,中间用 "*" 代替。

  • 是否是会员的checkbox的勾选,用字段 'flag('0' - 普通用户 | '1' - 会员)' 来控制

解决办法也很多,例如写一个简单的 filter 来进行过滤,返回的时候把flag转换成bool类型。

存在的问题:定义字段在理想的情况下是前端主导,且前后端有共同的认知,但是不排除特殊情况下接口字段定义混乱且不直观。

导致的后果:阅读代码时,接口字段不规范,在视图层展示时,会导致误解或者难以理解的代码逻辑。

优化思路:

  • 这些数据不止在一个View中使用,我们为何不在直接在接口的源头来进行过滤呢。

  • 在Service层加入一层Translator出来,在接口返回时,逐一将字段列举出来,将不符合规范的字段进行纠正,转换成更易理解的词语,将字段内容进行转换、和扩展。

  • 假设我们合理的对 Service层 进行拆分管理,那么所有用到该列表数据的View,请求源都会在一个 requestApi 中。

import { userTranslator } from './translators'export function getUserList() {return axios('/user/list').then(data => {return data.map(item => userTranslator(item));})
}

在 ./translators.js 中

export function userTranslator({userId,phoneNumber,flag,...
}) {return {id: userId, // 对不合理的字段名定义进行修正phoneNumber, // 由于业务需要,原有字段还是要保留phoneNumberStr: filterFunc(phoneNumber), // 隐藏中间四位数 '13579246810' -> '135****6810'isVip: flag === '1' ? true : false, // 仅仅flag不够直观。我们重新定义为isVip更容易理解,并赋值我们需要用到的bool类型的值...}
}

4.忽略业务整体

存在的问题:在一个庞大、多人协作的项目,作为其中一员很可能出现对整个系统理解不够,只知道自己负责的那几个页面,逐步恶化成“面向页面编程”。

导致的后果:这对整个项目的“成长”是不利的,会导致像上述举例代码中出现的“重复性”问题。假如开发者对整个项目有全局的了解,在编码时,会考虑更多的“可拓展性”与“预判未来性”,或者在接手其他成员负责的领域也会减少很多上手成本。

从业务的角度看,在需求评审的过程中,熟悉整体业务,会对其新的需求进行更深的思考,判断其对整个项目是否会有明显的“驱动”作用,而进一步考虑是否应该拒绝该需求或者提出更好的需求建议,避免成为产品经理说什么就做什么的“面向页面编程”工程师。

优化思路:将每一块业务划分成不同的领域,各领域下包含哪些服务,每个页面调用的并不是 API 接口,而是各自领域的服务。

二、Clean Architecture

特点:

Robert C. Martin 在 2012 年提出 Clean Architecture 架构,其主要特点为:

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

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

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

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

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

无关性:

  • Angular、Vue 和 React都是很不错的框架,不同的团队使用的也不一。试想,如果某个框架停止迭代,废弃了,那么我们可能就得重写整个应用。

  • 亦或者某个产品需要同时维护 mobile 和 PC 两端,那相同的在View层上的业务逻辑我们就必须维护两套。某天来了一个需求,需要做改动的较大的情况下,那么改了一个端后,另一个端还无法及时响应是事小,说不定因为平台或者兼容性的差异导致不一样的实现代码。

项目结构图:

上一版本的架构图:

为了让各层职责分明,视图层尽可能纯粹,我们将各功能块代码进行分层,形成了下面这种前端分层架构。

目录即分层

├── entity
│   ├── user.js // 数据实体,简单的数据模型,用来表示核心的业务逻辑
├── service
│   ├── user.translators.js // 映射层,主要是一些异构转换,一般以纯函数形式存在
│   └── user.js // 外部接口,例如封装 AJAX Websocket 请求,操作浏览器 cookie、locaStorage、indexDB,操作 native 提供的能力等
└── use-cases └── user-interactor.js // 领域事件,构建在核心实体之上,领域事件是对领域内发生的活动进行的建模。

这个实现相当不错,就是过于重视理论 —— 抽象相当的繁琐,导致有点不接地气。我的意思说,估计没有多少前端人员,愿意按照这个模式来写。

现在,当前端发起一个请求时,它的流程一般是这样的:View -> Usecase -> Service -> Controller(后端)

对应的返回顺序:

Controller(后端) -> Service -> Translators -> 如果返回的是Entity数据 -> Entity -> Usecase -> View

Entity 实体

当一个对象由其标识(而不是属性)区分时,这种对象称为实体(Entity)。返回的数据是否走 Entity 层,主要是看他是否有的标志符(例如id)。

而当一个对象用于对事务进行描述而没有标识时,它被称作值对象。

Usecases 领域事件

领域事件是对领域内发生的活动进行的建模。Usecases + Translators 作为逻辑层/抗腐朽层:

  • 业务逻辑处理。在数据传给后端之前,对一些必要的内容进行处理。

  • 接口聚合,数据异构转换。如果已有 BFF 的服务架构,那么该层会显得有些鸡肋。

1.业务场景

App 火车票查询预订流程中,列表页负责展示符合用户搜索条件的车次列表,并将用户带入中间页(坐席选择),其业务场景有以下特点:

  • 业务逻辑复杂。不同员工预订范围,预订权限不一

  • 交互复杂。筛选、排序、切换日期和查看浮层等

  • 展示信息多。车次信息、推荐航班等

  • 页面结构变化。单程、往返页面结构不同

2.领域划分设计

业务部分由多个Clean Architecture模块组成,外层模块处理页面路由和页面初始化数据,低价日历、列表展示和筛选作为子模块嵌套其中。每个模块的内部结构相同,并且可以方便的成为另一个模块的子模块或父模块。

3.具体案例

下面以火车票流程的日历模块为案例,分析模块内部结构设计和数据流向。

为了让界面逻辑和业务逻辑都能得到合理的表达,参照Clean Architecture 原则,模块内部划分为四层。

  • View层 由 Calendar Component 展示日期,节假日。

  • Usecases层 封装业务逻辑各领域事件。如日历的可选范围的控制与展示在如下几个模式中都有所不一,可拆分用例进行管理:单订模式(BookInteractor)、改签申请(ChangeInteractor)和出差申请模式(BusinessInteractor)。

  • Entity层 除日历(CalendarEntity)为一个实体外,日历中的每一天(DateEntity)也可分为一个实体。

Class Date {...// 关于这一天的描述。如节假日等  get label () { ... }
}

通过上面所有例子发现,我们始终在做一件事,就是把原来 Component 的 state 更加的 less 了 - 去 state 化,尽可能只保留相关框架特有的代码,毕竟说到底 React、Vue、Ng 仅仅只是构建用户界面的框架。

总结

前端位于研发的应用层,永远对迭代速度要求很高,同时又跟随业务在不断发展变化,团队也在快速发展和变化,工程化面临的挑战始终很大。

本篇通过由概念到实际业务讲述整洁架构在前端中的一个实践,本质是为了高内聚低耦合,紧靠本质,按自己的理解和团队情况来实践即可。

当然这种分层架构并不是银弹,其主要适用的场景是:实体关系复杂,而交互相对模式化,例如企业软件领域,通过领域驱动设计这个强大的武器,我们将系统解构的更加合理。相反实体关系简单而交互复杂多变就不适合这种分层架构了。

最后, 送人玫瑰,手留余香,觉得有收获的朋友可以点赞,关注一波 ,我们组建了高级前端交流群,如果您热爱技术,想一起讨论技术,交流进步,不管是面试题,工作中的问题,难点热点都可以在交流群交流,为了拿到大Offer,邀请您进群,入群就送前端精选100本电子书以及 阿里面试前端精选资料 添加 下方小助手二维码或者扫描二维码 就可以进群。让我们一起学习进步.

推荐阅读

(点击标题可跳转阅读)

[极客前沿]-你不知道的 React 18 新特性

[极客前沿]-写给前端的 K8s 上手指南

[极客前沿]-写给前端的Docker上手指南

[面试必问]-你不知道的 React Hooks 那些糟心事

[面试必问]-一文彻底搞懂 React 调度机制原理

[面试必问]-一文彻底搞懂 React 合成事件原理

[面试必问]-全网最简单的React Hooks源码解析

[面试必问]-一文掌握 Webpack 编译流程

[面试必问]-一文深度剖析 Axios 源码

[面试必问]-一文掌握JavaScript函数式编程重点

[面试必问]-全网最全 React16.0-16.8 特性总结

[架构分享]- 微前端qiankun+docker+nginx自动化部署

[架构分享]-石墨文档 Websocket 百万长连接技术实践

[自我提升]-送给React开发者十九条性能优化建议

[大前端之路]-连前端都看得懂的《Nginx 入门指南》

[软实力提升]-金三银四,如何写一份面试官心中的简历

觉得本文对你有帮助?请分享给更多人

关注「React中文社区」加星标,每天进步

点个

死磕前端架构之整洁架构在前端的应用实践【稀缺资源】相关推荐

  1. DDD进阶_DDD分层架构、整洁架构、六边形架构

    DDD从入门到精通,系列文章传送地址,请点击本链接. 本文主要讲解微服务不同架构下的特点,如果还不了解DDD分层架构的,请先学下DDD的分层架构 目录 一.整洁架构 二.六边形架构 三.三种微服务架构 ...

  2. DDD/ABP 洋葱架构aka整洁架构

    分层架构和传统三层架构 1.分层架构:把各个组件按照"高内聚.低耦合"的原则组织到不同的项目中. 2.传统的经典三层架构 三层架构的缺点:尽管有DAL data access la ...

  3. 「领域驱动设计」DDD,六边形架构,洋葱架构,整洁架构,CQRS的整合架构

    这篇文章是软件架构编年史的一部分,一系列关于软件架构的文章.在这些文章中,我写了我对软件架构的了解,我如何看待它,以及我如何使用这些知识.如果您阅读了本系列以前的文章,那么本文的内容可能更有意义. 大 ...

  4. tomcat中间件的默认端口号_死磕Tomcat系列(1)——整体架构

    点击上方"Java技术前线",选择"置顶或者星标" 与你一起成长 在许多的高端开发的岗位中都会或多或少有要求面试人员要研究过一些常用中间件源码.这是因为一切的秘 ...

  5. 【死磕DDD】领域驱动架构设计核心概念

    为什么领域驱动那么火? 它解决了架构师的一个通用问题:Do the RIGHT thing RIGHT! 领域驱动架构设计就是以客户和产品为导向,进行业务拆分的一套架构设计思路. 领域设计4层模型 它 ...

  6. 基于ASP.NET Core 6.0的整洁架构

    背景 最近尝试录制了一个系列视频:<ASP.NET Core 6.0+Vue.js 3 实战开发>,本节是视频内部整洁架构的理论和实战的文字稿.因为在录制之前,我通常会编写完整的文字内容作 ...

  7. tomcat lifecyclelistener_大公司程序员带你死磕Tomcat系列(五)——容器

    死磕Tomcat系列(5)--容器 回顾 在死磕Tomcat系列(1)--整体架构中我们简单介绍了容器的概念,并且说了在容器中所有子容器的父接口是Container.在死磕Tomcat系列(2)--E ...

  8. 死磕Tomcat系列(6)——Tomcat如何做到热加载和热部署的

    死磕Tomcat系列(6)--Tomcat如何做到热加载和热部署的 热部署就是在服务器运行时重新部署项目,热加载即在在运行时重新加载class,从而升级应用. 通常情况下在开发环境中我们使用的是热加载 ...

  9. redis 命令别名_【死磕 Redis】 哨兵(一):部署哨兵架构

    在介绍 Redis 主从复制的时候,提到了相比于单机的 Redis 架构,主从复制架构具有如下优势: 保证数据安全性.从节点作为主节点备份,一旦主节点不可用,从节点可以顶上去,保证了数据尽量不被丢失 ...

最新文章

  1. D1net阅闻:WhatsApp正式推出Windows和Mac本地桌面应用
  2. 谈谈 Tomcat 请求处理流程
  3. java校验码的设计_Java动态验证码单线设计的两种方法
  4. redhat6.5 yum register 问题
  5. 分类算法中常用的评价指标
  6. 【面向对象】面向对象程序设计测试题5-Java中的对象交互测试题
  7. 从IT的角度思考BIM(二):模式与框架
  8. Oracle Goldengate在HP平台裸设备文件系统OGG-01028处理
  9. 1.12 Linux查看用户信息
  10. Java集合---HashMap源码剖析
  11. android 集成融云客服,第三方客服
  12. 网狐棋牌游戏用户数据库QPAccountsDB开发文档
  13. ImageMagick (Magick++ for C++) configuration in Visual Studio 2012
  14. 【PHP编程】制作表单生成器——注册登录信息
  15. 黑科技VNET——最好用的Android抓包神器
  16. python爬虫可视化excel_Python爬虫以及数据可视化分析!
  17. Dubbo2.7.3入门
  18. 蒋涛作序盛赞Leo新作为 程序员职场实用百科全书 —— 程序员羊皮卷 连载 1
  19. ceonts6.8 nginx做前端代理apache做后端服务架构配置
  20. 怎么该软件创建桌面快捷方式

热门文章

  1. 【头歌】汉诺塔(Hanoi)的递归算法
  2. 如何修改网站标题和logo
  3. 【陀螺财经】数字货币每日行情简报0213
  4. 深圳市信息服务业区块链协会为理事单位“陀螺财经”授牌
  5. SAP 详细解析在建工程转固定资产
  6. SAP 固定资产减值准备
  7. 黑马传智播客第三十六期前端最新学习视频分享
  8. html 怎么写出实心园,HTML5绘制实心的文本
  9. wpl计算方法_【数据结构】树的应用-计算哈夫曼树的WPL值
  10. openFrameworks教程(一)