作者:ZhengYaWei
連結:https://www.jianshu.com/p/7bd1b0df8976
前言
看了一些關於元件化文章,決定寫篇文章稍稍做些總結。
一、元件化的誤解
首先筆者認為元件化
這個詞用的不合適,應該改為模組化
。按照筆者的理解元件通常是指比較小的功能模組,比如在RN中,元件(component
)通常就相當於 iOS 開發中的檢視模組,如tabBar、navBar等。而模組通常是指較大粒度的業務模組,比如一個商城類專案通常會有登入模組、購物車模組、清單模組模組等。為了下文不產生歧義,下麵模組和元件代表同一個意思,都是指較大顆粒度的業務模組。
二、為什麼要元件化
隨著公司業務的不斷發展,應用的程式碼體積將會越來越大,業務程式碼耦合也越來越多,程式碼量也是急劇增加。如果僅僅完成程式碼拆分還不足以解決業務之間的程式碼耦合,而元件化是一種能夠解決程式碼耦合、業務工程能夠獨立執行的技術。
三、元件化實現流程
在實施元件化之前首先要意識到,並不是所有專案都適合元件化。首先剛起步的專案可能模組不是十分清晰,上來就實施模組化方案,很有可能對後期程式碼維護或功能擴充套件帶來很多不便之處;其次,模組化更適合大型專案且是多人開發,如果專案比較小且開發者較少,使用元件化可能只會帶來更大的工作量。
3.1 使用 pod 管理公共庫和UI元件
封裝公共庫和專案中的UI元件庫,然後製作成私有化倉庫,透過 pod 在實際專案中使用。另外針對一些第三方庫,要在第三庫的基礎上再做一層封裝,這樣後期可以更方便的替換這些第三方庫。
3.2 拆分業務模組
對一些獨立的模組進行拆分,如登入模組、購物車模組、清單模組、商品詳情模組等。實際拆分的過程中需要註意,模組的顆粒度既不能太大,也不能太小。
3.3 實施元件化方案
抽出公共庫和UI元件以及拆分完業務模組之後,接下來就是實施元件化方案。關於元件化方案筆者主要看了蘑菇街和casa的方案,總結如下。
四、蘑菇街url-block
方案
蘑菇街最初採用的是 URL 跳轉樣式。如下程式碼,啟動時透過MGJRouter 註冊元件提供的服務,把呼叫元件使用的URL和元件提供的服務block對應起來,儲存到記憶體中。在使用元件的服務時,透過URL找到對應的block,然後呼叫對應block中的服務。
//註冊
[MGJRouter registerURLPattern:@"mgj://detail?id=:id" toHandler:^(NSDictionary *routerParameters) {
NSNumber *id = routerParameters[@"id"];
// create view controller with id
// push view controller
}];
//呼叫[MGJRouter openURL:@"mgj://detail?id=404"];
再具體點,就可以看下麵這個例子。觸發WRReadingViewController類中的+ (void)gotoDetail:(NSString *)bookId
方法,展示WRBookDetailViewController
介面。其中的Mediator
就可以理解為類似MGJRouter的中間媒介。Mediator
中的cache
屬性就可以理解為上述所說的URL和block的對映表。
//Mediator.m 中介軟體
@implementation Mediator
typedef void (^componentBlock) (id param);
@property (nonatomic, storng) NSMutableDictionary *cache
- (void)registerURLPattern:(NSString *)urlPattern toHandler:(componentBlock)blk {
[cache setObject:blk forKey:urlPattern];
}
- (void)openURL:(NSString *)url withParam:(id)param {
componentBlock blk = [cache objectForKey:url];
if (blk) blk(param);
}
@end
//BookDetailComponent 元件
#import "Mediator.h"
#import "WRBookDetailViewController.h"
+ (void)initComponent {
[[Mediator sharedInstance] registerURLPattern:@"weread://bookDetail" toHandler:^(NSDictionary *param) {
WRBookDetailViewController *detailVC = [[WRBookDetailViewController alloc] initWithBookId:param[@"bookId"]];
[[UIApplication sharedApplication].keyWindow.rootViewController.navigationController pushViewController:detailVC animated:YES];
}];
}
//WRReadingViewController.m 呼叫者
//ReadingViewController.m
#import "Mediator.h"
+ (void)gotoDetail:(NSString *)bookId {
[[Mediator sharedInstance] openURL:@"weread://bookDetail" withParam:@{@"bookId": bookId}];
}
url-block
方案具有非常明顯的幾個缺點:
-
1、元件本身和呼叫者都依賴了Mediator,耦合度較大。
-
2、記憶體裡需要儲存一份
url-block
對映表,增加了額外的記憶體。 -
3、非常規物件在元件間無法進行引數傳遞,因為實際引數傳遞透過URL傳遞,只能傳遞常規的字串引數,無法傳遞類似
UIImage
、NSData
等型別。 -
4、沒有拆分遠端呼叫和本地間呼叫,本地呼叫和遠端呼叫不應該公用同一個介面,不應該以遠端呼叫的方式為本地間呼叫提供服務。遠端App呼叫處理入參的過程比本地多了一個URL解析的過程,這是遠端App呼叫特有的過程。而本地完全可以避免引入URL解析這一步驟,直接呼叫。
五、蘑菇街protocol-class
方案
由於前面的url-block
方案不能夠傳遞非常規引數,因此有了第二種方案protocol-class
。
//註冊[ModuleManager registerClass:ClassA forProtocol:ProtocolA];
呼叫
[ModuleManager classForProtocol:ProtocolA];
這種方案實際上同url-block
方案非常類似,同樣需要中介軟體維護一個對映表/字典,該對映表/字典主要用來維護protocol和class的關係。該方案主要解決了url-block
方案中的非常規引數不能傳遞的問題,但是對於元件依賴中介軟體、記憶體中維護對映表等問題依然沒有給與解答。
六、casatarget-action
方案
上述兩個方案都存在很大的問題,接下來重點看casa給出的target-action
方案,相對於前面兩種方案而言,該方案比較好。case
在文章中長篇大論說了不少蘑菇街方案的弊端,以及自己這種方案的好處。總的來說該方案是先封裝一個中間層,其中中介軟體分別提供了本地呼叫和遠端呼叫介面。對於元件而言,每個元件會包裝一層。當需要呼叫元件的時候,就會透過中間層呼叫各個元件的包裝層,比較特別的地方是中間層透過runtime
呼叫元件的包裝層,做到真正意義上的解耦,這也是該方案的核心之處。
結合實際程式碼簡單看一下該方案的實現。以下程式碼來自casa
的元件化demo。
元件A
可以理解為下麵的DemoModuleADetailViewController類
元件A的包裝層(Target)
- (UIViewController *)Action_nativeFetchDetailViewController:(NSDictionary *)params;
- (UIViewController *)Action_nativeFetchDetailViewController:(NSDictionary *)params
{
// 因為action是從屬於ModuleA的,所以action直接可以使用ModuleA裡的所有宣告
DemoModuleADetailViewController *viewController = [[DemoModuleADetailViewController alloc] init];
viewController.valueLabel.text = params[@"key"];
return viewController;
}
中間層
+ (instancetype)sharedInstance;
// 遠端App呼叫入口
- (id)performActionWithUrl:(NSURL *)url completion:(void(^)(NSDictionary *info))completion;
// 本地元件呼叫入口
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget;
- (void)releaseCachedTargetWithTargetName:(NSString *)targetName;
中間層針對元件A介面的分類
// CTMediator+CTMediatorModuleAActions.h
- (UIViewController *)CTMediator_viewControllerForDetail;
// CTMediator+CTMediatorModuleAActions.m
- (UIViewController *)CTMediator_viewControllerForDetail
{
return [self performTarget:kCTMediatorTargetA action:kCTMediatorActionNativFetchDetailViewController params:@{@"key":@"value"} shouldCacheTarget:NO];
}
呼叫
// ViewController.h
#import "CTMediator+CTMediatorModuleAActions.h"
[self presentViewController:[[CTMediator sharedInstance] CTMediator_viewControllerForDetail] animated:YES completion:nil];
如果想使用元件,呼叫者只需要依賴中間層即可,而中間層透過target-action
樣式無需依賴元件,所以達到解耦的目的。
中間層CTMediator
將遠端呼叫和本地元件間呼叫拆開處理。之所以這樣做,主要因為遠端App呼叫處理入參的過程比本地多了一個URL解析的過程,這是遠端App呼叫特有的過程,而本地呼叫無需URL解析。
該方案中採用了去model化傳遞引數,在iOS的開發中,就是以字典的方式去傳遞引數。如果元件間呼叫不對引數做去model化的設計,就會導致業務形式上被元件化了,實質上依然沒有被獨立。既然是使用了字典作為引數傳遞,自然而然就引起了hardcode
問題。為了讓呼叫更方便知道接收方需要哪些key的引數以及哪些target可以被呼叫,該方案進一步就針對每一模組採用了category
的方式,從而縮小了範圍,方便程式碼定位和閱讀。
總結
以上簡單分析了蘑菇街url-block方案、蘑菇街protocol-class以及case
的target-action方案,分析的實際很淺。其實筆者在實際開發工作中完全沒有接觸過元件化開發,只是對元件化比較感興趣,看了些文章後,簡單做一些總結。
●編號279,輸入編號直達本文
●輸入m獲取文章目錄
Web開發
更多推薦《18個技術類微信公眾號》
涵蓋:程式人生、演演算法與資料結構、駭客技術與網路安全、大資料技術、前端開發、Java、Python、Web開發、安卓開發、iOS開發、C/C++、.NET、Linux、資料庫、運維等。