Xcode工程組件化之路(1)------ 組件間通信:中間件

1.前言

    當項目越來越來龐大,參與編寫的人員越來多,代碼分支和接入產品越發複雜時,項目組件化成了不二選擇,什麼是項目組件化?筆者簡略概述爲,以pod庫的形式將複雜的系統業務拆分成不同模塊,進而隔離不同的業務功能,然後分發與不同人員負責開發和維護,降低系統代碼耦合度,方便管理。概括地不全或有誤,請大家指正。

     組件化有什麼用呢?組件化最大的作用是隔離組件和功能。組件隔離是不同業務不會有直接的依賴,也就是說不會直接#import其他組件頭文件,在編譯時,各個組件是不耦合的,在進行開發調試測試各個時期時,可以單獨進行,而不需要依賴其他功能模塊,提高工作效率。

2.中間件的選取

     要實現組件化,不得不考慮的是組件間的通信問題,如果單純pod第三方,然後在主工程中調用,其實是不需要中間件來轉發組件間的通信的,但是項目組件化過程中,這是一個不得不做的功能。要實現不同組件間的通信,就需要一個消息轉發的中間層,這個中間層就是中間件。

    

    中間件的作用就是減少各個業務的相互依賴關係。各個組件的通信都交由中間件做消息轉發,解耦系統,增加項目工程的可管理性。

2.1 Router

    利用router路由的方式實現組件間的通信。用的比較多的就是MGJRouterHHRouterFFRouter。路由router實現的方法大致是在提供服務的組件中先註冊block,然後在需要調用的方法組件中通過URL調用block,來達到消息轉發的目的。

  MGJRouter是一個單例對象,在其內部維護着一個“URL -> Block”格式的註冊表,通過這個註冊表來保存服務方註冊的Block,以及使調用方可以通過URL映射出Block,並通過MGJRouter對服務方發起調用。MGJRouter是所有組件的調度中心,負責所有組件的調用、切換、特殊處理等操作,可以用來處理一切組件間發生的關係。除了原生頁面的解析外,還可以根據URL跳轉H5頁面。

- (void)demoFallback
{
    [MGJRouter registerURLPattern:@"mgj://" toHandler:^(NSDictionary *routerParameters) {
        [self appendLog:@"匹配到了 url,以下是相關信息"];
        [self appendLog:[NSString stringWithFormat:@"routerParameters:%@", routerParameters]];
    }];
    
    [MGJRouter registerURLPattern:@"mgj://foo/bar/none/exists" toHandler:^(NSDictionary *routerParameters) {
        [self appendLog:@"it should be triggered"];
    }];
    
    [MGJRouter openURL:@"mgj://foo/bar"];
}

    通過OpenURL:方法傳入的URL參數,對詳情頁已經註冊的Block方法發起調用。調用方式類似於Get請求,URL地址後面拼接參數。 

[MGJRouter openURL:@"mgj://detail?id=404"];

    也可以通過字典方式傳參,MGJRouter提供了帶有字典參數的方法,這樣就可以傳遞非字符串之外的其他類型參數。

[MGJRouter openURL:@"mgj://detail?" withParam:@{@"id" : @"404"}];

     

