组件化开发的背景
移动端的开发随着功能需求的增加,移动端的业务也越来越复杂。同时,为了支撑公司快速地尝试各种机会,已经开发的功能的复用也越来越成为非常重要的事情。为了公司适应业务模式,我们的移动端开发在2015年开始组件化的改造。从架构演进的来看,移动端的开发也是从单体模式往组件化发展。单体的痛点是代码高度耦合、无法大团队共同开发,维护成本高昂,也无法快速响应客户需求,更无法支撑不同的客户。这是显而易见的,我们无法在一个项目中适配不同的产品规格和功能需求。因此过渡到模块化是很自然的事情,也是移动端在代码库和依赖管理层而可以直接支持的机制。将不同的功能模块,拆成独立的Library,交给不同的团队开发,最后每个不同的产品都有一个 Application的工程,将所需要的这些Library包含进来,再做一些产品层面的开发,构建成一个应用。这是大多数开发团队会采用的方案。

在这样的方案下,可以把不同的功能模块拆解掉了,但是有一个痛点是功能模块间的直接依赖所带来的沟通协作问题,特别是由于允许代码直接依赖,是如果解耦设计不到位,很难完美地剥离一个功能模块。同时,这个方案有一个约束,就非常明显地显现出来,也就是,每次新增的产品及产品的功能演进,都必须由开发介入来操刀处理。对于业务比较聚焦,功能需求相对可控,开发团队人数不多的情况下,还是一个比较简易可行的方案。技术的演进始终是由业务驱动的,网龙的业务模式的一个特殊的地方在于,公司希望可以快速支撑任何看到的新的市场机会,所以,会有非常多的完全不同规格的产品被创建出来,给不同的客户演示和试用,投入正式使用的产品也会非常多。基于这样的需求,如果仅仅采用模块化的方案,产品的交付都需要开发同学的大量的介入,是很难满足需求的。因此,我们进一步的演进到下一个阶段,即组件化开发的阶段。
组件化开发
2.1 概念模型

上图是我们组件化开发的概念图。我们将组件定义为包含业务能力和交互的自包含的业务模块。所图所示,在组件化模式下,组件间的代码直接依赖被禁止。所有的组件都基于组件框架开发,由组件框架提供组件间的消息通信的机制、组件间页面跳转的机制以及跨组件数据访问的机制。同时,将组件的可配置部分剥离出来,作为配置层,包含了功能配置、皮肤、语言等。在这样的架构下,每个产品,通过挑选组件,配置组件的参数,修改语言资源,定制自己的皮肤,即可输出个性化的移动端App。另外,值得特别说明的是,在这个设计中,一个组件包含了Android和iOS两端,也就是 虽然代码实现不同,具体的实现规范因平台而略有差异,但是在抽象的层面上,Android和iOS保持功能规格、对外接口的一致性。2.2 组件的声明
<component namespace="com.nd" name="im" display="组件显示名称">
<pages>
<page type="cmp" name="chatlist" display="聊天列表">
<properties>
<property type="数据类型" name="prop_name" displayName="属性名称"/>
<properties>
page>
pages>
<properties>
<property type="数据类型" name="prop_name" displayName="属性名称"/>
properties>
<events>
<event name="im_on_message" desc="消息到达时通知"/>
events>
<handlers>
<handler name="goChat" desc="进入聊天页面"/>
handlers>
<android>
<class>com.nd.xxxxxxxclass>
<dependency>
[CDATA[
compile('com.nd.smartcan:appfactory:0.7.3.13-SNAPSHOT@aar'){
transitive =true
changing =true
}
]]>
dependency>
android>
<ios>
<class>xxxxxxxclass>
<dependency>
[CDATA[
pod 'Objection','0.x.0'
]]>
dependency>
ios>
component>
上面的组件声明,涉及到几个跟组件化相关的概念:组件页面:组件内暴露给外部的页面,用于外部容器页面或者页面跳转,通过URI的形式来描述,详见下面说明。事件: 因为组件内部逻辑产生的事件,但是可能对外产生影响,以事件形式触发,由外部组件接收后处理。事件处理器: 一个组件可以用于响应外部事件的入口。属性:组件或者页面,提供给外部的配置参数以改变组件的行为。在设计阶段,可以将抽取出一些可配置的参数,将组件或者页面的行为延迟到配置期由产品配置人员决定,以满足不同产品的差异化需求。在组件声明的最后声明了如果要使用这个组件,工程中要依赖的代码库。这是用于最后生成待编译的工程。2.3 主要议题2.3.1 页面跳转

