iOS開發的組件化方案的文章介紹已經不少了,可是不多有能介紹如何在項目工程中進行實施的,本文則是做者在實際項目中實施組件化方案後總結的一些經驗。本文不會討論太多理論上的知識,主要集中在實施方面。html
實施業務組件化是將每個業務模塊單獨封裝成pods,而後在主工程中經過CocoaPods以組件的方式將全部模塊集成進來。組件化的實施須要依賴Git和CocoaPods進行,因此在開始以前須要在macOS上安裝好Git和CocoaPods,同時準備好一個Git服務器。本文使用Github爲做爲例子,使用其它Git服務操做步驟不會有太大的差別。git
實際開發中,每個業務模塊對應一個Git倉庫,每個業務Git倉庫對應一個pods,建議將全部倉庫放在一個組織(organization)中。如圖所示,在Github中建立一個組織。github
建立完成以後就能夠在組織中建立你的業務模塊倉庫以及邀請你的開發小夥伴進入組織。objective-c
CocoaPods有個默認公共開放的pods倉庫,裏面存放了不少開源的iOS組件庫供開發者使用,並存放在Github上。可是開發公司項目,代碼不會對外開放,因此只能使用私有pods,那麼就須要自建私有pods倉庫來存放這些私有pods。如圖所示,在組織中建立一個空倉庫。bash
倉庫建立完成以後,須要添加一個本地私有pods倉庫並連接到該遠程Git倉庫。打開macOS的命令行,輸入:服務器
pod repo add ModularizationPod https://github.com/iOSShop/ModularizationPod.git
複製代碼
完成後咱們進入到CocoaPods的目錄下:網絡
cd ~/.cocoapods/repos/
open .
複製代碼
能夠看到目錄中看到兩個倉庫,master是CocoaPods的公共pods倉庫,ModularizationPod則是咱們建立的私有pods倉庫。架構
下一步咱們在ModularizationPod倉庫中添加一個.gitignore文件,能夠直接從master的倉庫裏面複製一個過去。框架
在命令行終端中cd到倉庫目錄:ide
cd /Users/caicai/.cocoapods/repos/ModularizationPod
複製代碼
而後咱們須要提交到遠程Git倉庫:
git add .
git commit -m "first commit"
git push
複製代碼
中間可能須要輸入帳號密碼進行Git的身份校驗,完成後能夠在遠程Git倉庫上查看。
之後這個遠程Git倉庫會存放咱們業務模塊的pods信息。
本文以一個簡單的商城業務來描述組件化方案實施,內容包括如何實現業務模塊的組件化、如何進行模塊間的調用以及如何進行模塊之間通訊。咱們以基礎的帳戶、商品、訂單、支付這四個模塊進行舉例。
實現業務模塊的組件化,是爲了將業務拆分出來,下降業務模塊之間的耦合性。好比在商品詳情頁面【點擊購買】後下一步就是進入訂單生成頁面,傳統的作法就是直接在商品詳情頁面的ViewController裏面直接import訂單生成頁面的ViewController,而後實例化ViewController傳個值後直接push過去就能夠了。當項目規模變大和業務邏輯變複雜的時候,這種直接引入代碼文件的作法就會使得模塊之間的依賴變得愈來愈強,甚至是牽一髮就動全身。即便舉例的只有四個模塊,相互間的依賴也比較多,以下圖所示:
其實這個問題屬於通用的軟件工程,不侷限於iOS開發中。解決的辦法也很簡單,提供一箇中間人(Mediator)。業務模塊之間不直接進行引用,經過Mediator間接造成引用關係,並且在Mediator能夠將模塊須要暴露出來的業務提供出來給其它模塊調用,不須要暴露出來的就不引入Mediator。好比帳戶模塊有登錄頁面和註冊頁面,實際場景中可能只會把登錄頁面給其餘業務模塊調用,註冊頁面只須要從登錄頁面跳轉過去就能夠了,並不須要提供給其它業務模塊調用。以下圖所示:
經過中間人的方式拆分業務模塊後也只是邏輯上清晰了一點,實際上仍是在引入業務的代碼文件,業務與業務之間的調用依舊很不清晰明瞭。好比在訂單頁面彈出一個登錄頁面,訂單模塊的開發人員須要先找到登錄頁面的UIViewController文件,而後import進來,接着實例化對象,最後再present或者push這個頁面。複雜一點的業務可能還須要以口頭或者文檔的形式告知調用方如何去使用類文件、如何去傳遞參數等等。而開發人員想着我只須要一個UIViewController實例化對象就能夠了,也不關心它是哪一個代碼文件、它內部是怎麼實現的。
咱們能夠經過服務的方式去解決這個問題,簡單的說就是你須要什麼,我給你什麼。經過Target-Action,業務提供方將全部的服務以對象方法的形式提供,經過方法的參數和返回值進行模塊間的調用和通訊。以下圖所示:
完成前兩步以後,還存在兩個問題:
第一個問題的解決辦法,經過組合的思想,使用Objective-C的分類(Category)將Mediator去中心化。針對每一個業務模塊建立一個Mediator的分類(Category),並將Target服務引入到分類(Category)中,至關於將Target服務再作一層方法封裝,其餘業務調用方只需引入相應的分類(Category)便可,這樣就能夠避免無關業務服務的多餘引入。同時業務模塊對外提供的服務修改後,相應的業務提供方只需修改本身的分類(Category)便可,Mediator也無需維護,達到真正的去中心化。以下圖所示:
經過上圖能夠看到模塊與模塊之間的調用已經沒有直接引入了,都是經過Category引入。在上圖的基礎上仍是能夠看到依賴並無減小,Category會引用Target-Action,並間接引用源代碼文件。
第二個問題的其實就是Category與Target-Action之間的依賴問題,解決辦法也很簡單粗暴。由於業務模塊中對外提供服務的Category中的方法實現其實就是直接調用的Target類裏面的Action方法,因此經過runtime的技術就能夠直接切段二者之間的依賴。
經過以上方法,Category能夠不用import就直接調用Target-Action的服務,並傳遞出去,這樣就完成了解除依賴。以下圖所示:
至此,業務架構設計就很是清晰明瞭。以上就是組件化工程實施的方案。
新建一個目錄ModularizationProject用於存放全部工程實施的文件,而後在ModularizationProject下新建ConfigPods目錄,用於存放一些配置文件,目錄及文件結構以下圖所示:
templates目錄下的文件都是幫助建立Xcode工程的,經過config.sh的腳本能夠快速建立工程並進行私有pods的配置。查看示例
gitignore能夠在Git進行提交時對文件過濾。
readme.md能夠對Git倉庫進行一些描述說明,按須要撰寫便可。
Podfile是建立cocoapods工程時必須的文件,示例文件裏面第一個source開頭後面的地址是私有pods倉庫的遠程Git倉庫地址,改爲本身的便可。
pod.podspec是將工程打包成pods的必要配置文件,裏面內容可按需修改。使用示例文件,建議只修改s.author後面的信息就能夠了。
upload.sh裏面是打包pods的命令,示例文件裏面push後面是私有pods倉庫名,--sources後面的參數中第一個是私有pods倉庫的遠程Git倉庫地址,兩個都須要改爲本身的。
config.sh是建立整個工程的腳本,建議不作修改直接使用示例文件。
在Git組織中建立帳戶模塊的遠程倉庫。
在ModularizationProject中建立一個名爲AccountModule的iOS工程,注意建立過程當中,Source Control不要勾選。
打開終端命令行cd到config.sh所在的目錄,而後執行
./config.sh
複製代碼
Enter Project Name:輸入工程的名字
AccountModule
Enter HTTPS Repo URL:輸入工程的遠程Git倉庫的https地址
Enter SSH Repo URL:輸入工程的遠程Git倉庫的地址
git@github.com:iOSShop/AccountModule.git
Enter Home Page URL:輸入工程的主頁:
confirm:覈對以上輸入的信息
y
帳戶模塊的遠程Git倉庫就建立完畢,並完成了第一次初始化的提交。
進入本地的AccountModule目錄中,而後執行
pod install
複製代碼
完成後,帳戶模塊的cocoapods工程就建立完畢,並能夠進行開發了。並且工程中自帶資格同名目錄,將全部代碼文件放在該目錄便可。
以上步驟通用於建立業務模塊工程。
在進入開發階段前,須要對各個業務模塊的職責進行劃分,並規則好各個業務模塊須要對外提供的服務,因此咱們能夠先完成業務模塊中大部分Target-Action和對應的Category的編寫。拿帳戶模塊來舉例,其餘業務模塊可能須要登錄頁面、用戶的登陸狀態、用戶登陸狀態的改變。
帳戶模塊Target-Action中的方法聲明:
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface Target_Account : NSObject
/**
*登陸
**/
- (UIViewController *)Action_nativeLoginViewController;
/**
*登錄狀態
**/
- (BOOL)Action_nativeLoginStatus;
/**
*登錄狀態改變
**/
- (NSString *)Action_nativeLoginStatusChangeNotificationName;
@end
NS_ASSUME_NONNULL_END
複製代碼
Mediator思想的實現來源於CTMediator,核心只有兩個文件就已經能知足大部分的使用場景。在實際項目開發中可直接依賴該框架,也能夠clone下來後按照須要進行修改。示例中對其進行修改後建立了新的CCMediator,並製做成私有pods庫供使用。
按照3.2的完整步驟建立一個名爲AccountModule_Category的工程,而後再進入AccountModule_Category工程中,編輯Podfile,加入pod 'CCMediator',而後再pod install。
建立CTMediator的Category,下面是Category中方法的聲明和實現
#import "CCMediator.h"
NS_ASSUME_NONNULL_BEGIN
@interface CCMediator (AccountModule)
/**
*登錄(presentViewController)
**/
- (UIViewController *)Account_viewControllerForLogin;
/**
*登錄狀態
**/
- (BOOL)Account_statusForLogin;
/**
*登錄狀態改變
**/
- (NSString *)Account_nameForLoginStatusChangeNotification;
@end
NS_ASSUME_NONNULL_END
複製代碼
#import "CCMediator+AccountModule.h"
NSString * const MediatorTargetAccount = @"Account";
NSString * const MediatorActionAccountLoginViewController = @"nativeLoginViewController";
NSString * const MediatorActionAccountLoginStatus = @"nativeLoginStatus";
NSString * const MediatorActionAccountLoginStatusChangeNotification = @"nativeLoginStatusChangeNotificationName";
@implementation CCMediator (AccountModule)
/**
*登錄(presentViewController)
**/
- (UIViewController *)Account_viewControllerForLogin {
UIViewController *viewController = [self performTarget:MediatorTargetAccount action:MediatorActionAccountLoginViewController params:nil shouldCacheTarget:NO];
if ([viewController isKindOfClass:[UIViewController class]]) {
return viewController;
} else {
return [[UIViewController alloc] init];
}
}
/**
*登錄狀態
**/
- (BOOL)Account_statusForLogin {
return [[self performTarget:MediatorTargetAccount action:MediatorActionAccountLoginStatus params:nil shouldCacheTarget:NO] boolValue];
}
/**
*登錄狀態改變
**/
- (NSString *)Account_nameForLoginStatusChangeNotification {
return [self performTarget:MediatorTargetAccount action:MediatorActionAccountLoginStatusChangeNotification params:nil shouldCacheTarget:NO];
}
@end
複製代碼
經過Category完成服務傳遞,同時在Mediator中解決了Category與Target-Action之間的依賴。
完成了Category和Target-Action的編寫,就能夠經過Git提交到遠程倉庫並生成pods供其它業務模塊引用。
步驟以下:
編輯podspec文件,修改s.version的版本,而後針對資源和依賴進行自定義設置,podspec的詳細用法可參考官方指導。
打開終端cd到工程目錄下,開始提交代碼。
git add .
git commit -m "add Target-Action"
git push
複製代碼
打標籤,製做並推送私有pods。tag須要與podspec的s.version保持一致,而後執行目錄下的upload.sh腳本。執行過程當中可能報錯,必定要按照提示去解決。
git tag 1.0.0
git push --tags
./upload.sh
複製代碼
製做完成後能夠在本地的pods倉庫和遠程Git倉庫中看到被推送的pods信息。
下面是經過pod search查找的結果:
其它業務模塊能夠直接在其工程的Podfile裏面集成AccountModule_Category和AccountModule就能夠調用帳戶模塊的服務了。
咱們以商品模塊爲例,進入在【個人商品界面】後,須要在該頁面判斷用戶是否登錄了,沒有登錄則提示登錄並能跳轉到登錄頁面,而且還要實時監聽用戶登錄狀態的改變。
實現用戶狀態的監聽
NSString *notificationName = [[CCMediator sharedInstance] Account_nameForLoginStatusChangeNotification];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(loginStatusChange) name:notificationName object:nil];
複製代碼
實現監聽的方法,當用戶登錄時就隱藏提示登錄的頁面,當用戶未登錄時顯示提示登錄的頁面。
- (void)loginStatusChange {
BOOL isLogin = [[CCMediator sharedInstance] Account_statusForLogin];
self.loginView.hidden = isLogin;
if (isLogin) {
[self.view bringSubviewToFront:self.loginView];
}
}
複製代碼
響應提示登錄的操做,彈出登錄頁面
- (void)clickLogin {
UIViewController *viewController = [[CCMediator sharedInstance] Account_viewControllerForLogin];
[self presentViewController:[[UINavigationController alloc] initWithRootViewController:viewController] animated:YES completion:nil];
}
複製代碼
以上是一些基本的服務調用方式,能知足大部分模塊間的調用場景。其餘類型的服務可自行思考如何處理,須要注意的是方法的返回值類型必定要是基本數據類型和常規對象。這裏的常規對象指的是Foundation框架、UIKit框架或者其它一些系統庫框架中的對象。若是使用的是自定義對象作返回值,帶來的將是強耦合關係。
不一樣的業務模塊之間進行調用時確定免不了須要通訊,好比從商品詳情頁面跳轉到訂單生成頁面,商品詳情頁面在調用訂單生成頁面時須要傳遞參數至少包括商品id和商品數量。那麼訂單生成的Category方法聲明以下:
#import "CCMediator.h"
NS_ASSUME_NONNULL_BEGIN
@interface CCMediator (OrderModule)
/**
*生成訂單
**/
- (UIViewController *)Order_viewControllerForMakeWithGoodsID:(NSNumber *)goodsID goodsCount:(NSInteger)goodsCount;
@end
NS_ASSUME_NONNULL_END
複製代碼
Category全部傳遞的參數都封裝到一個NSDictonary中,而後傳遞給對應的Target-Action。Category方法實現以下:
#import "CCMediator+OrderModule.h"
NSString * const MediatorTargetOrder = @"Order";
NSString * const MediatorActionOrderMakeViewController = @"nativeOrderMakeViewController";
@implementation CCMediator (OrderModule)
/**
*生成訂單
**/
- (UIViewController *)Order_viewControllerForMakeWithGoodsID:(NSNumber *)goodsID goodsCount:(NSInteger)goodsCount {
if (goodsID == nil) {
NSException *exception = [[NSException alloc] initWithName:@"Order_viewControllerForMakeWithGoodsID:goodsCount:提示" reason:@"goodsID不能爲空" userInfo:nil];
@throw exception;
}
if (goodsCount < 1) {
NSException *exception = [[NSException alloc] initWithName:@"Order_viewControllerForMakeWithGoodsID:goodsCount:提示" reason:@"goodsCount錯誤" userInfo:nil];
@throw exception;
}
NSMutableDictionary *params = [NSMutableDictionary dictionary];
params[@"goodsCount"] = [NSNumber numberWithInteger:goodsCount];
params[@"goodsID"] = goodsID;
UIViewController *viewController = [self performTarget:MediatorTargetOrder action:MediatorActionOrderMakeViewController params:params shouldCacheTarget:NO];
if ([viewController isKindOfClass:[UIViewController class]]) {
return viewController;
} else {
return [[UIViewController alloc] init];
}
}
@end
複製代碼
若是參數是必要的,能夠在傳遞到Target-Action以前進行檢測,不符合要求能夠直接拋出異常。固然也能夠根據產品的須要進行自定義處理。那麼對應的Target-Action方法聲明則是:
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface Target_Order : NSObject
/**
*生成訂單
**/
- (UIViewController *)Action_nativeOrderMakeViewControllerWithParams:(NSDictionary *)params;
@end
NS_ASSUME_NONNULL_END
複製代碼
帶參數和不帶參數的方法聲明多了一個WithParams,具體能夠看CCMediator中的實現。對應的Target-Action方法實現則是:
#import "Target_Order.h"
#import "OrderMakeViewController.h"
@implementation Target_Order
/**
*生成訂單
**/
- (UIViewController *)Action_nativeOrderMakeViewControllerWithParams:(NSDictionary *)params {
OrderMakeViewController *orderViewController = [[OrderMakeViewController alloc] init];
orderViewController.goodsCount = [params[@"goodsCount"] integerValue];
orderViewController.goodsID = params[@"goodsID"];
return orderViewController;
}
@end
複製代碼
爲何使用NSDictionary傳遞參數,由於它是個容器,屬於Foundation框架中的類,使用它不會形成Category和Target-Action間產生依賴,能夠把全部的參數統一封裝起來進行傳遞。並且模塊之間的參數傳遞應該儘量少,不然會使模塊間的耦合性加強。同時傳遞的參數也必須是基本數據類型和常規對象,不要傳遞自定義對象。
在商品詳情頁面調用訂單生成頁面
- (void)clickBuy {
UIViewController *viewController = [[CCMediator sharedInstance] Order_viewControllerForMakeWithGoodsID:self.goodsID goodsCount:99];
[self.navigationController pushViewController:viewController animated:YES];
}
複製代碼
上面描述了跨模塊的通訊,可是示例是正向的傳參,如何實現逆向傳參呢。例如常見的場景,我從A頁面到B頁面,B頁面作了一些操做後把一些參數傳遞給A。實現的辦法就是使用block,將block封裝到NSDictonary而後傳遞過去就能夠實現。示例場景中商品詳情頁面進入訂單生成頁面完成付款後返回成功的信息給商品詳情頁面進行顯示,以下圖所示:
如今訂單模塊的Category的方法聲明修改以下:
#import "CCMediator.h"
NS_ASSUME_NONNULL_BEGIN
typedef void(^SuccessBlock)(NSString *);
@interface CCMediator (OrderModule)
/**
*生成訂單
**/
- (UIViewController *)Order_viewControllerForMakeWithGoodsID:(NSNumber *)goodsID goodsCount:(NSInteger)goodsCount success:(SuccessBlock)successBlock;
@end
NS_ASSUME_NONNULL_END
複製代碼
Category的方法實現修改以下:
#import "CCMediator+OrderModule.h"
NSString * const MediatorTargetOrder = @"Order";
NSString * const MediatorActionOrderMakeViewController = @"nativeOrderMakeViewController";
@implementation CCMediator (OrderModule)
/**
*生成訂單
**/
- (UIViewController *)Order_viewControllerForMakeWithGoodsID:(NSNumber *)goodsID goodsCount:(NSInteger)goodsCount success:(SuccessBlock)successBlock {
if (goodsID == nil) {
NSException *exception = [[NSException alloc] initWithName:@"Order_viewControllerForMakeWithGoodsID:goodsCount:提示" reason:@"goodsID不能爲空" userInfo:nil];
@throw exception;
}
if (goodsCount < 1) {
NSException *exception = [[NSException alloc] initWithName:@"Order_viewControllerForMakeWithGoodsID:goodsCount:提示" reason:@"goodsCount錯誤" userInfo:nil];
@throw exception;
}
NSMutableDictionary *params = [NSMutableDictionary dictionary];
params[@"goodsCount"] = [NSNumber numberWithInteger:goodsCount];
params[@"goodsID"] = goodsID;
if (successBlock) {
params[@"successBlock"] = successBlock;
}
UIViewController *viewController = [self performTarget:MediatorTargetOrder action:MediatorActionOrderMakeViewController params:params shouldCacheTarget:NO];
if ([viewController isKindOfClass:[UIViewController class]]) {
return viewController;
} else {
return [[UIViewController alloc] init];
}
}
@end
複製代碼
訂單模塊的Target-Action實現中只須要在賦值操做時加入一行便可:
orderViewController.successBlock = params[@"successBlock"];
複製代碼
商品詳情頁面的調用修改以下:
- (void)clickBuy {
__weak __typeof(self)weakSelf = self;
UIViewController *viewController = [[CCMediator sharedInstance] Order_viewControllerForMakeWithGoodsID:self.goodsID goodsCount:99 success:^(NSString * _Nonnull successString) {
__strong __typeof(weakSelf)strongSelf = weakSelf;
strongSelf.textLabel.text = successString;
}];
[self.navigationController pushViewController:viewController animated:YES];
}
複製代碼
詳細的實現細節可去示例工程中查看。
通常的應用都是UITabBarController+UINavigationController,因此咱們的主工程基本都是搭建UITabBarController+UINavigationController的結構,作一些全局設置,以及處理一些初始化的邏輯等等。而後在Podfile裏面引入全部的業務模塊的Category工程以及對應的業務模塊工程便可。
從模塊間調用和通訊來看,解決依賴的辦法也帶來了一些硬編碼的工做,包括調用時須要對類名和方法名進行硬編碼,以及傳遞參數時對參數名的硬編碼。這些硬編碼沒法避免,可是都在可控範圍內,侷限於Cateogry和對應的Target-Action。因此同一業務模塊的Cateogry和Target-Action基本都是一我的編寫,也能保證不會出錯。
編寫podspec文件時須要注意依賴循環的問題,須要注意:
這麼作的緣由是,舉個例子:好比帳戶模塊會調用商品模塊的服務,商品模塊也會調用帳戶模塊的服務。若是商品模塊的Category工程的podspec依賴了商品模塊的業務工程,同時帳戶模塊的Category工程的podspec依賴了帳戶模塊的業務工程。那麼在商品模塊的業務工程中引入帳戶模塊的Category工程時,就會引入帳戶模塊的業務工程。接着帳戶模塊就會引入商品模塊的Category工程,商品模塊的Category工程又引入了商品模塊的業務工程中,而後就本身引入本身,因此確定沒法引入成功。以下圖所示:
tag小技巧,不少時候Git打完tag以後,在執行upload.sh上傳pods的時候會出錯。解決完錯誤後,會發現可能須要從新命名tag,致使版本號跳躍。因此能夠刪除失敗的時候打的tag,而後從新打這個tag。
git tag -d 1.0.0
git push origin :/refs/tags/1.0.0
複製代碼
至此,組件化方案實施的內容就到這裏結束了。本文提供了基本的思路,已經能知足大部分的業務開發場景。可是對於經常使用的網絡層、通用UI組件等這些部分沒有涉及。這個時候就須要思考了,這些功能是屬於業務類型的仍是非業務類型的。若是是業務類型的,那麼最好是作成Category+Target-Action的方式對外提供服務;若是是非業務類型的,好比網絡請求、通用UI框架等等,做者建議將這些功能封裝到一個基礎模塊中,製做成組件後讓全部業務模塊引用便可。因爲基礎模塊的組件是直接的文件引入,因此基礎模塊的功能不宜過多。由於一旦這個模塊過於龐大,形成的依賴和耦合也會更大。同時隨着項目的規模以及業務複雜度的提高,須要考慮的東西也會愈來愈多,這就更加考驗系統架構的設計能力了。組件化的實踐之路要一步一步走,也須要開發人員不斷的思考、探索和完善。
以上都是做者在實際開發中的總結,交流請聯繫:
cctomato@outlook.com
本文全部的代碼都託管在Github上,點擊查看。
本文的架構設計和實踐思路都來源於Casa Taloyum大神的博客: