組件化方案調研

前言

在最前面,先祝你們除夕節快樂,開開心心過大年~~html

這篇文章主要是我近段時間針對市面上存在的一些組件化方案的調研以後,再通過本身的反思和總結寫的,博客中部分文字和圖借鑑自下面的博客。各位看官大爺就當作一篇讀書筆記來看便可,主要是參考了以下幾篇文章,另外零零散散的也看了一些其餘資料,可是大多都是類似的ios

  1. 蘑菇街組件化之路
  2. iOS應用架構談 組件化方案
  3. iOS 組件化 —— 路由設計思路分析
  4. 滴滴iOS的組件化實踐與優化
  5. iOS組件化方案
  6. iOS 組件化方案探索
  7. 掌上鍊家組件化探索歷程
  8. 京東iOS客戶端組件管理實踐

看上去各家都是各顯神通,都有本身的技術方案,可是實際上均可以歸類到以下兩種方案:git

  1. 利用runtime實現的target-action方法
  2. 利用url-scheme方案

目前市面上流行的組件化方案都是經過url-scheme實現的,包括不少開源的組件化的庫都是如此,只有casa的方案獨樹一幟,是經過Target-Action實現的github

URL-Scheme庫:
  1. JLRoutes
  2. routable-ios
  3. HHRouter
  4. MGJRouter
Target-Action庫:
  1. CTMediator

上面這些第三方組件庫的具體對比,你們能夠參考霜神的這篇博客:web

iOS 組件化 —— 路由設計思路分析json

URL-Sheme方案通常都是各個組件把本身能夠提供的服務經過url的形式註冊到一箇中心管理器,而後調用發就能夠經過openURL的方式來打開這個url,而後中心管理器解析這個url,把請求轉發到相應的組件去執行設計模式

Target-Action方案利用了OC的runtime特性,無需註冊,直接在原有的組件以外加一層wrapper,把對外提供的服務都抽離到該層。而後經過runtime的TARGET performSelector:ACTION withObject:PARAMS找到對應的組件,執行方法和傳遞參數。安全

就我我的而言,我仍是比較推薦target-action方案,具體緣由咱們下面會進一步分析bash

爲什麼要組件化

在作一件事以前咱們通常都要搞清楚爲何要這麼作,好處是什麼,有哪些坑,這樣纔會有一個總體的認識,而後再決定要不要作。一樣咱們也要搞清楚到底需不須要實施組件化,那麼就要先搞清楚什麼是組件架構

組件的定義

組件是由一個或多個類構成,能完整描述一個業務場景,並能被其餘業務場景複用的功能單位。組件就像是PC時代我的組裝電腦時購買的一個個部件,好比內存,硬盤,CPU,顯示器等,拿出其中任何一個部件都能被其餘的PC所使用。

因此組件能夠是個廣義上的概念,並不必定是頁面跳轉,還能夠是其餘不具有UI屬性的服務提供者,好比日誌服務,VOIP服務,內存管理服務等等。說白了咱們目標是站在更高的維度去封裝功能單元。對這些功能單元進行進一步的分類,才能在具體的業務場景下作更合理的設計。

組件化的優勢

縱觀目前的已經在實施組件化的團隊來看,你們的通常發展路徑都是:前期項目小,須要快速迭代搶佔市場,你們都是用傳統的MVC架構去開發項目。等到後期項目愈來愈大,開發人數愈來愈多,會發現傳統的開發方式致使代碼管理混亂,發佈、集成、測試愈來愈麻煩,被迫走向組件化的道路。

其實組件化也不是徹底必須的,若是你的團隊只是開發一個小項目,團隊人數小於10我的,產品線也就是兩三條,那麼徹底能夠用傳統開發方式來開發。可是若是你的團隊在不斷髮展,產品線也愈來愈多的時候,預計後期可能會更多的時候,那麼最好儘早把組件化提上議程。

摘自casa的建議:

