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

一、框架

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 人日。

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,874评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,151评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,270评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,137评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,116评论 5 370
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,935评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,261评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,895评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,342评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,854评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,978评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,609评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,181评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,182评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,402评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,376评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,677评论 2 344

推荐阅读更多精彩内容