写点什么

携程 Trip.com App 首页动态化探索

2019 年 9 月 28 日

携程 Trip.com App 首页动态化探索

Trip.com 是携程面向国际市场的全新品牌,受全球化战略影响,随着商业化的脚步加快,首页业务针对产品迭代速度、迭代质量、线上效果验证等方面面临新的挑战。


首页无论在哪个 App 体系下都是主要的流量入口,迭代效率和质量,以及研发成本是我们追求和要解决的问题。


在首页业务迭代中我们遇到以下几个痛点:


1)新的业务需求需要跟版本发布:在商业化越来越快的同时,有大量的产品需求需要快速上线验证其效果,目前在一个大版本需要较长时间周期的大背景下,以及 iOS AppStore 审核机制的存在,最少也需要 1 天,最慢无限期的 Review 时间。在不断缩小发布周期的大方向下,通过动态下发的方式也是我们探索的一大方向。


2)多端的研发成本:在互联网越来越快的趋势下,如何节约人力成本,解放生产力,更好更快的业务需求迭代是我们一直的诉求,多端一致性和快速 UI 搭建也是我们一大痛点。


3)业务埋点维护性和及时性:在验证产品效果的同时,会产生很多难以维护的埋点代码,在页面越来越复杂的情况下,维护成本急剧上升,当线上埋点缺失,产品和运营无法验证产品效果必须等待下个版本搭车上线。


基于以上痛点,我们推出 FoxPage Native 平台,下文将会从方案选择,框架架构,以及对于业务与技术结合思考和实践的经验分享给大家。


名字来源:FoxPage 是 IBU (国际业务部)内部一个在线可视化制作页面的平台。我们决定基于 FoxPage 搭建 Native 平台。


一、框架

1.1、技术选型及思考

首先需要明确定位以及边界,我们需要一个怎么样的框架去解决存在的痛点。


需要的:


  • 由于在首页场景使用,高性能和稳定性是最基本的要求;

  • 为了不跟随版本发布,所以动态性也是要考虑的;

  • 为了解决研发成本,多端渲染也是需要解决的问题;


不考虑的场景:


  • 不需要处理复杂的业务逻辑;

  • 不支持动画精细的交互场景;

  • 不考虑多个组件的联动性;


通过梳理场景和边界使得目标清晰。我们需要一个跨平台支持动态性并且高性能 UI 渲染框架。


站在自身需求的角度,调研业界成熟的方案得出的结论如下表。



React Native:动态性高,但是学习成本和性能(加载性能、页面性能)不理想;


Flutter:谷歌的跨平台框架,性能高,但是无动态性;


通过以上的调研,我们打算用 Native 解析 JSON + Flexbox 的方式来作为最终方案。


这么做有什么优势?


1)学习成本低:Flexbox 布局方式被开发广泛接受(内部跨平台技术栈用的多的是 RN);


2)开发成本低:JSON 和 Flexbox(Yoga)都有成熟的高性能可靠的第三库直接使用,加快框架开发速度(在一个月内将框架完成并且上线);


3)兼容性强:Flexbox 完美兼容 Web 端布局的方式,FoxPage 同时支持 Web 端的 DSL 的输出;


4)自定义 &扩展强:由于自研,没有包袱,可以在设计上以最符合我们的场景来设计框架;


1.2、架构设计

如何做好架构设计,可以先了解下 Chrome 是如何完成一个 HTML 到 UI 的输出。



那么 Flutter 渲染流程是如何呢?



通过调研沉淀下我们的渲染流程:



各个模块的职责清晰且独立:


Downloader:主要负责 DSL 更新与下载。


CacheManager:顾名思义,负责 DSL 的缓存管理。


Parse:这层主要是做 DSL 解析,负责将 JSON 数据组织成节点,供下层使用。


Layout:此层职责为将 Parse 模块解析之后的数据计算布局,生成布局元素。