組件化方案在App業務穩定,且規模(業務規模和開發團隊規模)增加初期去實施很是重要,它助於將複雜App分而治之,也有助於多人大型團隊的協同開發。但組件化方案不適合在業務不穩定的狀況下過早實施,至少要等產品已經通過MVP階段時才適合實施組件化。由於業務不穩定意味着鏈路不穩定,在不穩定的鏈路上實施組件化會致使未來主業務產生變化時,全局性模塊調度和重構會變得相對複雜。

其實組件化也沒有多麼高大上,和咱們以前說的模塊化差很少,就是把一些業務、基礎功能剝離,劃分爲一個個的模塊,而後經過pods的方式管理而已,同時要搭配一套後臺的自動集成、發佈、測試流程

通常當項目愈來愈大的時候,無可避免的會遇到以下的痛點:

代碼衝突多,編譯慢。

每一次拉下代碼開發功能,開發完成準備提交代碼時,每每有其餘工程師提交了代碼,須要從新拉去代碼合併後再提交,即便開發一個很小的功能,也須要在整個工程裏作編譯和調試,效率較低。

迭代速度慢,耦合比較嚴重,沒法單獨測試。

各個業務模塊之間互相引入,耦合嚴重。每次須要發版時,全部的業務線修改都須要所有迴歸,而後審查看是否出錯,耗費大量時間。業務線之間相互依賴,可能會致使一個業務線必須等待另一個業務線開發完某個功能才能夠接着開發,沒法並行開發。還有一個問題,就是耦合致使沒法單獨測試某個業務線,可能須要等到全部業務線開發完畢,才能統一測試,浪費測試資源

爲了解決上述痛點,組件化應運而生,整體來講,組件化就是把整個項目進行拆分,分紅一個個單獨的可獨立運行的組件,分開管理,減小依賴。 完成組件化以後,通常可達到以下效果:

  1. 加快編譯速度,能夠把不會常常變更的組件作成靜態庫,同時每一個組件能夠獨立編譯,不依賴於主工程或者其餘組件
  2. 每一個組件均可以選擇本身擅長的開發模式(MVC / MVVM / MVP)
  3. 能夠單獨測試每一個組件
  4. 多條業務線能夠並行開發,提升開發效率

如何組件化

當咱們肯定須要對項目進行組件化了,咱們第一個要解決的問題就是如何拆分組件。這是一個見仁見智的問題,沒有太明確的劃分邊界,大體作到每一個組件只包含一個功能便可,具體實施仍是要根據實際狀況權衡。

當咱們寫一個類的時候,咱們會謹記高內聚,低耦合的原則去設計這個類,當涉及多個類之間交互的時候,咱們也會運用SOLID原則,或者已有的設計模式去優化設計,但在實現完整的業務模塊的時候,咱們很容易忘記對這個模塊去作設計上的思考,粒度越大,越難作出精細穩定的設計,我暫且把這個粒度認爲是組件的粒度。

組件能夠是個廣義上的概念,並不必定是頁面跳轉,還能夠是其餘不具有UI屬性的服務提供者,好比日誌服務,VOIP服務,內存管理服務等等。說白了咱們目標是站在更高的維度去封裝功能單元,把多個功能單元組合在一塊兒造成一個更大的功能單元,也就是組件。對這些功能單元進行進一步的分類,才能在具體的業務場景下作更合理的設計。

下面的組件劃分粒度,你們能夠借鑑一下

組件化先後對比

iOS裏面的組件化主要是經過cocopods把組件打包成單獨的私有pod庫來進行管理,這樣就能夠經過podfile文件,進行動態的增刪和版本管理了。

下面是鏈家APP在實行組件化先後的對比

能夠看到傳統的MVC架構把全部的模塊所有糅合在一塊兒,是一種分佈式的管理方法,耦合嚴重,當業務線過多的時候就會出現咱們上面說的問題。 而下圖的組件化方式是一種中心Mediator的方式,讓全部業務組件都分開,而後都依賴於Mediator進行統一管理,減小耦合。

組件化後,代碼分類也更符合人類大腦的思考方式

組件化方案對比分析

組件化如何解決現有工程問題

