1. 程式人生 > >iOS元件化實踐方案-LDBusMediator煉就

iOS元件化實踐方案-LDBusMediator煉就

一、中小型App為什麼要元件化

當專案App處於起步階段、各個需求模組趨於成熟穩定的過程中,元件化也許並沒有那麼迫切,甚至考慮元件化的架構可能會影響開發效率和需求迭代。而當專案迭代到一定時期之後,便會出現一些相對獨立的業務功能模組,而團隊的規模也會隨著專案迭代逐漸增長,這便是中小型應用考慮元件化的時機了。

為了更好的分工協作,團隊會安排團隊成員各自維護一個相對獨立的業務元件。這個時候我們引入元件化方案,一是為了解除元件之間相互引用的程式碼硬依賴,二是為了規範元件之間的通訊介面; 讓各個元件對外都提供一個黑盒服務,而元件工程本身可以獨立開發測試,減少溝通和維護成本,提高效率。

進一步發展,當團隊涉及到轉型或者有了新的立項之後,一個團隊會開始維護多個專案App,而多個專案App的需求模組往往存在一定的交叉,而這個時候元件化給我們的幫助會更大,我只需要將之前的多個業務元件模組在新的主App中進行組裝即可快速迭代出下一個全新App。

二、如何開始元件化工作

2.1 元件化的架構目標

在詳細說如何具體開始元件化工作之前,我們對於元件化的期望應該是這樣的,一個團隊維護一到兩個獨立App,每個獨立App除開包含一些產品相關的非獨立模組集之外,還需要用一些獨立的業務元件進行組裝。 而不管是產品的非獨立模組集、還是獨立業務元件都需要底層公共庫和基礎庫的支援。如下圖所示:


元件化目標圖.png

2.2 元件化第一步-剝離公共庫和產品基礎庫

在具體的專案開發過程中,我們使用cocoapod的元件依賴管理利器已經開始從Github上引入了一些第三方開源的基礎庫,比如說AFNetworking、SDWebImage、SVProgressHUD、ZipArchive等。除開這些第三方開源基礎庫之外,我們還需要做的事情就是將一些基礎元件從主工程剝離出來,形成產品自己的私有基礎庫倉庫,為我們進行業務獨立元件的分離做準備。

這部分我將其分為兩類:一類是公共基礎庫,用於跨產品使用;一類是產品基礎庫,在某個產品中強相關依賴使用。這裡以我們自己產品劃分為例,概述一下這兩類庫都包括哪些基礎元件:

公共庫包括:元件化中介軟體網路診斷第三方SDK管理封裝、長連線相關、Patch相關、網路和頁面監控相關、使用者行為統計庫、第三方分享庫、JSBridge相關、關於Device+file+crypt+http的基礎方法等。

產品基礎庫包括:通用的WebViewContainer元件(封裝了JSBridge)、自定義數字鍵盤、表情鍵盤、自定義下拉列表、迴圈滾動頁面、AFNeworking封裝庫(對上層業務隱藏AF的直接引用)、以及其他自定義的UI基礎元件庫。

2.2 元件化第二步-獨立業務模組單獨成庫

在基礎庫成體系的基礎上,我們就可以開始按照需求定性將一些相對獨立的業務模組獨立成庫,單獨在一個工程上進行開發、測試。

往往在這個階段有一個誤區,千萬不能為了元件化而強行將一些耦合嚴重的業務模組分出。如果在拆分過程中,拆分模組跟其他模組耦合太嚴重,那就先放棄這部分模組的獨立,畢竟產品是不會單獨拿出時間給你做元件化的。

另外拆分的粒度需要大一點,需要在功能模組的基礎上,將業務獨立性考慮進去,如果沒有就不拆,等以後有了相對獨立的模組之後再拆。

2.3 元件化第三步-對外服務介面最小化

元件化不是一蹴而就的,我們在完成第二步的時候並不要強行要求去掉元件之間程式碼的硬依賴,只需要保證單獨拆分出來的工程可以獨立執行和測試,並且能夠通過引用保證其他業務元件和主工程的依賴使用即可。

