iOS 組件化方案


一、基本概括

講解

在組件化之前,app都是在一個工程里開發的,開發的人員也是比較少的,業務發展也不是非常快,項目中不引用組件化開發也是合適的。但是當開發人員越來越多,代碼量也就越來越多,業務也就越來越復雜,這時候單一的開發模式會顯露出一些弊端:

  • 容易出現沖突(使用xib)
  • 耦合較嚴重(代碼沒有明確的約束,代碼臃腫)
  • 開發效率不夠高

為了解決這些問題,於是出現了組件化開發的策略,能帶來好處如下:

  • 加快編譯速度
  • 自由選擇開發姿勢(MVC/MVVM/FRP)
  • 方便有針對性測試
  • 提高開發效率

今天我們講解一下組件化開發,也是自己項目中使用到的一種方式Target-Action方式。主要是基於Mediator模式和Target-Action,中間采用了Runtime來完成調用。這套組件化將遠程和本地應用調用做了拆分,而且是本地調用是為遠程調用提供服務。

下面是target-action的工作圖:

 調用方式:

本地組件調用:本地組件A在一處調用[[CTMediator sharedInstance] performTarget:targetName action:actionName params:@{...}]向CTMediator發起了跨組件調用,CTMediator根據發送過來的target和action,然后經過OC的runtime機制轉為target實例以及action,最后調用到目標業務提供的邏輯,完成要求.

遠程應用的調取:遠程應用是通過openURL的方式,由iOS 系統根據info.plist里的scheme配置用來可以找到響應的URL的應用,應用直接通過AppDelegate接收到URL之后,調用了CTMediator的OpenURL方法將接收到的信息傳入進去.當然,CTMediator也可以用CTMediator的openURL:options:方式順便將option也接收,這取決於是否包含了option數據,傳入URL之后,CTMediator進行解析URL,將請求的路由到相對應的target-action中,隨后的過程就變成了上面的本地應用調用過程了,最終完成了響應.

對應着如下:

 

 組件化方案中的去Model化設計

組件化調用的時候,應該設計是要對參數做去Model化的。如果組件間調用不對的參數做去Model化的設計,就會導致有問題-業務形式上被組件化了,而實際上是沒有獨立。

如果模塊A和模塊B采用了Model化的方案,調用方法時傳遞的參數就是一個對象,因此使用對象化的參數無論是面向接口,帶來的結果就是業務模塊形式上被組件化了,但是實質上還是沒有被獨立。

在跨模塊場景中,參數最好還是使用去Model化的的方式去傳遞,在蘋果開發中,也就是以字典的形式去傳遞。這樣就可以做到只有調用方依賴mediator,而響應方是不需要依賴mediator。

在去Model的組件化的方案中,影響效率的總有兩個:

  • 調用方如何知道接收方需要哪些參數呢?
  • 調用方怎么知道有哪些target可以被調用呢?

其實后面問題,無論是不是具有去Model都有這個問題,但是為什么要一起說了呢,因為下面提供一種方案來將出現的兩個問題一起解決。

 

解決方案使用category

該方案基於是mediator和target-action模式組件化開發,通過運行時完成調用。mediator維護着若干個category,一個category對應着一個target,而一個target可以包含着多個action。

我們也可以這樣理解:一個業務組件包括了一個category組件,這個category中有個mediator的category,而category中有一個target,這個target對應着此業務組件。target中又有若干個接口方法,用來其他業務來獲取該業務組件中的業務。

category本身是一種組合模式,根據不同的分類提供不同方法,此時每個組件都是一個分類。