傳統模式的組件之間的跳轉都是經過直接import,當模塊比較少的時候這個方式看起來沒啥問題。但到了項目愈來愈龐大,這種模式會致使每一個模塊都離不開其餘模塊,互相依賴耦合嚴重。這種方式是分佈式的處理方式,每一個組件都是處理和本身相關的業務。管理起來很混亂,以下圖所示:

(借用霜神的幾張圖)

那麼按照人腦的思惟方式,改爲以下這種中心化的方式更加清晰明瞭:

可是上面這個圖雖然看起來比剛開始好了許多,可是每一個組件仍是和mediator雙向依賴,若是改爲以下圖所示就完美了:

這個時候看起來就舒服多了,每一個組件只須要本身管好本身就完了,而後由mediator負責在各個組件中間進行轉發或者跳轉,perfect~~ 那麼如何實現這個架構呢?只要解決下面兩個問題就行了:

  1. mediator做爲中間件,須要經過某種方式找到每一個組件,並能調用組件的方法
  2. 每一個組件如何得知其餘組件提供了哪些方法?只有這樣才能夠調用對方嘛

原始工程

假設咱們現有工程裏面有兩個組件A、B,功能很簡單,以下所示。

#import <UIKit/UIKit.h>

@interface A_VC : UIViewController
-(void)action_A:(NSString*)para1;
@end

==================================

#import "A_VC.h"

@implementation A_VC

-(void)action_A:(NSString*)para1 {
    NSLog(@"call action_A %@",para1);
}

@end

複製代碼
#import <UIKit/UIKit.h>

@interface B_VC : UIViewController

-(void)action_B:(NSString*)para1 para2:(NSInteger)para2;

@end

====================

#import "B_VC.h"

@implementation B_VC

-(void)action_B:(NSString*)para1 para2:(NSInteger)para2{
    NSLog(@"call action_B %@---%zd",para1,para2);
}

@end


複製代碼

若是是傳統作法,A、B要調用對方的功能,就會直接import對方,而後初始化,接着調用方法。如今咱們對他們實行組件化,改爲如上圖所示的mediator方式

target-action方案

該方案藉助OC的runtime特性,實現了服務的自動發現,無需註冊便可實現組件間調用。無論是從維護性、可讀性、擴展性方面來說,都優於url-scheme方案,也是我比較推崇的組件化方案,下面咱們就來看看該方案如何解決上述兩個問題的

Demo演示

此時A、B兩個組件不用改,咱們須要加一個mediator,代碼以下所示:

#import <Foundation/Foundation.h>

@interface Mediator : NSObject

-(void)A_VC_Action:(NSString*)para1;
-(void)B_VC_Action:(NSString*)para1 para2:(NSInteger)para2;
+ (instancetype)sharedInstance;

@end

===========================================

#import "Mediator.h"

@implementation Mediator

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


-(void)A_VC_Action:(NSString*)para1{
    Class cls = NSClassFromString(@"A_VC");
    NSObject *target = [[cls alloc]init];
    [target performSelector:NSSelectorFromString(@"action_A:") withObject:para1];
}


-(void)B_VC_Action:(NSString*)para1 para2:(NSInteger)para2{
    Class cls = NSClassFromString(@"B_VC");
    NSObject *target = [[cls alloc]init];
    [target performSelector:NSSelectorFromString(@"action_B:para2:") withObject:para1 withObject:para2];
}

@end


複製代碼

組件B調用組件A,以下所示:

[[Mediator sharedInstance]A_VC_Action:@"參數1"];

複製代碼

組件A調用組件B,以下所示:

[[Mediator sharedInstance]B_VC_Action:@"參數1" para2:123];

複製代碼

此時已經能夠作到最後一張圖所示的效果了,組件A,B依賴mediator,mediator不依賴組件A,B(也不是徹底不依賴,而是把用runtime特性把類的引用弱化爲了字符串)

反思

看到這裏,大概有人會問,既然用runtime就能夠解耦取消依賴,那還要Mediator作什麼?我直接在每一個組件裏面用runtime調用其餘組件不就完了嗎,幹嗎還要多一個mediator?

