12月10日在SFDC(SegmentFault Developer Conference)大会上初次介绍了手机天猫的Tangram方案,现场时间有限,讲得匆忙,特此整理记录。这篇内容是Tangram的整体介绍与相关业务开发实践的介绍,后续逐步会将更详细的方案整理成文分享出来。
顾名思义,Tangram中文名是七巧板的意思,我们希望这个框架提供一系列基本单元,就像积木块一样,通过快速拼装就能搭建出一个页面或者调整页面的结构。重运营的业务特别是电商业务,往往讲究灵活多变,需要对线上业务做实时调整,此类页面动态化的需求便应运而生。
对于客户端开发来说,版本发出去之后,再要修改代码,是一件成本比较高的事情,针对线上实时调整比较多的地方,往往就采用了H5的方式上线。由于H5的体验相对Native欠缺一些,就有了后来Facebook的ReactNative(RN),以及阿里自己的解决方案Weex,以Native的方式实现页面动态调整的能力。在如今,表面上看起来Tangram的方案会有些多余,但是通过了解它的设计与演变,那就知道它还是有存在的理由。
在历史上,大概两年前的这个时候,我们团队接手天猫首页的业务,迫切需要一套页面动态化方案。那个时候RN刚刚面世不久,特别是Android版本的RN还不稳定,更不用说后来的Weex了。而我们手里有的一套方案是自己开发过的Dynative(可以理解为初级版本的Weex)。但这些方案有个共同点就是比较重量级,它们都期望从基本的UI元素开始做一套纯动态的方案。在种种现有框架不成熟的时候,对于首页这种重量级的页面,我们还是希望以一种更加纯粹的Native开发模式来支撑业务。
在设计理念上,Tangram也有它的特殊之处,无论是H5还是Weex之类的方案,它们的动态能力在于随时可发布代码,它们是面向开发的动态化方案,发布代码异味着测试、灰度、发布等一系列流程。而Tangram是面向运营和产品的方案,它的动态能力体现在无须做代码改动,提供足够多的动态可配置的能力,通过在后台做样式的调整来达到页面调整的目的。所以简单比较如下:
H5 | Weex/RN | Tangram | |
---|---|---|---|
动态能力 | 强 | 较强 | 偏弱 |
面向人员 | 开发 | 开发 | 运营/产品 |
体量 | 完善的体系 | 较重量级 | 轻量级 |
体验 | 常规H5体验 | 鉴于H5和native之间 | 纯native体验 |
鉴于这样的设计目标,在这个框架里,重点有四个方面的抓手:
- 页面布局动态化,意思是页面的排版布局,可以通过后端数据的下发来调整。
- 组件业务化,这里的组件不是指基本的文本、图片、按钮等基本UI控件,而是指能承担一定业务能力的最小复用单元,因此它可能是一个文本和一个图片的组合这样子的一种形式。
- 动态能力粗粒度化,通过布局+组件的形式搭建整个页面,有多少种布局能力是内置在框架里的,有多少组件也是业务接入的时候注册到框架里的,后端下发的数据声明了用哪些布局、用哪些组件,通过布局嵌套组件的形式渲染整个页面。所以这个动态能力比较粗,不像H5或者Weex从基本的UI元素开始搭建整个页面。
- 组件的复用,为了承载那些个超长页面,需要对同类型的组件具备回收复用的能力,就像
ListView
、RecyclerView
那样。
从一个实例出发,上图中展示的是一个早期的天猫首页,根据导购页面的特点,我们将页面拆分成三个层次:页面——卡片——组件,页面(第一张图)指的就是整体可滑动页面实体,并没有特殊之处;卡片指的是页面内可按行划分的一个一个独立区块(参考第二张图),组件(参考第三张图)指的是卡片内部一个独立的、业务级别的单元,它可以是一张图,也可以是文字+图的组合。因此整体整个页面可以这样描述:一个页面嵌套了多个卡片,一个卡片嵌套了多个组件。
通过将页面拆分成三层结构,整个页面在model上就可以描述成这样一个树状结构。这里最重要的两个model是卡片和组件的model,整个页面的动态化将通过它们的动态化以及它们之间组合关系的动态化来完成。下面看这两层的具体协议描述:
卡片的职责是负责对组件进行布局,那么如何描述布局呢,前面说过,我们采用的是粗粒度的动态化方式,卡片的布局描述就是一种声明式的方式,因此卡片不需要布局模板,只要在model的数据里描述卡片的类型即可,至于卡片有哪些类型,则是注册在Tangram框架里的,业务方在接入框架的时候也可以注册自定义的卡片类型。这样就让Tangram省去了对布局模板的解析,简化了框架复杂度的同时,简化了开发复杂度。
卡片model描述上有四个组成:header、footer、body、style。最重要的是body部分,它包含了内嵌的组件model,如果卡片没有body,即没有组件,也就不在视觉上做渲染。卡片的布局也就是对body里包含的组件来进行布局。Tangram内置了一系列布局能力对组件进行布局,包括流式布局、瀑布流布局、吸顶布局、悬浮布局、轮播布局等等,基本上常见的布局方式都可以覆盖到。header、footer是卡片的标题和尾部,这是根据业务场景设计的可选内容,因为很多时候一块业务区域会有个标题之类的东西。在实现的时候,我们可以将他们转换到body里的组件,但在概念上,单独描述会更容易理解。style是对布局样式的描述,所有布局会有一些通用的样式属性,也有一些特有的,通过样式的描述,可以让布局能力更加丰富。画图举几个例子:
这里对卡片的样式做一些介绍,因为很多时候页面调整就是对样式的调整,结构调整也会涉及到样式调整,因此样式的动态性对页面的动态性具有重要贡献,这里举例的是几个通用的样式属性,如果卡片比较特殊,还可以自定义样式属性。
- backgroundColor: 卡片的背景,在做页面氛围的时候经常会用到。
- margin/padding: 卡片外边距、内边距,这是通用UI系统都会支持的属性。
- gap: 卡片内的组件往往需要增加间距,如果通过组件的margin来实现,会有很多不便之处,相邻组件间左右或者上下都配置了margin,则需要考虑去重的实现,要么就在配置的时候对相邻组件的margin做精心控制。用gap的概念则很方便,它可以指定水平方向间距、垂直方向间距。
- cols: 默认情况下,流式布局每一列宽度都是等分屏幕宽度的,如果需要做不等分的布局,就可以通过cols来指定每一列的占比,这样布局能力就能更加丰富了。
组件的职责是负责业务逻辑和UI元素展示,它是尽可能小的业务单元,一般以实际设计稿出发,抽象出最小可复用单元。组件也是声明式的,需要在model的数据里描述组件的类型,至于有哪些类型,也是业务方在接入时预先注册,因为组件的业务成分比较重,Tangram一般就不内置了。除了类型描述,model数据里剩下的就是组件的数据和样式描述了。组件的数据不做具体规范,一般满足组件自身的需求即可,样式也不做强制规范,但有一些和布局相关的样式在框架层面会进行支持,这个下文介绍。
在组件的实现上,它首先是一个普通的View,并特殊之处,如果脱离Tangram框架,它也应该能正常运行使用。但在Tangram里,我们为组件设计了一个统一的ViewModel,定义了几个生命周期事件;通过ViewModel对组件的属性进行赋值,在组件初始化时会调用init,在滑入屏幕绑定数据时候调用bind,在滑出屏幕解除绑定时调用unbind。
除此之外组件的行为基本上都是业务逻辑了,不做过多介绍,这里再介绍几个和页面动态性相关的样式。
- backgroundColor: 组件的背景,同样也是在做页面氛围的时候经常会用到。
- margin/padding: 组件外边距、内边距,同样也是UI系统都会支持的属性。
- display: 参考css的设计,特别是在流式布局里,组件默认都是内联(inline)的,当布局占满屏幕宽度时,再考虑换行。如果在正常的流式卡片布局里要横插一行,则可以将组件声明为block,不然的化,就得将这个卡片打散成三个卡片才行。
- colspan: 默认情况下,流式布局每一列宽度都是等分屏幕宽度的,也就是占用一个格子,组件上声明colspan可以让这个组件占用多个格子。它与卡片上的cols区别在于它占用的宽度值是离散的,而cols通过百分比可以做到宽度值的连续分布。
- width/height: 其实是组件的宽高比,用来对组件进行对齐,利于界面排版。
每个组件都可以声明额外的自定义样式属性,比如字体颜色、字体大小等等,这里就不做过多介绍。通过卡片和组件的样式,基本上就可以组合出大部分场景的页面结构了,也就是Tangram的初衷——像搭积木一样拼装一个页面。
上面介绍了整个Tangram的基本概念,花了这么多篇幅讲概念模型,除了告诉大家这个东西是什么、它做什么、它是怎么设计的,最重要传递的一个信息是,作为业务系统,需要首先在概念模型上做好架构设计,在协议规范上做好统一,这样具体的平台去实现的时候,都能根据这个规范来做实现,不管谁实现的,都属于Tangram,这就好比JAVA虚拟机规范和JAVA虚拟机的关系一样。对于我们团队来说,对Tangram的实现也经历了一系列变更,但基本规范没怎么变动,这也是能大规模去支持业务的一个重要支点。下面会介绍目前实现上的思路和重要技术点。
主要有这么几个组成:核心引擎、数据解析器、卡片库、组件库、布局框架,核心引擎负责调度整个流程,在启动框架的时候要核心引擎要做一系列初始化,包括初始化卡片库和组件库,也就将内置的卡片类型注册进框架,将外部业务提供的组件也注册好,同时也要将数据解析器初始化好,布局框架也要初始化好。当页面数据传入的时候,核心引擎调用数据解析器将数据转换成卡片和组件的model对象,解析过程会根据之前注册过的卡片、组件类型来解析,不认识的数据将会被抛弃,卡片和组件的基本样式也会解析。解析完毕的卡片、组件model将会扔给布局框架进行页面渲染。布局框架根据卡片提供的布局信息进行布局,根据组件提供的组件信息进一步获取组件实例,贴到布局容器里。
实现上难度最大的在于布局框架,布局框架的灵活性、性能决定了整个Tangram的灵活性和性能。在Android上,布局框架基于RecyclerView
+自定义LayoutManager
的方式实现;在iOS上,布局框架基于自定义的LazyScrollView来实现。这两框架基本上都能做到对页面的扁平化实现,提供了夸卡片的组件级别复用能力。先对这两块做一个介绍:
整个页面树被解析出卡片+组件的数据列表之后,会对块数据做进一步转换。首先提取所有组件model,也就是将组件都打平到同一级别的列表,这个列表会被传递给RecyclerView
的Adapter
,因此数据的位置其实就对应了RecyclerView
看到的组件位置。而卡片model,将会拿来构建一个个LayoutHelper
,这些LayoutHelper
是负责具体布局的对象,一种布局类型的卡片对应于一种LayoutHelper
,而且LayoutHelper
还包含了它负责的组件的位置起始区域,它们会被传递给自定义的LayoutManager
。当RecyclerView
开始渲染页面或者滑动时,它内部维护了一个布局状态,获取当前屏幕范围内还有多少区域是空白的,下一个要加载的View
的位置是多少,然后把这些信息告诉LayoutManager
去加载View
做布局。我们的自定义LayoutManager
拿到这个位置之后,就反向查找对应的LayoutHelper
,然后交给LayoutHelper
去布局,这个过程还会涉及到从回收复用池或者通过Adapter
获取一个组件实例。不同的LayoutHelper
会按照约定的协议进行进一步布局。
对于iOS来说,也有类似的布局逻辑,但这里重点介绍iOS的页面容器LazyScrollView
。这是一个自定义的滚动布局,具备回收复用能力。它的回收复用算法是这样的:在页面渲染前先计算所有组件的位置信息,根据组件在页面内位置的上边距做一个排序索引,根据下边距再做一个排序索引。页面滚动的时候通过滚动区域与上下边距的取交集,就可以获取到当前可见范围的组件是哪些,然后不可见范围内的组件实例可以回收,新进入可视区域的组件可以从回收复用池里拿到组件实例或者新创建一个组件实例贴到布局里。
上面介绍的内容构建里Tangram的基本骨架,但要支撑起业务,还需要很多辅助工具,如果没有这些扩展,将很难支撑业务。这些扩展有些是内部注册在框架,也有些是外部注入。
- 点击处理模块,组件都需要有点击交互,点击处理模块定义了接口,业务方根据接口实现具体模块然后注入。
- 曝光处理模块,与点击模块类似,可提供组件曝光时的业务逻辑加载。
- 通用定时器模块,用来提供计时功能,满足组件内的倒计时需求、定时需求。
- 事件总线,用来做组件与卡片的通信,或者组件与外部通信等等。
- 脚本动画,将动画脚本画,提供动画的动态能力,让组件的交互更加丰富。
- 通用请求模块,有时候卡片数据、或者组件数据需要调用远程接口更新,同通用请求也是定义了加载接口,外部业务方自行实现注入。
- 纯动态组件,解决组件动态问题,因为我们的动态化是粗粒度的,行走江湖免不了内置动态能力满足不了一个临时需求的场景。动态组件集成了集团内的动态化方案,目前最主要的就是阿里的Weex方案,通过Weex的动态能力来解决组件的动态能力。
有了这些扩展功能,整个Tangram落地到业务就非常方便了,目前我们支撑了天猫首页、天猫直播首页、天猫超市首页等重要业务,还推广到了集团其他部门。
最后分享一些业务开发的经验,分客户端和后端运营两方面介绍。延伸到其他业务,这些经验应该也是有借鉴意义的。
首先是规范与协议的统一,我们这个框架支撑了多个业务,不同业务、不同平台之间,只有规范统一,才能尽可能支撑多的业务,否则不同业务接入,要做转换,是一件成本很高的事情。在不同平台之间统一规范,也可以让一份数据在多端使用。
在客户端上开发,稳定性是一个非常重要的指标,整个应用应该有自己的保护模式,对于框架来说,我们也要有防御性编程的思维,特别是是动态化方案,往往根据数据来执行代码,访问数据本身要足够小心,像空字段、类型转换、数组越界都是场景的问题,通过安全方法的使用,可以在一个地方保证数据访问的安全性。
组件库,也是一个非常重要的建设,不同业务之间可以复用相同的组件,减少组件开发。
容器化是实现页面在线拼装的一个必要的建设,客户端需要有一个容器页面,就像webview一样,给一个url,就可以加载页面,后端也也需要做数据的容器化接入,能导入业务数据,按照Tangram协议输出。再利用组件库里的现有组件,就可以完成一个页面搭建,目前有一些简单的页面(搜索专辑、推荐专辑)就是这样完成上线的。
解耦也是一个老生常谈的问题,在Tangram里实现扩展能力的时候,解耦这一方面做得就特别棒,像脚本动画、Weex都是其他团队的成果,但是可以很方便的插入到Tangram框架里,而且可以热插拔,业务方不想使用就可以不接入。
Tangram是一个动态框架,虽然它的重点技术在客户端,但是没有后端的话是不完整的,必须要有一个完善的后端管理平台来做页面的日常运维才行。我们开发了一个专门的管理后台,可以对Tangram页面做多维度的管理。后端管理平台还承载了页面的稳定性、页面发布的效率、页面试错等能力。
Tangram页面动态调整都是配置发布,视觉调整。因此我们有独特的发布流程,首先后台变更完成之后不会直接发布,而是进入到预发布状态,在这个状态下,可以通过白名单预览提前检查变更效果,预览的方式是将变更生成一个二维码,在手机上扫码预览,检查最真实的效果。通过时间机器调整时间,不仅可以预览这次变更在当前时间的效果,还可以预览将来某个时间的效果,因为不同的时间点,生效的数据不一样,因此时间维度的预览特别有用。另外管理平台对变更人员也做了权限控制,每个业务方人员只能变更自己负责的业务,不会改动到其他业务的页面,通过对接流程平台,让每次变更都有记录可查,防止线上数据随意更改。
有了页面发布、变更的稳定性保证,发布的效率也是下一个重要考虑的问题。定时发布可以让变更在指定时间生效,比如双十一的时候很多东西要0点生效,如果0点做变更、预览、再上线,风险很大,有了定时发布,可以提前做好准备。一键下线的功能,可以做多版本里的某个共用卡片批量下线,特别是紧急情况。批量复制创建卡片和版本通配,都是为了解决新版本发布时候的效率,目前我们针对一个版本客户端就发布一份页面配置数据,有了版本通配,可以减少页面配置的数量,有了批量复制创建,可以在要创建新版本页面的时候复制页面。
快速试错,这是近年来非常热门的一个领域,当页面要进行调整的时候,我们希望看到调整的效果,这个时候abtest就派上了用场。天猫有一套自己的试错平台,Tangram前后端都对接了这条试错平台,在管理平台可以将变更做成实验变更,然后导入到试错平台下发到客户端,进入到实验分桶的用户就可以访问到实验变更,同时试验平台在端上也做了数据采集,这样可以在小范围内先试验变更的效果,根据数据来做接下来的决策。
本次分享整体性的介绍了Tangram的技术方案和一些开发经验,内容比较多,很多地方只能在整体思路上进行介绍,后续我们会逐步将一些细节开放出来分享。本文提到的一些天猫相关技术在团队博客上有介绍,欢迎访问: