首页 > 架构设计 > > H5火车票整洁架构实践
在9102年的今天,前端应用走向了 MV* 的架构方案,有了一层很重的 View。
随着业务场景的越来越专业化和复杂化,大型 SPA 应用的流行,前端承担的职责也越来越多。
即使在精心设计过的架构,也很容易导致迭代着迭代着发现代码改不动了
经过一年多前端团队在使用 Vue 实现众多业务的过程中,经历了前期少量探索,中期大量应用,后期架构和性能优化的三个阶段。
在该技术栈积累了一定经验之后,在 App 火车票查询列表页的相关业务模块,基于 Clean Architecture 整洁架构之道的思想,进行了一次技术大重构。
《Clean Architecture》里184页里有一句话 系统应该分层设计,低层的设计应该依赖于高层,因为低层容易变化,而高层不容易变化
用我的理解通俗易懂的解释一下,我们来看一下这些概念的 level,从高层到低层,举个例子:
这里dependency是从低级到高级6->5->4->3->2->1 (6依赖5依赖4....)
考虑可能的几种变化:
这个其实是在讲依赖倒置原则,高层不应该被“底层自己的细节变化”所影响。而底层应该应高层的要求而实现。
所以说,在一个合理分层的系统设计里,高层相比低层不容易变,是因为低层需要变的时候(设可能性为X), 高层不需要变(当然这也跟领导和PM水平有关,领导和PM把握不准产品定位,不知道用轻量的主意实验和用数据引导决策,那么上层就天天变)而高层需要变的时候(可能性为Y),低层的实现选择/支持一般都需要变。 所以低层的变化可能性为(X+Y), 而高层的变化可能性为Y。
这也就是DDD中所说的,业务决定技术实现, 而不是相反。
我们有一个 根据用户已维护的证件号码进行展示 功能点:
唔,不难,先来一串流水线的代码。
在某个 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 中。
上面的展示证件代码,在不同的产品都有同样的功能点,机票、火车票、酒店都有相同的展示。
存在的问题: 同样的逻辑在两个视图层中重复出现,这是团队协作经常会遇到的问题,假设例子中的逻辑较假设非常复杂的,各成员实现方式不一致,在后期维护将会造成许多问题。
导致的后果:
优化思路: 试图将某个实体抽象成一个类,比如将出行人抽象成 Traveler 类,类中有一个方法为 getIDNO 用来判断用户是否为签约客户,之后视图层只需要调用 Traveler.getIDNO() 便能够复用这块逻辑,并且容易理解其含义。
在用户列表中:
解决办法也很多,例如写一个简单的 filter 来进行过滤,返回的时候把flag转换成bool类型。
存在的问题: 定义字段在理想的情况下是前端主导,且前后端有共同的认知,但是不排除特殊情况下接口字段定义混乱且不直观。
导致的后果: 阅读代码时,接口字段不规范,在视图层展示时,会导致误解或者难以理解的代码逻辑。
优化思路:
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类型的值
...
}
}
存在的问题: 在一个庞大、多人协作的项目,作为其中一员很可能出现对整个系统理解不够,只知道自己负责的那几个页面,逐步恶化成“面向页面编程”。
导致的后果: 这对整个项目的“成长”是不利的,会导致像上述举例代码中出现的“重复性”问题。假如开发者对整个项目有全局的了解,在编码时,会考虑更多的“可拓展性”与“预判未来性”,或者在接手其他成员负责的领域也会减少很多上手成本。
从业务的角度看,在需求评审的过程中,熟悉整体业务,会对其新的需求进行更深的思考,判断其对整个项目是否会有明显的“驱动”作用,而进一步考虑是否应该拒绝该需求或者提出更好的需求建议,避免成为产品经理说什么就做什么的“面向页面编程”工程师。
优化思路: 将每一块业务划分成不同的领域,各领域下包含哪些服务,每个页面调用的并不是 API 接口,而是各自领域的服务。
Robert C. Martin 在 2012 年提出 Clean Architecture 架构,其主要特点为:
上一版本的架构图:

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

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

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

下面以火车票流程的日历模块为案例,分析模块内部结构设计和数据流向。
为了让界面逻辑和业务逻辑都能得到合理的表达,参照Clean Architecture 原则,模块内部划分为四层。
Class Date {
...
// 关于这一天的描述。如节假日等
get label () { ... }
}

通过上面所有例子发现,我们始终在做一件事,就是把原来 Component 的 state 更加的 less 了 - 去 state 化,尽可能只保留相关框架特有的代码,毕竟说到底 React、Vue、Ng 仅仅只是构建用户界面的框架。
前端位于研发的应用层,永远对迭代速度要求很高,同时又跟随业务在不断发展变化,团队也在快速发展和变化,工程化面临的挑战始终很大。
本篇通过由概念到实际业务讲述整洁架构在前端中的一个实践,本质是为了高内聚低耦合,紧靠本质,按自己的理解和团队情况来实践即可。
当然这种分层架构并不是银弹,其主要适用的场景是:实体关系复杂,而交互相对模式化,例如企业软件领域,通过领域驱动设计这个强大的武器,我们将系统解构的更加合理。相反实体关系简单而交互复杂多变就不适合这种分层架构了。
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/31537575/viewspace-3128955/ ,如需转载,请注明出处,否则将追究法律责任。