iOS组件化实践(三):实施

前言

上一篇中我们对组件化的准备工作做了介绍,这篇文章我们以SXNews为例进行组件化,Demo地址在这里,壳工程获取脚本在这里,希望本文能给你带来帮助。

一、修改配置

根据上一篇文章所述,你应该已经有了ModularizationDemo文件夹,此时该文件夹中只有configOldProject两个子文件夹。这时我们应该针对项目情况,修改config内容。

这里是SXNewsPodfile

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这个项目结构很简单,分为SearchDetailPhotoSetWeatherNewsReplyMain这7个业务组件和ToolsOther这两个公共文件夹。根据前两篇文章的内容,我们在这一节先将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.hNSString+Base64.h)以及RAC相关的依赖:
  • 编辑模版为我们生成好的Podfile,根据错误提示添加缺少的框架,然后pod install完成cocoapods配置
  • 打开Search.xcworkspace,尝试编译,发现还是缺少RAC,查看原项目发现,原项目中使用了pch引入公共库的头文件,因此依次在需要引入RAC的类中逐个添加#import <ReactiveCocoa/ReactiveCocoa.h>即可
  • 再次编译,发现缺少UILabel+Wonderful.hNSString+Base64.hSXDetailPage.h,对于这些项目内的依赖我们暂且不管,重复以上步骤拆分其他组件
  • 到这里所有的组件都应该拆分好了,但是这样的组件由于相互依赖,是无法独立运行的,接下来我们就通过Lothar这个中间件去除组件依赖
拆分好之后的项目目录

三、去除组件依赖

1. 提取公共代码

本系列文章的前两篇也说过,在项目开发中,难免会产生形如Common或者Tools这样的公共代码,在组件化中,应当将这些代码细分为各种二方库;在本例中,由于这一块代码量很少,因此直接将这部分所有代码生成一个私有pod,作为基础库供于其他组件使用。

  • 首先依旧使用config生成项目模版,并将Tools相关代码放入项目中,步骤与拆分组件类似
  • 编辑Podfile文件,只引入HLNetworking这个库,并pod install
  • 这里原项目使用的是自己编写的对AFN的简单封装,我这里直接将其改为依赖于HLNetworking

SXAdManager修改前

SXAdManager修改后
  • 编辑Tools.podspec文件,增加依赖s.dependency "HLNetworking"
编辑Tools.podspec文件
  • 提交并上传代码,打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中写一个方法并实现它:
- (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

  • 完成后在AppDelegateSXDetailPage中修改错误警告:


#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.storyboardSXSearchPage控制器设置为is Inital View Controller
    • 修改Target_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.storyboardNews.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模块中使用的图片从主工程中放入SearchAssets.xcassets里,Search模块直接编译运行,检查UI
  • 此时Search模块的拆分就全部完成了,其他模块同理,按照Search模块逐步拆分即可,最后旧工程应该只剩下全局配置代码、AppDetegatemain

3.提交并上传仓库

提示:如果出现pod search找不到私有仓库的情况,可以先使用rm ~/Library/Caches/CocoaPods/search_index.json命令清除pods的索引再搜索。

当所有组件都拆分完毕并调试无误之后,就可以给组件打tag并提交版本了,这里我们还是以Search为例,具体方法如下:

  • 首先先编辑Search.podspecs.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 pushgit 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完成所有操作

看完这篇文章,你应该已经基本完成项目的组件化,下一篇将阐述如何优化组件化后的代码以及相应的一些规范。

参考文章

念纪-模块化与解耦

limboy-蘑菇街组件化之路

limboy-蘑菇街组件化之路-续

Casa Taloyum-iOS应用架构谈-组件化方案

Skyline75489-浅析iOS应用组件化设计

philon-iOS组件化思路-大神博客研读和思考

philon-iOS组件化实践方案-LDBusMediator练就

bang-iOS组件化方案探索

携程移动端架构演进与优化之路

iOS-组件化架构漫谈

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,874评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,151评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,270评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,137评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,116评论 5 370
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,935评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,261评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,895评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,342评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,854评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,978评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,609评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,181评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,182评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,402评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,376评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,677评论 2 344

推荐阅读更多精彩内容