iOS組件化實(shí)踐方案-LDBusMediator煉就

一、中小型App為什么要組件化

當(dāng)項(xiàng)目App處于起步階段、各個需求模塊趨于成熟穩(wěn)定的過程中,組件化也許并沒有那么迫切,甚至考慮組件化的架構(gòu)可能會影響開發(fā)效率和需求迭代。而當(dāng)項(xiàng)目迭代到一定時(shí)期之后,便會出現(xiàn)一些相對獨(dú)立的業(yè)務(wù)功能模塊,而團(tuán)隊(duì)的規(guī)模也會隨著項(xiàng)目迭代逐漸增長,這便是中小型應(yīng)用考慮組件化的時(shí)機(jī)了。

為了更好的分工協(xié)作,團(tuán)隊(duì)會安排團(tuán)隊(duì)成員各自維護(hù)一個相對獨(dú)立的業(yè)務(wù)組件。這個時(shí)候我們引入組件化方案,一是為了解除組件之間相互引用的代碼硬依賴,二是為了規(guī)范組件之間的通信接口; 讓各個組件對外都提供一個黑盒服務(wù),而組件工程本身可以獨(dú)立開發(fā)測試,減少溝通和維護(hù)成本,提高效率。

進(jìn)一步發(fā)展,當(dāng)團(tuán)隊(duì)涉及到轉(zhuǎn)型或者有了新的立項(xiàng)之后,一個團(tuán)隊(duì)會開始維護(hù)多個項(xiàng)目App,而多個項(xiàng)目App的需求模塊往往存在一定的交叉,而這個時(shí)候組件化給我們的幫助會更大,我只需要將之前的多個業(yè)務(wù)組件模塊在新的主App中進(jìn)行組裝即可快速迭代出下一個全新App。

二、如何開始組件化工作

2.1 組件化的架構(gòu)目標(biāo)

在詳細(xì)說如何具體開始組件化工作之前,我們對于組件化的期望應(yīng)該是這樣的,一個團(tuán)隊(duì)維護(hù)一到兩個獨(dú)立App,每個獨(dú)立App除開包含一些產(chǎn)品相關(guān)的非獨(dú)立模塊集之外,還需要用一些獨(dú)立的業(yè)務(wù)組件進(jìn)行組裝。 而不管是產(chǎn)品的非獨(dú)立模塊集、還是獨(dú)立業(yè)務(wù)組件都需要底層公共庫和基礎(chǔ)庫的支持。如下圖所示:

組件化目標(biāo)圖.png

2.2 組件化第一步-剝離公共庫和產(chǎn)品基礎(chǔ)庫

在具體的項(xiàng)目開發(fā)過程中,我們使用cocoapod的組件依賴管理利器已經(jīng)開始從Github上引入了一些第三方開源的基礎(chǔ)庫,比如說AFNetworking、SDWebImage、SVProgressHUD、ZipArchive等。除開這些第三方開源基礎(chǔ)庫之外,我們還需要做的事情就是將一些基礎(chǔ)組件從主工程剝離出來,形成產(chǎn)品自己的私有基礎(chǔ)庫倉庫,為我們進(jìn)行業(yè)務(wù)獨(dú)立組件的分離做準(zhǔn)備。

這部分我將其分為兩類:一類是公共基礎(chǔ)庫,用于跨產(chǎn)品使用;一類是產(chǎn)品基礎(chǔ)庫,在某個產(chǎn)品中強(qiáng)相關(guān)依賴使用。這里以我們自己產(chǎn)品劃分為例,概述一下這兩類庫都包括哪些基礎(chǔ)組件:

公共庫包括:組件化中間件、網(wǎng)絡(luò)診斷、第三方SDK管理封裝、長連接相關(guān)、Patch相關(guān)、網(wǎng)絡(luò)和頁面監(jiān)控相關(guān)、用戶行為統(tǒng)計(jì)庫、第三方分享庫、JSBridge相關(guān)、關(guān)于Device+file+crypt+http的基礎(chǔ)方法等。

產(chǎn)品基礎(chǔ)庫包括:通用的WebViewContainer組件(封裝了JSBridge)、自定義數(shù)字鍵盤、表情鍵盤、自定義下拉列表、循環(huán)滾動頁面、AFNeworking封裝庫(對上層業(yè)務(wù)隱藏AF的直接引用)、以及其他自定義的UI基礎(chǔ)組件庫。

2.2 組件化第二步-獨(dú)立業(yè)務(wù)模塊單獨(dú)成庫

