iOS 一個輕量級的元件化思路

Ly夢k發表於2018-11-11

前言

說起元件化大家應該都不陌生,不過也再提一下,由於業務的複雜度擴充套件,各個模組之間的耦合度越來越高,不但造成了“牽一髮動全身”的尷尬境地,還增加了測試的重複工程,此時,元件化就值得考慮了。元件化就是將APP拆分成各個元件(或者說模組),同時解除這些元件之間的耦合,然後通過路由中介軟體將專案所需要的元件結合起來。這樣做的好處有:

  • 解耦合,增強可移植性,不用再自身業務模組中大量引入其他業務的標頭檔案。
  • 提高複用性,如果其他專案中有類似的功能,直接將模組引入稍作修改就能使用了。
  • 減少測試成本,當修改或者迭代某個小元件的過程中就不用進行大規模的迴歸測試。

網上關於元件化的方案不少,流傳最廣的是蘑菇街元件化的技術方案iOS應用架構談 元件化方案這裡不對大佬們的方案妄加評價,感興趣的同學可以自己看看。這裡我們聊聊另外的一種方式Protocol-Moudle

思路

在iOS中,協議(Protocol)定義了一個綱領性的介面,所有類都可以選擇實現。它主要是用來定義一套物件之間的通訊規則。protocol也是我們設計時常用的一個東西,相對於直接繼承的方式,protocol則偏向於組合模式。他使得兩個毫不相關的類能夠相互通訊,從而實現特定的目標。

在之前的一篇文章ResponderChain+Strategy+MVVM實現一個優雅的TableView中我們用到了protocol來為View提供公共的方法:

- (void)configCellDateByModel:(id<QFModelProtocol>)model;

為Model提供公共的方法:

- (NSString *)identifier;
- (CGFloat)height;
複製程式碼

那麼我們也可以以此來構建一個輕量級的路由中介軟體,定義一套各個元件的通訊規則,各自管理和維護各自的模組,對外提供必要的介面。

實踐

首先看一下這個Demo的結構圖和執行效果

結構

效果

路由

好了我們看看路由的一些細節,它只需要提供兩個關鍵的東西:

  1. 提供路由器單例
  2. 獲取對應的Moudle
    • 通過Protocol獲取
    • 通過URL獲取

首先提供單例:

+ (instancetype)router {
    
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _router = [[self alloc]init];
    });
    return _router;
}
複製程式碼

這樣的單例可能沒有人不會寫,but,這其實這僅僅是個“假的”單例,不信你可以使用[QFRouter router][[QFRouter alloc]init]以及[router copy]試試他們會不會生成三個記憶體地址不同的例項。可能你會說,誰會無聊這麼幹?但是如果設計的時候能更嚴謹的規避這種坑不會更好麼。 那麼怎麼才能才能做一個嚴謹的單例呢?可以重寫他的alloccopy方法避免建立多個例項,你可以在Demo工程中看到細節,這裡不做展開。

迴歸正題,我們看看如何獲取Module

通過Runtime的反射機制,我們可以通過NSString獲取一個class進而建立對應的物件,而Protocol又可以得到一個NSString,那麼是否可以由此入手呢?答案是可以的:

- (Class)classForProtocol:(Protocol *)protocol {
    NSString *classString = NSStringFromProtocol(protocol);
    return NSClassFromString(classString);
}
複製程式碼

這裡傳入一個protocol即可獲取對應的Module的class,再通過class即可以得到對應的Module的object。

通過Protocol或者URL獲取對應的Module:

#pragma mark - Public
- (id)interfaceForProtocol:(Protocol *)protocol {
    Class class = [self classForProtocol:protocol];
    return [[class alloc]init];
}

- (id)interfaceForURL:(NSURL *)url {
    id result = [self interfaceForProtocol:objc_getProtocol(url.scheme.UTF8String)];
    NSURLComponents *cp = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:NO];
    [cp.queryItems enumerateObjectsUsingBlock:^(NSURLQueryItem * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        [result setValue:obj.value forKey:obj.name];//KVC設定
    }];
    return result;
}

複製程式碼

這裡有個瑕疵就是呼叫方(外部元件)需要知道這個目標元件對外暴露的協議名稱,不過既然是協議,對外公開應該不算大問題吧,呼叫例項如下:
wayOne:

id <MoudleHome>homeMoudle = [[QFRouter router]interfaceForProtocol:@protocol(MoudleHome)];
複製程式碼

這樣就拿到了對應的目標元件例項,通過對外暴露的屬性可以對其進行傳值,通過其回撥block則可以拿到回撥引數。

wayTwo:

id <MoudleMe>meMoudle = [[QFRouter router]interfaceForURL:[NSURL URLWithString:@"MoudleMe://?paramterForMe=ModuleMe"]];
複製程式碼

這裡通過url傳入,通過KVC設定其屬性值。同樣地,通過其回撥block則可以拿到回撥引數。

公共協議

