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

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)

分享創造快樂