第二篇主要讲以下几点:
1、创建私有库。
2、对主工程进行组件化改造(主工程的逻辑有viewController->A->B)
3、将主工程的A页面做成A组件,解耦主工程和A页面
4、使用CTMediator方案(target-action)进行组件化搭建(核心在于每个组件的category可以对组件和主功能之间的解耦操作)
5、最终的效果是完成主工程与A组件的通信,A组件和主工程中B页面的通信
tips:当前组件化方案纯oc版本,swift还多几个工程文件,这一期不涉及;另外github使用ssh的话,需要设置一个sshkey,这一期不涉及,直接百度就好,或者我有时间了也写一下;
一、准备工作
1、首先要搞清楚为什么创建私有库?
创建私有库的目的是为了让团队成员可以共享私有库组件,项目用到该组件的时候可以直接pod下来组件及其category,下面以github为例,创建私有库,并且对不同工程间的关系进行说明
2、如何创建私有库
然后点击下面的CreateRepository即可。
3、打开终端,本地建立一个私有仓库并关联github上的私有仓库LSDRepo,
pod repo add [私有Pod源仓库名字] [私有Pod源的repo地址]
添加完成之后不会有任何返回,我们可以查看添加好的私有库
说明已经添加完成。
4、创立一个文件夹,例如Project。
(1)把我们的主工程文件夹放到Project下:~/Project/MainProject
(2)在~/Project下clone快速配置私有源的脚本repo:git clone git@github.com:casatwy/ConfigPrivatePod.git
(3) 将ConfigPrivatePod的templates_objc文件夹下Podfile中source '[git@pkg.poizon.com:duapp/iOS/DUSpecs.git]'
改成第一步里面你自己的私有Pod源仓库的repo地址git@github.com:DemoComponentization/LSDRepo.git
(tips:因为我本机的cocoaPods源是清华的,所以我把source 'https://github.com/CocoaPods/Specs.git'
改成了source 'https://mirrors.tuna.tsinghua.edu.cn/git/CocoaPods/Specs.git'
),此时podfile文件内容大致如下
# Uncomment this line to define a global platform for your project
# platform :ios, '9.0'
source 'https://mirrors.tuna.tsinghua.edu.cn/git/CocoaPods/Specs.git'
source 'git@github.com:DemoComponentization/LSDRepo.git'
use_frameworks!
use_modular_headers!
target '__ProjectName__' do
pod "CTMediator"
# private pods
pod "HandyFrame"
end
(4)将ConfigPrivatePod的templates_objc文件夹下upload.sh中所有DUSpecs
改成第二步里面你自己的私有Pod源仓库的名字(LSDRepo
),我当时修改的位置为第13,14,62行,三处。
至此,文件目录结构应当如下
Project
├── ConfigPrivatePod
└── MainProject
如果没有问题,那说明准备工作已经完成。
二、明确业务拆分
MainProject是一个非常简单的应用,一共就三个页面。首页push了AViewController,AViewController里又push了BViewController。我们可以理解成这个工程由三个业务组成:首页、A业务、B业务,我们这一次组件化的实施目标就是把A业务组件化出来,首页和B业务都还放在主工程。所以,我们需要为此做两个私有Pod:A业务Pod(以后简称A Pod)、方便其他人调用A业务的CTMediator category的Pod(以后简称A_Category Pod)。所有的组件都有自己的Category,工程和组件之间,组件和工程之间通信都是通过Category。
1、新建Xcode工程,命名为A,放到Projects下
此时文件目录结构如下:
Project
├── ConfigPrivatePod
├── MainProject
└── A
2、github上新建Repository,命名也为A,新建好了之后网页不要关掉
然后cd到ConfigPrivatePod下,执行./config_objc.sh
脚本来配置A这个私有Pod。脚本会问你要一些信息,Project Name就是A,要跟你的A工程的目录名一致。HTTPS Repo、SSH Repository网页上都有,Home Page URL就填你A Repository网页的URL。
说明创建成功,并且关联好了私有库的一些参数。
3、修改podspec文件
在A工程中,将主工程中的AViewController的.h和.m文件移动到Source文件夹下,修改A.podspec
文件中s.source_files
的内容为s.source_files = "A/Source/**/*.{h,m}"
,大概在98行。这样做的目的是将来pod组件的时候会把Source文件夹下的所有文件pod到你的项目里,所以你可以把将来需要pod的文件都放到Source文件夹下。
现在A工程的文件目录结构大致如下:
A
|── A
| ├── Category
| ├── Source
| │ ├── AViewController.h
| │ └── AViewController.m
| ├── AppDelegate.h
| ├── AppDelegate.m
| ├── ViewController.h
| ├── ViewController.m
| └── main.m
└── A.xcodeproj
4、重复步骤1,2,3,完成A_Category的准备工作
创建A的Category=>A_Category,在Project文件夹下创建工程A_Category,github上创建相同名字的Repository,然后cd到ConfigPrivatePod文件夹下,执行./config_objc.sh
脚本来配置A_Category这个私有Pod,上述步骤准备就绪之后,A_Category的目录结构大致如下:
A_Category
├── A_Category
│ ├── Category
│ ├── Source
│ │ └── Target
│ ├── AppDelegate.h
│ ├── AppDelegate.m
│ ├── Info.plist
│ ├── ViewController.h
│ ├── ViewController.m
│ └── main.m
├── A_Category.podspec
├── A_Category.xcodeproj
├── FILE_LICENSE
├── Podfile
├── readme.md
└── upload.sh
然后在A_Category文件夹下,在Podfile中添加一行pod "CTMediator"(如果有,就不用管了),在A_Category.podspec
文件的后面打开或者添加s.dependency "CTMediator"
,修改s.source_files = "A_Category/Source/*.{h,m}"
,然后执行pod install --verbose。完成之后打开A_Category工程,在Source文件夹下创建基于CTMediator的Category:CTMediator+A,删除无用文件,最终目录结构大致如下:
A_Category
├── A_Category
│ ├── Source
│ │ ├── CTMediator+A.h
│ │ └── CTMediator+A.m
│ ├── AppDelegate.h
│ ├── AppDelegate.m
│ ├── Info.plist
│ ├── ViewController.h
│ ├── ViewController.m
│ └── main.m
├── A_Category.podspec
├── A_Category.xcodeproj
├── FILE_LICENSE
├── Podfile
├── readme.md
└── upload.sh
到此,A和A_Category的准备工作就做好了。
3、完善代码逻辑使得每个部分都可以独立运行
1、完善主工程及其A_Category
打开主工程,在Podfile下添加pod "A_Category", :path => "../A_Category"
来本地引用A_Category,终端执行pod install。然后编译一下,说找不到AViewController的头文件。此时我们把AViewController.h
引用改成#import <A_Category/CTMediator+A.h>
。然后继续编译,说找不到AViewController这个类型。看一下这里是使用了AViewController的地方,于是我们在Development Pods下找到CTMediator+A.h,在里面添加一个方法(本质修改的是A_Category工程中Source文件夹下的CTMediator+A.h和CTMediator+A.m):
- (UIViewController *)A_aViewController;
再去CTMediator+A.m中,补上这个方法的实现,把主工程中调用的语句作为注释放进去,将来写Target-Action要用:
- (UIViewController *)A_aViewController
{
/*
AViewController *a = [[AViewController alloc] init];
*/
return [self performTarget:@"A" action:@"viewController" params:nil shouldCacheTarget:NO];
}
补充说明一下,performTarget:@"A"中给到的@"A"其实是Target对象的名字。一般来说,一个业务Pod只需要有一个Target就够了,但一个Target下可以有很多个Action。Action的名字也是可以随意命名的,只要到时候Target对象中能够给到对应的Action就可以了。至此,完成了Category的代码编写,后面也不需要再进行改动。接下来把主工程调用AViewController的地方改为基于CTMediator Category的实现:
UIViewController *a = [[CTMediator sharedInstance] A_aViewController];
[self.navigationController pushViewController:a animated:YES];
现在的话,编译就没问题了。这时候点击按钮没有反应,因为我们还没实现A工程的Target-Action。
2、实现A工程的代码逻辑,使得编译通过
打开两个工程:A_Category工程和A工程。
我们在A工程中的Source文件夹下创建一个文件夹:Targets,然后看到A_Category里面有performTarget:@"A",所以我们在Targets文件夹下新建一个对象,叫做Target_A。然后又看到对应的Action是viewController,于是在Target_A中新建一个方法:Action_viewController。这个Target对象是这样的:
//A.h
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@interface A : NSObject
- (UIViewController *)Action_viewController:(NSDictionary *)params;
@end
//A.m
#import "A.h"
#import "AViewController.h"
@implementation A
- (UIViewController *)Action_viewController:(NSDictionary *)params
{//写实现的时候,对照着之前在A_Category里面的注释去写就可以了
AViewController *a = [[AViewController alloc] init];
return viewController;
}
@end
另外补充一点,Target对象的Action设计出来也不是仅仅用于返回ViewController实例的,它可以用来执行各种属于业务线本身的任务。例如上传文件,转码等等各种任务其实都可以作为一个Action来给外部调用,Action完成这些任务的时候,业务逻辑是可以写在Action方法里面的(Action具备调度业务线提供的任何对象和方法来完成自己的任务的能力。它的本质就是对外业务的一层服务化封装。)。
现在重新编译,发现找不到BViewController。由于我们这次组件化实施的目的仅仅是将A业务线抽出来,BViewController是属于B业务线的,所以我们没必要把B业务也从主工程里面抽出来。但为了能够让A工程编译通过,我们需要提供一个B_Category来使得A工程可以调度到B,同时也能够编译通过。
B_Category的创建步骤跟A_Category是一样的,创建好B_Category(注意B_Category.spec文件里同样要设置依赖项和A_Category一样)之后,Source文件夹下的两个类如下:
//CTMediator+B.h
#import <CTMediator/CTMediator.h>
#import <UIKit/UIKit.h>
@interface CTMediator (B)
- (UIViewController *)B_viewControllerWithContentText:(NSString *)contentText;
@end
//CTMediator+B.m
#import "CTMediator+B.h"
@implementation CTMediator (B)
- (UIViewController *)B_viewControllerWithContentText:(NSString *)contentText
{
NSMutableDictionary *params = [[NSMutableDictionary alloc] init];
params[@"contentText"] = contentText;
return [self performTarget:@"B" action:@"viewController" params:params shouldCacheTarget:NO];
}
@end
我们在A工程的Podfile文件中pod "B_Category",:path=>"../B_Category"
,执行pod install,编译之前把涉及到BViewController的头文件和代码实现用B_Category及其方法替换,修改如下:
UIViewController *viewController = [[CTMediator sharedInstance] B_viewControllerWithContentText:@"hello, world!"];
[self.navigationController pushViewController:viewController animated:YES];
现在编译,就没有问题了,但是需要注意的是,A组件的依赖项需要添加一个B_Category,所以A.podspec文件的依赖项修改如下:
s.dependency "B_Category"
s.dependency "CTMediator"
现在组件A被完全剥离出主工程,成为了独立的组件,A和主工程,A和B之间完全解耦,唯一的问题是,点击A页面不会push到B页面,因为B的Action-target还没写。所以我们要去主工程创建一个B业务线的Target-Action。创建的时候其实完全不需要动到B业务线的代码,只需要新增Target_B对象即可:需要注意的是,主工程的Podfile文件中现在可以添加pod "A",:path=>"../A"
,然后pod install。
//Target_B.h:
#import <UIKit/UIKit.h>
@interface Target_B : NSObject
- (UIViewController *)Action_viewController:(NSDictionary *)params;
@end
//Target_B.m:
#import "Target_B.h"
#import "BViewController.h"
@implementation Target_B
- (UIViewController *)Action_viewController:(NSDictionary *)params
{
NSString *contentText = params[@"contentText"];
BViewController *viewController = [[BViewController alloc] init];
return viewController;
}
@end
Target_B对象在主工程内不存在任何侵入性,将来如果B要独立成一个组件的话,把这个Target对象带上就可以了。总结一下:
1、target类是和组件捆绑在一起的,它对象命名规则和方法的命名规则是根据组件的Category中performTarget Action方法确定的
2、组件的Category中Source文件夹下的Category用来给组件进行通信
3、组件的target是用来代替组件接受消息的,这里接受数据,然后对组件进行操作,因为这个工程里面可以引用到当前组件类。
至此,本地的组件化方案就已经完成了,接下来我们要做的事情就是给这三个私有Pod(A、A_Category、B_Category)发版,发版之前去podspec里面确认一下版本号和dependency。
4、发版
发版前需要强调一点,编译运行主工程能后实现组件化之前同样的功能并且所有逻辑都没问题之后再发版。
发版主要用的几个命令:
git add .
git commit -m "版本号"
git tag 版本号
git push origin master --tags
./upload.sh
命令行cd进入到对应的项目中,然后执行以上命令就可以了。需要注意的是这里的版本号要和podspec文件中的s.version给到的版本号一致。upload.sh是配置私有Pod的脚本生成的,如果你这边没有upload.sh这个文件,说明这个私有Pod你还没用脚本配置过。当然,组件化流程详解(一)中介绍了从创建仓库到发版的全过程,不熟悉这个脚本特性的,也可以按我那个教程操作。
最后,所有的Pod发完版之后,我们再把Podfile里原来的本地引用改回正常引用,也就是把:path...那一段从Podfile里面去掉就好了,改动之后记得commit并push。
最后感谢下教我文档和基友DC