背景介绍
动态化要解决的问题
现有各种解决方案的对比
版本的迭代过程
版本功能比较
整体架构
墨子SDK
融入现有开发模式
配套工具
实际使用流程
后续规划
总结
“UI动态化”是京东到家前端研发团队这两年来一直在做的一项重要工作,目前也取得了一些成果,在到家的几个业务场景中已经实现了动态化的接入。在开发动态化解决方案的过程中,我们遇到了一系列的问题,也有很多经验的总结,在这里一起与大家分享。
谈到“动态化”,就绕不开另一个比较重要的话题——“组件化”。“组件化”同样是到家前端研发团队几年来一直在做的一项重要工作,我们把界面中的显示信息,按照最小的粒度进行拆分,形成了一系列的原子组件,比如价格组件、打标组件、评分等。同时,在原子组件的基础上,通过拼装形成了很多的业务组件,比如商品组件、优惠券组件等。组件化在很大程度上提升了团队的整体开发效率和UI一致性。
随着京东到家业务的不断发展,来自产品和设计同学的需求越来越多,对组件本身的扩展性和功能性的要求也随之增加,往往同一个组件需要在不同的页面或模块里有一些样式上的差异。若通过在原生代码中内置大量的逻辑来满⾜业务的需求,就会导致前端代码越来越臃肿,⻓此以往,维护成本也将越来越⼤,任何的改动或许会引发巨⼤的问题,更别谈快速响应业务的需求;如果只是因为这些细小的差异去重新开发一套新的组件又同样会带来开发和将来的维护成本。
同时,对于很多新的业务,产品同学希望可以快速上线、快速验证,常规发版节奏也很难满足这样的需求。
对于上述问题,一种可以快速开发和部署、具备动态更新UI能力、轻量化、跨平台等特性的解决方案就呼之欲出了,而“墨子(Mozi)”,就是这个解决方案的内部代号。
动态化要解决的问题,优势、局限性
既然Mozi方案就是为了解决特定的问题,下面我们就来看下Mozi的几个特点:当然,Mozi不是“银弹”,他不可能用来解决所有的问题,必须要有一定的边界,否则就会无法控制甚至导致最终失败,因此我们也限定了Mozi的使用场景:
现有的各种解决方案的对比
在确定了Mozi的基础能力和特点之后,接下来就是技术选型阶段。从「支持动态化的程度」、「与原生体验的差异」、「方案集成与功能开发成本的高低」三个维度出发,将市面上的移动端动态化方案分成三个方向,最终站在自身需求的角度,调研业界成熟的方案得出的结论如下表:
React Native:动态性高,但是学习成本和性能(加载性能、页面性能)不理想;
Flutter:谷歌的跨平台框架,性能高,但是无动态性;
同时,我们也调研了当时其他厂商的类似的解决方案,发现大家的方案无外乎下图中的几种: 通过以上的调研,我们最终确定用 Native 解析 XML+Flexbox(iOS)/FlexBoxLayout(Android) 的方式来作为最终方案。1)学习成本低:Flexbox 布局方式被开发广泛接受(内部跨平台技术栈大多采用 RN);2)开发成本低:XML和 Flexbox(Yoga)都有成熟的高性能可靠的第三方库直接使用,加快框架开发速度;4)自定义&扩展性强:由于自研,没有包袱,可以在设计上以最符合我们的场景来设计框架;版本的迭代过程
技术选型确定后,接下来就进入到具体的开发阶段。Android端和iOS两端各有一人参与其中,经过2个月左右的开发,诞⽣了墨⼦1.0版本,并在当时的京东到家APP V6.3版本“分享卡⽚”需求⾥进⾏了使⽤,其质量也在线上得到了验证, 目前稳定运⾏中。
随后在2019 Q2季度中,我们对墨⼦进⾏了2.0版本的迭代,在1.0版本基础上着重增加了以下⼏点功能:墨⼦引擎动态布局能⼒,事件交互能⼒,模板⽀持简单的条件语句能⼒等。同时在京东到家APP V6.7版本的“百宝箱”功能进⾏了试⽤,在线上也得到了验证,处于良好运⾏中。
接着,从2019的下半年开始,结合线上的运行状态和实际使用中遇到的一些问题,我们对墨⼦引擎进了一个比较大的迭代,增加了墨⼦引擎相对布局能⼒、⾃动下载并管理模板功能、 模板格式⽀持多样式,以及性能方面的一些优化。尤其是Android平台,由于在大列表的测试中发现了一些潜在的组件复用以及Google FlexBoxLayout的问题,用VirtualView替换了FlexBoxLayout作为UI的底层渲染框架,同时也使Mozi引擎具备了处理更复杂布局的能力。经过了3.0的大的改版,目前的Mozi具备了如下的特点: 目前线上已有多个模块应用,如发现频道的优惠券、商品,还有刚刚在频道页上线的品牌墙等:
版本功能比较
整体的架构图
整体的Mozi框架(以iOS端为例):
- MoziTemplateManager :负责所有模版的管理、校验、统一调度
- DownloadManager:负责模板文件的更新与下载
- Parse:模板解析,负责将模板数据解析成Node节点,供下层使用,为了后续拓展性考虑,保留了支持其他模板类型的能力
- MoziLayout:将 Parse 模块解析之后的Node生成布局元素、并绑定数据生成视图
我们在Mozi引擎中内置了许多的原子组件,以便于利用这些原子组件去快速组装成业务组件,比如: - 原子组件:是在系统原生组件基础上进一步拓展出来的框架自身原子组件,保证了其组件更符合框架使用、使用更加灵活、更易于后续功能的拓展。
- 自定义组件(业务组件):这一部分基本都是与业务强关联的组件,保证了框架可以快速实现一些特殊的需求、以及与原生共用业务组件,避免了重复开发的工作。
- Category:保证了框架在支持系统的原生原子组件外,额外还能支持一些其他功能,便于更好的使用,比如“数据的传递”。
墨子SDK
为了使公司的其他团队也能方便的使用Mozi引擎,我们又在后续的迭代中将Mozi引擎的源代码进行抽离和重组,将墨子基础组件、模板管理器、下载管理器、渲染内核等核心功能进行组合。同时,考虑到其他业务会有自己独特的UI样式,于是将到家的墨子组件库从SDK中进行了剥离,最终形成了Mozi SDK,这样,公司内部其他App就可以方便地通过引入Mozi SDK来获取动态更新UI的能力了。下图展示了Mozi SDK的结构及与到家App的依赖关系:
如何融入现有的开发模式
实现了Mozi引擎之后,就可以在App中接入并实现动态化更新UI的需求了,紧接着要面对的就是如何最小成本的在到家目前的业务中接入Mozi。
到家端大多数页面都是以楼层的方式来展示,而楼层的数据和具体的样式由CMS系统负责管理。CMS将楼层数据下发给业务网关,业务网关再经过一些必要的处理后下发给前端。经过和CMS、业务网关的开发同学讨论之后,最终确定由CMS系统负责下发一个模板映射表,前端通过映射表中信息来判断使用动态化或原生方案进行界面的渲染,这样业务网关就不需要关心具体的渲染逻辑,也不需要为了支持动态化方案而做任何的修改。
配套工具
要在实际的业务中使用Mozi,除了需要有引擎提供支持,还需要有一系列的配套工具。随着上线的业务越来越多,势必需要有专门的平台去维护众多的模板;另外,当业务需求增多后,对于模板的开发效率也会越来越高,因此,我们和CMS系统制定了模板数据的格式,同时Web端同学为我们开发了模板管理平台。
Mozi引擎和这些配套的相关工具从前到后一起组成了一套动态化布局的解决方案,我们借用了周星驰一部经典电影《百变星君》的名字给这套解决方案命名,希望他动态、多变、无所不能的特性能给我们的业务、运营、开发带来效率上的巨大的提升和改变。最终的方案组成部分如下图所示:
实际的使用流程
下图展示了在真实的业务场景下应用“百变星君”的基本流程:
模板开发(以Android平台为例)
使用和安卓原生开发类似的xml格式定义,UI元素的style和value都定义到具体的xml中,同时支持数据的静态绑定:
<VHLayout
orientation="V"
layoutWidth="wrap_content"
layoutHeight="wrap_content"
borderRadius="12"
padding="12"
background="#ffffff"
gravity="h_center">
<NText
text="${floorTitle.floorName}"
textSize="16"
visibility="@{${floorTitle.floorName} ? visible : gone }"
textColor="#333333"
textStyle="bold"
paddingBottom="14"
layoutWidth="wrap_content"
layoutHeight="wrap_content" />
<Grid
layoutWidth="match_parent"
layoutHeight="wrap_content"
orientation="H"
colCount="3"
layoutMarginLeft="-15"
layoutMarginRight="-15"
itemHorizontalMargin="0"
itemVerticalMargin="16"
dataTag="${data}"/>
</VHLayout>
对于多个数据展示,可以直接绑定列表类型的数据源:
<Grid
layoutWidth="match_parent"
layoutHeight="wrap_content"
orientation="H"
colCount="3"
layoutMarginLeft="-15"
layoutMarginRight="-15"
itemHorizontalMargin="0"
itemVerticalMargin="16"
dataTag="${data}"/>
数据绑定:
基础组件的属性往往不能静态写死,需要动态地根据数据来设置。因此模板里支持使用一种简单的数据绑定表达式,在渲染UI的过程中,通过表达式里的定义去访问JSON数据中的实际值,然后再对应到组件的某个具体属性。
在创建组件的过程中,当解析属性碰到表达式的时候,就会将该属性的key、表达式值、所属的基础组件等关系存储起来,等真实的数据到达之后再将真实的数据绑定到基础组件的属性上。这里我们接收的数据是 JSON 格式的数据。通过表达式解析、访问得到的属性值,会缓存起来,当原始数据引用不变的时候,每次访问都会获取到缓存值。数据绑定引用以“$”开头,用“{ }”进行包裹,如下所示:
表达式使用:
开发业务组件的时候,UI 元素的基础属性或者样式往往不能在模板里直接写死,而是需要从数据里获取,所以借鉴其他模板化方案引入了用户数据绑定的表达式,包括对象属性的访问和三元操作符。如下所示:
visibility="@{${floorTitle.floorName} ? visible : gone }"
事件绑定及曝光处理:
flag="flag_exposure|flag_clickable"
可以在布局模板中静态地声明某个组件要响应的事件类型,目前支持组件的点击和曝光事件。对于曝光事件的规则,每个公司都有自己的定义方式,因此我们重新修改了VirtualView底层底曝光处理代码,是之满足到家业务的需要。定义了FrameLayou来处理曝光事件,通过添加如下的方法来进行曝光事件的回调:@Override
public void onAttachedToWindow() {
if (supportExposure()) {
mContext.getEventManager().emitEvent(EventManager.TYPE_ATTACH, EventData.obtainData(mContext, this));
}
}
@Override
public void onDetachedFromWindow() {
if (supportExposure()) {
mContext.getEventManager().emitEvent(EventManager.TYPE_DETACH, EventData.obtainData(mContext, this));
}
}
上面的两个方法分别在组件被显示及移出窗口时回调给监听者,Mozi引擎并不会在内部处理具体的曝光逻辑。考虑到每个业务场景的差异性,以及未来可能其他App的接入,具体的曝光逻辑留给业务方自己去处理。比如在到家的业务场景下,具体的曝光逻辑交给Android和iOS负责曝光的工具类进行处理。模板实时预览工具:
为了方便模板的开发,我们还开发了模板的实时预览工具。实时预览工具是一个安卓的App,模拟最终到家工程中运行的实际环境,通过Mac系统中自带的fswatch进行文件的监听,当模板编辑并保存后,会触发fswatch事件,并通过adb命令将模板及模拟的测试数据push到手机端进行模板展示,实现了实时预览的效果。这里我们也是借鉴了阿里VirtualView实时预览工具的实现方案:
模板管理平台
在编写好模板之后,接下来就是上传到模板管理平台。管理平台负责对模板的管理,包括模板的影响版本,生效的页面,京东到家业务中对应的楼层信息floorStyle,灰度百分比等等。模板管理平台还设置了不同的用户权限,使开发、测试、运营、产品各司其职,每个角色只能操作自己权限内的流程,通过这样的方式来确保上线流程的可控性和安全性。
如前所述,CMS管理平台给App提供了一个接口,用来判断哪个页面的哪个楼层使用Mozi楼层进行渲染,比如下面就是管理平台下发一个数据样例:
{
"msg": "操作成功!",
"result": [{
"tpl": "***19",
"grayScale": "10",
"pageType": "type",
"num": "8",
"floorType": "4",
"signKey": "7d0cef405d766981591ab526a1",
"id": "204",
"floorStyle": "**8",
"groupFlag": "1",
"url":"http://***/o2o-cms-template-manage/b0b07b2ff0/ChannelAct8Tpl19.zip"
}],
"code": "0",
"CODE_SUCCESS": "0",
"CODE_ERROR_BUSI": "6",
"CODE_ERROR": "-1",
"CODE_ERROR_PARAM": "5"
}
在上面这个数据字段中包含了模板需要的所有信息,App端的模板管理器根据pageType, floorStyle,tpl这些信息组成唯一的模板ID,为了防止模板被篡改,通过MD5算法对模板文件生成signKey,在每次App启动时,通过这个接口来获取所有当前可配置的模板。基本的流程如下图所示:
上图只展示了模板不存在的情况,对于已经存在的情况需要进行更新条件的判断,当然,还包括MD5的校验判断,此处就不一一列出。
后续规划
“百变星君”虽然已经上线运行,但是他距离“提效”,“便捷”,“业务赋能”等目标还有不小的距离,后续我们会继续对他进行迭代和升级。
接下来一个重点的突破方向是进一步提高模板的制作效率,降低模板的研发成本。我们会在今年尝试将UI设计师导出的Sketch模板文件一键转变为Android和iOS两个平台对应的模板的文件,这样开发人员就不必自己再去编写模板或者只需要做一些细微的调整即可。
另外就是扩展动态化组件库,通过实现更多的Mozi组件去满足多场景的业务需求,让越来越多的页面具备动态化更新UI的能力。
感谢
“百变星君”是一个整体的动态化更新UI的解决方案,在研发的过程中涉及到了App端、Web端、后端CMS系统已及UI设计师等多部门的共同参与,在这里也一并对这些参与的小伙伴表示感谢。当然,最需要感谢的是支持我们整个开发过程及业务落地的各位领导,为我们提供了方方面面的支持并协助、推动我们进行跨部门的沟通与协作。
结语
本文介绍了京东到家在动态化更新UI方面的开发历程、一些经验和成果,为了便于大家理解,更多的是以一种叙事的方式来介绍,并没有涉及很多底层的实现细节,如果大家想更深入的了解一些技术细节和实现过程,也请给我们留言,我们非常乐意与大家一起讨论和交流。