組件化一些見(jiàn)解

前言

這篇文章主要是對(duì)MGJRouter和CTMediator組件框架調(diào)研之后寫(xiě)的介紹與理解。
主要也是市面比較主流是URL-Scheme和Target-Action兩種方式,下面我會(huì)對(duì)這兩種分別說(shuō)明并且附上demo。

組件化的優(yōu)點(diǎn)

  • 業(yè)務(wù)劃分更加清晰,新人接手更加容易,可以按組件分配開(kāi)發(fā)任務(wù)。
  • 項(xiàng)目可維護(hù)性更強(qiáng),提高開(kāi)發(fā)效率
  • 單獨(dú)測(cè)試某個(gè)組件
  • 加快編譯速度,不需要編譯整個(gè)項(xiàng)目代碼

URL-Scheme庫(kù)(蘑菇街MGJRouter)

URL-Scheme庫(kù)我以蘑菇街為例,其他的庫(kù)也大同小異,有興趣可以去看。

  • JLRoutes
  • routable-ios
  • HHRouter
  • MGJRouter
    蘑菇街通過(guò)MGJRouter中間層,通過(guò)MGJRouter進(jìn)行組件間的消息轉(zhuǎn)發(fā),更像一種路由的形式。實(shí)現(xiàn)方式大致是,在提供服務(wù)的組件中提前注冊(cè)block,然后調(diào)用方組件通過(guò)URL調(diào)用block。

蘑菇街實(shí)現(xiàn)方式

注冊(cè)代碼

@interface MGJRouter ()
/**
 *  保存了所有已注冊(cè)的 URL
 *  結(jié)構(gòu)類似 @{@"beauty": @{@":id": {@"_", [block copy]}}}
 */
@property (nonatomic) NSMutableDictionary *routes;
@end

@implementation MGJRouter

+ (instancetype)sharedInstance
{
    static MGJRouter *instance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[self alloc] init];
    });
    return instance;
}

+ (void)registerURLPattern:(NSString *)URLPattern toHandler:(MGJRouterHandler)handler
{
    [[self sharedInstance] addURLPattern:URLPattern andHandler:handler];
}
    __weak typeof(self)weak = self;
    [MGJRouter registerURLPattern:@"zhs://businessa/changeText" toHandler:^(NSDictionary *routerParameters) {
        NSDictionary *userInfo = routerParameters[MGJRouterParameterUserInfo];
        NSLog(@"----------------%@",userInfo);
        weak.updateText = userInfo[@"changeText"];
        weak.updateLabel.text = weak.updateText;
    }];
[MGJRouter openURL:@"zhs://businessa/changeText" withUserInfo:@{@"changeText":@"組件B改變了我"} completion:^(id result) {
        NSLog(@"---------組件B改變了A的文字---------");
    }];

每個(gè)組件都需要提前注冊(cè),通過(guò)URL保存需要執(zhí)行的Block代碼到Router里面,URL-Router接受各個(gè)組件的注冊(cè),用字典保存了每個(gè)組件注冊(cè)過(guò)來(lái)的URL和對(duì)應(yīng)的服務(wù),只要其他組件調(diào)用了openURL方法,就會(huì)去這個(gè)字典里面根據(jù)URL找到對(duì)應(yīng)的block執(zhí)行(也就是執(zhí)行其他組件提供的服務(wù))
也可以通過(guò)objectForURL執(zhí)行block之后得到返回值。

+ (void)load {
    [MGJRouter registerURLPattern:@"zhs://businessa/createa" toObjectHandler:^id(NSDictionary *routerParameters) {
        NSDictionary *userInfo = routerParameters[MGJRouterParameterUserInfo];
        RouterBusinessAVC *businessVC = [[RouterBusinessAVC alloc]initWithId:userInfo[@"uuid"] text:userInfo[@"text"]];
        return businessVC;
    }];
}
id businessVC = [MGJRouter objectForURL:@"zhs://businessa/createa" withUserInfo:@{@"uuid":@"uw93428",@"text":@"我是通過(guò)MGJRouter過(guò)來(lái)的"}];
[self.navigationController pushViewController:businessVC animated:YES];

可以看出這種方式,里面有很多硬編碼,某個(gè)URL不小心寫(xiě)錯(cuò)就會(huì)匹配不到,另外參數(shù)都是字典,數(shù)據(jù)類型不明確,修改參數(shù)比較危險(xiǎn),需要組件去對(duì)應(yīng)修改,并不會(huì)編譯報(bào)錯(cuò)。對(duì)于這個(gè)問(wèn)題,蘑菇街在此基礎(chǔ)上推出Protocol-Class方案。

Protocol-Class方案

Protocol-Class方案和上面路由差別不是很大,只是把URL->Block變?yōu)镻rotocol-Class的形式。
每個(gè)組件都有一個(gè)protocol,組件實(shí)現(xiàn)了協(xié)議里面的方法,供其他組件調(diào)用,相當(dāng)于通過(guò)Protocol,組件對(duì)外提供一個(gè)可被調(diào)用的方法列表。
有一個(gè)ComponentProtocol用來(lái)引入組件協(xié)議,方便各組組件互相調(diào)用。
和MGJRouter一樣,每個(gè)組件都要注冊(cè),程序開(kāi)始運(yùn)行時(shí)將自身的Class注冊(cè)到Manager中,用Protocol反射出字符串當(dāng)做key。
代碼實(shí)例:

