移动组件化开发平台

2/21/2019 iOS

这篇文章之前在公司技术公众号发表过,今天转移到自己的blog来。原地址:https://mp.weixin.qq.com/s/npkoQtdnmx6JXymHNil_rw (opens new window)

随着业务的扩张,RN等新技术的引入,App进行着高速的版本迭代。技术面对业务的变更越来越频繁,App体量越来越大,功能也越来越复杂.... 开发人员面临的几大痛点:业务相互依赖过于紧密、多人同时开发、第三方依赖升级困难、手动打包。为了解决这些问题,从2017年开始,我们对应用整体的架构进行了重新的设计、对开发模式进行了非常重要的改变,开始进入组件化时代。开发方式从多人集中开发转变为松耦合的组件化开发。组件化方案解决了页面跳转的耦合、业务相互依赖的耦合。实现了组件的独立开发、独立发布。并实现App自动化的构建和发布。

# 1. 蜂巢架构

随着组件化的持续进行,也有新的问题暴露了出来,比如:

  • 组件的创建不够规范
  • 组件的发布需要开发人员手动输入命令
  • 组件数量越来越多,需要集中管理
  • 壳工程依赖文件需要频繁维护
  • App健康缺少有效的监控

为了解决暴露出来的新问题,提高开发人员的开发效率,蜂巢被提了出来,移动应用的架构也随之得到了演进。

在形成组件化之后,App由一个个组件组合而成,形成高度模块化的工程结构,整个App就像一个蜂巢,而蜂巢的结构带来了无限扩张的可能,所以我们使用蜂巢来命名这个项目。

蜂巢架构图2

# 2. 关于蜂巢

蜂巢整体由以下几部分组成:

  • 模板:组件模板基于CocoaPods的pod-template进行二次开发,用于创建App的组件。
  • 脚本工具:基于fastlane创建通用的Fastfile和Action,用于组件的发布、依赖的安装、App的构建、App的发布等
  • 可视化管理平台:管理平台采用Vue+Element UI+Spring Boot的前后端开发框架。包括应用管理、组件管理、构建管理、发布管理、性能监控、配置中心等。
  • 移动API:提供给App调用的API,主要是配置中心和APM的一些接口
  • 其他系统:包括实时数据CAAS系统、Jenkins集群、GitLab、ipapk发布系统、Apollo等

蜂巢架构

本文重点从以下几个方面介绍在开发使用蜂巢时的一些技术点。

# 2.1 组件创建

最初设想开发一个PC端的工具,开发人员在本地进行创建组件,考虑到Android和iOS的平台差别以及开发人员使用的操作系统不同,最终还是决定在平台上实现组件的创建。

首先,iOS和Android都有自己的组件模板,其中iOS使用CocoaPods的pod-template做二次开发,Android使用自己开发的组件模板。 其次,创建过程要先从模板所在仓库拉去模板,然后执行模板中的脚本,然后为组件在GitLab上创建仓库,并将组件推送到仓库中。

组件创建时序图

其中关键的两点是:

  1. GitLab在内网系统,蜂巢想要访问GitLab又要对App提供网关接口,这就要求平台必须部署在公共测试环境,而网关接口部分就要部署在生产环境。在多系统之间调用上出现了很多问题,踩了不少坑。
  2. iOS模板需要Ruby环境,需要在Linux上安装CocoPods,而且CocoPods执行权限不能为root用户权限,所以Linux上执行初始化脚本的时候需要先切换到指定的普通用户,再执行初始化脚本。

# 2.2 组件发布

组件化之后,所有的业务都被拆分到各个组件中,而且做到组件独立开发、独立编译、独立发布。目前蜂巢已经实现线上自助发布组件的功能,而且在发布之前会对所有代码做静态分析,从而保证代码质量。

iOS:可以发布源码、也可以发布静态Library、动态Framework Android:可以发布SNAPSHOT,也可以发布Release。

在组件发布方面,iOS也是遇到了很大的挑战。在使用蜂巢之前,组件的编译、发布都是使用Fastlane和自定义的Action在每个开发者本地进行的,如果在平台上做,还要考虑大家多任务并行的情况。这就需要有任务队列。这时候我们想到App的编译、发布也是用的Fastlane和自定义的Action,而且是使用Jenkins Job执行编译打包脚本。那我们的组件的发布也可以在Jenkins上来做,Jenkins来执行发布脚本,而且Jenkins本身就有队列管理,我们只需要建立一个Job池,当组件发布时,从Job池中选择空闲的Job来执行发布任务,而且Job池也可以随着业务的发展做快速的扩充。

组件发布时序图

组件的发布借鉴了App的发布,并创建了Job池,而发布过程中关键点还是之前就已经准备好的Fastlane脚本,我们支持了发布源码、发布静态Library,发布Framework。未来还会增加React Native组件的发布。

在CocoPods 1.5版本之前,我们发布的组件均为源码或者Framework,值得注意的事,如果代码是swift,swiftmodule也要copy到Framework中。

simulator_swift_module = "#{simulator_framework}/Modules/#{build_target}.swiftmodule/."
if File.exist?(simulator_swift_module)
 copy_swift_module_command = "cp -R #{simulator_swift_module} #{target_framework}/Modules/#{build_target}.swiftmodule"
 Actions.sh(copy_swift_module_command)
end
1
2
3
4
5

在架构演进的过程中,我们的组件二进制化从Framework转为静态库,有了这些支持,所有组件的转换用了很少的时间就可以转换完成。

此外,在组件发布过程中,我们还引入了Facebook的infer,来做静态检查,可以检测资源泄露、内存泄露、循环引用、空引用、参数不能为空检查,本地变量不能为空检查。

# 2.3 App依赖管理

在组件化的壳工程或者传统的项目中,Podfile文件中为target设置了一条一条的依赖,并经常被反复修改。在拍拍贷理财App中有100多个组件,为了避免这种现象的出现,我们通过PPDBIU脚本请求依赖列表,并进行依赖的安装。

现在的做法只需要两行代码,再也不需要频繁修改依赖文件了:

require 'ppdbiu'

makeup_pods('http://xxxxxxx.xxx/dep/list', {'applicationVersionId' => '4', 'pageSize' => '99999'}, 'POST')
1
2
3

而依赖的组件以及组件的版本修改只需要在平台上进行更改即可。

依赖列表1

在App构建的过程中,makeup_pods会根据参数去请求依赖列表,然后将依赖进行安装,从而达到和以前一样的效果。

核心处理如下:

def ppd_pod(dependencies, ignores)
      dependencies.each { |value|
        componentName = value.fetch("dependencyName", nil)
        # 忽略组件名称不存的依赖
        return unless componentName
        # 跳过在ignores列表中的依赖
        next if ignores.include? componentName

        version = value.fetch("componentVersion", nil)
        if version
          pod(componentName, version)
        else
          hash = {}
          value.each{ |rKey, rValue|
            hash[:git] = rValue if rKey == "gitUrl"
            hash[:branch] = rValue if rKey == "componentBranch"
            hash[:tag] = rValue if rKey == "tag"
            hash[:commit] = rValue if rKey == "commit"
            hash[:configuration] = rValue if rKey == "configuration"
            hash[:path] = rValue if rKey == "path"
            hash[:podspec] = rValue if rKey == "podspec"
            hash[:subspecs] = rValue if rKey == "subspecs"
          }
          pod(componentName, hash)
        end
      }
    end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# 2.4 应用发布

应用的发布在iOS和Android直接差异还是蛮大的。

首先App构建完成后,我们会将构建产物通过ssh上传到指定服务器目录,当发布的时候,通过接口调用服务器目录下的发布脚本,将应用进行发布到内测平台、TestFlight、阿里云及共享目录。

对于内测发布,Android和iOS均发布到ipapk发布系统,平台提供二维码,测试人员在平台上可以直接扫描二维码进行下载,而且Android还要支持多渠道发布。

对于生产发布,iOS通过脚本发布到TestFlight,Android首先发布到阿里云,再在阿里云上执行生成渠道包的脚本。为了方便运营人员上传渠道包到各个商店,蜂巢提供两种方式供运营人员下载渠道包:

  1. 生成渠道包后,平台生成渠道包下载链接,可以在蜂巢上直接下载。
  2. 渠道包拷贝到指定共享目录,可以在共享目录拷贝渠道包。

应用发布流程

在应用发布的实践过程中也踩了许多的坑:

  • 应用发布脚本为Fastlane,此Ruby库需要的环境只能在Mac上执行,我们就必须将构建产物在Mac上备份,而且还要部署一个站点提供API供蜂巢调用来发布。
  • 阿里云发布需要ssh上传并执行脚本,ssh的权限以及目录需要动态配置。
  • Fastlane发布到TestFlight需要二次校验,session过期等。

# 2.5 配置中心

配置中心提供对线上App功能的动态配置,配置内容的数据结构兼顾统一性和个性化。

  • 配置中心的数据采用Key-List-Object三层数据结构。也就是说配置数据整体是一个Map,Map中每一项都是一个List,而List中的项则不规定数据结构,可以自由发挥。
  • 为了支持多版本的需求,还需要设计平台-应用-版本-模块的元信息模型。

元信息模型和数据模型结合起来组成了完整的配置中心数据模型。

另外配置内容还支持导入、导出以及多版本同步修改功能,从而简化配置操作。

配置中心1

同样的,配置中心在App端也提供一个组件来获取配置内容且支持增量更新,同时提供友好的API给业务组件来调用获取配置内容,开发人员不再关注配置是如何下发的,如何保存的,只需要关注自己配置的key和value。比如获取WebView组件的URL缓存策略:

NSString *configItem = [ConfigCenter configItem:@"cacheRegex" ofComponent:@"LenderWeb"];
NSArray *cacheRegexArray = [configItem objectFromJSONString];
1
2

# 总结

通过集成App整个开发周期的管理,App的开发迭代和测试分发效率得到了一定程度的提升,相对于之前有了明显的改变:

  • App依赖组件得到可视化的配置,依赖文件不需要频繁修改,减少了出错的概率
  • 组件的创建更加规范,不同类型的组件都统一了前缀,并根据模板自动创建工程和仓库
  • 组件的发布不再需要开发人员输入命令,完全做到自动化发布
  • App的构建和发布统一在平台上完成,不再需要切换到Jenkins上去手动执行

虽然蜂巢在开发阶段解决了开发人员的痛点,但是系统在还存在一些不足,比如远程日志、Memory Dump和Method Trace等问题,我们此前开发过一版APM的组件和平台对应的功能,包括Crash、Abort、卡顿、网络等,但目前还不够完善和强大,我们小伙伴也正在努力完善这块的功能,后续在使用中逐步将平台做的更好。

Last Updated: 1/15/2023, 2:48:14 PM