可是這樣作會存在以下問題:

  1. 調用者寫起來很噁心,代碼提示都沒有, 參數傳遞很是噁心,每次調用者都要查看文檔搞清楚每一個參數的key是什麼,而後本身去組裝成一個 NSDictionary。維護這個文檔和每次都要組裝參數字典很麻煩。
  2. 當調用的組件不存在的時候,無法進行統一處理

那麼加一個mediator的話,就能夠作到:

  1. 調用者寫起來不噁心,代碼提示也有了, 參數類型明確。
  2. Mediator能夠作統一處理,調用某個組件方法時若是某個組件不存在,能夠作相應操做,讓調用者與組件間沒有耦合。

改進

聰明的讀者可能已經發現上面的mediator方案仍是存在一個小瑕疵,受限於performselector方法,最多隻能傳遞兩個參數,若是我想傳遞多個參數怎麼辦呢?

答案是使用字典進行傳遞,此時咱們還須要個組件增長一層wrapper,把對外提供的業務所有包裝一次,而且接口的參數所有改爲字典。 假設咱們如今的B組件須要接受多個參數,以下所示:

-(void)action_B:(NSString*)para para2:(NSInteger)para2 para3:(NSInteger)para3 para4:(NSInteger)para4{
    NSLog(@"call action_B %@---%zd---%zd----%zd",para1,para2,para3,para4);
}

複製代碼

那麼此時須要對B組件增長一層wrapper,以下:

#import <Foundation/Foundation.h>

@interface target_B : NSObject
-(void)B_Action:(NSDictionary*)para;

@end

=================
#import "target_B.h"
#import "B_VC.h"

@implementation target_B

-(void)B_Action:(NSDictionary*)para{
    NSString *para1 = para[@"para1"];
    NSInteger para2 = [para[@"para2"]integerValue];
    NSInteger para3 = [para[@"para3"]integerValue];
    NSInteger para4 = [para[@"para4"]integerValue];
    B_VC *VC = [B_VC new];
    [VC action_B:para1 para2:para2 para3:para3 para4:para4];
}
@end

複製代碼

此時mediator也須要作相應的更改,由原來直接調用組件B,改爲了調用B的wrapper層:

-(void)B_VC_Action:(NSString*)para1 para2:(NSInteger)para2 para3:(NSInteger)para3 para4:(NSInteger)para4{
    Class cls = NSClassFromString(@"target_B");
    NSObject *target = [[cls alloc]init];
    [target performSelector:NSSelectorFromString(@"B_Action:") withObject:@{@"para1":para1, @"para2":@(para2),@"para3":@(para3),@"para4":@(para4)} ];
}

複製代碼

如今的組件A調用組件B的流程以下所示:

此時的項目結構以下:

繼續改進

作到這裏,看似比較接近個人要求了,可是還有有點小瑕疵:

  1. Mediator 每個方法裏都要寫 runtime 方法,格式是肯定的,這是能夠抽取出來的。
  2. 每一個組件對外方法都要在 Mediator 寫一遍,組件一多 Mediator 類的長度是恐怖的。

接着優化就是casa的方案了,咱們來看看如何改進,直接看代碼:

針對第一點,咱們能夠抽出公共代碼,當作mediator:

#import "CTMediator.h"
#import <objc/runtime.h>

@interface CTMediator ()

@property (nonatomic, strong) NSMutableDictionary *cachedTarget;

@end

@implementation CTMediator

#pragma mark - public methods
+ (instancetype)sharedInstance
{
    static CTMediator *mediator;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        mediator = [[CTMediator alloc] init];
    });
    return mediator;
}

/*
 scheme://[target]/[action]?[params]
 
 url sample:
 aaa://targetA/actionB?id=1234
 */