+ (instancetype)sharedInstance {
    static ModuleManager *mediator;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        mediator = [[ModuleManager alloc] init];
    });
    return mediator;
}

- (void)registerClass:(Class)cls
          forProtocol:(Protocol *)proto {
     [self.protocolCache setObject:cls forKey:NSStringFromProtocol(proto)];
}

- (Class)classForProtocol:(Protocol *)proto {
    return self.protocolCache[NSStringFromProtocol(proto)];
}

注冊(cè):

+ (void)load {
    [[ModuleManager sharedInstance]registerClass:[self class] forProtocol:@protocol(ProtocolA)];
}

組件A協(xié)議的方法

@protocol ProtocolA <NSObject>

- (void)configureAVCWithUuid:(NSString *)uuid
                        text:(NSString *)text;

@end

- (void)configureAVCWithUuid:(NSString *)uuid
                        text:(NSString *)text {
    self.updateText = text;
    self.uuid = uuid;
}

使用:

Class vcClass = [[ModuleManager sharedInstance]classForProtocol:@protocol(ProtocolA)];
UIViewController<ProtocolA> *compontAVC = [[vcClass alloc]init];
[compontAVC configureAVCWithUuid:@"i9342f8" text:@"我是通過(guò)Protocol過(guò)來(lái)的"];
[self.navigationController pushViewController:compontAVC animated:YES];

這種方式彌補(bǔ)了上面URL-Router硬編碼和參數(shù)不明確的問(wèn)題,但是組件方法的調(diào)用是分散在各地的,沒(méi)有統(tǒng)一的入口,也就沒(méi)法做組件方法不存在時(shí)的統(tǒng)一處理,使用的這個(gè)方法的每個(gè)組件都需要去處理。蘑菇街是兩種方式混用,使用者還要區(qū)分不同的參數(shù)要使用的不同的方法,這種其實(shí)也不太友好。

內(nèi)存問(wèn)題

蘑菇街這兩種方式都有注冊(cè)到字典,常駐內(nèi)存。

  • block實(shí)現(xiàn)方式可能導(dǎo)致的內(nèi)存問(wèn)題,需要避免循環(huán)引用的問(wèn)題。
    經(jīng)過(guò)暴力測(cè)試,證明并不會(huì)導(dǎo)致內(nèi)存問(wèn)題。被保存在字典中是一個(gè)block對(duì)象,而block對(duì)象本身并不會(huì)占用多少內(nèi)存。在調(diào)用block后會(huì)對(duì)block體中的方法進(jìn)行執(zhí)行,執(zhí)行完成后block體中的對(duì)象釋放。
    block自身的實(shí)現(xiàn)只是一個(gè)結(jié)構(gòu)體,也就相當(dāng)于字典中存放的是很多結(jié)構(gòu)體,所以內(nèi)存的占用并不是很大。
  • 對(duì)于協(xié)議這種實(shí)現(xiàn)方式,和block內(nèi)存常駐方式差不多。只是將存儲(chǔ)的block對(duì)象換成Class對(duì)象,如果不是已經(jīng)實(shí)例化的對(duì)象,內(nèi)存占用還是比較小的。

Target-Action

這個(gè)是casatwy大神提出來(lái)的,利用runtime特性,不需要注冊(cè),在外層通過(guò)CTMediator調(diào)用performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget找到對(duì)應(yīng)的組件,執(zhí)行方法和傳遞參數(shù)。CTMediator內(nèi)部主要是通過(guò)[target performSelector:action withObject:params]去執(zhí)行對(duì)應(yīng)組件的方法,接下來(lái)我們分開(kāi)來(lái)講target、action、params。

Target

[target performSelector:action withObject:params]中這個(gè)target是方法執(zhí)行的類,每一個(gè)組件都有一個(gè)執(zhí)行的Target,里面放著這個(gè)組件對(duì)外可調(diào)用方法。CTMediator這個(gè)Target命名也有規(guī)定,Target_前綴開(kāi)始,避免命名沖突。

#import "Target_CompoentA.h"
#import "CTCompoentAVC.h"

@implementation Target_CompoentA

- (UIViewController *)Action_compoentAVC:(NSDictionary *)params{
    CTCompoentAVC *vc = [[CTCompoentAVC alloc]initWithUuid:params[@"uuid"] text:params[@"text"]];
    return vc;
}

@end

Action

[target performSelector:action withObject:params]中這個(gè)action是被執(zhí)行的方法,定義在target里面,action有Action_作為前綴,每個(gè)組件命名最好根據(jù)組件來(lái),這樣好區(qū)分。

#import "Target_CompoentB.h"
#import "CTCompoentBVC.h"

