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

alt

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

alt

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

alt

  • 编辑模版为我们生成好的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 alt
    • 由于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

alt

  • 完成后在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-组件化架构漫谈