通過上面我們瞭解到:通過Protocol可以獲取對應的元件例項,那麼這個協議放在哪兒?如何管理呢?
在日常開發過程中,跨元件的互動場景最多的應該就是:從元件A附帶引數跳轉到元件B的某個頁面,元件B的這個頁面中做一些操作,再回到元件A(可能有回撥引數,也可能不回撥引數),那麼我們的協議應該能處理這兩個最常見和基礎的操作,所以給protocol定義了兩個屬性:

typedef void(^QFCallBackBlock)(id parameter);

#pragma mark - 基礎協議
@protocol QFMoudleProtocol <NSObject>

/// 暴露給元件外部的控制器,一般為該元件的主控制器
@property (nonatomic, weak) UIViewController *interfaceViewController;
/// 回撥引數
@property (nonatomic, copy) QFCallBackBlock callbackBlock;

@end
複製程式碼

這裡的interfaceViewController為何宣告成了weak屬性?這個問題先留一下,後面會聊到這一點。

有了這裡的兩個屬性我們即可完成,對應的跳轉和引數回撥,但是如何正向傳值呢?

應該還需要對應的屬性來做入參,但是元件何其多,入參何其多,如果都把正向的屬性寫入這裡面,那麼隨著時間和業務的增長,這個協議可能會十分雜亂和臃腫。

所以這裡把這個協議定為基礎協議,對應的元件都繼承自它,然後定義各自的需要的入參屬性:

首頁元件:

#pragma mark - ”首頁“元件
@protocol MoudleHome <QFMoudleProtocol>

/// 元件“Home”首頁所需要的引數
@property (nonatomic, copy) NSString *paramterForHome;

/// 元件“Home”中詳情頁面所需要的引數
@property (nonatomic, copy) NSString *titleString;

/// 元件“Home”中詳情頁面所需要的引數
@property (nonatomic, copy) NSString *descString;

/// 元件“Home”所需要暴露的特殊介面,比如其他元件也要跳轉到該頁面
@property (nonatomic, weak) UIViewController *detailViewController;

@end

複製程式碼

可以看到,由於首頁元件需要對外暴露一個主頁面 QFHomeViewController 和詳細頁面 QFDetailViewController所以引數會多一點。

我的元件:

#pragma mark - “我的”元件
@protocol MoudleMe <QFMoudleProtocol>

/// 元件“Me”所需要的引數
@property (nonatomic, copy) NSString *paramterForMe;

@end
複製程式碼

而“我的”元件,只對外提供一個QFMeViewController頁面,引數比較簡單。

這樣,基本算是達成了對協議的處理,但是無可避免的問題就是: 這個公共協議中定義了各個元件的協議,所以需要對多個開發團隊可見,感覺這也是元件化過程中的一個普遍問題,目前沒找到好的解決方式。

Module

上面我們說到了公開protocol中定義了一些屬性,比如interfaceViewController 那麼這些屬性由誰提供呢?沒錯,就是Module,通過上面的步驟我們可以獲取到對應的Module例項,但是我們跳轉需要的是Controller,所以,在此時就需要Module的幫助了, Module通過公共協議定義的屬性為外部提供Controller介面:

- (UIViewController *)interfaceViewController {
    QFHomeViewController *homeViewController = [[QFHomeViewController alloc]init];
    homeViewController.interface = self;
    interfaceViewController = (UIViewController *)homeViewController;
    return interfaceViewController;
}
複製程式碼

因為Module是在對應的元件中,所以可以隨意引用自己元件內部的標頭檔案完成初始化,
而對應的控制器中,需要元件外部的引數,所以這裡把Module例項也暴露給對應的控制器例項,也就是homeViewController.interface = self;所做的事情。

在上面說協議的時候我們提到為什麼要使用weak,至此,應該比較明朗了 ———— 打破迴圈強引用。

通過公共協議解耦獲取到Module,Module完成為元件內和元件外的搭橋鋪路工作,由此,使得跨元件傳值、呼叫、引數回撥得以實現。更多細節請看QFMoudle

由於時間關係,如何製作私有庫就不再贅述了,有需要歡迎留言,我們一起手把手建立一個屬於你自己的pod私有庫。

後記

在這種元件化的實踐中,一般會把對應的元件、路由以及公共協議做成pod私有庫,而需要多個團隊知道的也就只有這個公共協議庫,把所有的協議放入這個公開協議庫中,每次升級也只需要更新這個庫就好了。

關於對元件化的態度:上面說了那麼多好處,下面說說弊端(親身體會)
如果團隊規模較小業務複雜度較低的話,不太建議做私有庫,原因:

  1. 它的 修改 -> 升級 -> 整合 -> 測試是一個比較耗時的過程,可能一點小小的改動(比如改個顏色,換個背景)需要耗費成倍的時間。
  2. 增加專案的呼叫複雜度,增加新成員的熟悉成本。

任何事情都是雙面的,有利有弊,望各位看官自行取捨。最後由於筆者文筆有限,若給您造成了困擾,實在抱歉,有任何疑問or意見or建議,歡迎交流討論,當然,如果對您有用,希望順手給個Star,點個關注。贈人玫瑰,手留餘香,您的支援是我最大的動力!

相關文章