在基礎(chǔ)庫成體系的基礎(chǔ)上,我們就可以開始按照需求定性將一些相對獨(dú)立的業(yè)務(wù)模塊獨(dú)立成庫,單獨(dú)在一個工程上進(jìn)行開發(fā)、測試。

往往在這個階段有一個誤區(qū),千萬不能為了組件化而強(qiáng)行將一些耦合嚴(yán)重的業(yè)務(wù)模塊分出。如果在拆分過程中,拆分模塊跟其他模塊耦合太嚴(yán)重,那就先放棄這部分模塊的獨(dú)立,畢竟產(chǎn)品是不會單獨(dú)拿出時(shí)間給你做組件化的。

另外拆分的粒度需要大一點(diǎn),需要在功能模塊的基礎(chǔ)上,將業(yè)務(wù)獨(dú)立性考慮進(jìn)去,如果沒有就不拆,等以后有了相對獨(dú)立的模塊之后再拆。

2.3 組件化第三步-對外服務(wù)接口最小化

組件化不是一蹴而就的,我們在完成第二步的時(shí)候并不要強(qiáng)行要求去掉組件之間代碼的硬依賴,只需要保證單獨(dú)拆分出來的工程可以獨(dú)立運(yùn)行和測試,并且能夠通過引用保證其他業(yè)務(wù)組件和主工程的依賴使用即可。

當(dāng)?shù)诙酵瓿芍?,我們可以在此基礎(chǔ)上總結(jié)其他組件和主工程的需求調(diào)用,根據(jù)需求總結(jié)和抽象出當(dāng)前業(yè)務(wù)組件對外服務(wù)的最小化接口以及頁面跳轉(zhuǎn)調(diào)用。經(jīng)過多次總結(jié),我們可以發(fā)現(xiàn)組件之間的通信需求無外乎三個方面:URL導(dǎo)航+服務(wù)接口調(diào)用+消息變量定義。如下圖所示:

組件通信需求.png

在這個階段,我們大多數(shù)應(yīng)用會選擇JLRoute(蘑菇街的MGJRoute方案也類似)去做URL導(dǎo)航的需求,會通過OpenServiceImpl + Protocol的方案(將所有對外服務(wù)提供的接口都在OpenServiceImpl中實(shí)現(xiàn))去做組件間的服務(wù)調(diào)用,消息變量的聲明可以放到對外服務(wù)接口的Protocol定義中。

到了這個階段,我們的業(yè)務(wù)組件也已經(jīng)相對獨(dú)立,JLRoute能夠去掉頁面引用的頭頭文件依賴。OpenServiceImpl+Protocol也將我們最小化的對外服務(wù)接口約束到Protocol接口文件中。 如果對于項(xiàng)目組件化要求不高的話,到這一步就可以了。

三、徹底組件化-LDBusMediator煉就

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

通過第二部分的講述,我們的組件化工作差不多完成了80%,但是我們依然發(fā)現(xiàn),組件化并不夠徹底。

先來看服務(wù)調(diào)用方面,我們需要對外提供OpenServiceImpl的頭文件,外部模塊仍然保持著對業(yè)務(wù)組件的強(qiáng)依賴,OpenServiceImpl的不兼容變化必然導(dǎo)致所有調(diào)用部分的更改,我們期望的黑盒服務(wù)便無法實(shí)現(xiàn)。如果所有類別的服務(wù)接口都在OpenServiceImpl中實(shí)現(xiàn),OpenServiceImpl中的代碼會越來越多,難以維護(hù)和管理。 另外Protocol文件和OpenServiceImpl的頭文件都需要對外披露,如果放到組件實(shí)現(xiàn)中,兩個組件相互之間有調(diào)用,就會導(dǎo)致Podspec的相互循環(huán)依賴。

再看URL導(dǎo)航方面,在我們的項(xiàng)目中,我們在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短鏈分布式注冊時(shí),導(dǎo)航代碼的重復(fù)拷貝;
  • 無法通過URL返回一個controller實(shí)例;(TabController也就無法從獨(dú)立業(yè)務(wù)組件中不引用Controller頭文件獲取Controller實(shí)例完成設(shè)置)
  • class的load方法完成注冊,太多對啟動時(shí)Main線程有影響;
  • 同一個url短鏈的導(dǎo)航方式單一固定,依賴注冊
  • 單一業(yè)務(wù)組件中可導(dǎo)航URL分散,無法統(tǒng)一查看;
  • Debug階段url傳遞參數(shù)錯誤、not found沒有提示;

3.2 LDBusMediator總體方案