以上为页面URI的形式。对于页面的跳转,Android通过Intent来实现,iOS通过UIController来实现,都需要在代码层面来处理,对于组件间的彻底解耦,都不太有利,同时还有深深的平台的差异性。我们参考http协议规范,定义了抽象的页面URI描述形式来描述一个页面的地址,就可以在任何位置利用这个页面的URI通过我们的框架提供的goPage方法来执行页面的跳转切换。同时,利用这种文本化的方式,也非常有利于配置。同时,这样通用的URI形式,也为我们将来支持非原生的组件形式提供了非常一致的页面表示形式。2.3.2 事件绑定从逻辑上,组件通过触发事件的方式告诉外部发生了一件事,但是并不关心哪个组件对这个事件作了响应。组件间通过事件来通信,部分情况下,相关的业务组件,可以在代码中直接作事件的绑定,实现业务的对接。但是,在很多情况下,在开发期并确定一个事件发生后的交互,可以在组件声明中声明这个事件,将来通过可视化的配置工具来跟外部组件的事件处理器作绑定。以实现配置期对产品功能的定制。通过事件的机制,各个组件间实现了代码的解耦。最直接的收益就是,不会因为加入一个组件,会因为组件代码上的依赖关系,带入非常多预期之外的组件代码。2.3.3 跨组件数据访问在实际业务中,经常会有跨组件的数据访问。在模块化方式下,直接调用所需的数据接口,即可得到。而在组件化架构下,如果通过事件通信的机制,需要数据提供方在收到事件后,再通过事件将数据返回回去,这样的方式业务开发起来,总是会比较绕。代码不那么直观和好理解。特别是在我们的组件化架构下,每个组件都应该是可以被替换的,同样的,除了某些特别的组件,每个组件也不能假设某个组件一定会被打包进来。同时,数据提供者也可能是多个。我们的组件化框架提供类似Android的ContentProvider的跨组件数据访问协议,使不同的业务组件可以进行数据访问。核心思想是通过一个过滤器(filter)来解耦数据提供者和数据的消费者。数据提供者声明自己实现一个 filter,系统中可以有多个数据提供者声明同一个filter。而数据的消费者,通过filter来查找数据提供者,得到符合条件的数据提供者后,即可使用标准的方案获取数据。我们在实际的实现中,根据数据提供者提供的数据是列表类型还是明细数据,提供不同的获取接口。2.3.4 组件的注册发现这部分需要有一个组件库,把上面的组件声明入库,先不考虑太多复杂的关系的话,这个组件的注册、发现暂时就只是一个元数据的管理和使用。2.3.5 组件的生命周期在组件化的架构下,我们的应用实际上是通过应用框架集成多们业务组件,并通过业务组件实现应用的业务功能。因此,就象应用会有启动、运行、结束的生命周期一样,框架需要为组件提供相应的生命周期事件,让组件在框架的调度下实现组件的初始化、事件的注册监听、组件的销毁等操作。2.4、混合组件开发移动端开发,有多种跨平台开发的技术方案,最常见的当属 H5和React Native(RN)。在很多情况下,跨平台开发的技术方案可以满足快速交付的要求,同时,也可以契合局部动态更新的需求,因此,组件化框架也需要支持H5和RN开发的组件。从抽象的角度组件化框架需要为这些组件提供一致的编程接口,使得一个组件在跟另一个组件有交互时,不需要了解对方是用原生开发的还是H5或者RN开发的。对于H5或者RN开发的组件,需要有JSBridge来提供原生能力的调用,这部分的设计需要保持一致性。由于今天 我们讨论的是整体的组件化架构,这个细节,先不在这里讨论。我们可以认为组件是按照相同的组件协议接入到组件框架中。而不同类型的组件(原生、H5、RN)在接入时,需要有对应的组件管理器来管理对应类型的组件,完成协议的功能。如下图所示:

从概念上看,每个不同的组件管理器,都是不同开发语言下对组件协议的封装,或者说是不同的组件协议的实现实例。因此,我们需要在全局实现一个协议管理器,来实现对所有不同的组件协议实例的注册管理和调度。

具体来说,协议管理器需要完成以下职责:1、上面的组件页面URI的协议部分,可以扩充其它类型的组件协议,例如:
rn://react-native-component-name/page
h5://h5-native-component-name/page
所有的页面跳转,由协议管理器根据具体的协议头派发给对应的组件管理器执行。2、事件响应的通道,事件通过协议管理器向几种不同的组件管理器转发,并获得响应。事件发送方不需要了解事件是由哪种类型的组件触发的。事件处理方也不需要关心事件的来源。通过协议管理器,可以将组件彻底解耦。
开发流程和工具支持
在这样的组件化的整体设计之下,必然需要调整整体的开发、测试、发布的流程和工具来与之配合。这里我也稍微展开介绍一下。3.1 组件的开发、测试、发布流程

