淘系基础链路电商套件
NO.0
基础链路整体介绍
淘系基础链路是覆盖淘宝和天猫等场景下最核心和最基础的链路,包括但不限于首页(手淘首页、h5首页、淘宝和天猫PC首页)、商品详情、微详情、下单、订单、购物车、支付成功、我的淘宝、足迹等。
在这个业务体系下,安卓、iOS、H5、容器化版本等移动端如何保证体验、稳定的前提下快速迭代,我们需要解决以下问题:在尽量不发版的情况下做到四端一致。
这里面我们的思路有两种:第一种叫前端优先,采用 weex 或 rn 实现 web、安卓、iOS 的一致。第二种叫 native 优先,渲染使用还是纯 native 的方式,同时把逻辑后置到服务端,实现渲染和逻辑的动态化,前端也基于此实现一套在前端的渲染方案。
在这里我们选择了第二种方案。虽然rn或者weex这种方式是很灵活,不受发版影响,但是因为需要在js engine 和native之间频繁通信,性能是有一定损耗的,除此之外更重要的是安卓和iOS对这种动态化技术是随时都有可能禁止的,这是我们不能接受的。
今天我们的重点是在容器化版本和h5 这两端如何基于这种方案实现快速迭代,帮助业务在双十一实现自己的目标。整个文章我们尝试回答如下几个问题:
在已有了h5 端的情况下为什么还要做容器化版本?
在这个过程中我们遇到了什么问题,怎么解决的呢?
在h5 和容器化版本之间如何做到尽量的代码复用甚至真正跨端呢?
我们容器化版本给业务带来了什么价值?
NO.1
容器化版本 vs H5
技术是演进的,在没有本质的突破前,我们只是在既要、又要、还要中做选择
1.1 H5 链路
在当前业务下很长的一段时间内,前端都基本只负责h5这一端,它的优点很明显:
很灵活,在端内端外等场景都适用
随时随地可以发布上线,快速迭代
它的缺点也很明显:
太灵活而导致的不可控,比如商品详情,任何一个人拼上一个id就能拿去用,有一天我想升级换链接就会非常的困难;还有就是在百川下场景,三方使用的情况下,h5 就很难做安全和权限的把控。
体验相对不好,导致h5 主要被作为唤端拉新使用。
1.2 容器化版本链路
从各家公司都在推容器化版本来看,容器化版本的业务价值大家都是认可的,即开即用,免安装,体验一致,使用了针对容器化版本优化过的渲染容器和缓存机制让其性能都比大部分的H5 要好。
换句话讲,在一些特定的端内H5 是不是一样可以优化的和容器化版本一样甚至比容器化版本体验还好,当然也是可以的,业界各个公司都有webview的预加载,预渲染等各种优化。有一种说法是容器化版本是web各种优化的集大成者,说的就是这个意思。但是如果你的业务是投放在各端的,并不是所有的app都有这种优化机制,但一般支持了容器化版本的就自带这种优化。
如果单从性能体验来讲,还不足以让我们下定决心支持容器化版本,促成我们使用的是另外一个特性:容器化版本插件(https://opendocs.alipay.com/mini/plugin)的订阅制。这个顺便解决了我们非常头疼的一个问题:谁在用我的页面?因为不知道谁在用我的页面,我就没办法在发布的时候通知他们回归来保障稳定性,也没法针对他们的场景做优化等。
总结下来:
容器化版本体验一致,性能良好。
容器化版本插件订阅让我们非常清楚的知道哪些业务在使用我们的容器化版本(插件),我们可以更有针对性的去做定制和优化,帮助业提高业务转化;对于第三方的使用的场景也能满足业务强把控的诉求。
最后,在端外不支持容器化版本的场景,h5 仍然是我们的目前唯一的选择。
NO.2
在这个过程我们遇到了什么问题?
根据上面的结论,我们选择了容器化版本和H5 齐头并进在合适的场景使用合适的方案。但在新增的容器化版本场景下,我们还是遇到了一些挑战。
上面一开始我们讲到,我们选择了native优先的动态化方案实现多端的一致性。所谓native优先,就是这个方案在native上基本上没有太多缺点,能够满足:
UI的编写可以统一DSL,一次编写两端都能渲染且一致。这套方案我们叫DinamicX(DinamicX 是一套跨容器、动态化渲染方案,采用类 Android XML 的 DSL 描述布局界,配合服务端下发模版,实现了一份模版,多端渲染,并能动态更新。目前已支持 Android、iOS、H5、MiniApp、Flutter 5种渲染容器)。
交互(比如事件)、页面结构、业务数据等完整的一套协议都是服务端下发,实现逻辑后置和动态化。这套方案我们叫新奥创(包含新奥创协议、服务端sdk生成协议、客户端和前端sdk解析协议、新奥创平台搭建协议等)。
但这也给前端带来了新的挑战。
上面我们说容器化版本只是性能良好而不是很好是有原因的:容器化版本可以认为是提供了统一的离线包方案,解决了资源加载的性能问题,这里毫无疑问是变快了;但是在渲染性能上,容器化版本 worker render 双线程渲染架构相对于浏览器、Weex 是变慢的,双线程本质是为了解决安全、隔离的问题,线程通信瓶颈使得渲染效率下降,对于简单应用渲染效率完全够用,但是随着应用复杂度上升,渲染效率问题会被放大。说到底容器化版本是安全管控优先的技术方案,在此前提下保证一定性能体验。
DinamicX 的 DSL 从 Android 演化而来,取其精华,利用 Native 自绘能力,能够使用系统底层绘制 API 直接控制渲染,从而能实现相对原生更高渲染效率的渲染引擎。而对于 Web,最大的问题在于 DinamicX 的布局系统与浏览器的流式布局、Flex 布局不同,这意味需要通过 JS 计算模拟实现,并且计算依赖浏览器渲染结果,这样就出现了计算 -> 渲染 -> 校正这样一个反复的重排重绘过程,这与 Web 性能优化的最佳实践是冲突的,容器化版本上双线程渲染架构面对这种场景,渲染性能会更差。这对容器化版本本身带来的性能提升进行了抵消。
我们遇到的第一个问题就是DinamicX的在容器化版本上的性能问题。
我们现在前端有了容器化版本和h5, 但部分逻辑代码,比如事件,基础的DinamicX 控件都是要写两遍,如何提高效率,在h5 和容器化版本之间实现跨端是我们遇到的第二个问题。
诸如dinamicX的语法是XML的,表达式很晦涩,有没有一种前端友好的DSL方案呢?整个新奥创协议是怎么样子的,新奥创平台如何做到端到端的研发的等等这些,我们留到后面跟大家再做整体的介绍。
今天我们先聊聊上面提到的两个问题以及如何解决:
该方案下容器化版本的性能优化
该方案下容器化版本和h5之间实现跨端
2.1 DinamicX 性能优化
上面我们知道了dinamicX 下让容器化版本变慢了,而更根本的原因还要从DinamicX 如何实现容器化版本的动态化说起:
2.1.1 DOM 树构建
DOM 树节点、层级都是动态的,因此必须要有递归遍历实例化组件的能力。在 Web 上可以通过 document.createElement 实现,而在容器化版本中并不能在逻辑层通过 API 调用实例化组件,只能通过 axml 在 UI 层完成,想要在 axml 层实现递归与组件实例化,只能使用容器化版本的 Template(https://opendocs.alipay.com/mini/framework/axml-template) 模版能力。
如下图所示,定义了一个 dx-render 作为入口组件,传入 DOM 树,在 dx-render 实现中遍历子节点,动态拼接出组件名,从而实例化每个节点对应的组件;组件中进一步分为布局组件与原子组件,布局组件中再递归调用 dx-render,完成 DOM 树遍历。
2.1.2 布局计算
DX 定义的布局样式大多能在 Web 上找到映射的属性,但是也有无法映射的情况,比如 FrameLayout(https://developer.android.com/reference/android/widget/FrameLayout) 帧布局,在 Web 上需要通过绝对定位实现,而绝对定位的坐标值获取、设置都依赖 DOM 操作,在容器化版本下通过为节点添加 id,利用 my.createSelectorQuery(https://opendocs.alipay.com/mini/api/selector-query) API 实现。在布局算法实现与 Web 一致。
我们具体分析了慢的原因,我们就进一步的根据这些原因进行优化。
2.1.3 渲染优化
在 DX 布局算法实现中,依赖浏览器渲染结果,存在计算 -> 渲染 -> 校正这样一个反复的重排重绘过程,在容器化版本中频繁触发 worker、render 线程通信,渲染性能不太理想。
想要提升渲染效率,最直接的方法就是减少计算、通信,基于此我们设计了 DinamicX 的预渲染方案。预渲染与 SSR 思路类似,利用 DinamicX 模版平台的 H5 渲染,将模版渲染结果作为构建产物下发到端侧,减少运行时渲染、计算消耗。
可以看到,下发的模版构建产物部分节点已经转为了标准 HTML 元素以及 CSS 样式,运行时无需计算直接使用。当然这里的样式并非最终样式,尤其是 FrameLayout 帧布局这种依赖绝对定位的元素,尺寸布局取决于模块数据。对于这种情况需要做样式校正,通过 createSelectorQuery 读取元素实际尺寸,对比后再决定是否要更新。
2.1.4 加载优化
预渲染以及同步渲染都是优化单个模版的渲染效率,在实际业务中,页面中会包含很多模版,如果一次性全量渲染,渲染速度会随着模版数线性下降,懒加载也是一个有效的优化手段。
开启懒加载后,DinamicX SDK 内通过容器化版本 my.createIntersectionObserver 监听每个模块位置,默认渲染一个空 div 作为 Placeholder,当模块出现在屏幕中时,再进行解析渲染。
this._intersectionObserver = my.createIntersectionObserver().relativeToViewport({bottom: 0});
this._intersectionObserver.observe(`#mod${index}`, (res) => {
// 相交区域占目标节点的布局区域的比例
if (res.intersectionRatio > 0) {
this.renderMod();
}
this._intersectionObserver.disconnect();
});
2.1.5 优化效果
本地测试下单页优化效果,首屏可交互时间能减少 40% (840ms -> 440ms)左右,实际业务优化效果还与具体模版相关。
优化前vs优化后:
2.2 实现 H5 与容器化版本的跨端
在新奥创和 Dinamicx 的加持下,前端与客户端能够共用协议数据和组件模板,在页面动态性和跨端一致性上已经有了巨大的提升。但是,在前端工程开发上,依然面临两个问题:
开发 DinamicX 基础标签时,前端依然需要在 H5 和容器化版本上分别开发;
新奥创下发的事件需要在 H5 和容器化版本上分别实现,无法复用;
为了解决多端重复开发的问题,提升开发效率,我们设计并实现了一套新奥创体系下的前端跨端方案。
2.2.1 构建配置统一
早期新奥创前端开发没有固定框架,每个业务都有自己的构建配置,接入 SDK 的方式千奇百怪;同时,早期同一个业务如果同时涉及 H5 和容器化版本版本,相同的业务需要开发两遍,随后的迭代维护也需要开发两次。新的跨端方案统一了构建配置,将 H5 和容器化版本的公共配置抽象出来,再针对不同的 BUILD_TARGET 启用不同环境特有的构建配置,一套构建配置同时满足了两个版本的代码构建。
2.2.2 页面 / 组件生命周期统一
容器化版本组件的生命周期钩子和 React 存在一些差异,早期开发组件时,由于各端会分开实现,故不存在生命周期适配的问题。同构后,为了保证生命周期的一致性,同时尽可能提升代码复用,我们对组件(页面)的运行时进行了封装,规定了可用的生命周期,由封装后的运行时保证生命周期在 H5 和容器化版本上都能正确调用。
2.2.3 代码复用
除了生命周期之外,新奥创也会下发许多事件。在之前的实现中,事件的 Handler 是需要各端分别实现的。但实际上在 Handler 中有许多代码是可以复用的。举个例子:点击某个按钮会触发 openUrl 事件,URL 需要动态拼接。在之前的分端开发中,拼接的代码需要实现多次,在新的跨端方案下,这些可复用的代码仅需开发一次即可。新的跨端方案提供了运行环境变量(静态 BUILD_TARGET,动态判断可借助 @ali/universal-env),对于刚才的例子,在不同环境下执行不同的页面跳转方法即可。这样可以最大程度保证代码逻辑的复用。
未来,我们希望利用 DinamicX 的视图表达能力,将视图层进行统一,即:不止将 DinamicX 作为服务端动态下发的试图模板语言,同时在端侧实现自己的静态 DinamicX,开发者使用类似 JSX 的方式开发页面,这些 JSX 代码会被自动翻译成 DinamicX 视图文件,在不同端利用各端的 DX SDK 进行解析渲染,实现真正的视图层跨端。
NO.3
容器化版本给业务带来的价值
技术终归是给业务服务的,不能给业务带来价值的技术都是自嗨。
在我们业务在接入容器化版本插件之前,我们专门在该业务下进行ABTest来验证同样场景下,h5 升级为容器化版本后给业务带来的价值,其中最关键的一个就是转化率(比如商品详情链路的转化率公式 访问商品详情的UV / 支付成功的UV),结果表明平均有80%左右的转化提升,结果还是让人满意的。
最后的最后,我们对基础链路的优化仍在继续,同时我们在基于DinamicX 和新奥创体系帮助淘系和集团的其它电商体系构建端到端的研发体系,请期待我们的下次分享,同时如果你感兴趣我们也非常高兴你的加入(北京杭州均可)。
欢迎关注东半球最大的前端团队
喜欢就点这里