iOS從零到一搭建組件化項目框架
隨著公司業(yè)務需求的不斷迭代發(fā)展,工程的代碼量和業(yè)務邏輯也越來越多,原始的開發(fā)模式和架構已經無法滿足我們的業(yè)務發(fā)展速度了,這時我們就需要將原始項目進行一次重構大手術了。這時我們應該很清晰這次手術的動刀口在哪,就是之前的高度耦合的業(yè)務組件和功能組件,手術的目的就是將這些耦合拆分成互相獨立的各個組件。
工程效果預覽

組件化工程示例項目地址
下面我們圍繞這幾個問題來展開講解
- 為什么要用組件化,它給我們帶來哪些優(yōu)勢
- 各個組件該如何進行拆分,拆分的顆粒度該如何控制
- 如何從零到一搭建組件化架構項目
為什么要用組件化
我們先來張圖看看在沒有使用組件化前,我們各個模塊間的依賴關系

從上面這種各個業(yè)務組件的依賴關系來看,他們是互相依賴的,業(yè)務組件和業(yè)務組件間產生了嚴重的耦合關系,這樣一來對我們工程的擴展性就會大大的降低,維護成本就會變高。
舉個例子:假設某天產品經理說,咱們公司的業(yè)務發(fā)展的太好了,咱們的營銷模塊需要獨立出來成一個單獨的應用,以便于咱們可以添加更多高效的營銷手段。這時我們就傻眼了,需要獨立出一個app出來,這可怎么搞啊,營銷模塊的代碼和其他的很多業(yè)務代碼耦合在一起了,現(xiàn)在要獨立出來,那就只能重新寫一個營銷應用了,之前的代碼剝離不干凈了。
從上面我們列舉的一個簡單的例子可以體會到:在項目沒有做到真真意義上的組件化之前,各個業(yè)務模塊和業(yè)務模塊間的高度耦合,功能組件和功能組件間的高度耦合對未來公司的業(yè)務擴展來說,成本很高,不能做到同樣業(yè)務邏輯的代碼的高度復用,這樣對我們開發(fā)來說也是效率的降低。
好了,有的同學可能會說,既然上面各個模塊間耦合這么高,那我就來將這些耦合解耦,于是,可能會出現(xiàn)下面這張圖的模塊間的關系。

從下面這張圖來看,我們發(fā)現(xiàn),現(xiàn)在確實能做到各個業(yè)務模塊間完全的解耦了,他們不再互相依賴了,同時我們引入了一個中間調度者的一個角色,現(xiàn)在是各個業(yè)務模塊和這個中間調度者角色產出了嚴重的依賴。我們思考下發(fā)現(xiàn),我們的各個業(yè)務模塊依賴這個中間調度者,這個是完全正常的,因為他們需要這個調度者來做統(tǒng)一的事件分發(fā)工作,但是這個調度者卻又依賴了每個業(yè)務模塊,這層依賴是有必要的嗎?我們回頭想想真正的組件化開發(fā)是完全的去依賴化,這個依賴是完全沒有必要的。例如:假設我們現(xiàn)在有一個新的B APP需要開發(fā),這時我們也需要用到這個中間調度者組件,但是我們不能直接拿過來用,因為它又依賴了很多A App的業(yè)務組件。因此,我們的組件化架構設計又需要一次升級變更了,升級成如下圖所示的模型。

