动态化 UI 在 Qunar 客户端首页的应用
一、背景
在上线动态化 UI 之前,Qunar 大客户端的首页内容基本都是基于 Native 进行开发的。在保证 APP 的冷启动性能,为用户带来流畅的体验的同时,其实也存在着以下的一些痛点:
首页功能的修改大部分需要发版解决,导致了功能迭代周期较长
线上如果发现问题的话,修复成本较高,修复时机会有较大延迟
针对以上的痛点,我们针对首页,引入了动态化 UI,当时的期望如下:
性能媲美 native,保证用户体验;
功能可以通过热发迭代,对动态化内容进行在线实时发布,实时灰度,实时下线,保证功能的稳定可控;
动态化内容易于编辑,学习成本较低,前端开发可以轻松进行 UI 样式的开发、实时预览;
支持数据绑定,模板样式与业务模型解耦;
二、方案调研
目前业内存在了一些 UI 动态化的一些成功实践,在我们着手进行开发之前,我们对目前业内比较主流的一些方案进行了调研比较
最终得出以下结论:
首先 RN 是我们优先考虑的,RN 的动态性和功能定制性很好,性能虽然不能媲美 Native,但是随着几个大的版本的不断迭代,已经有了很大的提升。但是由于我们的应用场合大部分都是首页功能,对用户的首屏感知指标要求会较其他页面更高,最终经过评估 RN 的加载速度还是不能达到我们的预期效果,所以只好先放弃该方案。
Flutter 是谷歌维护的最近比较热门的前端框架,它有跨平台、高性能的优势,但是目前 release 编译模式下,Flutter 没有我们所需要的动态化特性,还不能实现热更新,达不到动态化的效果。
Naitve+DSL 方案,即通过 Naitve 实现对自定义模板(DSL 处理)的功能支持,通过动态下发模板,最终实现 UI 动态化效果。
这里先简单介绍一下 DSL(Domain Specific Language 领域特定语言),它指的是专注于某个应用程序领域的计算机语言,不同于普通的跨领域通用计算机语言(GPL),领域特定语言只用在某些特定的领域,比如用来显示网页的 html 和数据库语言 SQL。另外,使用通用语言的特定语法,基于语言的一部分特性来处理一个大系统的一个小方面问题,也可以被看做一种内部 DSL。
虽然定义 DSL 和 Native 支持这两块的工作量偏大,但是它具有以下的不可比拟的优势:
性能:组件的初始化、布局、渲染都是通过 Native 实现的,保证了框架对性能的最小损耗;
DSL 自定义:可以结合 Qunar 大客户端的业务需求,进行定制化的模板功能支持,后续的组件的可拓展性与可维护性;
学习成本较低:我们的 DSL 主体上是基于 Flexbox 的布局思想进行涉及,有过 React 或 Flutter 经验的前端开发可以很快上手进行模板的编写;
基于这些考虑,我们最终选择了 Naitvie+DSL 作为我们的技术实现方案。Native 部分,iOS 会基于 yoga 框架,Android 则使用 Litho 进行相关的实现;关于 DSL,在组件描述部分,我们会基于 JSON 的可嵌套层级特性来实现声明式 UI 功能,在取值逻辑部分,我们会基于自定义的动态表达式解析引擎来实现取值逻辑的描述。
三、客户端功能实现原理
以下是我们的功能架构图
SDK 内部我们分了以下几个模块:DSL 解析、数据绑定、虚拟 DOM 处理、数据校验,这些模块最终会把读取到的模板内容和数据,组合成供底层引擎渲染的节点。
模板的渲染流程如下:
可以看到,动态化 UI 的核心为模板,模板经过解析后,会使对应的组件进行各种布局属性和组件功能属性的设置,然后映射成各个层级的组件,最终通过 yoga 引擎,进行布局计算和渲染;
在实际的开发过程中,我们也经历了以下的一些问题和思考:
DSL 如何设计体验最佳?
DSL 的设计是我们动态化 UI 中最为核心的工作,我们要保证面对动态化 UI 模板的开发者,DSL 是易于上手的、符合使用习惯、没有异义的;面对功能实现,DSL 要承载起用最少的描述承载足量细节且目的确定的信息量的工作。
我们对视图的节点进行了抽象,现在每个视图节点会拥有如下四个维度的描述信息:视图类型、视图样式、视图点击事件、子视图节点,通过对以上几个维度的信息补充及设置,我们会得到一个拥有足够细节的视图节点,通过子视图节点的层层嵌套,最终会产生一个包含所有细节的节点树,供后续渲染出来。
另外,有一些模板会有必传字段的需求,如果字段为空则判断数据无效不予展示。针对这种情况,我们在模板中设置了“required”字段,模板开发者可以在该字段中传入需要校验的字段,DSL 解析模块会在真正解析前自动校验必传字段,从而保证了数据的有效性。
数据如何进行绑定?
模板解析中有一个重要环节就是将模板中的动态表达式在数据源中读取出来,如 ${data.imageList[0].imageUrl},代表将 data 对象中 imageList(这是一个 List 类型字段)中的第 0 个对象的 imageUrl 读取出来,这里需要我们通过分析语义,进行相关的操作拆分。这里我们基于 AST(抽象语法树)的思想,实现了动态表达式的解析取值。具体实现为通过正则匹配出相关运算符和需操作数据,对运算符进行优先级排序,然后依次对要处理的运算符进行操作封装,最终通过递归完成整个动态表达式的解析取值。
目前动态表达式解析模块的能力不仅仅包括类似 ${data.imageList[0].imageUrl}字段路径取值这种基础能力,还拓展了字符串比较、数据判空、数学运算符、字符串拼接、三目运算等高级特性,比如以下比较复杂的表达式:
${tripData.depTerminal? tripData.depAirport+tripData.depTerminal: tripData.depAirport},我们会基于三目运算>字符串判空>节点取值>字符串拼接这样的流程,最终基于判断条件得出相关的输出。
渲染前的布局计算,通过虚拟 DOM 解决
在 iOS 端的开发中,我们遇到了一个问题,某些场景下,需要在组件完成布局渲染前就要拿到组件所需尺寸。但是我们最初的实现是需要组件完成布局之后才可以拿到组件的最终尺寸,这样是不能完全满足以上场景的需求的。后续我们引入了虚拟节点的方案,虚拟节点只存储布局相关的信息,模板解析后不再映射成具体的 UI 组件,而是相应的携带布局信息的虚拟节点,通过对虚拟节点树的布局计算,最终拿到节点树对应的 UI 组件的所需尺寸。
可以看到,我们抽象了一个节点接口,它定义了一个节点所需要的行为。我们为 UIView 和 VirtualView 类分别实现了该接口,这样当我们只需要获取布局信息而不是真正渲染的时候,使用虚拟节点树来进行布局计算即可,虚拟节点因为不涉及真正的 UI 组件初始化,资源会得到极大的节约。
四、模版管理系统的设计
动态化 UI 的动态化特性,需要通过模板的动态下发来完成,这要求我们要对模板的编辑、发布、下发、应用这几个环节,提供一个闭环的管理,我们发布了动态化 UI 模板管理平台来支持以上功能。
下图是模板管理系统的一个简单组成。
可以看到,整体的流程为:模板的开发者通过模板管理平台进行编辑->灰度->发布的操作,将相关模板下发到客户端,客户端对模板加载至内存中,并在需要的时候绑定数据源并进行解析渲染,从而完成动态化 UI 组件的展示。
我们对模板引入了版本字段,以进行版控相关的逻辑。同时引入了“相关场景”的概念,后端接口会限制只在有效的场景下才下发模板可用的数据。具体来说,我们为每个模板加入了接口的关联,当请求后端时,会将与后端接口相关联的模板数据作为参数上传后端,后端会基于关联数据下发前端模板渲染所需数据,这样就避免了前端模板和后端数据出现不匹配导致无法应用并展示数据的问题。
五、真机预览功能完善
整个动态化 UI 方案中,还有一个比较重要的角度,即模板开发者的开发体验。模板的实时预览功能,无疑是体验提升的重要一环。针对模板的预览方案,最初我们有两种路线:
通过 Web 应用,开发者在浏览器输入模板,网页进行模板的实时展示,类似于现在比较主流的各个低代码平台的体验。但是要实现上述的体验,有个关键点是,需要在 web 端也实现和手机客户端相同的模板渲染逻辑,并且后续也要保证 web 端逻辑与客户端的同步,否则难以保证 web 的预览效果与客户端的完全一致。从体验上来讲,该方案确实有很多优点,比如无需搭建开发环境、开发者可以更多关注业务,但是因为增加了 Web 端的开发与维护工作,不可避免地一定程度增加了动态化 UI 方案落地的成本。
通过本地 node 服务,监控指定路径模板文件的修改,客户端打开一个预览页面,与本地服务建立 websocket 长连接,并在客户端实时刷新渲染该模板。该方案虽然需要开发者下载运行相关工具,但是因为在客户端预览,从而保证了“所见即所得”。经过以上的综合考虑,我们选择了第二种方案完成了预览功能的实现。
六、动态化 UI 方案在大客户端的落地
目前首页的待出行卡片是通过 native 实现的,该组件用于用户出行提醒,实时展示出行信息,卡片内容会根据不同的业务线展示不同样式,交互事件主要为点击事件,比较符合动态化 UI 组件的“低交互、多样式”原则。经过评估,我们决定将其作为首个接入动态化 UI 的业务功能。
在业务接入的过程中,我们还是发现了一系列动态化 UI 的 SDK 开发时没有考虑的问题,比如需支持渐变背景、Android/iOS 两端某些样式默认值不统一等,我们记录问题并对 SDK 相关功能进行了补充和优化。如上所述,SDK 为业务提供了相关能力,同时业务接入过程中,又会反馈新的能力诉求,从而形成了交替迭代,最终完成了功能的接入和上线。
七、项目收益
目前我们已接入了首页的多个二屏卡片和收藏浏览弹窗功能,共发布模板 15 个、并进行了数十次的版本迭代。我们做到了:
首页功能无需发版,即时发布,效率提升。之前首页的功能,由于都是 native 实现的,即使一个较小的改动,也需要发版解决,动态化 UI 方案做到了在不降低首页性能指标的前提下,首页相关功能的热发布。
模板代码跨平台,开发成本降低约 50%。目前 iOS 和 Android 端已接入动态化 UI 的功能相关需求,只需要发布一个模板即可完成开发上线工作,开发成本显著降低。
JIT 式开发,所见即所得。通过 Node 服务监听指定路径的文件变化并与客户端建立长连接通信,我们提供了模板开发的实时预览功能,开发者可以在开发模板的过程中,通过 mock 数据实时查看模板的最终呈现效果,大大提升了开发效率和开发体验。
DSL 学习接近零成本。前端开发同学可以即时上手,基于 flexBox 思想进行各种样式的开发和实时验证。
同时,在后续的需求开发的技术选型方面,我们也多了一种新的选择。
八、后续规划
DSL 的功能拓展。我们计划后续会针对 DSL 通过增加迭代器定义来支持循环,以使模板支持不定长度的列表数据源。
前后端模板同步环节的持续优化。因为首页的特殊场景,为避免文件 IO 的延迟影响首页启动指标,目前的内置模板是通过硬编码的方式实现的,后续我们会通过优化缓存策略、利用 MVVM 框架的性能优势,改造成通过文件内置模板,进行逻辑和资源的隔离。
九、结语
这次动态化 UI 的尝试,我们遇到了很多的挑战,同时也有很多的收获。尤其是在前端技术飞速迭代的今天,后续要做的肯定还有很多,很高兴能和大家分享这一阶段中的遇到一些问题和相关的一些思考,希望能给读者带来一些启发。
✦END✦
评论