Draw:此层职责为将 Parse 生成节点设置 Layout 层的布局信息输出 Native 视图树并提交系统完成渲染。


遵守职责化最小原则,每个模块被赋予的职责最小化并且彼此独立,使后续的维护,可以将影响范围限制模块内部,而不影响其他模块的稳定性,增强系统维护性。也使得对模块定制优化提供基础。


下文会继续针对框架内部原理做深入介绍。


1.3、DSL 的定义

数据绑定


想象一下,在我们日常开发中,往往是数据对应一个 UI 元素的显示,需要有一定的绑定数据机制。


{    "parentId":"cfb87570-82d8-11e9-811f-0906d1cca8d4",    "name": "image",    "props": {        "url":"{{product.imageUrl}}",    },    "extendId": "",    "conditions": [],    "type": "trip-app.image",}
复制代码


变量的格式为{{变量名}},如以上示例:我们的 image 组件中 url 的属性被设置为 product 对象属性中的 imageUrl 的值。


{    "parentId":"cfb87570-82d8-11e9-811f-0906d1cca8d4",    "name": "image",    "props": {        "url":"{{products[0].imageUrl}}",    },    "extendId": "",    "conditions": [],    "type":"trip-app.image",}
复制代码


数组取值格式为{{数组[Index]}},如上,我们可以通过此方法获取 products 数组中的第一个元素的图片。


事件


在组件触发事件的时侯,我们希望能做一些自定义的事情,如跳转页面,怎么定义呢?


{    "onClick": {        "name":"router_call",        "props": {            "value": {            "plugin":"router",            "method":"openURL",            "args": {                "url":"{{products.deeplink}}"            }            }        },        "type":"function.call",    }}
复制代码


以上示例表示在点击事件中通过 router 中的 openURL 打开了一个新的页面。


条件判断


在某些条件成立才渲染的场景下,我们也提供了条件判断,示例:


{    "props": {        "hidden": {            "name":"render-activity",            "type": 1,            "props": {                "items": [                    {                        "key":"{{data.showActivities}}",                        "operation":"eq",                        "value":"1"                    }                ]            }        }    },    "type":"trip-app.image",}
复制代码


如上示例: image 组件是否隐藏通过{{data.showActivities}} =="1"来控制。


埋点机制


我们还定义了动态埋点的一些规范,如示例:


{    "name": "image",    "props": {        "$traceData": {            "onClick": {                "eventName":"home.click.deals.item",                "data": {                    "url":"{{data.operatingActivities.0.deeplink}}",                    "type":"operating_activities"                }            }        }    },    "type":"trip-app.image"}
复制代码


在 image 组件中声明了点击事件,并且把需要的参数,通过 data 字段一并上传服务端。


1.4、布局

我们的目标是为了解决动态性和多端一致性,那么具备一个完备的布局能力是一个基础要求。通过调研,有以下 3 种方案可选。



1)自定义:完全自定义一套规则,实现成本高,布局效率取决于实现程度,所以这边是“中”,因为是自定义,所有通用性是三者最差的,几乎独家专属。


2)Web CSS:实现一套 Web CSS 样式集,可想而知,如果实现这样的一套系统代价是极高的,为了兼容众多的 CSS 样式,布局效率必然会下降,但是此方案通用性也是最佳,多端共享。


3)Flexbox:弹性盒子布局,从 Web CSS 子集发展而来,在 RN 已得到充分证明它的适用性,由于 Yoga 的存在,让我们在实现成本上得到下降。


Yoga 是 Facebook 基于 Flexbox 的跨平台布局引擎开源库,被用于 RN,Weex 等项目中,也证明了其高性能和可靠性。


在实际使用过程中,Yoga 在 Android 中发现了一些问题,不过我们通过定制源码完美解决,并且在实际体验下来 Yoga 完美的胜任了布局的任务。