從上面的這張圖,我們可以看出,各個業(yè)務模塊間只會依賴中間調度者,并且中間調度者不對各個模塊產生任何的依賴。
好了,從上面的三張圖之間的對比,我們就可以很好的理解為什么我們的工程急需要實現(xiàn)組件化架構開發(fā)了,以及各自的優(yōu)劣勢。
各個組件該如何進行拆分
關于組件該如何拆分,這個沒有一個完整的標準,因為每個公司的業(yè)務場景不一樣,對應衍生出來的各個業(yè)務模塊也就不一樣,所以業(yè)務組件間的拆分,這個根據自己公司的業(yè)務模塊來進行合理的劃分即可。這里我們來說下整個工程的組件大致的劃分方向
- 項目主工程:當我們工程完全使用組件化架構進行開發(fā)后,我們會驚奇的發(fā)現(xiàn)我們的主工程就成了一個空殼子工程。因為所有的主工程呈現(xiàn)出來的內容都被拆分成了各個獨立的業(yè)務組件了,包括各個工具組件也是各自互相獨立的。這樣我們發(fā)現(xiàn)開發(fā)一個完整的APP就像是搭建樂高積木一樣,各個部件都有,任我們隨意的組合搭建,這樣是不是感覺很爽。
- 業(yè)務組件:業(yè)務組件就是我們上面示例圖所示的各個獨立的產品業(yè)務功能模塊,我們將其封裝成獨立的組件。例如示例Demo中的電子發(fā)票業(yè)務組件,業(yè)務組件A,業(yè)務組件B。我們通過組裝各個獨立的業(yè)務組件來搭建一個完整的APP項目。
- 基礎工具類組件:基礎工具類是各個互相獨立,沒有任何依賴的工具組件。它們和其它的工具組件、業(yè)務組件等沒有任何依賴關系。這類組件例如有:對數組,字典進行異常保護的Safe組件,對數組功能進行擴展Array組件,對字符串進行加密處理的加密組件等等。
- 中間件組件:這個組件比較特殊,這個是我們?yōu)榱藢崿F(xiàn)組件化開發(fā)而衍生出來的一個組件,上面示例圖中的中間調度者就是一個功能獨立的中間件組件。
- 基礎UI組件:視圖組件就比較常見了,例如我們封裝的導航欄組件,Modal彈框組件,PickerView組件等。
- 業(yè)務工具組件:這類組件是為各個業(yè)務組件提供基礎功能的組件。這類組件可能會依賴到其他的組件。例如:網絡請求組件,圖片緩存組件,jspatch組件等等
至于組件的拆分顆粒度,這個著實不好去斷定,因人而異,不同的需求功能復雜度拆分出來的組件大小也不盡相同
如何從零到一搭建組件化架構
在講如何從零到一來實現(xiàn)一個組件化架構項目前,我們需要熟練掌握使用pod來制作組件庫。下面我們就圍繞提供的組件化示例項目來展開講解。
首先,我們來看示例Demo中包含哪些業(yè)務組件(如下圖所示:):

示例Demo中,我提供了三個業(yè)務組件來作為演示效果,其中業(yè)務模塊A和業(yè)務模塊B是臨時業(yè)務模塊組件,電子發(fā)票業(yè)務組件時真實的企業(yè)需求功能組件。
我們再來看下示例Demo中都提供了哪些工具組件(如下圖所示)
注意了:這里提供的6個工具組件也都是作者已經封裝好的功能組件,大家也可以直接 install 安裝使用的哦。