@implementation Target_CompoentB

- (UIViewController *)Action_compoentBVC:(NSDictionary *)params{
    CTCompoentBVC *vc = [[CTCompoentBVC alloc]init];
    return vc;
}

- (UIViewController *)Action_compoentBVCwithBlock:(NSDictionary *)params {
    CTCompoentBVC *vc = [[CTCompoentBVC alloc]initWithBlockParams:params];
    return vc;
}

@end

這樣組件定義好了方法就可以通過(guò)CTMediator去調(diào)用了,如果組件隨著業(yè)務(wù)變多,方法更多,那這個(gè)類里面方法會(huì)越來(lái)越多,類越來(lái)越大,用category可以解決這個(gè)問(wèn)題,各個(gè)組件方法都分散到各自組件的category里面去。

#import "CTMediator+CompoentAAction.h"

NSString *const kCTMediatorTargetA = @"CompoentA";

NSString *const kCTMediatorTargetARootVC = @"compoentAVC";

@implementation CTMediator (CompoentAAction)

- (UIViewController *)compoentAVC {
    UIViewController *vc = [self performTarget:kCTMediatorTargetA action:kCTMediatorTargetARootVC params:@{@"uuid":@"ei3423d",@"text":@"我是從Mediator過(guò)來(lái)的"} shouldCacheTarget:NO];
    if([vc isKindOfClass:[UIViewController class]]){
        return vc;
    }
    return nil;
}

@end
@implementation CTMediator (CompoentBAction)

- (UIViewController *)compoentBVC {
    UIViewController *vc = [self performTarget:kCTMediatorTargetB action:kCTMediatorTargetBRootVC params:@{@"uuid":@"9oieru3"} shouldCacheTarget:NO];
    if([vc isKindOfClass:[UIViewController class]]){
        return vc;
    }
    return nil;
}

@end

調(diào)用組件方法

UIViewController *compoentAVC = [[CTMediator sharedInstance] compoentAVC];
[self.navigationController pushViewController:compoentAVC animated:YES];

CTMediator不用注冊(cè),可以直接調(diào)用。如果某個(gè)組件的方法參數(shù)變了,修改組件及組件對(duì)應(yīng)CTMediator的categroy方法,如果里面帶參數(shù)的話,需要修改其他組件,不修改會(huì)編譯報(bào)錯(cuò)預(yù)警,MGJRouter需要到組件去修改,可以編譯通過(guò)。CTMediator命名需要按照規(guī)則,命名大寫(xiě)字母開(kāi)頭加_,可能對(duì)一些開(kāi)發(fā)者有點(diǎn)不適應(yīng)。

總結(jié)

蘑菇街方式都需要維護(hù)一張表,參數(shù)類型和修改參數(shù)對(duì)開(kāi)發(fā)者也不是很友好,硬編碼比較多;CTMediator不用注冊(cè),但是創(chuàng)建的類比較多,不是很直觀,另外組件間的交互需要額外再設(shè)計(jì)。

摘自casa的建議:

組件化方案在App業(yè)務(wù)穩(wěn)定,且規(guī)模(業(yè)務(wù)規(guī)模和開(kāi)發(fā)團(tuán)隊(duì)規(guī)模)增長(zhǎng)初期去實(shí)施非常重要,它助于將復(fù)雜App分而治之,也有助于多人大型團(tuán)隊(duì)的協(xié)同開(kāi)發(fā)。但組件化方案不適合在業(yè)務(wù)不穩(wěn)定的情況下過(guò)早實(shí)施,至少要等產(chǎn)品已經(jīng)經(jīng)過(guò)MVP階段時(shí)才適合實(shí)施組件化。因?yàn)闃I(yè)務(wù)不穩(wěn)定意味著鏈路不穩(wěn)定,在不穩(wěn)定的鏈路上實(shí)施組件化會(huì)導(dǎo)致將來(lái)主業(yè)務(wù)產(chǎn)生變化時(shí),全局性模塊調(diào)度和重構(gòu)會(huì)變得相對(duì)復(fù)雜。

tips

前期開(kāi)發(fā)中項(xiàng)目可能項(xiàng)目比較小,可能組件化可能更復(fù)雜,但是后期產(chǎn)品線多,業(yè)務(wù)更多,那組件化就需要提上議程,開(kāi)發(fā)時(shí)就需要做好這個(gè)業(yè)務(wù)模塊抽離出來(lái)打成私有pod準(zhǔn)備。需要注意以下幾點(diǎn):

  • 基礎(chǔ)功能打成私有pod ,然后盡量用里面的方法
  • 圖片資源盡量放在一起,公有圖片資源抽出
  • token最好傳值進(jìn)去,因?yàn)橛锌赡苓@個(gè)業(yè)務(wù)模塊被好幾個(gè)app使用,每個(gè)app獲取token的方式都不一樣
  • 業(yè)務(wù)模塊的統(tǒng)計(jì)最好也抽離出來(lái)

參考

組件化架構(gòu)漫談
組件化方案調(diào)研
組件化方案

demo

demo地址

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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