这里我们有一些工具的支持:1、开发者门户:所有的开发团队在开发者门户上创建移动组件,会自动创建git仓库,进行代码管理。2、组件化CI/CD支持:对应的组件在特性开发完成后,可以在开发者门户上作线上提测、构建、发布的流程,最终组件的代码构建出Library进入我们的 maven/cocopods/npm 仓库。3、组件化功能测试和集成测试工具支持:开发团队所开发的组件,是无法独立运行的,必须要跟框架集成,跟其它组件打包在一起,形成一个产品。测试工具集成了相关的基础能力(包含iOS的开发者证书的支持),让组件开发只关注于组件本身的业务。4、国际化翻译的工具和流程支持,从上图可以看到,开发者只需要提供中文的语言资源,其它的语言资源会由翻译团队提供,并通过QA打包到App中验证后发布。5、还有其它组件化治理相关的工具,主要解决一些组件规范的问题,及早发现问题,避免跨团队的问题排查,如资源冲突的扫描,App上架规范的扫描等等。3.2 应用的配置、发布流程开发完成了组件,最终要组装成App,需要有一个装配到发布的流程,如下:

上面的流程,通过我们的应用工厂这样的可视化的配置和完整的流程工具来支持。1、应用的可视化配置:创建应用后,在应用的编辑介面上选择组件,配置应用级的全局参数,组件的参数,以及组件间的页面跳转关系,绑定事件响应关系,同时,需要创建一些聚合的页面,例如App首页。2、应用的线上构建、测试、发布流程:我们提供了在线编辑器,用于创建App,挑选组件,配置组件,最后作打包、测试、发布。其中,由于应用的测试和发布,并没有开发参与,每次的新特性需要小范围验证,因此,我们还支持了灰度发布的特性。也就是,一个应用的新版本,可以指定符合一定条件的用户先体验,作功能测试或者功能验证。灰度验证一段时间后,再决定是否发布这个版本。从上面的流程图,我们可以很清楚地看出,我们将开发、测试、产品的职责很好地分开了。开发人员只需要负责组年的设计、开发。组件QA负责组件的测试、发布。而产品方创建和发布产品,只需要产品方的配置和产品QA就可以了。从而可以从流程上,实现了职责分离,提高效率。从整体的效果上看,网龙的移动端开发团队在 Android和iOS都有超过100人的规模时,可以很好的分工协作,分别以比较小的团队开发业务组件,最后集成出功能复杂的产品。而产品方可以在不需要开发配合的情况下,就可以打出想要的App,有力地支持了网龙的业务模式。
组件化治理
大团队的组件化开发,除了可以获得各组件解耦后各自迭代,有利于大团队分工协作的好处外,也必然引入一些问题,需要建立规范和提供必要的工具,实现组件化的治理。4.1 包名/类名规范Android下避免类名冲突比较简单,只要不同的组件使用不同的包名即可。这个部分我们在创建项目时分配,可以确保了唯一性。而iOS下,要避免类名冲突,没有其它办法,就只能通过类名前缀了,这个需要做些约定。4.2 资源规范Android下,不同组件的资源,在App集成时会合并,此时同名的资源会冲突造成App构建不成功。为了避免不同组件的资源冲突,我们约定资源的名称以组件包名为前缀,这样就可以解决这个问题。iOS下,不同的组件创建自己的bundle即可作资源的隔离了。4.2 第三方依赖库规范在App开发过程中,不可避免地需要引入第三方库,成熟的第三方库对于现在的开发至关重要,让大家不去重复造轮子,集中精力于当前的业务。组件化后,第三方库的治理有两个目的:1、避免重复的引入同类的不同第三方库,这样会造成大量的代码冗余,造成应用包太大,引发一系列的问题。例如,Android的类方法超标、启动性能问题,甚至无法上架。2、需要协调第三方库的版本,第三方库的不同版本,不一定是兼容的,不同的组件如果依赖同一个第三方库的不版本,在产品集成时,按默认规则会引入高版本,而高版本如果存在不兼容,在一些组件下可能会引起崩溃。因此网龙的移动端第三方库的引入,使用了白名单的机制,白名单以外的第三方库不被允许,并且直接在产品构建时失败,无法出包。在白名单机制下,引入新的第三方库,需要引入团队提交报告,说明引入的目的和第三方库选型的依据。特别要说明这个新的第三方库与其它同类库相比的优势在哪里,因为第三方库的排它性,原则上,将来会拒绝同类第三方库的引入,这一步需要比较慎重。同时,第三方库的版本变更,也需要所有依赖的组件协同,共同验证后才能升级。本文介绍了网龙在移动端组件化开发方面整体的技术方案和思路,整个方案涉及到的流程比较长,面也比较广,很多方面只是点到为止,希望可以给大家带来一些启发。其中涉及到的一些课题或者技术细节,在以后的分享中有机会再展开。(作者: 资深软件开发工程师 王杰光)