一些布局示例:


 "props": {    "$layoutStyle": {        "position":"absolute",        "bottom": "12",        "flexDirection":"column",        "marginLeft": "8",        "width": "100%",        "paddingRight":"16"    }}
复制代码


单位定义


width、height 等实际数字单位定义中,我们定义了如下的数字单位。



1.5、DSL 解析

解析层,要做的事情比较简单,为了提高性能,并且对于相同的 DSL 模板,只会做一次解析,之后便会把结果做一个缓存,以下的流程图代表着解析流程。



1.6、视图构建

视图构建相对简单,通过解析层解析之后,每个视图组件都会 ViewNode 节点一一对应视图在虚拟视图树中的状态,包括了视图布局属性,视图属性等元素信息。


以下为 iOS 代码示例:


/** 视图节点,映射 FoxPage 中的组件 */@interfaceFPViewNode : NSObject/** 视图属性 */@property(nonatomic, strong) FPViewAttribute *attribute; /** 子视图 */@property(nonatomic, copy) NSArray<FPViewNode *> *children; /** 绑定关系的值 */@property(nonatomic, copy, readonly) NSArray <NSDictionary *> *bindValues; /** 绑定关系的值 */@property(nonatomic, strong) NSMutableArray<NSString *> *conditions; /** 真正的视图引用 */@property(nonatomic, weak) UIView<FPViewComponentProtocol> *view; /** 添加的父视图 */@property(nonatomic, weak) UIView<FPViewComponentProtocol> *superView; @end
复制代码


在 ViewNode 树准备好之后,我们会将树传递到渲染层中进行渲染操作,在渲染层中,根据 ViewNode 节点的类型,通过代理的方式,从注册的组件之中创建出视图实例,配合 Yoga 布局属性,转换到 Native 视图的映射,由系统完成最终的渲染。


1.7、数据更新

在解析渲染完成之后,关于数据流是怎么处理的呢?以下是处理流程图:



为了优化性能,我们针对 UI 元素有变化的部分做 dirty 处理,会触发 Layout 和 Draw 模块重计算和重绘。


1.8、动态更新

动态更新能力是重要的一环,在云端更新了页面布局或者样式之后,App 需要即时拉取到最新的 DSL 模板。以下是流程中的时序图:



需要注意几点:


1)App 打包需要把线上目前可用的 DSL 模板打包进 App 中,避免第一次打开 App DSL 模板未下载的时候的空窗口现象;


2)版本升级需要做好数据隔离和清除;


3)DSL 最新版本下发,需要做好 backup 与异常校验;


通过动态更新机制,改变了我们发布需要跟随版本的痛点,有问题,修复之后可以直接下发到用户的 App。


1.9、可视化页面搭建平台

看到这里的看官,心中肯定会有疑问?你们提供变量、布局、事件、埋点追踪,条件,手动来写这很模板复杂度肯定是很高的,不是普通人可以胜任的吧。


是的,我们也意识到了此问题,所以配套了一套可视化的编辑界面。如下面示例图:



左边是可视化编辑页面,右侧为实际在 App 场景的使用效果,可以看出还原度还是很高的。


属性编辑界面:



二、页面工程化的转变

通过动态化的转变之后,首页的业务需求开发的工程模式与研发流程也由此发生变化。


在旧模式下,研发人员更加关注业务需求如何实现,首页的业务需求如何在已有的框架体系之内跑起来。新模式下,研发人员更注重的是,业务组件如何设计,如何完成的一个高质量的业务组件。


将研发人员关注的复杂度从面降为点,使得首页的各个业务模块之间的独立性更高,以及更加稳定。模块之间的复用性提升,如其他业务部门也想用广告组件,他们只需要在其页面做些简单的配置。


在组件生态不断补充的未来,各个业务线之间共享彼此的组件模块,想完成一个新的业务或者产品,只需要像乐高积木一样堆砌即可。


三、构建业务运营闭环

