ITPub博客

首页 > 架构设计 > > H5火车票整洁架构实践

H5火车票整洁架构实践

作者:浪花小雨滴 时间:2020-05-20 15:40:26 0 删除 编辑

■ 前言

在9102年的今天,前端应用走向了 MV* 的架构方案,有了一层很重的 View。
随着业务场景的越来越专业化和复杂化,大型 SPA 应用的流行,前端承担的职责也越来越多。

即使在精心设计过的架构,也很容易导致迭代着迭代着发现代码改不动了

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

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


What is Clean?

通俗的讲:

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

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

  1. 某APP要增加用户粘性
  2. 根据1,PM判断要增加Plus member特有的服务是正确的增加用户粘性的方法
  3. APP开了只有Plus member可以使用的plus购物优惠服务
  4. 为了让服务受欢迎,我们需要一个科学模型来计算最受欢迎的的产品是什么
  5. 科学模型需要使用大量数据
  6. 根据技术调研,来选取合适的底层技术来存取大数据

这里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 仅仅只是构建用户界面的框架。

总结

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

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

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

相关

来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/31537575/viewspace-3128955/ ,如需转载,请注明出处,否则将追究法律责任。

请登录后发表评论 登录
全部评论

注册时间:2018-05-16

  • 博文量
    4259
  • 访问量
    39959