- (id)performActionWithUrl:(NSURL *)url completion:(void (^)(NSDictionary *))completion
{
    NSMutableDictionary *params = [[NSMutableDictionary alloc] init];
    NSString *urlString = [url query];
    for (NSString *param in [urlString componentsSeparatedByString:@"&"]) {
        NSArray *elts = [param componentsSeparatedByString:@"="];
        if([elts count] < 2) continue;
        [params setObject:[elts lastObject] forKey:[elts firstObject]];
    }
    
    // 這裏這麼寫主要是出於安全考慮,防止黑客經過遠程方式調用本地模塊。這裏的作法足以應對絕大多數場景,若是要求更加嚴苛,也能夠作更加複雜的安全邏輯。
    NSString *actionName = [url.path stringByReplacingOccurrencesOfString:@"/" withString:@""];
    if ([actionName hasPrefix:@"native"]) {
        return @(NO);
    }
    
    // 這個demo針對URL的路由處理很是簡單,就只是取對應的target名字和method名字,但這已經足以應對絕大部份需求。若是須要拓展,能夠在這個方法調用以前加入完整的路由邏輯
    id result = [self performTarget:url.host action:actionName params:params shouldCacheTarget:NO];
    if (completion) {
        if (result) {
            completion(@{@"result":result});
        } else {
            completion(nil);
        }
    }
    return result;
}

- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget
{
    
    NSString *targetClassString = [NSString stringWithFormat:@"Target_%@", targetName];
    NSString *actionString = [NSString stringWithFormat:@"Action_%@:", actionName];
    Class targetClass;
    
    NSObject *target = self.cachedTarget[targetClassString];
    if (target == nil) {
        targetClass = NSClassFromString(targetClassString);
        target = [[targetClass alloc] init];
    }
    
    SEL action = NSSelectorFromString(actionString);
    
    if (target == nil) {
        // 這裏是處理無響應請求的地方之一,這個demo作得比較簡單,若是沒有能夠響應的target,就直接return了。實際開發過程當中是能夠事先給一個固定的target專門用於在這個時候頂上,而後處理這種請求的
        return nil;
    }
    
    if (shouldCacheTarget) {
        self.cachedTarget[targetClassString] = target;
    }

    if ([target respondsToSelector:action]) {
        return [self safePerformAction:action target:target params:params];
    } else {
        // 有可能target是Swift對象
        actionString = [NSString stringWithFormat:@"Action_%@WithParams:", actionName];
        action = NSSelectorFromString(actionString);
        if ([target respondsToSelector:action]) {
            return [self safePerformAction:action target:target params:params];
        } else {
            // 這裏是處理無響應請求的地方,若是無響應,則嘗試調用對應target的notFound方法統一處理
            SEL action = NSSelectorFromString(@"notFound:");
            if ([target respondsToSelector:action]) {
                return [self safePerformAction:action target:target params:params];
            } else {
                // 這裏也是處理無響應請求的地方,在notFound都沒有的時候,這個demo是直接return了。實際開發過程當中,能夠用前面提到的固定的target頂上的。
                [self.cachedTarget removeObjectForKey:targetClassString];
                return nil;
            }
        }
    }
}

- (void)releaseCachedTargetWithTargetName:(NSString *)targetName
{
    NSString *targetClassString = [NSString stringWithFormat:@"Target_%@", targetName];
    [self.cachedTarget removeObjectForKey:targetClassString];
}

#pragma mark - private methods
- (id)safePerformAction:(SEL)action target:(NSObject *)target params:(NSDictionary *)params
{
    NSMethodSignature* methodSig = [target methodSignatureForSelector:action];
    if(methodSig == nil) {
        return nil;
    }
    const char* retType = [methodSig methodReturnType];

    if (strcmp(retType, @encode(void)) == 0) {
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
        [invocation setArgument:&params atIndex:2];
        [invocation setSelector:action];
        [invocation setTarget:target];
        [invocation invoke];
        return nil;
    }

    if (strcmp(retType, @encode(NSInteger)) == 0) {
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
        [invocation setArgument:&params atIndex:2];
        [invocation setSelector:action];
        [invocation setTarget:target];
        [invocation invoke];
        NSInteger result = 0;
        [invocation getReturnValue:&result];
        return @(result);
    }

    if (strcmp(retType, @encode(BOOL)) == 0) {
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
        [invocation setArgument:&params atIndex:2];
        [invocation setSelector:action];
        [invocation setTarget:target];
        [invocation invoke];
        BOOL result = 0;
        [invocation getReturnValue:&result];
        return @(result);
    }

    if (strcmp(retType, @encode(CGFloat)) == 0) {
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
        [invocation setArgument:&params atIndex:2];
        [invocation setSelector:action];
        [invocation setTarget:target];
        [invocation invoke];
        CGFloat result = 0;
        [invocation getReturnValue:&result];
        return @(result);
    }

    if (strcmp(retType, @encode(NSUInteger)) == 0) {
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
        [invocation setArgument:&params atIndex:2];
        [invocation setSelector:action];
        [invocation setTarget:target];
        [invocation invoke];
        NSUInteger result = 0;
        [invocation getReturnValue:&result];
        return @(result);
    }

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    return [target performSelector:action withObject:params];
#pragma clang diagnostic pop
}