針對組件化不徹底的實(shí)際問題,結(jié)合之前手淘分享的總線架構(gòu)以及蘑菇街的組件化分享博客,我們完成了一個通用的LDBusMediator中間件幫助我們徹底完成組件化。

LDBusMediator開源Git地址:

我們先來看總體的組件化方案:所有的業(yè)務(wù)組件通過Connector連接到總線中,Connector需要遵循Connector Protocol方可接入。Connector協(xié)議規(guī)定了URL導(dǎo)航接入和服務(wù)接入的協(xié)議,Connector通過Class的Load方法將自己的實(shí)例注冊到中間件的Cache數(shù)組中,方便其他組件在調(diào)用時(shí)中間件可以通過服務(wù)發(fā)現(xiàn)的方式進(jìn)行URL導(dǎo)航和服務(wù)調(diào)用。(具體見如下的圖示)



@implementation Connector_A

#pragma mark - register connector

/**
 * 每個組件的實(shí)現(xiàn)必須自己通過load完成掛載;
 * load只需要在掛載connector的時(shí)候完成當(dāng)前connecotor的初始化,掛載量、掛載消耗、掛載所耗內(nèi)存都在可控范圍內(nèi);
 */
+(void)load{
    @autoreleasepool{
        [LDBusMediator registerConnector:[self sharedConnector]];
    }
}
@end

3.3 LDBusMediator-URL導(dǎo)航方案

URL導(dǎo)航的總線中間件方案很簡單,只需要在Connector中實(shí)現(xiàn)URL導(dǎo)航接入的接口即可,如圖所示:

LDBusMediator-URL導(dǎo)航.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)當(dāng)調(diào)用方需要通過判斷URL是否可導(dǎo)航顯示界面的時(shí)候,告訴調(diào)用方該組件實(shí)現(xiàn)是否可導(dǎo)航URL;可導(dǎo)航,返回YES,否則返回NO;
 * (2)這個方法跟connectToOpenURL:params配套實(shí)現(xiàn);如果不實(shí)現(xiàn),則調(diào)用方無法判斷某個URL是否可導(dǎo)航;
 */
-(BOOL)canOpenURL:(nonnull NSURL *)URL{
    if ([URL.host isEqualToString:@"ADetail"]) {
        return YES;
    }

    return NO;
}
@end
/**
 * (1)通過connector向busMediator掛載可導(dǎo)航的URL,具體解析URL的host還是path,由connector自行決定;
 * (2)如果URL在本業(yè)務(wù)組件可導(dǎo)航,則從params獲取參數(shù),實(shí)例化對應(yīng)的viewController進(jìn)行返回;如果參數(shù)錯誤,則返回一個錯誤提示的[UIViewController paramsError]; 如果不需要中間件進(jìn)行present展示,則返回一個[UIViewController notURLController],表示當(dāng)前可處理;如果無法處理,返回nil,交由其他組件處理;
 * (3)需要在connector中對參數(shù)進(jìn)行驗(yàn)證,不同的參數(shù)調(diào)用生成不同的ViewController實(shí)例;也可以通過參數(shù)決定是否自行展示,如果自行展示,則用戶定義的展示方式無效;
 * (4)如果掛接的url較多,這里的代碼比較長,可以將處理方法分發(fā)到當(dāng)前connector的category中;
 */
- (nullable UIViewController *)connectToOpenURL:(nonnull NSURL *)URL params:(nullable NSDictionary *)params{
    //處理scheme://ADetail的方式
    // tip: url較少的時(shí)候可以通過if-else去處理,如果url較多,可以自己維護(hù)一個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導(dǎo)航方案,有效的解決了前文提出的JLRoute的缺陷:

  1. url短鏈分布式注冊時(shí),導(dǎo)航代碼的重復(fù)拷貝
  • LDBusNavigator+PresentMode:將通用的導(dǎo)航方式即成到LDBusNavigator中,而無需每個URL注冊時(shí)重復(fù)拷貝。
  1. 無法通過URL返回一個controller實(shí)例;(TabController)
  • *URL-Block —> URL-ViewController實(shí)例:將之前JLRoute的url-block方式改成了url-ViewController方式,即可滿足。
  1. class的load方法完成注冊,太多對啟動時(shí)Main線程有影響;
  • 服務(wù)發(fā)現(xiàn)的方式,只在load時(shí)注冊Connector實(shí)例:中間件只對每個業(yè)務(wù)組件的connector實(shí)例進(jìn)行注冊,相比URL注冊量大量減少load使用。
  1. 同一個url短鏈的導(dǎo)航方式單一固定,依賴注冊
  • 調(diào)用時(shí)指定Present、Push、Share方式:之前JLRoute只能在注冊時(shí)候決定導(dǎo)航方式,通過LDBusMediator如何導(dǎo)航顯示由調(diào)用方?jīng)Q定,默認(rèn)是Push;Share方式是指pop到導(dǎo)航層次中已經(jīng)存在的viewController處。
  1. 單一業(yè)務(wù)組件中可導(dǎo)航URL分散,無法統(tǒng)一查看;
  • 單一組件的connector中集中管理所有可導(dǎo)航URL
  1. Debug階段url傳遞參數(shù)錯誤、not found沒有提示;
  • Debug階段的錯誤Controller提示、包括參數(shù)錯誤、notFound、notSupportController:如果參數(shù)錯誤、notfound無法生成一個viewController實(shí)例,中間件在debug階段會提示。如果URL不支持返回一個Controller,同樣會給與提示。

