一,概述
隨着公司業務需求的不斷迭代發展,工程的代碼量和業務邏輯也越來越多,原始的開發模式和架構已經無法滿足我們的業務發展速度了,這時我們就需要將原始項目進行一次重構大手術了。這時我們應該很清晰這次手術的動刀口在哪,就是之前的高度耦合的業務組件和功能組件,手術的目的就是將這些耦合拆分成互相獨立的各個組件。
二,為什么要用組件化
我們先來張圖看看在沒有使用組件化前,我們各個模塊間的依賴關系


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


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


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


五,詳細操作步驟
-
第一步:
我們先創建一個空的iOS工程項目:MainProject,這個空項目作為我們的主工程項目,就是上面所說的殼子工程項目,然后初始化pod,這里不清楚pod的使用的小伙伴們請自行查閱資料。 - 第二步:
我們創建一個空工程項目:ModuleA,這個ModuleA 項目作為我們的業務A組件。然后我們初始化pod,初始化podspec文件。
- 第三步:
我們創建一個空工程項目:ModuleB,這個ModuleB 項目作為我們的業務B組件。然后我們初始化pod,初始化podspec文件。
- 第四步:
我們創建一個空工程項目:ComponentMiddleware,這個項目就是我們上面所說的中間調度者。然后我們初始化pod,初始化podspec文件。
- 第五步:
我們創建一個空工程項目: ModuleACategory,這個工程是對應業務組件A的一個分類工程。然后我們初始化pod,初始化podspec文件。
- 第六步:
我們創建一個空工程項目: ModuleBCategory,這個工程是對應業務組件B的一個分類工程。然后我們初始化pod,初始化podspec文件。
好了,上面的主工程和兩個業務組件工程,以及兩個組件分類工程都已創建完畢,下面我們來講解他們各個之間如何工作的。我就從主工程加載業務組件開始往下捋,順藤摸瓜式的引出每個工程的用意。
- 第七步:
我們在主工程MainProject的Podfile中引入我們的業務組件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 到業務組件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 實現。
.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
我們發現這個分類實現非常的簡單,就是對外暴露一個函數,然后執行
[self performTarget:@"ModuleB" action:@"viewController" params:params shouldCacheTarget:NO];
並將執行的返回值返回出去。
這個分類的作用你可以理解為我們提前約定好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
。細心的小伙伴們就會發現,我們沒有看到過哪里有這個
Target_ModuleB
類啊,更沒有看到Target_ModuleB
調用它的Action_viewController:
方法啊。是的,這個
Target_ModuleB
類和類的Action_viewController
方法就在第十步中講解到。
- 第十步:
終於到了最后一步了,寫的好艱辛,嗯,小伙們不要捉急,快了,快講完了
細心的小伙們發現,我們上面講的9步中,好像都沒有提業務組件B的東西。是的,業務組件B除了提供組件B的業務功能外,業務組件B還需要為我們提供一個Target文件。
我們先來看下業務組件B的業務代碼:
示例代碼如下:
#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業務組件"; 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業務功能組件" 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. }
我們發現,業務組件B的業務代碼也很簡單,就是做一個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
從上面的實現文件中,我們可以看到,Target文件的作用也很簡單,就是為我們提供導航跳轉的目標控制器實例對象。這里的目標控制器實例就是業務組件B的
ModuleBViewController
實例。細心的小伙伴們發現,咦!我們在第九步中打印出來的
target
和action
不就正是Target文件的Target_ModuleB
和Action_viewController:
。上面我們只是串講了業務組件B的一系列流程,業務組件A的用法和業務組件B的用法一樣,如果后面再有業務組件C,D,都是一樣的道理,就不再一一講解了。
好了,現在小伙伴們應該看懂了這一連串的工作流程了吧,如果還沒有看懂,可以看看Casa的講解CTMediator。作者建議直接運行提供的示例Demo項目進行調試,這樣便於理解各個組件之間的關系。
六,總結
上面我們講解的只是簡單的項目組件化架構的基礎框架搭建,但是在真正的企業開發中,我們只搭建這樣一個簡單項目框架結構還遠遠不能滿足需求的開發,我們還需要在項目框架中添枝加葉來滿足現有需求。
最后,我們再來看張組件化完整的架構圖:

參考文獻