#pragma mark - getters and setters
- (NSMutableDictionary *)cachedTarget
{
    if (_cachedTarget == nil) {
        _cachedTarget = [[NSMutableDictionary alloc] init];
    }
    return _cachedTarget;
}

@end

複製代碼

針對第二點,咱們經過把每一個組件的對外接口進行分離,剝離到多個mediator的category裏面,感官上把原本在一個mediator裏面實現的對外接口分離到多個category裏面,方便管理

下面展現的是個組件B添加的category,組件A相似

#import "CTMediator.h"

@interface CTMediator (B_VC_Action)
-(void)B_VC_Action:(NSString*)para1 para2:(NSInteger)para2 para3:(NSInteger)para3 para4:(NSInteger)para4;

@end

====================
#import "CTMediator+B_VC_Action.h"

@implementation CTMediator (B_VC_Action)
-(void)B_VC_Action:(NSString*)para1 para2:(NSInteger)para2 para3:(NSInteger)para3 para4:(NSInteger)para4{
    [self performTarget:@"target_B" action:@"B_Action" params:@{@"para1":para1, @"para2":@(para2),@"para3":@(para3),@"para4":@(para4)} shouldCacheTarget:YES];
}
@end

複製代碼

此時調用者只要引入該category,而後調用便可,調用邏輯其實和上面沒有拆分出category是同樣的。此時的項目結構以下:

URL-Scheme方案

這個方案是流傳最廣的,也是最多人使用的,由於Apple自己也提供了url-scheme功能,同時web端也是經過URL的方式進行路由跳轉,那麼很天然的iOS端就借鑑了該方案。

如何實現

Router實現代碼
#import <Foundation/Foundation.h>
typedef void (^componentBlock) (NSDictionary *param);

@interface URL_Roueter : NSObject
+ (instancetype)sharedInstance;
- (void)registerURLPattern:(NSString *)urlPattern toHandler:(componentBlock)blk;
- (void)openURL:(NSString *)url withParam:(id)param;
@end

====================


#import "URL_Roueter.h"

@interface URL_Roueter()
@property (nonatomic, strong) NSMutableDictionary *cache;
@end


@implementation URL_Roueter

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



-(NSMutableDictionary *)cache{
    if (!_cache) {
        _cache = [NSMutableDictionary new];
    }
    return _cache;
}


- (void)registerURLPattern:(NSString *)urlPattern toHandler:(componentBlock)blk {
    [self.cache setObject:blk forKey:urlPattern];
}

- (void)openURL:(NSString *)url withParam:(id)param {
    componentBlock blk = [self.cache objectForKey:url];
    if (blk) blk(param);
}


@end

複製代碼
組件A
#import "A_VC.h"
#import "URL_Roueter.h"

@implementation A_VC

//把本身對外提供的服務(block)用url標記,註冊到路由管理中心組件
+(void)load{
    [[URL_Roueter sharedInstance]registerURLPattern:@"test://A_Action" toHandler:^(NSDictionary* para) {
        NSString *para1 = para[@"para1"];
        [[self new] action_A:para1];
    }];
}