當第二步完成之後,我們可以在此基礎上總結其他元件和主工程的需求呼叫,根據需求總結和抽象出當前業務元件對外服務的最小化介面以及頁面跳轉呼叫。經過多次總結,我們可以發現元件之間的通訊需求無外乎三個方面:URL導航+服務介面呼叫+訊息變數定義。如下圖所示:


元件通訊需求.png

在這個階段,我們大多數應用會選擇JLRoute(蘑菇街的MGJRoute方案也類似)去做URL導航的需求,會通過OpenServiceImpl + Protocol的方案(將所有對外服務提供的介面都在OpenServiceImpl中實現)去做元件間的服務呼叫,訊息變數的宣告可以放到對外服務介面的Protocol定義中。

到了這個階段,我們的業務元件也已經相對獨立,JLRoute能夠去掉頁面引用的頭標頭檔案依賴。OpenServiceImpl+Protocol也將我們最小化的對外服務介面約束到Protocol介面檔案中。 如果對於專案元件化要求不高的話,到這一步就可以了。

三、徹底元件化-LDBusMediator煉就

3.1 元件化方案不徹底之處和JLRoute的缺陷

通過第二部分的講述,我們的元件化工作差不多完成了80%,但是我們依然發現,元件化並不夠徹底。

先來看服務呼叫方面,我們需要對外提供OpenServiceImpl的標頭檔案,外部模組仍然保持著對業務元件的強依賴,OpenServiceImpl的不相容變化必然導致所有呼叫部分的更改,我們期望的黑盒服務便無法實現。如果所有類別的服務介面都在OpenServiceImpl中實現,OpenServiceImpl中的程式碼會越來越多,難以維護和管理。 另外Protocol檔案和OpenServiceImpl的標頭檔案都需要對外披露,如果放到元件實現中,兩個元件相互之間有呼叫,就會導致Podspec的相互迴圈依賴。

再看URL導航方面,在我們的專案中,我們在ViewController的類別中通過load方法註冊URL-Block,這樣能夠解決JLRoute的中心化註冊問題,但是JLRoute仍然存在其他一些缺陷。JLRoute去中心化的具體使用方式如下:

+ (void)load
{
    @autoreleasepool {
        [JLRoutes addRoute:@"/xxxx" handler:^BOOL(NSDictionary *parameters) {

            UIViewController *baseViewController = parameters[kLDRouteViewControllerKey];
            if (!baseViewController) {
                baseViewController = [UIViewController topmostViewController];
            }
            if (!baseViewController) {
                return YES;
            }

            XXXXViewController *viewController = [[XXXXViewController alloc] init];

            if ([baseViewController isKindOfClass:[UINavigationController class]]) {
                [(UINavigationController*)baseViewController pushViewController:viewController animated:YES];
            }else if (baseViewController.navigationController) {
                [baseViewController.navigationController pushViewController:viewController animated:YES];
            } else {
                UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:viewController];
                [baseViewController presentViewController:navController animated:YES completion:NULL];
            }
            return YES;
        }];
    }
}

如上所用,JLRoute的缺陷如下:

  • url短鏈分散式註冊時,導航程式碼的重複拷貝;
  • 無法通過URL返回一個controller例項;(TabController也就無法從獨立業務元件中不引用Controller標頭檔案獲取Controller例項完成設定)
  • class的load方法完成註冊,太多對啟動時Main執行緒有影響;
  • 同一個url短鏈的導航方式單一固定,依賴註冊
  • 單一業務元件中可導航URL分散,無法統一檢視;
  • Debug階段url傳遞引數錯誤、not found沒有提示;

3.2 LDBusMediator總體方案

針對元件化不徹底的實際問題,結合之前手淘分享的匯流排架構以及蘑菇街的元件化分享部落格,我們完成了一個通用的LDBusMediator中介軟體幫助我們徹底完成元件化。

LDBusMediator開源Git地址:

我們先來看總體的元件化方案:所有的業務元件通過Connector連線到匯流排中,Connector需要遵循Connector Protocol方可接入。Connector協議規定了URL導航接入和服務接入的協議,Connector通過Class的Load方法將自己的例項註冊到中介軟體的Cache陣列中,方便其他元件在呼叫時中介軟體可以通過服務發現的方式進行URL導航和服務呼叫。(具體見如下的圖示)

