响应式编程在 SAP 标准产品 UI 开发中的一个实践
Jerry 在从事 SAP Commerce Cloud 前台 Angular 开发时,脑子里始终记挂着自己曾经习得的 SAP UI5 开发技术。我刻意要求自己将 SAP UI5 和 Angular 各方面做对比,只希望自己能在这两个前端开发框架上,都有一定的技术积累。
最近遇到 SAP 电商云前台开发的一个问题,涉及到 CombineLatest 这个操作符的用法,所以有了这篇文章。
在 SAP 电商云源代码里根据关键字 CombineLatest 进行搜索,得到 170 条搜索结果。这说明其在 SAP 电商云前台开发里使用是相当广泛的。
那么这个 CombineLatest 操作符,是应用在什么样的业务场景下呢?答案是响应式编程 (Reactive Programming) 领域。
Jerry 之前的文章,,曾经提到过 SAP Commerce Cloud 新一代基于开源项目 Spartacus 项目的前端界面,支持响应式 (Responsive) 布局和自适应 (Adaptive) 布局的特性。再加上本文的响应式 (Reactive) 编程,这三个形容词,我刚开始接触的时候觉得很容易弄混淆。
Responsive 设计:响应式设计通过各种前端技术,为页面元素赋予了根据屏幕分辨率的变化而自动调整显示行为,以达到最佳显示效果的能力。
Adaptive 设计:为不同类别的设备分别实现不同的页面,检测到设备分辨率后调用对应的网页。
Reactive 编程:响应式编程是一种编程风格的名称,是我们解决异步和并发领域编程问题的一把利器。响应式编程通常包含事件驱动,推送机制,观察者发布者模式等特征, 本质上工作于异步数据流上。响应式编程构建出的事件反应系统具备高度的可扩展性,本文后续会通过例子给大家展示。
为了降低例子的复杂度,便于大家理解,Jerry 把之前在 SAP Commerce Cloud 中遇到的问题,抽象成一个简单的模型,分别用 SAP UI5 传统的事件处理方式,和使用 Angular RxJs 响应式编程库两种方法分别实现,大家从中可以感受差异。
首先用 SAP UI5 实现该模型。绘制一个 Red 和 一个 Black 按钮,点击后,其各自的计数器加一。同时,还有第三个计数器 Total,无论哪种按钮被点击,这个 Total 计数器也加一。
下图状态表明,当前 Red 按钮被点击 5 次,Black 按钮被点击 2 次。
我给视图控制器绑定了一个 JSON 模型,里面包含了 red, black, total 三个属性,分别绑定到 XML 视图的三个计数器里。当两个按钮被点击时,触发 Press 事件,对应的处理函数 onPress 被调用,在函数内更新对应计数器的值。
这个 SAP UI5 应用的实现源代码:
https://github.com/wangzixi-diablo/ui5-toolset/tree/main/combineLatest
再来了解如何使用响应式编程思想解决这个问题。
本文后半部分的 Angular 应用,采用响应式编程模式实现了同样的需求,编程工具库选择了 RxJs,一个响应式编程领域里大名鼎鼎的工具库,同时也以陡峭的学习曲线著称。
RxJs,全称 Reactive Extensions Library for JavaScript.
Jerry 曾经录制了一个简单的视频,介绍了使用 SAP UI5 和 Angular RxJs 开发的这两个应用的运行时效果:
本文提到的 Angular 应用的源代码:
https://github.com/wangzixi-diablo/angular-sandbox/tree/master/src/app/rxjs/combine-latest
下图是 Angular 应用的视图,这是一个原生的 HTML 视图,定义了两个按钮,和三个 div 标签实现的计数器。
基于 RxJs 的响应式编程,核心逻辑就下图 27 ~ 39 行代码,总共 12 行 代码,行数虽少,但信息量巨大。
Observable(可观察对象) 是 RxJs 响应式编程模式的核心概念,是 RxJs 对异步事件流的封装和抽象。
比较下图,传统的采取 addEventListener 实现的按钮事件订阅机制,以及基于 RxJs Observable 两种实现方式的比较。
有的朋友可能不太理解,引入 Observable 对象这个额外的抽象层之后,似乎没有什么用。
那就让我们回到本文 Angular 这个例子来。
fromEvent 操作符接收两个参数,产生事件的数据源(比如页面控件的 DOM 元素,通过 document.getElementById 返回),和事件名称 click.
fromEvent 操作符返回一个 Observable 对象,封装了异步事件源。这个异步事件源,随着用户的点击,会释放(RxJs 中的术语称为 emit) 出包含鼠标点击明细信息的 MouseEvent 事件对象。fromEvent 返回的 Observable 对象,随着时间的推移,释放出 MouseEvent 对象的行为,描述在下图第一根横线中。
横线里的 M 图例,代表 MouseEvent 对象实例,M 图例所在横线上的坐标,代表该按钮发生鼠标点击的时间戳。
Observable 对象的 pipe 方法,支持传入各种操作符 (Operators),比如上图第二根横线所示的 pipe(mapTo1(1)). 这个操作的语义是,将用户点击按钮时释放出的 MouseEvent 对象,映射成常数 1. MouseEvent 对象包含了用户点击事件的明细,比如发生点击的时间戳,点击时鼠标的 X 和 Y 坐标等等。然而我们的需求仅仅是统计点击次数,所以使用 mapTo(1), 将 MouseEvent 对象映射成 1 进行计数即可。然后再使用 Scan 操作符,这个操作符接收一个累加函数作为输入值,能在其内部维护累加值,实现计数的需求。
可以把 Observable,Operator,Observable 释放的事件对象,以及订阅 Observable 的处理函数,分别类比成现实中流水线传送带上待加工的零件,以便于理解。
上图左下角的数控机床:相当于 Observable 对象,能源源不断地释放待加工的零件,即事件对象。
流水线上的零件:相当于 Observable 对象释放的事件对象。零件会依次经过流水线上的若干机械臂 (Operators),被后者加工处理。机械臂处理后的零件,外形上有所变化,好比本文例子里的 MouseEvent,经过 mapTo(1) 处理后,变形为常量 1.
机械臂:RxJs 里众多的 Operators.
流水线终端的操作人员:给最终加工好的零件贴上标签,好比 Observable 对象的订阅者(事件对象的消费者)。
再回过头看本文 Angular 例子中的 combineLatest 操作符。它可以将任意数目的原始 Observable 对象组合起来(下图红色输入参数),返回一个新的 Observable 对象(下图蓝色输出参数),我称其为联合异步事件对象。
在本文例子里,Red 按钮和 Black 按钮点击事件对应的 Observable 对象,被 combineLatest 加工,返回的联合异步事件对象,再被下图第 28 行的匿名箭头函数订阅。传入该匿名对象的输入参数 values 是一个数组,包含两个元素,值分别为当前 Red 和 Black 按钮总的点击次数。这些总的点击次数,就是通过前面描述的 Observable pipe 方法里传入的 mapTo 和 scan operator,基于按钮点击产生的 MouseEvent 加工后生成的值。而 values 数组里两个元素之和,即为当前按钮总的点击次数。因此代码 28 ~ 30 行,依次将 values 数组中的元素,赋给 red,black 和 total 三个计数器的 innerHTML 属性,完成界面渲染。
下图绿色虚线方框所示的联合异步事件对象,代表了用户点击 4 次 Red 按钮,3 次 Black 按钮之后,该对象释放出的 MouseEvent 和其被 Operators 处理的过程。下图底部紫色横线和蓝色的图例,代表了任意一次用户点击按钮之后,total 计数器值的计算逻辑:即当前两种按钮总的点击次数求和。
本文前面提过,基于 RxJs 构造出的响应式编程的异步事件模型,具备高度的可扩展性。假设我们按钮点击计数的需求更进一步:在一秒之内,无论客户点击多少次按钮,均只计数一次。
显然,这是一个典型的函数防抖的场景。Jerry 之前的文章,,曾经分享过 SAP UI5 如何实现函数防抖。
使用 Angular RxJs,可以更优雅地实现这个需求:在 pipe 方法里,插入一个防抖操作符 debounceTime 即可。防抖间隔为 1000 毫秒,语义是,无论用户在 1 秒之内快速点击多少次按钮,Observable 对象只会发送 1 个 MouseEvent 对象,给 debounceTime 后续的操作符,即 mapTo(1).
SAP UI5 实现函数防抖思路类似,只是需要应用开发人员自行编写防抖函数:
最后,总结一下在这个具体的统计按钮点击次数的例子里,响应式编程从程序设计的角度讲,究竟体现出了什么优势。
按钮是生产者,是产生的 MouseEvent 的数据源,用户的点击动作,触发了 MouseEvent 的产生。按钮点击事件处理函数,相当于 MouseEvent 的消费者。在 SAP UI5 实现的消费者代码里,除了编写把计数器最新值刷新到 UI 的逻辑之外,还负责维护计数器的累加值。这就好比现实中流水线终端的工人,既要负责给零件贴标签,又要负责对零件进行加工。这种做法一定程度上违反了单一职责 (Single Responsibility) 和关注点分离原则 (Seperation of Concerns).
最理想的情况,就是像 Angular RxJs 那样,引入 Observable 及 Operators 这一中间层,这样三种角色各司其职,职责清晰:
按钮负责生成事件对象
Observable 和 Operators 负责将事件对象进行加工
事件对象的订阅者对加工完毕的事件对象直接进行消费
理解本文介绍的这一系列响应式编程的概念,是读懂 SAP 电商云源代码里这些令人眼花缭乱的 RxJs Operators 链式调用的基础。
那人笑了笑,说道:“一个人的武功分了派别,已自落了下乘。姑娘若是跟着我去,包你一新耳目,教你得知武学别有天地。”
Jerry 之前用 SAP UI5 做前端开发,从去年接触了 Angular 之后,也有了“前端开发别有天地”的感受。
考一考大家,上面这段红色斜体文字,出自金庸哪部小说里的哪位高手?感谢阅读。