-(void)viewDidLoad{
    [super viewDidLoad];
    UIButton *btn = [UIButton new];
    [btn setTitle:@"調用組件B" forState:UIControlStateNormal];
    btn.frame = CGRectMake(100, 100, 100, 50);
    [btn addTarget:self action:@selector(btn_click) forControlEvents:UIControlEventTouchUpInside];
    [btn setBackgroundColor:[UIColor redColor]];
    
    self.view.backgroundColor = [UIColor blueColor];
    [self.view addSubview:btn];
    
}

//調用組件B的功能
-(void)btn_click{
    [[URL_Roueter sharedInstance]openURL:@"test://B_Action" withParam:@{@"para1":@"PARA1", @"para2":@(222),@"para3":@(333),@"para4":@(444)}];
}


-(void)action_A:(NSString*)para1 {
    NSLog(@"call action_A: %@",para1);
}

@end

複製代碼

組件B實現的代碼相似,就不在貼了。上面都是簡化版的實現,不過核心原理是同樣的。

從上面的代碼能夠看出來,實現原理很簡單:每一個組件在本身的load方面裏面,把本身對外提供的服務(回調block)經過url-scheme標記好,而後註冊到URL-Router裏面。

URL-Router接受各個組件的註冊,用字典保存了每一個組件註冊過來的url和對應的服務,只要其餘組件調用了openURL方法,就會去這個字典裏面根據url找到對應的block執行(也就是執行其餘組件提供的服務)

存在的問題

經過url-scheme的方式去作組件化主要存在以下一些問題:

須要專門的管理後臺維護

要提供一個文檔專門記錄每一個url和服務的對應表,每次組件改動了都要即便修改,很麻煩。參數的格式不明確,是個靈活的 dictionary,一樣須要維護一份文檔去查這些參數。

內存問題

每一個組件在初始化的時候都須要要路由管理中心去註冊本身提供的服務,內存裏須要保存一份表,組件多了會有內存問題。

混淆了本地調用和遠程調用

url-scheme是Apple拿來作app之間跳轉的,或者經過url方式打開APP,可是上述的方案去把他拿來作本地組件間的跳轉,這會產生問題,大概分爲兩點:

  1. 遠程調用和本地調用的處理邏輯是不一樣的,正確的作法應該是把遠程調用經過一箇中間層轉化爲本地調用,若是把二者二者混爲一談,後期可能會出現沒法區分業務的狀況。好比對於組件沒法響應的問題,遠程調用可能直接顯示一個404頁面,可是本地調用可能須要作其餘處理。若是不加以區分,那麼久沒法完成這種業務要求。

  2. 遠程調用只能傳能被序列化爲json的數據,像 UIImage這樣很是規的對象是不行的。因此若是組件接口要考慮遠程調用,這裏的參數就不能是這類很是規對象,接口的定義就受限了。出現這種狀況的緣由就是,遠程調用是本地調用的子集,這裏混在一塊兒致使組件只能提供子集功能(遠程調用),因此這個方案是天生有缺陷的

  3. 理論上來說,組件化是接口層面的東西,應該用語言自身的特性去解決,而url是用於遠程通訊的,不該該和組件化扯上關係

改進

針對上述第二點描述的沒法傳遞常規對象的問題,蘑菇街作了改進,經過protocol轉class的方式去實現,可是我想說這種實現辦法真是越高越複雜了。具體看代碼就知道了

protocolMediator實現:
功能:經過protocol的字符串存儲class

#import <Foundation/Foundation.h>

@interface ProtocolMediator : NSObject
+ (instancetype)sharedInstance;
- (void)registerProtocol:(Protocol *)proto forClass:(Class)cls;
- (Class)classForProtocol:(Protocol *)proto;

@end

============

#import "ProtocolMediator.h"

@interface ProtocolMediator()
@property (nonatomic,strong) NSMutableDictionary *protocolCache;

@end
@implementation ProtocolMediator


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

-(NSMutableDictionary *)protocolCache{
    if (!_protocolCache) {
        _protocolCache = [NSMutableDictionary new];
    }
    return _protocolCache;
}

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

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


@end

複製代碼
commonProtocol實現:
功能:全部須要傳遞很是規參數的方法都放在這裏定義,而後各個組件本身去具體實現(這裏爲了演示方便,使用的常規的字符串和int類型。固然也能夠傳遞UIImage等很是規對象)