@implementation Connector_A

#pragma mark - register connector

/**
 * 每個元件的實現必須自己通過load完成掛載;
 * load只需要在掛載connector的時候完成當前connecotor的初始化,掛載量、掛載消耗、掛載所耗記憶體都在可控範圍內;
 */
+(void)load{
    @autoreleasepool{
        [LDBusMediator registerConnector:[self sharedConnector]];
    }
}
@end

3.3 LDBusMediator-URL導航方案

URL導航的匯流排中介軟體方案很簡單,只需要在Connector中實現URL導航接入的介面即可,如圖所示:


LDBusMediator-URL導航.png

具體使用如下:

@protocol LDBusConnectorPrt <NSObject>

-(BOOL)canOpenURL:(nonnull NSURL *)URL;

- (nullable UIViewController *)connectToOpenURL:(nonnull NSURL *)URL params:(nullable NSDictionary *)params;

@end

@implementation Connector_A

#pragma mark - LDBusConnectorPrt 

/**
 * (1)當呼叫方需要通過判斷URL是否可導航顯示介面的時候,告訴呼叫方該元件實現是否可導航URL;可導航,返回YES,否則返回NO;
 * (2)這個方法跟connectToOpenURL:params配套實現;如果不實現,則呼叫方無法判斷某個URL是否可導航;
 */
-(BOOL)canOpenURL:(nonnull NSURL *)URL{
    if ([URL.host isEqualToString:@"ADetail"]) {
        return YES;
    }

    return NO;
}
@end
/**
 * (1)通過connector向busMediator掛載可導航的URL,具體解析URL的host還是path,由connector自行決定;
 * (2)如果URL在本業務元件可導航,則從params獲取引數,例項化對應的viewController進行返回;如果引數錯誤,則返回一個錯誤提示的[UIViewController paramsError]; 如果不需要中介軟體進行present展示,則返回一個[UIViewController notURLController],表示當前可處理;如果無法處理,返回nil,交由其他元件處理;
 * (3)需要在connector中對引數進行驗證,不同的引數呼叫生成不同的ViewController例項;也可以通過引數決定是否自行展示,如果自行展示,則使用者定義的展示方式無效;
 * (4)如果掛接的url較多,這裡的程式碼比較長,可以將處理方法分發到當前connector的category中;
 */
- (nullable UIViewController *)connectToOpenURL:(nonnull NSURL *)URL params:(nullable NSDictionary *)params{
    //處理scheme://ADetail的方式
    // tip: url較少的時候可以通過if-else去處理,如果url較多,可以自己維護一個url和ViewController的map,加快遍歷查詢,生成viewController;
    if ([URL.host isEqualToString:@"ADetail"]) {
        DemoModuleADetailViewController *viewController = [[DemoModuleADetailViewController alloc] init];
        if (params[@"key"] != nil) {
            viewController.valueLabel.text = params[@"key"];
        } else if(params[@"image"]) {
            id imageObj = params[@"image"];
            if (imageObj && [imageObj isKindOfClass:[UIImage class]]) {
                viewController.valueLabel.text = @"this is image";
                viewController.imageView.image = params[@"image"];
                [[UIApplication sharedApplication].keyWindow.rootViewController presentViewController:viewController animated:YES completion:nil];
                return [UIViewController notURLController];
            } else {
                viewController.valueLabel.text = @"no image";
                viewController.imageView.image = [UIImage imageNamed:@"noImage"];
                [[UIApplication sharedApplication].keyWindow.rootViewController presentViewController:viewController animated:YES completion:nil];
                return [UIViewController notURLController];
            }
        } else {
            // nothing to do
        }
        return viewController;
    }


    else {
        // nothing to to
    }

    return nil;
}