詳細操作步驟
第一步:
我們先創(chuàng)建一個空的iOS工程項目:MainProject,這個空項目作為我們的主工程項目,就是上面所說的殼子工程項目,然后初始化pod,這里不清楚pod的使用的小伙伴們請自行查閱資料。
第二步:
我們創(chuàng)建一個空工程項目:ModuleA,這個ModuleA 項目作為我們的業(yè)務A組件。然后我們初始化pod,初始化podspec文件。
第三步:
我們創(chuàng)建一個空工程項目:ModuleB,這個ModuleB 項目作為我們的業(yè)務B組件。然后我們初始化pod,初始化podspec文件。
第四步:
我們創(chuàng)建一個空工程項目:ComponentMiddleware,這個項目就是我們上面所說的中間調度者。然后我們初始化pod,初始化podspec文件。
第五步:
我們創(chuàng)建一個空工程項目: ModuleACategory,這個工程是對應業(yè)務組件A的一個分類工程。然后我們初始化pod,初始化podspec文件。
第六步:
我們創(chuàng)建一個空工程項目: ModuleBCategory,這個工程是對應業(yè)務組件B的一個分類工程。然后我們初始化pod,初始化podspec文件。
好了,上面的主工程和兩個業(yè)務組件工程,以及兩個組件分類工程都已創(chuàng)建完畢,下面我們來講解他們各個之間如何工作的。我就從主工程加載業(yè)務組件開始往下捋,順藤摸瓜式的引出每個工程的用意。
第七步:
我們在主工程MainProject的Podfile中引入我們的業(yè)務組件B工程ModuleB,以及引入我們的ModuleB的分類工程:ModuleBCategory。然后我們pod install。這時已將這兩個組件庫引入到我們的主工程中了。
示例代碼如下:
# Uncomment the next line to define a global platform for your project
platform :ios, '8.0'
source 'https://github.com/CocoaPods/Specs.git'
source 'https://github.com/guangqiang-liu/GQSpec.git'
target 'GQComponentDemo' do
pod 'ModuleB'
pod 'ModuleBCategory'
end
然后我們在主工程中添加一個按鈕事件,這個事件是點擊 push 到業(yè)務組件B的 頁面。
示例代碼如下:
#import <ModuleBCategory/ComponentScheduler+ModuleB.h>
- (void)moduleB {
UIViewController *VC = [[ComponentScheduler sharedInstance] ModuleB_viewControllerWithCallback:^(NSString *result) {
NSLog(@"resultB: --- %@", result);
}];
[self.navigationController pushViewController:VC animated:YES];
}
第八步:
上面第七步中,我們用到了ModuleBCategory 這個分類工程。這個工程我們只對外暴露了兩個文件。這兩文件是上面的中間調度者的分類,也就是說是中間件的分類。我們先來看下這個分類文件的.h 和.m 實現(xiàn)。
.h
#import "ComponentScheduler.h"
@interface ComponentScheduler (ModuleB)
- (UIViewController *)ModuleB_viewControllerWithCallback:(void(^)(NSString *result))callback;
@end
.m
#import "ComponentScheduler+ModuleB.h"
@implementation ComponentScheduler (ModuleB)
- (UIViewController *)ModuleB_viewControllerWithCallback:(void(^)(NSString *result))callback {
NSMutableDictionary *params = [[NSMutableDictionary alloc] init];
params[@"callback"] = callback;
return [self performTarget:@"ModuleB" action:@"viewController" params:params shouldCacheTarget:NO];
}
@end
我們發(fā)現(xiàn)這個分類實現(xiàn)非常的簡單,就是對外暴露一個函數,然后執(zhí)行[self performTarget:@"ModuleB" action:@"viewController" params:params shouldCacheTarget:NO]; ,并將執(zhí)行的返回值返回出去。
這個分類的作用你可以理解為我們提前約定好Target的名字和Action的名字,因為這兩個名字中間件組件中會用到。
上面的performTarget:action:params:shouldCacheTarget 函數是中間件提供的函數。因為ModuleBCategory 是 ComponentScheduler(中間件)的分類文件,所以可以調用到這個函數啦。
在ModuleBCategory 工程中需要引用到了中間件工程所以我們需要在ModuleBCategory 的Podfile文件中引用 中間件組件
示例代碼如下:
# Uncomment the next line to define a global platform for your project
platform :ios, '8.0'
source 'https://github.com/CocoaPods/Specs.git'
source 'https://github.com/guangqiang-liu/GQSpec.git'
target 'ModuleB-Category' do
# Uncomment the next line if you're using Swift or would like to use dynamic frameworks
# use_frameworks!
# Pods for ModuleB-Category
pod 'ComponentScheduler'
end
第九步:
因為上面第八步中引用到中間件工程,這里我們就來看下中間件工程到底做了什么工作。還記得上面第八步中,我們調用了一個中間件提供的函數:performTarget:action:params:shouldCacheTarget 吧,這個是中間件核心函數。
核心函數代碼塊如下:

還記得上面第八步中,我們調用這個函數傳遞的參數吧,我們在把調用代碼拿過來看下
[self performTarget:@"ModuleB" action:@"viewController" params:params shouldCacheTarget:NO];
我們可以看到 TargetName 是我們傳遞的 ModuleB,action是我們傳遞的viewController,然后我們將 這兩個參數傳給了下面的函數:
[self safePerformAction:action target:target params:params];
我們來看下這兩個參數的值具體是什么:

