隨著公司業務需求的不斷迭代發展,工程的程式碼量和業務邏輯也越來越多,原始的開發模式和架構已經無法滿足我們的業務發展速度了,這時我們就需要將原始專案進行一次重構大手術了。這時我們應該很清晰這次手術的動刀口在哪,就是之前的高度耦合的業務元件和功能元件,手術的目的就是將這些耦合拆分成互相獨立的各個元件。
工程效果預覽
元件化工程示例專案地址
下面我們圍繞這幾個問題來展開講解
- 為什麼要用元件化,它給我們帶來哪些優勢
- 各個元件該如何進行拆分,拆分的顆粒度該如何控制
- 如何從零到一搭建元件化架構專案
為什麼要用元件化
我們先來張圖看看在沒有使用元件化前,我們各個模組間的依賴關係
從上面這種各個業務元件的依賴關係來看,他們是互相依賴的,業務元件和業務元件間產生了嚴重的耦合關係,這樣一來對我們工程的擴充套件性就會大大的降低,維護成本就會變高。
舉個例子:假設某天產品經理說,我們們公司的業務發展的太好了,我們們的營銷模組需要獨立出來成一個單獨的應用,以便於我們們可以新增更多高效的營銷手段。這時我們就傻眼了,需要獨立出一個app出來,這可怎麼搞啊,營銷模組的程式碼和其他的很多業務程式碼耦合在一起了,現在要獨立出來,那就只能重新寫一個營銷應用了,之前的程式碼剝離不乾淨了。
從上面我們列舉的一個簡單的例子可以體會到:在專案沒有做到真真意義上的元件化之前,各個業務模組和業務模組間的高度耦合,功能元件和功能元件間的高度耦合對未來公司的業務擴充套件來說,成本很高,不能做到同樣業務邏輯的程式碼的高度複用,這樣對我們開發來說也是效率的降低。
好了,有的同學可能會說,既然上面各個模組間耦合這麼高,那我就來將這些耦合解耦,於是,可能會出現下面這張圖的模組間的關係。
從下面這張圖來看,我們發現,現在確實能做到各個業務模組間完全的解耦了,他們不再互相依賴了,同時我們引入了一箇中間排程者的一個角色,現在是各個業務模組和這個中間排程者角色產出了嚴重的依賴。我們思考下發現,我們的各個業務模組依賴這個中間排程者,這個是完全正常的,因為他們需要這個排程者來做統一的事件分發工作,但是這個排程者卻又依賴了每個業務模組,這層依賴是有必要的嗎?我們回頭想想真正的元件化開發是完全的去依賴化,這個依賴是完全沒有必要的。例如:假設我們現在有一個新的B APP需要開發,這時我們也需要用到這個中間排程者元件,但是我們不能直接拿過來用,因為它又依賴了很多A App的業務元件。因此,我們的元件化架構設計又需要一次升級變更了,升級成如下圖所示的模型。
從上面的這張圖,我們可以看出,各個業務模組間只會依賴中間排程者,並且中間排程者不對各個模組產生任何的依賴。
好了,從上面的三張圖之間的對比,我們就可以很好的理解為什麼我們的工程急需要實現元件化架構開發了,以及各自的優劣勢。
各個元件該如何進行拆分
關於元件該如何拆分,這個沒有一個完整的標準,因為每個公司的業務場景不一樣,對應衍生出來的各個業務模組也就不一樣,所以業務元件間的拆分,這個根據自己公司的業務模組來進行合理的劃分即可。這裡我們來說下整個工程的元件大致的劃分方向
- 專案主工程:當我們工程完全使用元件化架構進行開發後,我們會驚奇的發現我們的主工程就成了一個空殼子工程。因為所有的主工程呈現出來的內容都被拆分成了各個獨立的業務元件了,包括各個工具元件也是各自互相獨立的。這樣我們發現開發一個完整的APP就像是搭建樂高積木一樣,各個部件都有,任我們隨意的組合搭建,這樣是不是感覺很爽。
- 業務元件:業務元件就是我們上面示例圖所示的各個獨立的產品業務功能模組,我們將其封裝成獨立的元件。例如示例Demo中的電子發票業務元件,業務元件A,業務元件B。我們通過組裝各個獨立的業務元件來搭建一個完整的APP專案。
- 基礎工具類元件:基礎工具類是各個互相獨立,沒有任何依賴的工具元件。它們和其它的工具元件、業務元件等沒有任何依賴關係。這類元件例如有:對陣列,字典進行異常保護的Safe元件,對陣列功能進行擴充套件Array元件,對字串進行加密處理的加密元件等等。
- 中介軟體元件:這個元件比較特殊,這個是我們為了實現元件化開發而衍生出來的一個元件,上面示例圖中的中間排程者就是一個功能獨立的中介軟體元件。
- 基礎UI元件:檢視元件就比較常見了,例如我們封裝的導航欄元件,Modal彈框元件,PickerView元件等。
- 業務工具元件:這類元件是為各個業務元件提供基礎功能的元件。這類元件可能會依賴到其他的元件。例如:網路請求元件,圖片快取元件,jspatch元件等等
至於元件的拆分顆粒度,這個著實不好去斷定,因人而異,不同的需求功能複雜度拆分出來的元件大小也不盡相同
如何從零到一搭建元件化架構
在講如何從零到一來實現一個元件化架構專案前,我們需要熟練掌握使用pod來製作元件庫。下面我們就圍繞提供的元件化示例專案來展開講解。
首先,我們來看示例Demo中包含哪些業務元件(如下圖所示:):
示例Demo中,我提供了三個業務元件來作為演示效果,其中業務模組A和業務模組B是臨時業務模組元件,電子發票業務元件時真實的企業需求功能元件。
我們再來看下示例Demo中都提供了哪些工具元件(如下圖所示)
注意了:這裡提供的6個工具元件也都是作者已經封裝好的功能元件,大家也可以直接 install 安裝使用的哦。
詳細操作步驟
第一步:
我們先建立一個空的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專案進行除錯,這樣便於理解各個元件之間的關係。
元件化工程示例專案地址
最後,我們再來看張元件化完整的架構圖:
總結
上面我們講解的只是簡單的專案元件化架構的基礎框架搭建,但是在真正的企業開發中,我們只搭建這樣一個簡單專案框架結構還遠遠不能滿足需求的開發,我們還需要在專案框架中添枝加葉來滿足現有需求。在上面提供的示例Demo中,我將電子發票業務元件獨立成一個完整的工程,並結合了當下比較流行的MVVM設計模式和RAC資料繫結框架來實現電子發票模組的功能開發。如果有小夥們對 MVVM + RAC 實戰開發感興趣的,可以單獨 install 電子發票工程檢視,工程地址:iOS-MVVM-RAC
好啦,又一次寫到凌晨了,不早了,本篇教程到此就講完了。下篇教程講解如何使用MVVM+RAC進行實戰開發。小夥伴們,感覺文章對你有幫忙,幫忙點個讚唄,開源元件化工程專案 iOS-Component-Pro 也幫忙點個 star 吧 ,先謝過了。
參考文獻
本篇文章主要借鑑了casatwy的CTMediator思想重新實踐了一遍,下面也有蘑菇街的MGJRouter 和 阿里的 BeeHive 供大家學習參考。
- casatwy.com/modulizatio…
- github.com/casatwy/CTM…
- github.com/alibaba/Bee…
- limboy.me/tech/2016/0…
- github.com/meili/MGJRo…
更多文章
- 作者開源React Native專案OneM(按照企業開發標準搭建框架):OneM:歡迎小夥伴們 star
- 作者開源mpvue美團外賣小程式:mpvue-meituan:歡迎小夥伴們 star
- 作者掘金技術專欄:juejin.im/user/590ebe…
- 作者簡書主頁:包含60多篇RN開發相關的技術文章www.jianshu.com/u/023338566… 歡迎小夥伴們:多多關注,多多點贊
- React Native QQ技術交流群(600+ RN工程師):620792950 歡迎小夥伴進群交流學習
- iOS QQ技術交流群:678441305 歡迎小夥伴進群交流學習