一直以来,无线应用都在不断寻求动态化页面的解决方案,在阿里巴巴集团内,除了风风火火地 Weex 项目外,各个团队都有大大小小的解决方案。咱们猫客一直持续基于 Tangram 方案来解决页面动态化的问题,然而在面对持续升级的业务需求时,原有的开发模式也慢慢变得没法胜任,本年度以来,咱们 Tangram 体系在各个层面都进行了大跨度的技术升级(可参考文章天猫APP改版之首页架构&开发模式全面升级),本文再详细介绍一下页面内组件体系升级方案。html
在原有的 Tangram 体系里,主要解决了页面内布局结构的动态化能力,经过 json 数据描述能够组合出经常使用的页面结构。然而页面内具体的坑位样式,咱们称之为业务组件,是采用常规的 native 代码开发的,除非内置了足够多的逻辑,不然组件的样式调整或者新组件的开发都要发布版本,没法知足业务节奏;固然咱们也尝试过使用 Weex 开发业务组件贴到页面上,可是在体验和性能上仍是有较大的缺陷。git
因此总结起来,就是两点问题:github
对于上述问题,解决思路实际上是比较通用的,要动态更新界面视图,就须要用界面模板描述视图,模板与数据分离。将动态下发的模板和数据在端上绑定渲染。要提高性能,也有三大着力点——减小视图层级与个数,结构尽可能扁平化;异步布局渲染流程,解放主线程计算量;回收与复用组件,减小内存开销。json
新的组件体系就是在模板化描述视图,动态更新视图,减小视图层级几个方面作文章,至于组件的回收复用,则是在页面级别统一完成;而异步布局渲染流程,则是后续的优化方向。canvas
新的组件方案称之为 VirtualView,简称 VV,也称为2.0组件,它的设计遵循如下几个思路:后端
先从总体上预览一下整个方案的大致结构:数组
自下往上,自左往右的顺序介绍各个模块:安全
有了上述基础,当咱们要开发新的业务组件的时候,除了有新增 Native 逻辑的需求场景(好比新增视频功能),大部分需求均可以告别原生代码的编写,转而编写组件模板。性能优化
值得注意的是,在上述架构及流程里,描述了一个完整的实践经验,但对于本方案来讲,核心点在于提供了对组件从编写到展现流程的实现,其周边的配套设施,并无内置在框架里,包括客户端上的模板管理、更新、注册模块,以及后端的模板发布服务,由于这些模块每每涉及业务逻辑,且与各个应用的基础设施相关,内置在框架里反而限制了使用方的接入。这里提供一些可供参考的经验:bash
对于组件,咱们作了以下定义,每个基础的原子组件或者容器组件都会有如下属性,自定义的基础组件应当继承自基础定义并作扩展。
名称 | 类型 | 默认值 | 描述 |
---|---|---|---|
id | int | 0 | 组件id |
layoutWidth | int/float/enum(match_parent/wrap_content) | 0 | 组件的布局宽度,与Android里的概念相似,写绝对值的时候表示绝对宽高,match_parent表示尽量撑满父容器提供的宽高,wrap_content表示根据自身内容的宽高来布局 |
layoutHeight | int/float/enum(match_parent/wrap_content) | 0 | 组件的布局宽度,与Android里的概念相似,写绝对值的时候表示绝对宽高,match_parent表示尽量撑满父容器提供的宽高,wrap_content表示根据自身内容的宽高来布局 |
layoutGravity | enum(left/right/top/bottom/v_center/h_center) | left|top | 描述组件在容器中的对齐方式,left:靠左,right:靠右,top:靠上,bottom:靠底,v_center:垂直方向居中,h_center:水平方向居中,可用或 组合描述 |
autoDimX | int/float | 1 | 组件宽高比计算的横向值 |
autoDimY | int/float | 1 | 组件宽高比计算的竖向值 |
autoDimDirection | enum(X/Y/NONE) | NONE | 组件在布局中的基准方向,用于计算组件的宽高比,与autoDimX、autoDimY配合使用,设置了这三个属性时,在计算组件尺寸时具备更高的优先级。当autoDimDirection=X时,组件的宽度由layoutWidth和父容器决策决定,但高度 = width * (autoDimY / autoDimX),当autoDimDirection=Y时,组件的高度由layoutHeight和父容器决策决定,但宽度 = height * (autoDimX / autoDimY) |
minWidth | int/float | 0 | 最小宽度 |
minHeight | int/float | 0 | 最小高度 |
paddingLeft | int/float | 0 | 左内边距 |
paddingRight | int/float | 0 | 右内边距 |
paddingTop | int/float | 0 | 上内边距 |
paddingBottom | int/float | 0 | 下内边距 |
layoutMarginLeft | int/float | 0 | 左外边距 |
layoutMarginRight | int/float | 0 | 右外边距 |
layoutMarginTop | int/float | 0 | 上外边距 |
layoutMarginBottom | int/float | 0 | 下外边距 |
background | int | 0 | 背景色 |
backgroundImage | string | null | 背景图地址 |
borderWidth | int | 0 | 边框宽度 |
borderColor | int | 0 | 边框颜色 |
visibility | enum(visible/invisible/gone) | visible | 可见性,与Android里的概念相似,visible:可见,invisible:不可见,但占位,gone:不可见也不占位 |
gravity | enum(left/right/top/bottom/v_center/h_center) | left|top | 描述内容的对齐,好比文字在文本组件里的位置、原子组件在容器里的位置,left:靠左,right:靠右,top:靠上,bottom:靠底,v_center:垂直方向居中,h_center:水平方向居中,可用或 组合描述 |
方案内内置了一系列基础组件,完整的组件列表以下:
上文提到虚拟化开发的组件的技术,简称虚拟组件。不少作性能优化的方案、建议都会提到采用 Canvas 直接绘制的方式来减小 View 的个数,虚拟将这个开发流程作了抽象与规范,可让开发人员像定义原生组件同样定义虚拟组件。
具体来说,基础组件须要遵循一个接口的规范,这个口定义了渲染过程当中须要的三个流程:计算尺寸阶段、布局阶段、绘制阶段;定义这个三个阶段是为了更好的与系统平台特别是 Android 平台对接,由于在 Android 原平生台下也会有这个三个阶段,在 iOS 平台下则也须要按照本方案里要求的规范去处理。计算尺寸阶段定义要触发一次尺寸计算,须要对其包含的子组件进行计算调用;布局阶段定义了要触发一次布局,将子元素按照计算好的位置尺寸排布,也要对包含的子组件进行布局调用;绘制阶段定义要进行视图绘制,固然也要对起包含的子组件进行绘制的调用;对于虚拟组件,就在这些接口里实现相关逻辑,而对于原生组件,在这些接口实现里调用原生组件的对应逻辑。
不管是虚拟化组件仍是原生组件,都采用上述相同的模型来定义,再加上相同的尺寸计算接口、布局接口、绘制接口,这样对于宿主容器来讲,包装在内部的组件就不分虚拟化仍是原生,一视同仁,暴露给外面的接口也是同样的,只要将宿主容器像普通的 View 同样添加到的视图界面上,就能够在后续的渲染过程当中显示出来。若是虚拟组件使用的越多,View 的个数就越少,对于系统来讲层级越扁平。如下图示例的组件来讲,最终呈现的 View 只有宿主容器和两个图片组件,若是将图片也用虚拟化的方式实现,最终 View 只有一个宿主容器,而界面仍然保持不变。
经过 XML 编写的业务组件,并不直接在客户端里运行使用,而是先进行一次二进制序列化操做,原始的 XML 模板文件保存成文件的时候,就是以纯文本的形式存在,会包含不少冗余信息,好比空格、换行、还有重复出现的字符串等,文件体积比较大,以xml解析器去解析的时候,也会须要大量字符串操做,效率和性能不能达到最优。而将它编译成二进制格式,会避免这些问题,好比文件重复出现的字符串只保留一份,经过字符串索引去引用它,全部的组件类型也都会被转换成一个数字索引,在客户端内经过数字索引反过来找到对应的类实例化。这样文件格式会很是紧凑,体积更小。整个设计也借鉴了 Android 系统编译模板文件的思路。它的具体格式说明以下:
按照图中从左往右、从上往下的顺序分别说明每一个段的做用:
开发业务组件的时候,基础属性或者样式每每不能在模板里直接写死,而是须要从数据里获取,因此引入了用户数据绑定的表达式,语法和实现上目前比较简单,参考了不少同类的设计,尽量符合开发人员的直觉。
语法上以 ${ 开头,以 } 结束。对于Map,经过 . 操做符进行访问,对于 Array 或者 List 经过 [] 操做符进行访问。
好比:
${benefitImgUrl}
${data[0].benefitImgUrl}
复制代码
用来给那些须要根据数据中某个字段来设置值的属性,语法上以 @{ 开头,以 } 结束,中间部分为表达式的具体内容。
条件表达式 ? 结果表达式[1] : 结果表达式[2]
复制代码
当条件表达式成立的时候,使用结果表达式[1],不然使用结果表达式[2]。 其中: 条件表达式支持布尔类型、字符串类型、JSONObject、JSONArray。 如下场景均为 false:
好比:
@{${logoUrl} ? visible : invisible }
复制代码
考虑到篇幅限制,不能将上述架构和流程中的每一细节彻底展开,详情能够参考苹果核这里的文档。
VirtualView 方案是 Tangram 的极大补充,能够解决80%场景下的动态化需求,而 Tangram 依赖的数据则经过 TAC 提供解决,三者结合能够造成一个闭环,让一个开发从端到端地解决整块业务的开发。
以双十一期间为例,90%的双十一业务组件都是动态下发的,且随时可根据业务节奏调整。
尽管在功能流程上已经逐步稳定,能承载起平常及大促的需求变动,咱们的方案仍是有不少不足之处的,好比咱们指望更高的运行效率、更加扁平化的UI结构、更加方便的开发体验,对此也作了更进一步的规划建设:
功能 | 计划 |
---|---|
提供更加完善的文档和教程、Demo,内外版本同步,创建以 github 为中心的迭代开发机制 | 17年12月 |
组件建立、布局计算、数据绑定机制优化,提高性能 | 18年1月 |
重构模板编译工具,提高编译开发体验 | 18年1月 |
提供预览服务,提高开发效率 | 18年3月 |
提供配套的后端数据服务与基础设施,即 TAC 平台开放 | 18年3月 |