#import <Foundation/Foundation.h>

@protocol A_VC_Protocol <NSObject>
-(void)action_A:(NSString*)para1;

@end

@protocol B_VC_Protocol <NSObject>
-(void)action_B:(NSString*)para para2:(NSInteger)para2 para3:(NSInteger)para3 para4:(NSInteger)para4;
@end

複製代碼
組件A實現:
#import <UIKit/UIKit.h>
#import "CommonProtocol.h"

@interface A_VC : UIViewController<A_VC_Protocol>
@end


=============================

#import "A_VC.h"
#import "ProtocolMediator.h"


@implementation A_VC

//註冊本身的class
+(void)load{
    [[ProtocolMediator sharedInstance] registerProtocol:@protocol(A_VC_Protocol) forClass:[self class]];

}
     

//調用組件B,先經過protocol字符串取出類class,而後再實例化之調用組件B的方法    
-(void)btn_click{
    Class cls = [[ProtocolMediator sharedInstance] classForProtocol:@protocol(B_VC_Protocol)];
    UIViewController<B_VC_Protocol> *B_VC = [[cls alloc] init];
    [B_VC action_B:@"param1" para2:222 para3:333 para4:444];
}


-(void)action_A:(NSString*)para1 {
    NSLog(@"call action_A: %@",para1);
}

@end

複製代碼
組件B實現
#import <UIKit/UIKit.h>
#import "CommonProtocol.h"


@interface B_VC : UIViewController<B_VC_Protocol>
@end

=============

#import "B_VC.h"
#import "ProtocolMediator.h"

@implementation B_VC

+(void)load{
    [[ProtocolMediator sharedInstance] registerProtocol:@protocol(B_VC_Protocol) forClass:[self class]];
}


-(void)btn_click{
    Class cls = [[ProtocolMediator sharedInstance] classForProtocol:@protocol(A_VC_Protocol)];
    UIViewController<A_VC_Protocol> *A_VC = [[cls alloc] init];
    [A_VC action_A:@"param1"];
}


-(void)action_B:(NSString*)para1 para2:(NSInteger)para2 para3:(NSInteger)para3 para4:(NSInteger)para4{
    NSLog(@"call action_B: %@---%zd---%zd---%zd",para1,para2,para3,para4);
}

@end


複製代碼

原理和缺點

每一個組件先經過 Mediator 拿到其餘的組件對象class,而後在實例化該class爲實例對象,再經過該對象去調用它自身實現的protocol方法,由於是經過接口的形式實現的方法,因此任何類型參數都是能夠傳遞的。

可是這會致使一個問題:組件方法的調用是分散在各地的,沒有統一的入口,也就無法作組件不存在時的統一處理。

從上面的實現就能夠看出來A調用B不是直接經過mediator去調用,而是先經過mediator生成其餘組件的對象,而後本身再用該對象去調用其餘組件的方法,這就致使組件方法調用分散在各個調用組件內部,而不能像target-action方案那樣對全部組件的方法調用進行統一的管理。

再者這種方式讓組件同時依賴兩個中心:ProtocolMediator和CommonProtocol,依賴越多,後期擴展和遷移也會相對困難。

而且這種調用其餘組件的方式有點詭異,不是正常的使用方法,通常都是直接你發起一個調用請求,其餘組件直接把執行結果告訴你,可是這裏確實給你返回一個組件對象,讓你本身在用這個對象去發起請求,這操做有點蛋疼。。。

總結

其實蘑菇街的url-scheme加上protocol-class方案一塊兒提供組件間跳轉和調用會讓人無所適從,使用者還要區分不一樣的參數要使用的不一樣的方法,而target-action方案能夠用相同的方法來傳遞任意參數。綜上所述,target-action方案更優。

Demo下載

  1. url-scheme
  2. protocol-class
  3. target-action

組件化方案實施

從早上起牀寫到凌晨,實在寫不動了,留個坑,過年來在寫。

收拾收拾行李準備回家過年啦,提早給你們拜個早年 ~~~

相關文章
相關標籤/搜索