+ (void)load
{
    DemoDetailViewController *detailViewController = [[DemoDetailViewController alloc] init];
    [DemoListViewController registerWithTitle:@"基本使用" handler:^UIViewController *{
        detailViewController.selectedSelector = @selector(demoBasicUsage);
        return detailViewController;
    }];
    
    [DemoListViewController registerWithTitle:@"中文匹配" handler:^UIViewController *{
        detailViewController.selectedSelector = @selector(demoChineseCharacter);
        return detailViewController;
    }];
    
    [DemoListViewController registerWithTitle:@"自定義參數" handler:^UIViewController *{
        detailViewController.selectedSelector = @selector(demoParameters);
        return detailViewController;
    }];
    
    [DemoListViewController registerWithTitle:@"傳入字典信息" handler:^UIViewController *{
        detailViewController.selectedSelector = @selector(demoUserInfo);
        return detailViewController;
    }];
    
    [DemoListViewController registerWithTitle:@"Fallback 到全局的 URL Pattern" handler:^UIViewController *{
        detailViewController.selectedSelector = @selector(demoFallback);
        return detailViewController;
    }];

    整體調用流程:

  1. 在進入程序後,先使用MGJRouter對服務方組件進行註冊。每個URL對應一個Block的實現,Block中的代碼就是服務方對外提供的服務,調用方可以通過URL調用這個服務。
  2. 調用方通過MGJRouter調用OpenURL:方法,並將被調用代碼對應的URL傳入,MGJRouter會根據URL查找對應的Block實現,從而調用服務方組件的代碼進行通信。
  3. 調用和註冊Block時,Block有一個字典用來傳遞參數。這樣的優勢就是參數類型和數量理論上是不受限制的,但是需要很多硬編碼的key名在項目中。

 2.2 Mediator

    Mediator中間件使用開源庫CTMediator。對於服務方的組件來說,每個組件都提供一個或多個Target類,在Target類中聲明Action方法。Target類是當前組件對外提供的一個“服務類”,Target將當前組件中所有的服務都定義在裏面,CTMediator通過runtime主動發現服務。在Target中的所有Action方法,都只有一個字典參數,所以可以傳遞的參數很靈活,這也是CTMediator提出的去Model化的概念。在Action的方法實現中,對傳進來的字典參數進行解析,再調用組件內部的類和方法。

 

    上圖就爲中間件Mediator,CTMediator是一個單例對象,它包含遠程APP調用入口方法和本地組件調用入口方法。

// 遠程App調用入口
- (id)performActionWithUrl:(NSURL *)url completion:(void(^)(NSDictionary *info))completion;
// 本地組件調用入口
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget;

以下爲CTMediator的消息轉發機制

- (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 *swiftModuleName = params[kCTMediatorParamsKeySwiftTargetModuleName];
    
    // generate target
    NSString *targetClassString = nil;
    if (swiftModuleName.length > 0) {
        targetClassString = [NSString stringWithFormat:@"%@.Target_%@", swiftModuleName, targetName];
    } else {
        targetClassString = [NSString stringWithFormat:@"Target_%@", targetName];
    }
    NSObject *target = self.cachedTarget[targetClassString];
    if (target == nil) {
        Class targetClass = NSClassFromString(targetClassString);
        target = [[targetClass alloc] init];
    }

    // generate action
    NSString *actionString = [NSString stringWithFormat:@"Action_%@:", actionName];
    SEL action = NSSelectorFromString(actionString);
    
    if (target == nil) {
        // 這裏是處理無響應請求的地方之一,這個demo做得比較簡單,如果沒有可以響應的target,就直接return了。實際開發過程中是可以事先給一個固定的target專門用於在這個時候頂上,然後處理這種請求的
        [self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params];
        return nil;
    }
    
    if (shouldCacheTarget) {
        self.cachedTarget[targetClassString] = target;
    }

    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 NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params];
            [self.cachedTarget removeObjectForKey:targetClassString];
            return nil;
        }
    }
}

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

#pragma mark - private methods
- (void)NoTargetActionResponseWithTargetString:(NSString *)targetString selectorString:(NSString *)selectorString originParams:(NSDictionary *)originParams
{
    SEL action = NSSelectorFromString(@"Action_response:");
    NSObject *target = [[NSClassFromString(@"Target_NoTargetAction") alloc] init];
    
    NSMutableDictionary *params = [[NSMutableDictionary alloc] init];
    params[@"originParams"] = originParams;
    params[@"targetString"] = targetString;
    params[@"selectorString"] = selectorString;
    
    [self safePerformAction:action target:target params:params];
}

- (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;
}

而CTMediator的Category提供了一個獲取控制器VC並且跳轉的功能,外界的其他組件依賴於CTMediator,通過CTMediator的擴展提供的方法進行參數的傳遞或者界面的跳轉控制,下面爲Category提供的方法接口。

#import "CTMediator+CTMediatorModuleAActions.h"

NSString * const kCTMediatorTargetA = @"A";

NSString * const kCTMediatorActionNativFetchDetailViewController = @"nativeFetchDetailViewController";
NSString * const kCTMediatorActionNativePresentImage = @"nativePresentImage";
NSString * const kCTMediatorActionNativeNoImage = @"nativeNoImage";
NSString * const kCTMediatorActionShowAlert = @"showAlert";
NSString * const kCTMediatorActionCell = @"cell";
NSString * const kCTMediatorActionConfigCell = @"configCell";

@implementation CTMediator (CTMediatorModuleAActions)

- (UIViewController *)CTMediator_viewControllerForDetail
{
    UIViewController *viewController = [self performTarget:kCTMediatorTargetA
                                                    action:kCTMediatorActionNativFetchDetailViewController
                                                    params:@{@"key":@"value"}
                                         shouldCacheTarget:NO
                                        ];
    if ([viewController isKindOfClass:[UIViewController class]]) {
        // view controller 交付出去之後,可以由外界選擇是push還是present
        return viewController;
    } else {
        // 這裏處理異常場景,具體如何處理取決於產品
        return [[UIViewController alloc] init];
    }
}

而組件中提供CTMediator消息轉發服務的對象,定義在了Target_A中,下面代碼爲Target_A中具體實現,在Target_A(kCTMediatorTargetA)返回具體控制器。這些Target_A的服務可以被CTMediator通過runtime的方式調用,而且在Target_A可以對相關的參數進行處理和block調用等,保證組件間的業務邏輯是隔離的,只通過CTMediator來進行調用。

當外界組件依賴並開始使用CTMediator的Category方法時,通過CTMediator底層的消息轉發機制,獲取相關Target_A中相關的控制器,再交由CTMediator的Category方法進行跳轉或者參數的傳遞,完成不同組件間的通信。

- (UIViewController *)Action_nativeFetchDetailViewController:(NSDictionary *)params
{
    // 因爲action是從屬於ModuleA的,所以action直接可以使用ModuleA裏的所有聲明
    DemoModuleADetailViewController *viewController = [[DemoModuleADetailViewController alloc] init];
    viewController.valueLabel.text = params[@"key"];
    return viewController;
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章