起源
截至目前组件化在 iOS 也已经有了几年的讨论和应用了,笔者从去年开始公司项目也开始慢慢有意识的往组件化靠拢。因不可能组件化而停止业务的开发所以前期准备是在业务开发的同时有意识的封装和抽取整理一些独立于业务的类等。在接下来的几篇文章会大家分享笔者在组件化过程中的想法和遇到的问题,欢迎大家留言讨论。
0x0 前期准备
当你想要进行组件化开发的时候,第一步是考虑将公司的项目进行一个大的拆解,了解到自己公司项目的业务模块,做到拆分时心中有数。而很少有公司直接停滞业务的开发而将人员全量的投入到组件化中,所以我们可以在业务开发的同时就将组件化的需求考虑进去,例如将无关业务的代码单独剥离,
组件化的分层主要是分为三层(抛开三方库),笔者也是采取了此种结构。
在顶层是业务层,这块和公司业务相关每个公司业务层分出来的业务模块都不是完全一样的。这个自行结合公司业务拆分。
中间层是弱业务层(基础业务层),在这层和公司的主业务的关联性应该是弱的,弱业务也就是说和业务的关联性低,但是顶层直接依赖的下层。
底层是与业务完全无关的代码,主要是对三方库的封装及开发过程中的工具的封装。这就意味这套代码不会依赖上层的任何业务,是独立运行的,我们可以对它进行迁移到任意项目中直接使用。这层其实是生命力最强的一层,可以在开发周期中不断的完善,但也是牵一发而动全身的一层,所以我们需要在拆解的时候就要尽可能考虑到上层如何能达到最佳使用(少埋点坑)。
以上就是大概的一个项目的分层情况,各个公司业务不尽相同,但是思路都是大同小异的。
在组件化的前期的过程是很痛苦的,因为项目历史原因,可能会碰到你要抽取项目中有很多底层业务中依赖业务的代码。但是当完成了组件化之后你会感觉整个项目划分的很明确,各层之间依赖关系明确,这对以后的开发有这很好的正面导向。
下面给出一些组件化前期准备的建议:
了解cocoapods的私有库建立及podspec如何编写(可参考cocoapods官网)。
了解组件化中不同中间件的优缺点,技术选型。
开始规避使用 pch 文件。
有意识的规整不同业务的代码。抽离相对独立的下层代码。
下层组件中参数是图片的话,可以考虑直接传递 图片 而非 imageName, 参考我之前写过一个 UIButton 的 Category 方法快速创建按钮,未组件化时代码如下:
+ (UIButton *)buttonWithFrame:(CGRect)frame normalImageName:(NSString *)imageName normalTitle:(NSString *)normalTitle normalTitleColor:(UIColor *) normalTitleColor font:(UIFont *)font;
而 imageName:
方法会从 mainBundle
中寻找图片,但是当我们组件化后我们的资源会放在不同的 bundle 中所以如果直接使用 imageName 去加载图片的话很大可能是找不到图片。
0x1 cocoapods的私有库
有关如何使用cocoapods建立私有库并上传到自己的git仓库,网上有很多教程。读者可以自行 baidu 或 google 到,一般按照教程建立完一个私有库对整个过程都会有所了解。重复几遍基本上都没有问题。也许在验证spec pod spec lint
过程中后碰到许许多多的问题,但大都可以通过搜索引擎解决。个人建议可以将每个私有库的 pod spec lint ...
和 pod repo push
命令行代码粘贴复制一份放在备忘录中因为之后很长一段时间会和它们打交道。ps: 组件的过程一般都是自底向上的进行的,所以我们可以先从底层开始,同时这个也不会影响到业务的开发。
这里给出制作cocoapods的私有库主要的流程:
-
pod lib create <组件名>
创建本地代码组件模版库 - 将代码放入到
classes
文件夹内,资源类文件(图片、Xib)放入Assets
文件夹内。(注:你也可以使用自定义文件夹,但需要在 spec 中指定。推荐使用默认文件夹)。 -
cd Example
下 执行pod install
, 在编译项目是否通过,如未通过再做修改 - 编译通过后修改
podspec
文件 - 编译运行通过后,提交代码到远程代码库并打tag.
(可以通过以下命令行或者直接使用类似 sourceTree 的GUI来实现)
- git add .
- git commit -m “xxx"
- git remote add origin 远程代码仓库地址
- git push origin master
- git tag 版本号 (注:这里的版本号必须和podspec里写的版本号一致)
- git push --tags
- 通过pod spec lint --verbose --allow-warnings 命令验证podspec索引文件
- 验证通过后,pod repo push <本地索引库> <索引文件名.podspec> --verbose --allow-warnings 提交索引文件到远程索引
0x2 项目运行后的问题解决
一般来说在你制作某个组件的时候,编译过程会有很多依赖的报错,因为可能在老的工程中因为使用 pch 文件,很多类都被隐式的引入。而当我们剥离原来的代码,然后就要针对每个编译错误导入不同的头文件,如果是同一个组件/模块时用 #import "xx"
,如果是依赖其它的模块则使用 #import <xx/xx.h>
的方式引入。当我们都编译通过后,可能还会有其它的坑需要我们填,这里给出一些笔者碰到问题的解决方法。如果podspec中使用的是 s.resource_bundles 就会遇到下面几个问题
- ViewController是xib的时候,如果直接通过 [[xx alloc] init]创建的话,出来的ViewController是空白不会显示任何内容,解决方法时重写 init 方法
- (instancetype)init {
//在这个路径下找到子bundle的路径 (xxx指的是在podspec文件中的s.name)
NSBundle *bundle = [NSBundle subBundleWithBundleName:@"xxx" targetClass:[self class]];
self = [super initWithNibName:NSStringFromClass([self class]) bundle:bundle];
if (self != nil) {
// init method
}
return self;
}
// 此处是笔者的 NSBundle 的 Category
+ (instancetype)subBundleWithBundleName:(NSString *)bundleName targetClass:(Class)targetClass{
//并没有拿到子bundle
NSBundle *bundle = [NSBundle bundleForClass:targetClass];
//在这个路径下找到子bundle的路径
NSString *path = [bundle pathForResource:bundleName ofType:@"bundle"];
//根据路径拿到子bundle
return path?[NSBundle bundleWithPath:path]:[NSBundle mainBundle];
[bundle pathForResource:@"BXSocialModule" ofType:@"bundle"];
[bundle pathsForResourcesOfType:@"bundle" inDirectory:bundle.bundlePath];
}
- 代码中加载图片,前文提过
imageName:
方法会从mainBundle
中寻找图片,所以需要我们指定bundle去加载模块内的图片。
UIImage *image = [UIImage bundle_imagePathWithName:@"about_phone_icon" bundle:@"BXUserCenterModule" targetClass:self.class];
/// 笔者项目中 UIImage 的 Category
+ (instancetype)bundle_imagePathWithName:(NSString *)imageName bundle:(NSString *)bundle targetClass:(Class)targetClass {
NSBundle *currentBundle = [NSBundle bundleForClass:targetClass];
NSString *bundlePath = [currentBundle pathForResource:bundle ofType:@"bundle"];
return [UIImage imageNamed:imageName inBundle:[NSBundle bundleWithPath:bundlePath] compatibleWithTraitCollection:nil];
}
- 加载 Xib 的 UIView 或者 Cell,同理我们需要从每个模块对应的bundle中加载。
XX *view = [XX loadCustomNibFromBundle:@"bundleName"];
/// UIView 的 Category方法,
+ (instancetype) loadCustomNibFromBundle:(NSString *)bundleName {
NSBundle *bundle = [NSBundle subBundleWithBundleName:bundleName targetClass:[self class]];
return [[bundle loadNibNamed:NSStringFromClass([self class]) owner:self options:nil] firstObject];
}
/// TableViewCell
NSBundle *bundle = [NSBundle subBundleWithBundleName:@"XX" targetClass:self.class];
[_tableView registerNib:[UINib nibWithNibName:@"cellName" bundle: bundle] forCellReuseIdentifier:kBlockMachineDetailCellIdentifier];
0x3 组件完成后的开发流程
随着一个个组件的拆分,我们的整体复用度有明显提升,但开发效率却意外的受到了影响。多库开发在版本的发布与集成中增加了很多人工操作:依赖冲突、lock文件冲突等问题都阻碍了我们的开发效率进一步提升,而这就是之前“关于组件化”中提到的副作用。
笔者原来项目开发流程采用的是 gitFlow 的开发流程,所以组件化后的开发流程也将会继续采用 gitFlow。关于 gitFlow 讨论可以参考 阮一峰老师的Git 工作流程。
目前开发流程中会在组件化后项目的壳工程中的 podfile 文件中将项目组件所有的依赖项目指定 tag 版本。当开发需求过来之后,针对需求属于哪个业务模块创建对应的业务 Feature 分支。而在壳工程的 podfile
中改变业务模块依赖本地路径。等开发测试通过后业务模块打 tag。壳工程在改动 podfile
依赖tag。
以上都是手动打包发布的流程。笔者之前项目使用的是 Jenkins + FastLane 自动打测试包,但组件化完成后脚本的编写可能更加复杂所以目前还停留在手动上。其实组件化也是倒逼 自动发版与自动集成 的演进。这块可能会在以后继续完善。