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

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

项目结构图:

上一版本的架构图:

f3ea51d4d007250c24725de12a745eee.png

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

a159258283022800025281642cb3f69b.png

目录即分层

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

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

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

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

  • 页面结构变化。单程、往返页面结构不同5b36cf213bce501981da5eb620c03997.png

2.领域划分设计

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

6f6b27804415fa917c0f13ba9397c22b.png

3.具体案例

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

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

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

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

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

Class Date {
  ...

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

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

总结

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

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

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

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

4dc1d236dffc49e1683c673c0166f940.png


推荐阅读

(点击标题可跳转阅读)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

b4b2e3430186f828d1fac21b97b67c83.jpeg   

点个赞👍🏻,顺便点个 在看 支持下我吧!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值