从 PC 时代、移动时代到万物互联的 IoT时代,伴随终端设备的日趋多样化,跨端复用的种子自此落地,开始生根发芽。从业务角度出发,跨端技术演进更多是在不同阶段、不同时间段内业务效率上的选择,美团民宿业务在大前端融合的浪潮中逐浪前行,不断探索和迭代抉择,为解决业务痛点而孵化出跨端框架技术,在这个过程中,我们进行了很多的探索和实践的思考,希望能给大家一些启发。本文主要分享美团民宿在跨端复用技术探索和业务实践过程中的经验。
从 PC 时代、移动时代到万物互联的 IoT 时代,伴随终端设备的日趋多样化,跨端复用的种子自此落地,开始生根发芽。从依靠容器能力、各类离线化预装包的 Hybrid 方案,到通过 JSC 连接 JavaScript 生态与原生控件,结合视图框架(React、Vue等)寻找效率、动态性和性能更均衡的 Native 容器方案(React Native、Weex 等),接着由微信牵头的以多进程 WebView、容器标准化的小程序方案出世,各平台小程序随之春笋萌发,随后带来了国内Taro、uni-app、Rax、Remax等多端框架的百家争鸣。
从业务角度出发,跨端技术演进更多是在不同阶段、不同时间段内业务效率上的选择,美团民宿业务就是在大前端融合的浪潮中逐浪前行,不断探索和迭代抉择,为解决业务痛点而孵化出跨端框架技术。本文主要分享美团民宿在跨端复用技术探索层面以及业务实践过程中积累的经验,希望能给大家带来一些帮助或者启发。
美团民宿专注为消费者提供“住得不一样”的旅居体验,提供的服务包括民宿、酒店、公寓、客栈、短租、宾馆、旅行住宿等,同时包括树屋、房车、INS 风等新奇的网红民宿。美团民宿自上线之后,业务发展迅猛,在供给侧,房源类型不断丰富,各类分销、直销、直连、境外陆续推出,房源信息维度不断扩展,筛选、推荐、信息呈现也不断变得复杂。同时伴随着营销方式的丰富、房东管理、经营、服务的不断扩充,民宿的业务也越来越复杂。美团民宿大前端伴随业务的发展不断自我迭代,移动端整体架构也随之不断调整、升级,以寻求匹配业务多样化、复杂化的发展诉求。
业务的发展和跨端复用技术的不断演化,让美团民宿客户端从业务刚起步的单端 Native App,到跨 App(民宿 App、美团 App、点评 App )的 Native 复用和以 SSR 弥补性能差距的 Hybrid 的结合方案,在这场性能和效率的博弈中,客户端最终落脚以 React Native(以下简称 RN)为核心的复用框架。在此同时,民宿小程序端也随着微信小程序的诞生、生态壮大、多平台化的趋势不断成长,逐渐形成多平台复用的小程序架构。
上图是美团民宿移动端原始架构图,左侧是客户端的技术架构,iOS 和 Android 系统层之上是独立的 Native 基建层,再往上通过了 RN 打开双端的复用之门,接着以 RN 容器标准化屏蔽了宿主应用间差异,保障了容器化的一致性,进而实现了业务层的复用和跨 App 的复用。右侧是民宿小程序当前简化的架构图,我们在基建层做了多端适配,通过多平台复用构建工具实现了各平台小程序的复用。当前客户端和小程端相关独立,开发维护也相互独立,团队各司其职。
尽管美团民宿 App 已经通过 RN 实现 iOS 和 Android 的跨端复用,但是由于 App 和小程序仍然需要投入双倍的人力成本进行业务迭代,所以我们思考一个问题:是否可以更进一步,使用一套代码解决多端,把 iOS App、Android App、小程序进行大一统。
近几年,在微信小程序产品牵头下,业界也随之诞生出各种小程序应用,各端技术差异使得开发和维护成本都成倍增加。为了抹平原生开发、小程序开发、Web 开发等技术差异,一些优秀多端框架也就此诞生了。比如 Taro、uni-app、Rax、Remax 等,这些框架都是以自身定义 DSL (一般是 React DSL、Vue DSL)转换成各端应用(微信小程序、RN、H5等),从而实现一套代码,多端运行。
在美团民宿业务中,App 的交易占比较大,从业务角度出发需优先保障 App 的性能体验和需求开发效率,而当前的民宿 App 已迁移至 RN 技术栈。基于这两点,我们希望跨端复用方案的是: RN 转到小程序平台方案,所以上述的多端框架并不能满足我们的 RN-小程序跨端复用的诉求,为此美团民宿参考了业界多端设计方案,实现了基于 RN 转小程序复用的方案。
RN 采用的是 React 语法,因此如何将 RN 转换为小程序,首先要思考如何将 React 代码转换成小程序可运行的代码(简称小程序代码),其次是 RN 基础组件库的适配。随着这几年的发展, React 代码转换成小程序代码在业界实践也是层出不穷,业界方案分为编译时与运行时两类,以下是这两类方案的简单对比:
框架分类 | 重编译 | 重运行 |
---|---|---|
典型代表 | Taro2.0 / Rax 编译时 | Taro Next / Remax |
原理 | 编译时将 React 代码直接转换成小程序代码 | 运行是通过 React 自定义渲染器完成页面绘制 |
优势 | 性能损耗低 | 无语法限制 |
劣势 | 语法限制大 | 性能损耗大 |
对比来看,重编译方案有一个严重的问题:语法限制。因为大部分前端开发者们已经对灵活的语法有一定的依赖性,比如会使用高阶组件、在条件判断的时候写很多 return 等等,这种写法很难在编译过程被准确命中。因此,编译时方案就会制定一些语法规则来限制开发者的写法。重运行方案则没有语法限制问题,可以随意使用各种 React 特性。它的实现原理是通过 react-reconciler 实现小程序平台对应的 React 渲染器(以下简称 MP-Renderer),从而来渲染虚拟 DOM 树。不过小程序没有 DOM API 可以更新界面,所以生成的虚拟 DOM 树数据是通过小程序的 setData 触发渲染层的更新,在渲染层里有一个通用模板可以用来渲染这些数据。
因重编译语法限制的问题,我们决定采用重运行时方案来实现 RN 转小程序。但重运行方案存在性能问题,难以满足业务的要求,我们经不断探索后设计了对应的方案极大提升了性能,下文会详细描述如何解决这个问题的。
整体架构分为两个部分:编译过程、运行过程。它的渲染方式与上文描述重运行时方案类似,都是通过 MP-Renderer 来处理 React 代码。下面我们来简要分析这两个过程:
(1) 编译过程:该阶段对 RN 源码进行一定的转换处理,用于运行过程,编译后主要产生有以下产物:
(2) 运行过程:运行过程分为逻辑层和视图层两部分。
综上所述,上述整体设计与业界多端框架有点类似,但是也有不同点,主要体现在适配组件库和合并模板。适配组件库上文有解释比较好理解,而合并模板这里可能大家还是比较有疑惑的。其实这个合并模板内容是由编译过程的 “静态编译” 转换生成的,这样的处理方式是为提升转换后的小程序性能,接下来,我们会着重来讲述这个性能解决方案。
重运行时方案性能损耗原因是什么?正如上文所说,重运行时方案会将所有 React 代码对应的 TreeData,再通过小程序 setData 传输到渲染层,当页面初始化或者大数据更新的话,setData 就需要传递比较大的一个数据,因此也就会造成对应的性能问题。所以要解决这种方案的性能问题,核心就是要减少 TreeData 数据量。
在上述 RN 转小程序方案,有提到适配组件库、样式转换等是可以起到对应性能优化作用的,它的优化原理正是通过减少 TreeData 数据的方式。尽管这些方式可以优化性能,但是在页面比较复杂的时候,TreeData 数据量仍然会保留比较大,因此优化效果并不明显。为此,我们思考一种新的方式来进一步压缩 TreeData 的数据量,也就是前文所提到的结合静态合并树节点方案,在讲述该方案前我们先来看下一个 RN 代码转换为 TreeData 的例子:
如上图所示,RN 代码转换后的 TreeData 是一个描述 UI 树的 JSON 数据,等同于右侧的 UI 树,将这颗树的节点进行分类,可以分为静态数据和动态数据,比如 View、Text 节点就是静态数据,而 “Hello”、“World” 则是动态数据。所谓静态数据,就是编译过程可预知的,因此这些数据是不是可以转换另一种形式来描述 UI 呢,从而减少 TreeData 的数据量。答案是肯定的,静态编译合并树节点正是通过这样的原理来实现的,如下流程所示:
这个方案有两个动作,分别是静态编译和合并树节点,静态编译就将 RN 代码的转换成合并模板,如上图序号 2 代码所示,合并模板的名称为 “b1”,内容就是一段与 RN JSX 代码对应的 WXML 结构片段。而合并节点是将已经静态编译的节点进行合并,如上图序号 2 至序号 3 流程所示,原本五个节点被合并到顶层的 View 节点,这个 View 节点称为合并节点,合并节点需要记录合并模板的名称和相关的动态数据,目的是为了渲染时让合并节点可以找到对应的合并模板进行渲染,经过这样合并节点后,最终生成的 TreeData,如上图序号 4 所示。可以看到 TreeData 相比之前的数据量就减少了 60% 左右!
看到这里,是不是有同学就有疑问了,上文不是提到静态编译会有语法限制,那这里是否会有语法限制?确实,如果是完全静态编译,是会有语法限制,而这里所说的结合静态编译是有选择性的编译,即在编译过程,首先会通过 AST 分析节点是否静态数据,如果是的话,再转换成对应的合并模板。如果遇到不可预测的动态节点,则按照运行时方案去处理。因此,最终生成的 UI 树节点即会包含合并节点、也会包含原本的组件节点,如下图所示:
通过这样的方式,既可以保证语法无限制,又能通过编译结合的手段最大化优化性能。当然了这种方案也是有缺点,因为这种方案其实是用空间换性能的方式,生成的合并模板会影响会影响包大小,不过对于一些需要追求性能的页面,这点包大小的增加是值得付出的。
为了更好地衡量解决方案对性能的提升程度,我们参考 Taro 官网的实验(实验内容),对优化前后以及原生和 Taro 3.0 运行后的性能指标进行采集与比较。经过实验,统计出各框架在初始化、加载数据、加载大量数据的操作耗时,如下表所示:
操作耗时 \ 框架 | 优化前 | 优化后 | 原生 | Taro 3.0.17 |
---|---|---|---|---|
初始化(首屏渲染时间) | 897ms | 423ms | 210ms | 675ms |
加载普通数据(20条) | 1124ms | 198ms | 110ms | 640ms |
加载大量数据(400条) | 5330ms | 1041ms | 470ms | 3919ms |
从上表中可以看出:性能优化后,得益于更少的渲染数据与更精简的节点树,加载数据的操作耗时比优化前减少 80% ,初始化耗时减少了 52%。与同类型的框架 Taro 3.0 相比,也有更好的性能表现。
与原生相比,优化后性能差距明显减少,但是由于运行时方案相对于原生需要更多的 setData 数据开销和更复杂渲染流程,所以从原理上运行时方案和原生性能差距客观存在。尽管如此,业务实践上两者差距并不会那么明显,因为在测评实验中测试数据比较纯粹,setData 数据使用率较高,但在业务实践中原生开发 setData 数据难免冗余且难以优化,而运行时方案会默认优化冗余数据使得两者性能差距更接近,从我们历史业务实践数据上看,性能与原生差距在 10% 左右。
在跨端复用探索中,我们用创新的方案解决了性能和特性限制的难题,设计了 RN-小程序跨端复用框架。虽然跨端复用属于“利器在手”,但是这是一把“双刃剑”,用得其所则事半功倍,处理不当则隐患丛生。那么,如何在业务实践中驾驭好这把利刃呢?我们先介绍在业务实践中遇到的问题,然后介绍解决这些问题的方案。
为了解决跨端复用在业务实践中遇到的各种问题,我们重新设计了跨端复用应用架构,从架构分层管理、复用方式设计、流程规范、质量保障方面入手,重点解决跨端差异化、质量隐患、流程规范各种问题,并寻求复用的最大化和性能上的均衡。
在这里,先贴出动态的架构演进过程,让大家有一个宏观的认识。我们先简单地描述下演进过程,后续会基于最终的架构图再做详细的介绍。大致演进过程如下:
整个民宿的 RN-小程序跨端复用架构图如上,我们按照从下到上,从左到右的视角进行解读:
3.3 跨端复用方式设计
来详细讲解。差异化问题,一直是跨端复用场景中的一个痛点,双端的产品上、平台上、代码上的差异如何妥善的处理、适配,也是我们一直思考的问题。而好的差异化处理方案可以提升代码的可维护性、降低质量隐患、提升开发效率。我们从复用设计层面出发,探索出页面复用模式、组件复用模式、“组件+逻辑复用”模式等三种复用设计方式,并且根据不同的场景下采用不同的复用模式,可以较好地处理跨端差异化问题,同时能兼顾效率提升、性能体验和可维护性。
我们自研的复用框架提供两种复用模式,如下图所示:
页面复用模式:页面模式基于页面维度的,可以直接把页面的网络层、逻辑层、数据层以及页面内的组件集全部转换复用,这样可以达到复用的最大化,代码复用率能达到 90% 以上,人效提升明显。
组件复用模式:组件模式是基于组件维度的,复用以页面中的业务组件为目标,把页面的所有组件抽象、解耦、规范化之后抽取为复用组件。组件模式只能复用组件内代码,对于页面容器的逻辑交互、网络层都需要小程序自己实现,代码复用率相对较低,但是组件复用更灵活、可控,可随意插拔、拼接、定制。
以下是两种复用模式的优劣分析。
页面复用模式
优势
劣势
组件复用模式
优势
劣势
两组复用模式各有利弊,页面模式复用率高,但是灵活性低、性能欠佳;组件模式轻便灵活,性能可控,能较好的处理平台差异化问,但是复用率低、维护成本高。我们在想有没有一种方案能保留组件模式的灵活性,又能降低组件维护成本、提高复用程度。在业务实践中,我们探索出一套“组件+逻辑复用”的模式,可以较好地解决上面提到的问题。
“组件+逻辑复用”模式依然保留组件复用的方式,但是在组件复用基础上增加了逻辑层(包括页面逻辑、网络、数据层)的复用,这样保留了组件灵活性,也增加了复用性。具体设计如下图:
整个组件+逻辑复用模式设计图如上,我们按照图片标注的序号进行一一解读:
这种方案的优势很明显,它保留组件模式的灵活特性,可以比较方便做差异化处理和性能优化。而逻辑复用层把 Redux 包含进来了,这样不仅转化容易、不易出错,而且逻辑复用接口基于 Redux 的 Store,接口较好设计,容易维护、不易出错。而对于逻辑层,可以根据业务上一些差异做 Reducer 与 Saga 分拆,把不需要复用的代码逻辑排查在外,逻辑层复用也可以做到像组件一样热插拔,按需引入,这样也比较好做差异化代码管理,挺高项目的可维护性,同时也能优先减少包大小风险。
为在代码跨端复用过程中尽可能提升开发效率并避免引入质量问题,我们制定了差异化编码规范、需求同步规范、复用组件规范等开发流程规范,以下将通过 RN 到小程序产品需求同步过程进行简单的介绍。
1. 评估业务需求是否需要同步
针对 PM 提出需要同步的需求,客户端尽量将 RN 业务代码复用至小程序,以提升开发效率。无需同步的需求将通过差异编码规范进行控制,避免同步至小程序后增加潜在风险与测试成本。通常可使用平台判断(如 iOS、Android、WX_Platform )的方式控制业务代码是否打入复用组件包,也可通过 module.rn.js、module.wx.js 不同后缀文件方式完成相同接口不同逻辑的实现。
2. 评估是否有关联依赖需求
如明确业务需求需要同步,先判断该需求是否有前置需求依赖,再评估技术方案。如无依赖可直接开始复用适配工作;如有依赖,需判断前置需求能否一起同步或做适当降级,以此递推,避免因前置依赖需求未同步出现不符合预期的问题。
3. 制定 RN 组件适配与小程序接入方案
明确需求同步范围评估工作后,需完成以下技术评估工作:(1)明确需求是否需要新建复用组件还是在原有的复用组件上进行迭代。如需新建复用组件 NPM 包,需根据组件复用规范进行技术选型,确定使用“组件+逻辑复用模式”、“页面模式”还是“组件模式”,并制定相应的复用组件接口协议;(2)明确该需求是否需要开发 RN-小程序映射方法、组件,并评估相应的开发量。完成技术评估后需提前与小程序侧沟通接入排期。
4. RN 组件适配开发
客户端完成 RN 侧需求开发后,便可进行复用组件适配小程序开发。完成适配开发工作后需在 RN 页面与小程序 Demo 页面中对复用组件同时进行测试,避免在适配小程序过程中引入 RN 页面 Bug。复用组件测试完毕后将 NPM 包以及相应的接口文档提供给小程序接入,但在打包前需严格审查当前版本与上个版本间的 diff,避免不符合预期的代码也被同步至小程序。
5. 小程序接入 RN 适配组件
适配完成后将组件打包提供给小程序侧接入,接入后需在美团民宿小程序环境下再次进行自测。原则上客户端同学提供适配好的 RN 组件后,由小程序侧同学接入并测试,但我们也鼓励客户端在完成 RN 组件开发与复用适配后,一并完成小程序侧的组件接入工作,这样需求开发完整度更高,并能有效减少跨端开发下的沟通成本。后续随着大前端融合推进,RN-小程序代码复用率将逐步提升,客户端(iOS、Android)与 小程序代码将倾向由一名同学完成多端开发。
6. RN 适配代码合入迭代分支
需求在小程序测试完毕后,将 RN 组件适配 Feature 分支代码合入 Release 迭代分支,并在客户端(iOS、Android)打包上线。
跨端复用场景下存在包括复用组件接口兼容性问题、组件间的依赖隐患问题、测试和监控的缺失问题,以及故障排查困难等各种质量隐患,我们在业务实践中,也探索出一系列解决这些隐患的质量保障措施,包括组件接口维护、组件依赖管理、双重自测卡控、异常监控融合、双端故障 SOP、跨端复用流程规范 。这些措施能有效保障复用场景下双端的线上质量,民宿业务在跨端复用推进中,因为这些措施的保障护航,没有出现任何的线上故障。
1. 组件接口维护
复用组件随着业务迭代会不断更新升级,组件升级过程中便会带来的组件接口、输入、输出的变动,进而产生兼容性隐患,比如组件输入参数类型变动,而小程序端或RN端没有及时兼容或者未知晓,非常容易引发线上质量问题。为此,我们制定了组件接口维护计划,包括复用组件接口规范、组件版本管理规范、组件接口文档建设等。复用组件接口规范要求复用组件接口、参数必须严格按照规范来,如参数类型使用基础类型、只增少减原则、接口命名清晰、参数个数限制等等,减少双端的接入组件难度,避免参数频繁变动产生质量隐患。组件版本管理规范要求组件版本升级必须遵循語意化 2.0,并且有相应的版本升级文档。组件接口文档建设也是很重要的一环,每个复用组件都有相应的文档维护,记录参数的增删改查,接入方对组件接口变动一目了然,自然减少了接入风险。
2. 组件依赖管理
组件依赖主要存在两个问题,第一,复用框架本身也在不断升级优化、新的复用组件可能用新的编译版本转化而来并且依赖新的运行时渲染器,但是旧的复用组件可能会出现不兼容问题,因此我们开发相关的工具,如果组件依赖的运行时渲染器版本和小程序内置的不一致就会发出警告,提示组件兼容性问题。第二,因为不同的复用组件来自不同的RN模块,它们可能依赖不同版本的第三方库,容易产生版本不一致的质量问题。目前的解决方案是把这些依赖库分别打入各自的包里,这样复用组件间依赖相互独立,互不影响。再结合 Tree-Sharking 的优化,打入的依赖的真实包大小并不大,用小量的包大小换取更稳健的质量保证。
3. 双重自测卡控
在跨端复用场景下,一个复用模块的改动要考虑双端兼容和新旧版兼容问题,相比与之前有更高的出错风险,更全面的自测能帮我们尽早暴露问题,减少故障风险。所以我们在 App 侧和小程序侧做了代码自测覆盖率卡控,要求改动代码执行覆盖率超过 90% 才能提测和上线。复用组件既在 RN 侧自测过一遍,在小程序接入后又强制要求再自测一遍,双重自测卡控更能保障组件质量和线上质量。
4. 异常监控融合
RN 和小程序侧都有单独的异常监控机制,包括 JS 异常监控、API 异常监控、自定义异常监控等。但是双端的异常监控机制差别较大,在复用场景下两者交叉混用导致异常监控体系混乱,上报数据格式、策略、日志不统一而造成监控体系误告、漏告、排查困难、运维混乱等问题。所以我们把双端的异常监控模块打通,适配了底层异常上报逻辑,统一了双端的上报规范,告警策略、日志、处理流程。异常监控体系双端融合后,异常上报、监控、运维都顺畅许多,也帮我们发现不少的线上异常,是 RN-小程序跨端复用场景线上质量的坚固屏障。
5. 双端故障SOP
鉴于双端同学存在技术上的隔阂和信息不对称,当出现复用组件的故障或异常时,如何快速排查问题成为一个痛点,小程序的错误日志 RN 同学不熟悉,小程序同学不熟悉 RN 的业务代码实现,框架层面的错误更难排查。为此,我们整体了梳理双端故障 SOP,这里面包括常见日志分析帮助鉴别是复用组件、小程序端、底层复用框架的问题和相应的解决方案,同时开发了 Source Map 错误反解工具协助RN同学反解小程序日志帮助快速排错等等。这些 SOP 和工具能够在第一时间帮助双端同学自主或协助排查相关故障,快速止损。
6. 跨端复用流程规范
流程规范包括前面提到的复用组件规范、编码规范、需求同步规范、分支管理规范等等也是质量保障的重要的一环,它让研发流水线每一环都有严格的法律约束,保障整条研发流水线最终能把完整的产品交付到用户手里。
RN-小程序跨端复用的设计方案在业务实践中不断完善,探索出效率相对最大化的复用模式。从开发效率角度来看,提升显著。我们总结了代码复用率与人效提升率来评估效率的提升,两个指标具体计算公式如下:
根据转换采用的模式不同,可以得出代码复用率与人效提升率,如下表所示:
转换模式 | 代码复用率 | 人效提升率 |
---|---|---|
组件+逻辑复用模式 | 76% | 42.36% |
页面复用模式 | 91% | 46.71% |
从表中可以看出,页面转换模式复用了页面与组件的代码,代码复用率可以达到 90% 以上;组件复用模式复用了组件与部分业务逻辑代码,复用率也可以达到 76%。在人效提升方面,所有模式都能达到较高的人效提升率,代码复用率越高人效提升率也越高,页面转换模式可以复用页面与数据状态处理逻辑人效提升比组件转换模式更高。
民宿大前端团队为解双端研发效率之痛,倾力而寻跨端之技,浅尝百草、深谙其理而后自建之,举偏补弊、终解跨端框架性能之桎梏,青出蓝而胜于蓝。而后践于实业,瑕弊昭然若揭。为此,重设框架以谋其变(复用架构设计),寻之新式以尽其效(复用模式设计),立之新法以固其序(跨端复用流程规范),磨之利器以护其城(跨端复用质量保障),至此成果初成。然朝夕变化不休,路漫远兮,吾当持之求索以适其变、顺其道。跨端复用前行之鉴,故记以文,望有启示,文毕。
凯林、森伟、熙辰、戈弋、少元等,均为美团民宿前端团队研发工程师。
美团民宿长期招聘 Android、iOS、FE 前端工程师,坐标在福建厦门。感兴趣的同学可将简历发送至:[email protected](邮件主题请注明:美团民宿大前端)。
阅读美团技术团队更多技术文章合集
前端 | 算法 | 后端 | 数据 | 安全 | 运维 | iOS | Android | 测试
| 在公众号菜单栏对话框回复【2020年货】、【2019年货】、【2018年货】、【2017年货】等关键词,可查看美团技术团队历年技术文章合集。
| 本文系美团技术团队出品,著作权归属美团。欢迎出于分享和交流等非商业目的转载或使用本文内容,敬请注明“内容转载自美团技术团队”。本文未经许可,不得进行商业性转载或者使用。任何商用行为,请发送邮件至[email protected]申请授权。