前言
上一篇中我们对组件化的准备工作做了介绍,这篇文章我们以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
完成所有操作
看完这篇文章,你应该已经基本完成项目的组件化,下一篇将阐述如何优化组件化后的代码以及相应的一些规范。