通過LDBusMediator的URL導航方案,有效的解決了前文提出的JLRoute的缺陷:

  1. url短鏈分散式註冊時,導航程式碼的重複拷貝
    • LDBusNavigator+PresentMode:將通用的導航方式即成到LDBusNavigator中,而無需每個URL註冊時重複拷貝。
  2. 無法通過URL返回一個controller例項;(TabController)
    • *URL-Block —> URL-ViewController例項:將之前JLRoute的url-block方式改成了url-ViewController方式,即可滿足。
  3. class的load方法完成註冊,太多對啟動時Main執行緒有影響;
    • 服務發現的方式,只在load時註冊Connector例項:中介軟體只對每個業務元件的connector例項進行註冊,相比URL註冊量大量減少load使用。
  4. 同一個url短鏈的導航方式單一固定,依賴註冊
    • 呼叫時指定Present、Push、Share方式:之前JLRoute只能在註冊時候決定導航方式,通過LDBusMediator如何導航顯示由呼叫方決定,預設是Push;Share方式是指pop到導航層次中已經存在的viewController處。
  5. 單一業務元件中可導航URL分散,無法統一檢視;
    • 單一元件的connector中集中管理所有可導航URL
  6. Debug階段url傳遞引數錯誤、not found沒有提示;
    • Debug階段的錯誤Controller提示、包括引數錯誤、notFound、notSupportController:如果引數錯誤、notfound無法生成一個viewController例項,中介軟體在debug階段會提示。如果URL不支援返回一個Controller,同樣會給與提示。

3.4 LDBusMediator-服務呼叫方案

為了更好的通過中介軟體支撐元件間的服務呼叫方案,我們在元件實現和中介軟體之間增加了一層協議介面層。 每個業務元件將自己對外提供的服務介面抽象到一個統一的業務元件協議集合中。 業務元件的實現依賴自己的對外服務介面集並進行介面的實現。

每個業務元件中的協議部分有兩種:一種是服務協議,其他元件可以通過Mediator拿到對外開放的服務例項呼叫服務介面;一種是Model協議,服務協議中的介面可以給其他元件一個協議化物件,其他元件也可以組裝一個協議化物件通過引數傳入。

為了方便業務元件實現和協議集合的版本對應,需要保證協議集合的大版本(如x.y)和業務元件的大版本(如x.y.z)中的x保持一致;協議集合中一般沒有補丁版本的迭代,當其他業務元件呼叫需要增加介面進行相容版本升級(y+1),減少或者修改介面則需要協議集合和業務元件中的x同時+1(x+1); 如果自身業務元件升級不能影響對外協議介面的呼叫,升級版本主要為補丁版本迭代(z+1)或 相容版本升級(y+1);

元件協議集合 單獨通過一個Git地址進行管理,單獨配置podspec,單獨通過協議的版本倉庫進行管理;所有的協議集合的git統一放到Git的一個組中進行管理。

具體方案如下:


LDBusMediator-服務呼叫.png
@protocol LDBusConnectorPrt <NSObject>
/**
 * 業務模組掛接中介軟體,註冊自己提供的service,實現服務介面的呼叫;
 * 
 * 通過protocol協議找到元件中對應的服務實現,生成一個服務單例;
 * 傳遞給呼叫者進行protocol介面中屬性和方法的呼叫;
 */
- (nullable id)connectToHandleProtocol:(nonnull Protocol *)servicePrt;  

@end


@implementation Connector_A
/**
 * (1)通過connector向BusMediator掛接可處理的Protocol,根據Protocol獲取當前元件中可處理protocol的服務例項;
 *  (2)具體服務協議的實現可放到其他類實現檔案中,只需要在當前connetor中引用,返回一個服務例項即可;
 *  (3)如果不能處理,返回一個nil;
 */
- (nullable id)connectToHandleProtocol:(nonnull Protocol *)servicePrt{
    if (servicePrt == @protocol(ModuleAXXXServicePrt)) {
        return [[self class] sharedConnector];
    }
    return nil;
}
@end

LDBusMediator中介軟體的服務呼叫方案的優勢:

  1. 通過中介軟體支撐,不暴露任何實現檔案的標頭檔案;
    • 元件對外提供的服務通過最小化抽象的“協議介面集”披露;
    • 元件的實現Pod不暴露任何標頭檔案;
  2. 每個業務元件提供黑盒服務
    • 呼叫者不用關心具體實現細節;
    • 業務元件的實現升級、或者更換(包括整個業務元件更換)不影響呼叫者的呼叫修改;
  3. 為業務元件Framework化、自動化構建奠定基礎