美团民宿专注为消费者提供“住得不一样”的旅居体验,提供的服务包括民宿、酒店、公寓、客栈、短租、宾馆、旅行住宿等,同时包括树屋、房车、INS风等新奇的网红民宿。美团民宿自上线之后,业务发展迅猛,在供给侧,房源类型不断丰富,各类分销、直销、直连、境外陆续推出,房源信息维度不断扩展,筛选、推荐、信息呈现也不断变得复杂。同时伴随着营销方式的丰富、房东管理、经营、服务的不断扩充,民宿的业务也越来越复杂。美团民宿大前端伴随业务的发展不断自我迭代,移动端整体架构也随之不断调整、升级,以寻求匹配业务多样化、复杂化的发展诉求。
尽管美团民宿App已经通过RN实现iOS和Android的跨端复用,但是由于App和小程序仍然需要投入双倍的人力成本进行业务迭代,所以我们思考一个问题:是否可以更进一步,使用一套代码解决多端,把iOSApp、AndroidApp、小程序进行大一统。
在美团民宿业务中,App的交易占比较大,从业务角度出发需优先保障App的性能体验和需求开发效率,而当前的民宿App已迁移至RN技术栈。基于这两点,我们希望跨端复用方案的是:RN转到小程序平台方案,所以上述的多端框架并不能满足我们的RN-小程序跨端复用的诉求,为此美团民宿参考了业界多端设计方案,实现了基于RN转小程序复用的方案。
RN采用的是React语法,因此如何将RN转换为小程序,首先要思考如何将React代码转换成小程序可运行的代码(简称小程序代码),其次是RN基础组件库的适配。随着这几年的发展,React代码转换成小程序代码在业界实践也是层出不穷,业界方案分为编译时与运行时两类,以下是这两类方案的简单对比:
对比来看,重编译方案有一个严重的问题:语法限制。因为大部分前端开发者们已经对灵活的语法有一定的依赖性,比如会使用高阶组件、在条件判断的时候写很多return等等,这种写法很难在编译过程被准确命中。因此,编译时方案就会制定一些语法规则来限制开发者的写法。重运行方案则没有语法限制问题,可以随意使用各种React特性。它的实现原理是通过react-reconciler实现小程序平台对应的React渲染器(以下简称MP-Renderer),从而来渲染虚拟DOM树。不过小程序没有DOMAPI可以更新界面,所以生成的虚拟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的数据量。答案是肯定的,静态编译合并树节点正是通过这样的原理来实现的,如下流程所示:
看到这里,是不是有同学就有疑问了,上文不是提到静态编译会有语法限制,那这里是否会有语法限制?确实,如果是完全静态编译,是会有语法限制,而这里所说的结合静态编译是有选择性的编译,即在编译过程,首先会通过AST分析节点是否静态数据,如果是的话,再转换成对应的合并模板。如果遇到不可预测的动态节点,则按照运行时方案去处理。因此,最终生成的UI树节点即会包含合并节点、也会包含原本的组件节点,如下图所示:
通过这样的方式,既可以保证语法无限制,又能通过编译结合的手段最大化优化性能。当然了这种方案也是有缺点,因为这种方案其实是用空间换性能的方式,生成的合并模板会影响会影响包大小,不过对于一些需要追求性能的页面,这点包大小的增加是值得付出的。
从上表中可以看出:性能优化后,得益于更少的渲染数据与更精简的节点树,加载数据的操作耗时比优化前减少80%,初始化耗时减少了52%。与同类型的框架Taro3.0相比,也有更好的性能表现。
与原生相比,优化后性能差距明显减少,但是由于运行时方案相对于原生需要更多的setData数据开销和更复杂渲染流程,所以从原理上运行时方案和原生性能差距客观存在。尽管如此,业务实践上两者差距并不会那么明显,因为在测评实验中测试数据比较纯粹,setData数据使用率较高,但在业务实践中原生开发setData数据难免冗余且难以优化,而运行时方案会默认优化冗余数据使得两者性能差距更接近,从我们历史业务实践数据上看,性能与原生差距在10%左右。
在跨端复用探索中,我们用创新的方案解决了性能和特性限制的难题,设计了RN-小程序跨端复用框架。虽然跨端复用属于“利器在手”,但是这是一把“双刃剑”,用得其所则事半功倍,处理不当则隐患丛生。那么,如何在业务实践中驾驭好这把利刃呢?我们先介绍在业务实践中遇到的问题,然后介绍解决这些问题的方案。
为了解决跨端复用在业务实践中遇到的各种问题,我们重新设计了跨端复用应用架构,从架构分层管理、复用方式设计、流程规范、质量保障方面入手,重点解决跨端差异化、质量隐患、流程规范各种问题,并寻求复用的最大化和性能上的均衡。
在这里,先贴出动态的架构演进过程,让大家有一个宏观的认识。我们先简单地描述下演进过程,后续会基于最终的架构图再做详细的介绍。大致演进过程如下:
整个民宿的RN-小程序跨端复用架构图如上,我们按照从下到上,从左到右的视角进行解读:
差异化问题,一直是跨端复用场景中的一个痛点,双端的产品上、平台上、代码上的差异如何妥善的处理、适配,也是我们一直思考的问题。而好的差异化处理方案可以提升代码的可维护性、降低质量隐患、提升开发效率。我们从复用设计层面出发,探索出页面复用模式、组件复用模式、“组件+逻辑复用”模式等三种复用设计方式,并且根据不同的场景下采用不同的复用模式,可以较好地处理跨端差异化问题,同时能兼顾效率提升、性能体验和可维护性。
我们自研的复用框架提供两种复用模式,如下图所示:
页面复用模式:页面模式基于页面维度的,可以直接把页面的网络层、逻辑层、数据层以及页面内的组件集全部转换复用,这样可以达到复用的最大化,代码复用率能达到90%以上,人效提升明显。组件复用模式:组件模式是基于组件维度的,复用以页面中的业务组件为目标,把页面的所有组件抽象、解耦、规范化之后抽取为复用组件。组件模式只能复用组件内代码,对于页面容器的逻辑交互、网络层都需要小程序自己实现,代码复用率相对较低,但是组件复用更灵活、可控,可随意插拔、拼接、定制。
以下是两种复用模式的优劣分析。
页面复用模式
优势
1)提效明显:整个页面包括所有组件、页面逻辑层网络层一并打包转换复用,代码复用率极高,开发效率提升幅度更大。2)接入成本低:整个页面直接转化同步复用,无需小程序同学协助接入,减少双端协助、接口沟通带来的出错风险。
劣势
1)灵活性低:业务差异和小程序特性不易处理,双端差异适配只能在RN上做,代码易出错,维护成本高。2)性能劣势:整体页面由RN转换复用而来,页面一次性渲染,性能上会略差一些,而且做页面级的性能优化困难。3)包大小风险大:整页复用情况下包大小较大,且不能动态调配(比如页面内某一模块需求迭代较少,不想复用,但是页面模式做不到动态移除)。
组件复用模式
1)轻便灵活:组件如插件般可随意插拔、拼接、定制,可较好解决App和小程序双端的差异性问题,针对差异点双端可以独立实现,提高项目的可维护性。2)性能较好:页面容器依然是小程序原生组件,如滚动、滑动组件采用原生可减少性能损耗,另外组件分布式setData渲染有更好的性能,不会像整页一次性渲染导致setData数据量较大影响首屏加载性能。3)性能优化空间大:不会影响做页面维度的性能优化(如首屏优先、请求前置)。4)包大小可控:组件是否复用可以动态调配,比如把页面中迭代较少的组件不复用以减少包大小。
1)提效有限:组件模式只能复用组件内的代码,代码复用率较低,页面容器、逻辑层、网络层小程序依然要自己维护一份代码。2)复用组件维护成本高:组件的接口要考虑组件升级迭代的兼容性、可维护性问题,管理不当,容易产生质量隐患。3)接入成本较高:小程序需要实现RN的页面逻辑,然后按照组件接口进行接入,有更高的接入成本。
两组复用模式各有利弊,页面模式复用率高,但是灵活性低、性能欠佳;组件模式轻便灵活,性能可控,能较好的处理平台差异化问,但是复用率低、维护成本高。我们在想有没有一种方案能保留组件模式的灵活性,又能降低组件维护成本、提高复用程度。在业务实践中,我们探索出一套“组件+逻辑复用”的模式,可以较好地解决上面提到的问题。
“组件+逻辑复用”模式依然保留组件复用的方式,但是在组件复用基础上增加了逻辑层(包括页面逻辑、网络、数据层)的复用,这样保留了组件灵活性,也增加了复用性。具体设计如下图:
整个组件+逻辑复用模式设计图如上,我们按照图片标注的序号进行一一解读:
这种方案的优势很明显,它保留组件模式的灵活特性,可以比较方便做差异化处理和性能优化。而逻辑复用层把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.组件依赖管理
3.双重自测卡控
在跨端复用场景下,一个复用模块的改动要考虑双端兼容和新旧版兼容问题,相比与之前有更高的出错风险,更全面的自测能帮我们尽早暴露问题,减少故障风险。所以我们在App侧和小程序侧做了代码自测覆盖率卡控,要求改动代码执行覆盖率超过90%才能提测和上线。复用组件既在RN侧自测过一遍,在小程序接入后又强制要求再自测一遍,双重自测卡控更能保障组件质量和线上质量。
4.异常监控融合
RN和小程序侧都有单独的异常监控机制,包括JS异常监控、API异常监控、自定义异常监控等。但是双端的异常监控机制差别较大,在复用场景下两者交叉混用导致异常监控体系混乱,上报数据格式、策略、日志不统一而造成监控体系误告、漏告、排查困难、运维混乱等问题。所以我们把双端的异常监控模块打通,适配了底层异常上报逻辑,统一了双端的上报规范,告警策略、日志、处理流程。异常监控体系双端融合后,异常上报、监控、运维都顺畅许多,也帮我们发现不少的线上异常,是RN-小程序跨端复用场景线上质量的坚固屏障。
5.双端故障SOP
6.跨端复用流程规范
流程规范包括前面提到的复用组件规范、编码规范、需求同步规范、分支管理规范等等也是质量保障的重要的一环,它让研发流水线每一环都有严格的法律约束,保障整条研发流水线最终能把完整的产品交付到用户手里。
RN-小程序跨端复用的设计方案在业务实践中不断完善,探索出效率相对最大化的复用模式。从开发效率角度来看,提升显著。我们总结了代码复用率与人效提升率来评估效率的提升,两个指标具体计算公式如下:
根据转换采用的模式不同,可以得出代码复用率与人效提升率,如下表所示:
从表中可以看出,页面转换模式复用了页面与组件的代码,代码复用率可以达到90%以上;组件复用模式复用了组件与部分业务逻辑代码,复用率也可以达到76%。在人效提升方面,所有模式都能达到较高的人效提升率,代码复用率越高人效提升率也越高,页面转换模式可以复用页面与数据状态处理逻辑人效提升比组件转换模式更高。
民宿大前端团队为解双端研发效率之痛,倾力而寻跨端之技,浅尝百草、深谙其理而后自建之,举偏补弊、终解跨端框架性能之桎梏,青出蓝而胜于蓝。而后践于实业,瑕弊昭然若揭。为此,重设框架以谋其变(复用架构设计),寻之新式以尽其效(复用模式设计),立之新法以固其序(跨端复用流程规范),磨之利器以护其城(跨端复用质量保障),至此成果初成。然朝夕变化不休,路漫远兮,吾当持之求索以适其变、顺其道。跨端复用前行之鉴,故记以文,望有启示,文毕。
凯林、森伟、熙辰、戈弋、少元等,均为美团民宿前端团队研发工程师。
美团民宿长期招聘Android、iOS、FE前端工程师,坐标在福建厦门。感兴趣的同学可将简历发送至:tech@meituan.com(邮件主题请注明:美团民宿大前端)。