3.4 LDBusMediator-服務(wù)調(diào)用方案

為了更好的通過中間件支撐組件間的服務(wù)調(diào)用方案,我們在組件實(shí)現(xiàn)和中間件之間增加了一層協(xié)議接口層。 每個業(yè)務(wù)組件將自己對外提供的服務(wù)接口抽象到一個統(tǒng)一的業(yè)務(wù)組件協(xié)議集合中。 業(yè)務(wù)組件的實(shí)現(xiàn)依賴自己的對外服務(wù)接口集并進(jìn)行接口的實(shí)現(xiàn)。

每個業(yè)務(wù)組件中的協(xié)議部分有兩種:一種是服務(wù)協(xié)議,其他組件可以通過Mediator拿到對外開放的服務(wù)實(shí)例調(diào)用服務(wù)接口;一種是Model協(xié)議,服務(wù)協(xié)議中的接口可以給其他組件一個協(xié)議化對象,其他組件也可以組裝一個協(xié)議化對象通過參數(shù)傳入。

為了方便業(yè)務(wù)組件實(shí)現(xiàn)和協(xié)議集合的版本對應(yīng),需要保證協(xié)議集合的大版本(如x.y)和業(yè)務(wù)組件的大版本(如x.y.z)中的x保持一致;協(xié)議集合中一般沒有補(bǔ)丁版本的迭代,當(dāng)其他業(yè)務(wù)組件調(diào)用需要增加接口進(jìn)行兼容版本升級(y+1),減少或者修改接口則需要協(xié)議集合和業(yè)務(wù)組件中的x同時(shí)+1(x+1); 如果自身業(yè)務(wù)組件升級不能影響對外協(xié)議接口的調(diào)用,升級版本主要為補(bǔ)丁版本迭代(z+1)或 兼容版本升級(y+1);

組件協(xié)議集合 單獨(dú)通過一個Git地址進(jìn)行管理,單獨(dú)配置podspec,單獨(dú)通過協(xié)議的版本倉庫進(jìn)行管理;所有的協(xié)議集合的git統(tǒng)一放到Git的一個組中進(jìn)行管理。

具體方案如下:

LDBusMediator-服務(wù)調(diào)用.png
@protocol LDBusConnectorPrt <NSObject>
/**
 * 業(yè)務(wù)模塊掛接中間件,注冊自己提供的service,實(shí)現(xiàn)服務(wù)接口的調(diào)用;
 * 
 * 通過protocol協(xié)議找到組件中對應(yīng)的服務(wù)實(shí)現(xiàn),生成一個服務(wù)單例;
 * 傳遞給調(diào)用者進(jìn)行protocol接口中屬性和方法的調(diào)用;
 */
- (nullable id)connectToHandleProtocol:(nonnull Protocol *)servicePrt;  

@end


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

LDBusMediator中間件的服務(wù)調(diào)用方案的優(yōu)勢:

  1. 通過中間件支撐,不暴露任何實(shí)現(xiàn)文件的頭文件;
  • 組件對外提供的服務(wù)通過最小化抽象的“協(xié)議接口集”披露;
  • 組件的實(shí)現(xiàn)Pod不暴露任何頭文件;
  1. 每個業(yè)務(wù)組件提供黑盒服務(wù)
  • 調(diào)用者不用關(guān)心具體實(shí)現(xiàn)細(xì)節(jié);
  • 業(yè)務(wù)組件的實(shí)現(xiàn)升級、或者更換(包括整個業(yè)務(wù)組件更換)不影響調(diào)用者的調(diào)用修改;
  1. 為業(yè)務(wù)組件Framework化、自動化構(gòu)建奠定基礎(chǔ)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容