zoukankan      html  css  js  c++  java
  • 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,回到即将跳转到

    上面就是整个项目中使用的方式,希望对大家有所帮助!!! 

  • 相关阅读:
    怎样理解 display:none 和 visibility:hidden
    怎样设置鼠标悬浮时弹出的文字提示框内容
    怎样获取当前元素节点的语言类型
    怎样控制元素节点的是否可拖动属性
    怎样读写分配给当前元素的快捷键
    怎样获取元素节点的标签名称
    怎样查看或修改元素节点的id属性
    怎样使用js将文本复制到系统粘贴板中
    怎样创建一个子树遍历器
    怎样创建一个子节点遍历器
  • 原文地址:https://www.cnblogs.com/guohai-stronger/p/11768677.html
Copyright © 2011-2022 走看看