歡迎光臨
每天分享高質量文章

iOS組件化方案

作者: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傳遞,只能傳遞常規的字串引數,無法傳遞類似UIImageNSData等型別。

  • 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以及casetarget-action方案,分析的實際很淺。其實筆者在實際開發工作中完全沒有接觸過組件化開發,只是對組件化比較感興趣,看了些文章後,簡單做一些總結。



編號279,輸入編號直達本文

●輸入m獲取文章目錄

推薦↓↓↓

Web開發

更多推薦18個技術類微信公眾號

涵蓋:程式人生、演算法與資料結構、黑客技術與網絡安全、大資料技術、前端開發、Java、Python、Web開發、安卓開發、iOS開發、C/C++、.NET、Linux、資料庫、運維等。

赞(0)

分享創造快樂