if (indexPath.row == 0) {
        UIViewController *viewController = [[CTMediator sharedInstance] CTMediator_viewControllerForDetail];

        // 獲得view controller之后,在這種場景下,到底push還是present,其實是要由使用者決定的,mediator只要給出view controller的實例就好了
        [self presentViewController:viewController animated:YES completion:nil];
    }

    if (indexPath.row == 1) {
        UIViewController *viewController = [[CTMediator sharedInstance] CTMediator_viewControllerForDetail];
        [self.navigationController pushViewController:viewController animated:YES];
    }

    if (indexPath.row == 2) {
        // 這種場景下,很明顯是需要被present的,所以不必返回實例,mediator直接present了
        [[CTMediator sharedInstance] CTMediator_presentImage:[UIImage imageNamed:@"image"]];
    }

    if (indexPath.row == 3) {
        // 這種場景下,參數有問題,因此需要在流程中做好處理
        [[CTMediator sharedInstance] CTMediator_presentImage:nil];
    }

    if (indexPath.row == 4) {
        [[CTMediator sharedInstance] CTMediator_showAlertWithMessage:@"casa" cancelAction:nil confirmAction:^(NSDictionary *info) {
            // 做你想做的事
            NSLog(@"%@", info);
        }];
    }

上面就是我對Mediator下target-action模式的基本內容講解,下面講解CTMediator代碼的具體實現方式!!!

 

二、代碼講解

2.1 獲取CTMediator單例對象

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

 

2.2 遠程App調用入口

/*
 scheme://[target]/[action]?[params]
 
 url sample:
 aaa://targetA/actionB?id=1234&title=title
 
 [url query]:  id=1234&title=title
 [url path]:  /actionB
 [url host]:  targetA
 */

- (id)performActionWithUrl:(NSURL *)url completion:(void (^)(NSDictionary *))completion
{
    //url參數的處理
    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;
}

 

2.3 本地組件調用的入口

/**
 *  本地組件調用入口
 *
 *  @param targetName 類對象   OC中類對象是要Target_為前綴的
 *  @param actionName 方法名稱  最后實際調用的是以Action_為前綴的
 *  @param params     參數
 *  @param shouldCacheTarget 是否緩存拼接后的類對象
 *
 *  @return return value JSon格式的字符串
 */
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget
{
    //供swift項目使用
    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];
    }

    // 拼裝方法字符串
    NSString *actionString = [NSString stringWithFormat:@"Action_%@:", actionName];
    // 生成SEL
    SEL action = NSSelectorFromString(actionString);
    //先從緩存取,取不到去創建,但是也有可能創建失敗的情況(targetName值不正確)
    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;
        }
    }
}

注意: 想要調用此方法,定義的類必須要是Target_為前綴的,並且方法必須是Action為前綴的!!!

另外代碼也對無響應的情況分了兩種情況:

  1. target == nil會觸發NoTargetActionResponseWithTargetString這個方法
  2. action不能響應的時候,會先調用notFound方法,如果notFound還沒有響應,依然還是會調用NoTargetActionResponseWithTargetString方法

 在實際的開發中,可以給無響應的事件提前做一個固定的target,頂上這種特殊情況.

#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];
}

注意:代碼中Target_NoTargetAction用來統一處理無響應的時候給的固定的target,action_response就是用來調用的方法.

 

 2.4 清除緩存

#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];
}

上面是CTMediator的具體代碼,大家可以多看看關於本地組件調用的實現代碼!!! 下面講解本項目中使用到的具體內容.

 

三、項目使用

從智能引擎搜索結果頁-交易商詳情頁界面如上,跨越了兩個模塊,項目采用了CTMediator的Target-Action模式.下面按照執行的順序截圖如下:

3.1 點擊tableViewCell->func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)

項目采取的方式是MVP架構模式!!!

 

3.2 進入openBrokerDetailScene,開始調CTMediator

 

3.3 開始進入EngineToBroker_viewController,調用self.performTarget("Broker", action: "brokerDetailVC", params: params, shouldCacheTarget: false)

 

3.4 開始進入Target_類,以及Action_方法中

 

經過這層層進入,最終得到了viewController

 

3.5 最終得到了ViewController,回到即將跳轉到

 

上面就是整個項目中使用的方式,希望對大家有所幫助!!! 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM