前言
上一篇中我们对组件化的准备工作做了介绍,这篇文章我们以SXNews为例进行组件化,Demo地址在这里,壳工程获取脚本在这里,希望本文能给你带来帮助。
一、修改配置
根据上一篇文章所述,你应该已经有了ModularizationDemo文件夹,此时该文件夹中只有config和OldProject两个子文件夹。这时我们应该针对项目情况,修改config内容。
这里是SXNews的Podfile

根据这个文件,我们可以得知该项目主要使用了RAC等框架开发,因此为了方便起见,我们需要修改一下我们的config:
- 通过终端进入config文件夹内,后面的路径是你的文件夹路径
cd ~/ModularizationDemo/config
- 修改config/templates/Podfile,在
source 'https://github.com/CocoaPods/Specs.git'后添加我们的私有pod源source 'git@github.com:ModularizationDemo/PodSpec.git':

- 复制config.sh文件为config_category.sh,复制templates文件夹为templates_category
cp config.sh config_category.sh
cp -r templates/ templates_category/
- 修改
config_category.sh文件,这里我使用的是vi,用:进入命令模式,输入以下代码,将所有的templates改为templates_category
%s#templates#templates_category#g
- 进入templates文件夹,修改里面的Podfile这个文件,由于该项目的所有模块都适用了RAC、AFN、MJExtension、SDWebImage,因此我们添加给该模版添加这些方便后面处理(其中为了后面网络请求代码方便,我使用了自己的HLNetworking,详细的使用方法可以在该框架的主页查阅):
pod 'ReactiveCocoa','2.5'
pod 'HLNetworking', '~> 2.0.0.beta'
pod 'MJExtension', '~> 2.0'
pod 'SDWebImage','~> 3.7'

- 进入templates_category文件夹,同样修改里面的Podfile这个文件,category类工程后面的作用主要是做中间件,因此我们添加给该模版添加中间件框架Lothar,详细的使用方法可以在该框架的主页查阅):
pod 'Lothar', '~> 1.0.5'
至此,配置文件已经修改完成,这里有我写好的配置文件,可以根据需求更改里面的参数。
二、创建组件模块
SXNews这个项目结构很简单,分为Search、Detail、PhotoSet、Weather、News、Reply、Main这7个业务组件和Tools、Other这两个公共文件夹。根据前两篇文章的内容,我们在这一节先将7个业务组件拆分出来。
先根据划分好的业务组件建立组件仓库:
- 首先在git上创建需要组件化的组件仓库,例如
Search,该仓库为空仓库即可,并记录仓库的https地址、ssh地址和项目主页地址,这里我们以github的例子为例:
HTTPS: https://github.com/ModularizationDemo/Search.git
SSH: git@github.com:ModularizationDemo/Search.git
HomePage: https://github.com/ModularizationDemo/Search
- 然后在终端中进入文件夹
config,然后执行config.sh文件脚本(组件使用config.sh脚本,组件的action使用config_category.sh脚本)
cd ~/ModularizationDemo/config
./config.sh
- 此时终端会显示提示信息,根据提示信息输入
作者,项目名,组织名,仓库HTTPS URL,仓库SSH URL,主页 URL,这些信息我们在上一步中就已经获得了,逐个填入即可:

- 完成后我们会发现
ModularizationDemo目录下已经多了一个Search目录,其目录大概如下:

- 接着我们打开项目中的xcodeproj文件,将原项目中
Search相关的部分拖入新项目的Search文件夹内,记得选上Copy items if needed

- 完成后的Search项目目录应该如下:

- 接着我们尝试编译一下,发现出现了一些警告,从警告中得知,这里主要是缺少部分公共代码(
UILabel+Wonderful.h、NSString+Base64.h)以及RAC相关的依赖:

- 编辑模版为我们生成好的
Podfile,根据错误提示添加缺少的框架,然后pod install完成cocoapods配置 - 打开
Search.xcworkspace,尝试编译,发现还是缺少RAC,查看原项目发现,原项目中使用了pch引入公共库的头文件,因此依次在需要引入RAC的类中逐个添加#import <ReactiveCocoa/ReactiveCocoa.h>即可 - 再次编译,发现缺少
UILabel+Wonderful.h、NSString+Base64.h、SXDetailPage.h,对于这些项目内的依赖我们暂且不管,重复以上步骤拆分其他组件 - 到这里所有的组件都应该拆分好了,但是这样的组件由于相互依赖,是无法独立运行的,接下来我们就通过
Lothar这个中间件去除组件依赖

三、去除组件依赖
1. 提取公共代码
本系列文章的前两篇也说过,在项目开发中,难免会产生形如Common或者Tools这样的公共代码,在组件化中,应当将这些代码细分为各种二方库;在本例中,由于这一块代码量很少,因此直接将这部分所有代码生成一个私有pod,作为基础库供于其他组件使用。
- 首先依旧使用config生成项目模版,并将Tools相关代码放入项目中,步骤与拆分组件类似
- 编辑Podfile文件,只引入
HLNetworking这个库,并pod install:

- 这里原项目使用的是自己编写的对AFN的简单封装,我这里直接将其改为依赖于
HLNetworking



- 编辑Tools.podspec文件,增加依赖
s.dependency "HLNetworking":

- 提交并上传代码,打tag,验证pod是否可用,最后上传至私有pod
git add .
git commit -m "Tools 1 init"
git push
git tag 1
pod lib lint // 如果这一步通过了就上传tag
git push --tags
pod repo push myspec --allow-warnings // myspec是上一篇文章中准备好的私有pod仓库的名字,--allow-warnings是忽略警告,pod提供了很多参数,具体请查阅cocoapods.org
如果一切正常,完成的结果在终端显示如下:

这样我们的公共代码就提取好了。
2. 解除组件横向依赖
接下来我们以Search模块为例,介绍其解除于其他模块耦合的方法,限于篇幅,Lothar相关的方法就不写说明了,具体请查阅Lothar的项目介绍及注释
第一步 创建组件的服务接口
首先将
SXSearchViewModel中对AFN的依赖改为HLNetworking的调用,然后根据缺失的依赖添加基础库的import如果一切顺利,我们会发现
SXSearchPage.m中引入了Detail模块的SXDetailPage.h(顶部有#import "SXDetailPage.h"),我们先暂时将其注释掉然后在旧工程中删掉
Search文件夹,编译,发现很多地方提示没有SXSearchPage,查看错误代码,发现Search相关依赖主要是需要生成SXSearchPage控制器-
根据该需求,我们创建
Search模块的Lothar扩展,提供此服务:- 在git中创建
Search-Category项目 - 终端进入config文件夹,输入
./config_category.sh配置项目模版,项目名为Search-Category - 终端进入Detail-Category文件夹,输入
pod install完成Lothar的安装,完成后打开Search-Category.xcworkspace - 在
Search-Category文件夹中创建Lothar的category
- 由于
Search需求的无非是跳转页面并将keyword传值过去,因此我们在Lothar+Search中写一个方法并实现它:
- 在git中创建
- (nullable UIViewController *)Search_aViewControllerWithKeyword:(nullable NSString *)keyword;
- (nullable UIViewController *)Search_aViewControllerWithKeyword:(nullable NSString *)keyword {
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
if (keyword) {
dict[kSearchKeyword] = keyword;
}
return [self performTarget:@"Search" action:@"aViewController" params:[dict copy] shouldCacheTarget:YES];
}
}
其中target字符串是提供服务的组件名,action的字符串是
Search中的Target的方法名去掉:,@"keyword"则是参数解包用的key,这里的两个字符串即前两篇说的硬编码编译一下,没什么问题,这个服务的接口就OK了
-
接着在旧工程中的Podfile写入
pod "Search-Category", :path => "../Search-Category",执行pod install,提示我们target所支持的development版本不对,我们暂时先将Podfile的platform :ios, '7.0'改为platform :ios, '8.0',再次执行pod install 完成后在
AppDelegate及SXDetailPage中修改错误警告:
#import <Search-Category/Lothar+Search.h>
UIViewController *viewController = [[Lothar shared] Search_aViewControllerWithKeyword:sender.titleLabel.text];
[self.navigationController pushViewController:viewController animated:YES];
- 此时旧工程就应该能正常编译了,但是
SXSearchPage相关的代码会没有效果,接下来我们实现Search模块的服务
第二步 实现组件服务
接着我们在Search模块中支持这个服务:
- 打开Search的workspace,创建
Target_Search类,创建并实现接口方法:
- (UIViewController *)Action_aViewController:(NSDictionary *)params;
- (UIViewController *)Action_aViewController:(NSDictionary *)params {
NSString *keyword = params[@"keyword"];
UIStoryboard *sb = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
SXSearchPage *sp = [sb instantiateViewControllerWithIdentifier:@"SXSearchPage"];
sp.keyword = keyword;
return sp;
}
- 我们发现该控制器是从旧工程一个公共的
Main.storyboard中创建出来的,为了明确控制器归属,我们将这个storyboard拆分:- 在Search中创建一个叫做
SXSearchPage的storyboard - 找到
Main这个storyboard,将SXSearchPage剪切,复制到Search中SXSearchPage.storyboard内 - 将
SXSearchPage.storyboard中SXSearchPage控制器设置为is Inital View Controller - 修改
Target_Search的实现为
- 在Search中创建一个叫做
- (UIViewController *)Action_aViewController:(NSDictionary *)params {
NSString *keyword = params[@"keyword"];
UIStoryboard *sb = [UIStoryboard storyboardWithName:@"SXSearchPage" bundle:nil];
SXSearchPage *sp = sb.instantiateInitialViewController;
sp.keyword = keyword;
return sp;
}
- 然后尝试编译,发现
SXSearchPage.m中与SXDetailPage相关代码编译不通过,接着回到旧工程查看相关代码,该部分属于Detail模块的范围内,因此我们创建一个Detail-Category,创建方式跟Search-Category相同, -
Detail-Category中创建以下方法提供服务接口:
// Lothar+Detail.h
- (nullable UIViewController *)Detail_aViewControllerWithDocid:(nonnull NSString *)docid;
// Lothar+Detail.m
- (UIViewController *)Detail_aViewControllerWithDocid:(NSString *)docid {
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
if (docid) {
dict[@"docid"] = docid;
}
return [self performTarget:@"Detail" action:@"aViewController" params:[dict copy] shouldCacheTarget:YES];
}
- 在
Search的Podfile中加入pod 'Detail-Category', :path => '../Detail-Category',以使用Detail的服务,将出错代码修改为
UIViewController *viewController = [[Lothar shared] Detail_aViewControllerWithDocid:[self.searchListArray[indexPath.row] docid]];
[self.navigationController pushViewController:viewController animated:YES];
- 此时
Search模块应该编译通过了,但此时Detail-Category的服务接口并未实现,且Detail-Category尚未从旧工程中拆分出来,接下来我们先暂时在主工程中实现该服务
第三步 旧工程中实现组件服务
- 关闭所有的xcode窗口,找到
Detail文件夹,创建Target子文件夹 - 创建
Target_Detail类,实现Detail-Category提供的接口
// Target_Detail.h
- (UIViewController *)Action_aViewController:(NSDictionary *)params;
// Target_Detail.m
- (UIViewController *)Action_aViewController:(NSDictionary *)params {
SXNewsEntity *model = [[SXNewsEntity alloc]init];
model.docid = params[@"docid"];
UIStoryboard *sb = [UIStoryboard storyboardWithName:@"News" bundle:nil];
SXDetailPage *devc = (SXDetailPage *)[sb instantiateViewControllerWithIdentifier:@"SXDetailPage"];
devc.newsModel = model;
return devc;
}
- 补全因从
Main.storyboard和News.storyboard拆分出来而失效的push操作 - 在旧工程的Podfile中加入以下仓库,用于统一编译,添加完后执行
pod install
pod 'Tools', '1'
pod 'Search-Category', :path => '../Search-Category'
pod 'Search', :path => '../Search'
pod 'Detail-Category', :path => '../Detail-Category'
- 最后将
Search模块中使用的图片从主工程中放入Search的Assets.xcassets里,Search模块直接编译运行,检查UI - 此时
Search模块的拆分就全部完成了,其他模块同理,按照Search模块逐步拆分即可,最后旧工程应该只剩下全局配置代码、AppDetegate和main了
3.提交并上传仓库
提示:如果出现
pod search找不到私有仓库的情况,可以先使用rm ~/Library/Caches/CocoaPods/search_index.json命令清除pods的索引再搜索。
当所有组件都拆分完毕并调试无误之后,就可以给组件打tag并提交版本了,这里我们还是以Search为例,具体方法如下:
- 首先先编辑
Search.podspec,s.version为版本号,s.dependency为依赖的库,这里主要改这两个,有的组件会依赖一些动态库或者静态库,文件模版里有示例,如果还是无法通过校验,请参照cocoapods.org - 添加好依赖之后,终端进入
Search,输入pod lib lint --allow-warnings --sources=myspec,master进行校验 - 如果校验通过,输入
git add .添加所有文件,项目模版配置时已包含.gitignore因此不会添加Pods相关文件 - 输入
git commit -m "提交信息",完成提交 - 输入
git tag 1给当前的commit打上tag - 输入
git push和git push --tags,push代码和tag - 输入
pod repo push myspec --allow-warnings --sources=myspec,master,将podspec文件上传至私有podspec仓库 - 最后在主工程里将
Podfile中的pod 'Search', :path => '../Search'改为pod 'Search', '~> 1,执行pod install完成所有操作
看完这篇文章,你应该已经基本完成项目的组件化,下一篇将阐述如何优化组件化后的代码以及相应的一些规范。