這個函數最終調用到蘋果官方提供的函數:[target performSelector:action withObject:params];
看到 performSelector: withObject: 大家應該就比較熟悉了,iOS的消息傳遞機制。
[Target_ModuleB performSelector:Action_viewController withObject:params];
上面這行偽代碼意思是: Target_ModuleB這個類 調用它的 Action_viewController: 方法,然后傳遞的參數為 params。
細心的小伙伴們就會發(fā)現(xiàn),我們沒有看到過哪里有這個Target_ModuleB 類啊,更沒有看到Target_ModuleB 調用它的 Action_viewController: 方法啊。
是的,這個Target_ModuleB類和類的Action_viewController方法就在第十步中講解到。
第十步:
終于到了最后一步了,寫的好艱辛,嗯,小伙們不要捉急,快了,快講完了
細心的小伙們發(fā)現(xiàn),我們上面講的9步中,好像都沒有提業(yè)務組件B的東西。是的,業(yè)務組件B除了提供組件B的業(yè)務功能外,業(yè)務組件B還需要為我們提供一個Target文件。
我們先來看下業(yè)務組件B的業(yè)務代碼:
示例代碼如下:
#import "ModuleBViewController.h"
#import "PageBViewController.h"
@interface ModuleBViewController ()
@end
@implementation ModuleBViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.title = @"我是模塊B業(yè)務組件";
self.view.backgroundColor = [UIColor whiteColor];
UIButton *btn = [UIButton buttonWithType:UIButtonTypeCustom];
btn.frame = CGRectMake(0, 0, 300, 100);
btn.backgroundColor = [UIColor greenColor];
btn.center = self.view.center;
[btn setTitle:@"模塊B業(yè)務功能組件" forState: UIControlStateNormal];
[btn addTarget:self action:@selector(push) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:btn];
}
- (void)push {
PageBViewController *VC = [[PageBViewController alloc] init];
[self.navigationController pushViewController:VC animated:YES];
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
我們發(fā)現(xiàn),業(yè)務組件B的業(yè)務代碼也很簡單,就是做一個push 跳轉操作,從PageA 控制器跳轉到 PageB 控制器。 這個沒有什么好講的
我們再來看上面提到的target文件
示例代碼如下:
.h
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@interface Target_ModuleB : NSObject
- (UIViewController *)Action_viewController:(NSDictionary *)params;
@end
.m
#import "Target_ModuleB.h"
#import "ModuleBViewController.h"
@implementation Target_ModuleB
- (UIViewController *)Action_viewController:(NSDictionary *)params {
ModuleBViewController *VC = [[ModuleBViewController alloc] init];
return VC;
}
@end
從上面的實現(xiàn)文件中,我們可以看到,Target文件的作用也很簡單,就是為我們提供導航跳轉的目標控制器實例對象。這里的目標控制器實例就是業(yè)務組件B的ModuleBViewController 實例。
細心的小伙伴們發(fā)現(xiàn),咦!我們在第九步中打印出來的target 和 action 不就正是Target文件的Target_ModuleB 和 Action_viewController: 。
上面我們只是串講了業(yè)務組件B的一系列流程,業(yè)務組件A的用法和業(yè)務組件B的用法一樣,如果后面再有業(yè)務組件C,D,都是一樣的道理,就不再一一講解了。
好了,現(xiàn)在小伙伴們應該看懂了這一連串的工作流程了吧,如果還沒有看懂,可以看看??參考文獻中的更多用法。作者建議直接運行提供的示例Demo項目進行調試,這樣便于理解各個組件之間的關系。
組件化工程示例項目地址
最后,我們再來看張組件化完整的架構圖:

總結
上面我們講解的只是簡單的項目組件化架構的基礎框架搭建,但是在真正的企業(yè)開發(fā)中,我們只搭建這樣一個簡單項目框架結構還遠遠不能滿足需求的開發(fā),我們還需要在項目框架中添枝加葉來滿足現(xiàn)有需求。在上面提供的示例Demo中,我將電子發(fā)票業(yè)務組件獨立成一個完整的工程,并結合了當下比較流行的MVVM設計模式和RAC數據綁定框架來實現(xiàn)電子發(fā)票模塊的功能開發(fā)。如果有小伙們對 MVVM + RAC 實戰(zhàn)開發(fā)感興趣的,可以單獨 install 電子發(fā)票工程查看,工程地址:iOS-MVVM-RAC
好了,又寫到凌晨了,不早了,本篇教程到此就講完啦。下篇教程講解如何使用MVVM+RAC進行實戰(zhàn)開發(fā)。小伙伴們,感覺文章對你有幫忙,簡書點個贊唄,開源組件化工程項目 RAC+MVVM 也幫忙點個 star ,先謝過啊。
參考文獻
- https://casatwy.com/modulization_in_action.html
- https://github.com/casatwy/CTMediator
- https://github.com/alibaba/BeeHive/blob/master/README-CN.md
- http://limboy.me/tech/2016/03/10/mgj-components.html
- https://github.com/meili/MGJRouter
更多文章
- 作者React Native開源項目OneM地址(按照企業(yè)開發(fā)標準搭建框架完成開發(fā)的):https://github.com/guangqiang-liu/OneM:歡迎小伙伴們 star
- 作者簡書主頁:包含60多篇RN開發(fā)相關的技術文章http://www.itdecent.cn/u/023338566ca5 歡迎小伙伴們:多多關注,多多點贊
- React Native QQ技術交流群(600+ RN工程師):620792950 歡迎小伙伴進群交流學習
- iOS QQ技術交流群:678441305 歡迎小伙伴進群交流學習