在提供技术基础的条件下,我们继续思考技术和业务之间的关系,如何将业务价值最大化,UI 搭建可以通过平台搭建,是不是可以把产品运营同学也一起参与进来,构建一个业务运营闭环。



1)产品运营同学提出需求;


2)研发人员介入需求开发,开发组件;


3)组件搭建业务上线之后,一站式追踪线上业务价值;


4)根据平台的数据来实时进行运营策略,如修改页面模块,下线模块,添加模块等等;


5)然后反推产品同学提出更合理的产品需求;


如何优化链路,科学的运营体系构建运营业务闭环也是重中之中,并且未来会持续不断在此方向上探索。


四、总结

FoxPage Native 平台上线短短 2 个月,承载了 30+ 次的业务调整,5+ 次临时调整埋点的需求,这是之前做不到的。并且合理系统的设计也增加了框架稳定性,上线至今无任何异常发生。在保证的高质量的交付的同时,也大大减少我们研发成本,如一个复杂的展示模块开发,从原来的双端 4 人日降低到双端 1 人日。


在首页动态化的探索中,遇到了很多的挑战,也有很多收获,后续有很多的功能和需求还需要继续优化和完善,并且需要考虑更多的场景支持。我们相信这是一个好的开头。


作者介绍


马增翼,高级软件工程师,目前负责 Trip.com App 公共业务研发和迭代,专注跨平台开发技术研究。热爱和拥抱开源社区。


本文转载自公众号携程技术中心(ID:ctriptech)


原文链接


https://mp.weixin.qq.com/s?__biz=MjM5MDI3MjA5MQ==&mid=2697268865&idx=1&sn=72f6a42a305622aba8709d0a17cc844c&chksm=8376f1b5b40178a3173488ea451d114aa4f12f988d0687a433ad3c9dee5fcf14d52cf03c18ec&scene=27#wechat_redirect


2019 年 9 月 28 日 08:002704

评论

发布
暂无评论
发现更多内容

第十二周作业

TheSRE

极客大学架构师训练营

架构师训练营 week13总结

FG佳

极客大学架构师训练营

架构师系列 10: JAVA 虚拟机的原理

桃花原记

第九周总结

孤星

架构师训练营第2期 第9周总结

月下独酌

极客大学架构师训练营

架构师训练营第二期 Week 9 作业

bigxiang

极客大学架构师训练营

JVM&秒杀案例

幸福小子

JVM原理

架构师训练营 2 期 Week09 总结

CJian

架构师训练营week13 作业

FG佳

极客大学架构师训练营

第十三周 数据应(二)作业

钟杰

第九周作业

Jack

成为架构师 - 架构师训练营第 07 周

陈永龙Vincent

JVM垃圾回收原理

幸福小子

JVM垃圾回收原理

架构师训练营第九周笔记

李日盛

训练营第九周作业

大脸猫

极客大学架构师训练营

[架构师训练营第 1 期] 第 13 周命题作业

猫切切切切切

极客大学架构师训练营

大数据应用

garlic

极客大学架构师训练营

第 9 周 系统架构作业

心在那片海

架构师训练营第二期 Week 9 总结

bigxiang

极客大学架构师训练营

【第十三周】数据应用(二)

云龙

架构1期 第十三周作业

haha

极客大学架构师训练营

第九周作业

孤星

成为架构师 - 架构师训练营第 08 周

陈永龙Vincent

训练营第九周总结

大脸猫

极客大学架构师训练营

周练习 13

何毅曦

架构师 01 期,第十三周课后作业

子文

架构师训练营 - 第 13 周课后作业(1 期)

阿甘

week13学习总结

追风

架构师一期

第十三周 数据应用(二)总结

钟杰

架构训练营第九周作业

一期一会

week13作业

追风

架构师一期

技术为帆,纵横四海- Lazada技术东南亚探索和成长之旅

技术为帆,纵横四海- Lazada技术东南亚探索和成长之旅

携程 Trip.com App 首页动态化探索-InfoQ