iOS组件化-路由设计分析

2020/7/12 23:09:31

本文主要是介绍iOS组件化-路由设计分析,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

组件化也是一个老生常谈的话题了,本文主要说一下在组件化中,站比较重要的位置的路由设计
你的项目里可能是直接依赖了三方的路由组件,也可能是自己根据项目的实际需求私人订制了一套路由组件,下面我想通过几个呼声比较高的三方组件来聊一聊路由的设计和分析。这里不推荐说大家用哪个好哪个不好,只是学习他们设计思想。就好比我们看三方库源码,应该都是学习编程和设计的思想为主。

前言

随着App的需求越来越多,业务越来越复杂,为了更高效的迭代以及提高用户体验,降低维护成本,对一个更高效的框架的需求也越来越急切。
所以我们可能都经历过项目的重构、组件化,根据项目的实际需求,新的框架可能需要横向,纵向不同粒度的分层,为了以后更有效率的开发和维护。随之而来的一个问题,如何保持“高内聚,低耦合”的特点,下面就来谈谈解决这个问题的一些思路。

路由能解决哪些问题

列举几个平时开发中遇到的问题,或者说是需求:

  1. 推送消息,或是网页打开一个url需要跳转进入App内部的某个页面
  2. 其他App,或者自己公司的别的App之间的相互跳转
  3. 不同组件之间页面的跳转
  4. 如何统一两端的页面跳转逻辑
  5. 线上某个页面出现bug,如何能降级成一个其他的H5或者错误页面
  6. 页面跳转埋点
  7. 跳转过程中的逻辑检查

以上这些问题,都可以通过设计一个路由来解决,下面带着这些问题继续看如何实现跳转。

实现跳转

通过上面的问题,我们希望设计一套路由,实现App外部和内部的统一跳转,所以先说一下App外部跳转的实现。

URL Scheme

在info.plist里面添加URL types - URL Schemes

然后在Safari中输入这里设置的URL Schemes就可以直接打开App

- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation
{
    
}
复制代码

通过上面这个方法就可以监听到外部App的调用,可以根据需要做一些拦截或者其他操作。

App也是可以直接跳转到系统设置的。比如有些需求要求检测用户有没有开启某些系统权限,如果没有开启就弹框提示,点击弹框的按钮直接跳转到系统设置里面对应的设置界面。

Universal links

Universal links这个功能可以使我们的App通过http链接来启动。

  • 如果安装过App,不管是在Safari还是其他三方浏览器或别的软件中,都可以打开App
  • 如果没安装过,就会打开网页

设置方式:

注意必须要applinks:开头

以上就是iOS系统中App间跳转的二种方式。

路由设计

说完App间的跳转逻辑,接下来就进入重点,App内部的路由设计。
主要要解决两个问题:

  • 各个组件之间相互调用,随着业务越来越复杂,如果组件化的粒度不太合适,会导致组件越来越多,组件间不可避免的依赖也越来越多
  • 页面和他所在组件之间的调用,组件内例如push一个VC ,就需要import这个类,从而导致强依赖,这样写死的代码也无法在出现线上bug的时候降级为其他页面

综合上面所说的两个问题,我们该如何设计一个路由呢?当然是先去看看别人造好的轮子-。-,下面会列举几个我在开发中用到过,以及参考过的轮子,有拿来主义直接使用的,也有借鉴人家思想自己封装的,总之都值得学习。

Route分析

JLRoutes

JLRoutes目前GitHub上star5.3k,应该是星最多的路由组件了,所以我们第一个分析他的设计思路。

  1. JLRoutes维护了一个全局的JLRGlobal_routeControllersMap,这个map以scheme为key,JLRoutes为value,所以每一个scheme都是唯一的。
+ (instancetype)routesForScheme:(NSString *)scheme
{
    JLRoutes *routesController = nil;
    
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        JLRGlobal_routeControllersMap = [[NSMutableDictionary alloc] init];
    });
    
    if (!JLRGlobal_routeControllersMap[scheme]) {
        routesController = [[self alloc] init];
        routesController.scheme = scheme;
        JLRGlobal_routeControllersMap[scheme] = routesController;
    }
    
    routesController = JLRGlobal_routeControllersMap[scheme];
    
    return routesController;
}
复制代码
  1. scheme可以看做是一个URI,每一个注册进来的字符串都会进行切分处理
- (void)addRoute:(NSString *)routePattern priority:(NSUInteger)priority handler:(BOOL (^)(NSDictionary<NSString *, id> *parameters))handlerBlock
{
    NSArray <NSString *> *optionalRoutePatterns = [JLRParsingUtilities expandOptionalRoutePatternsForPattern:routePattern];
    JLRRouteDefinition *route = [[JLRGlobal_routeDefinitionClass alloc] initWithPattern:routePattern priority:priority handlerBlock:handlerBlock];
    
    if (optionalRoutePatterns.count > 0) {
        // there are optional params, parse and add them
        for (NSString *pattern in optionalRoutePatterns) {
            JLRRouteDefinition *optionalRoute = [[JLRGlobal_routeDefinitionClass alloc] initWithPattern:pattern priority:priority handlerBlock:handlerBlock];
            [self _registerRoute:optionalRoute];
            [self _verboseLog:@"Automatically created optional route: %@", optionalRoute];
        }
        return;
    }
    
    [self _registerRoute:route];
}
复制代码
  1. 按优先级插入JLRoutes的数组中,优先级高的排列在前面
- (void)_registerRoute:(JLRRouteDefinition *)route
{
    if (route.priority == 0 || self.mutableRoutes.count == 0) {
        [self.mutableRoutes addObject:route];
    } else {
        NSUInteger index = 0;
        BOOL addedRoute = NO;
        
        // search through existing routes looking for a lower priority route than this one
        for (JLRRouteDefinition *existingRoute in [self.mutableRoutes copy]) {
            if (existingRoute.priority < route.priority) {
                // if found, add the route after it
                [self.mutableRoutes insertObject:route atIndex:index];
                addedRoute = YES;
                break;
            }
            index++;
        }
        
        // if we weren't able to find a lower priority route, this is the new lowest priority route (or same priority as self.routes.lastObject) and should just be added
        if (!addedRoute) {
            [self.mutableRoutes addObject:route];
        }
    }
    
    [route didBecomeRegisteredForScheme:self.scheme];
}

复制代码
  1. 查找路由
    根据URL初始化一个JLRRouteRequest,然后在JLRoutes的数组中依次查找,直到找到一个匹配的然后获取parameters,执行Handler
- (BOOL)_routeURL:(NSURL *)URL withParameters:(NSDictionary *)parameters executeRouteBlock:(BOOL)executeRouteBlock
{
    if (!URL) {
        return NO;
    }
    
    [self _verboseLog:@"Trying to route URL %@", URL];
    
    BOOL didRoute = NO;
    
    JLRRouteRequestOptions options = [self _routeRequestOptions];
    JLRRouteRequest *request = [[JLRRouteRequest alloc] initWithURL:URL options:options additionalParameters:parameters];
    
    for (JLRRouteDefinition *route in [self.mutableRoutes copy]) {
        // check each route for a matching response
        JLRRouteResponse *response = [route routeResponseForRequest:request];
        if (!response.isMatch) {
            continue;
        }
        
        [self _verboseLog:@"Successfully matched %@", route];
        
        if (!executeRouteBlock) {
            // if we shouldn't execute but it was a match, we're done now
            return YES;
        }
        
        [self _verboseLog:@"Match parameters are %@", response.parameters];
        
        // Call the handler block
        didRoute = [route callHandlerBlockWithParameters:response.parameters];
        
        if (didRoute) {
            // if it was routed successfully, we're done - otherwise, continue trying to route
            break;
        }
    }
    
    if (!didRoute) {
        [self _verboseLog:@"Could not find a matching route"];
    }
    
    // if we couldn't find a match and this routes controller specifies to fallback and its also not the global routes controller, then...
    if (!didRoute && self.shouldFallbackToGlobalRoutes && ![self _isGlobalRoutesController]) {
        [self _verboseLog:@"Falling back to global routes..."];
        didRoute = [[JLRoutes globalRoutes] _routeURL:URL withParameters:parameters executeRouteBlock:executeRouteBlock];
    }
    
    // if, after everything, we did not route anything and we have an unmatched URL handler, then call it
    if (!didRoute && executeRouteBlock && self.unmatchedURLHandler) {
        [self _verboseLog:@"Falling back to the unmatched URL handler"];
        self.unmatchedURLHandler(self, URL, parameters);
    }
    
    return didRoute;
}
复制代码

CTMediator

CTMediator 目前github上star 3.3k ,这个库特别的轻量级,只有一个类和一个category,一共也没几行代码,更可的是作者还在关键代码处添加了中文注释以及比较详细的example
主要思想是利用Target-Action,使用runtime实现解耦。这种模式每个组件之间互不依赖,但是都依赖中间件进行调度。
头文件中暴露了两个分别处理远程App和本地组件调用的方法

// 远程App调用入口
- (id _Nullable)performActionWithUrl:(NSURL * _Nullable)url completion:(void(^_Nullable)(NSDictionary * _Nullable info))completion;
// 本地组件调用入口
- (id _Nullable )performTarget:(NSString * _Nullable)targetName action:(NSString * _Nullable)actionName params:(NSDictionary * _Nullable)params shouldCacheTarget:(BOOL)shouldCacheTarget;
复制代码

对于远程App,还做了一步安全处理,最后解析完也是同样调用了本地组件处理的方法中

- (id)performActionWithUrl:(NSURL *)url completion:(void (^)(NSDictionary *))completion
{
    if (url == nil) {
        return nil;
    }
    
    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;
}
复制代码

对于无响应的请求还统一做了处理

- (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类,以Target_为前缀命名
    这个类里面就要添加上所有需要被其他组件调度的方法,方法以Action_为前缀命名。
  • 为每一个组件创建一个CTMediator的category
    这个category就是供调用方依赖完成调度的,这样category中所有方法的调用就很统一,全部是performTarget: action: params: shouldCacheTarget:
  • 最终的调度逻辑都交给CTMediator
  • 调用方只需要依赖有调度需求的组件的category

感兴趣的还可以看一下作者的文章,详细介绍了CTMediator的设计思想以及为已有项目添加CTMediator
iOS应用架构谈 组件化方案
在现有工程中实施基于CTMediator的组件化方案

MGJRouter

MGJRouter 目前github上star 2.2k

这个库的由来:JLRoutes 的问题主要在于查找 URL 的实现不够高效,通过遍历而不是匹配。还有就是功能偏多。 HHRouter 的 URL 查找是基于匹配,所以会更高效,MGJRouter 也是采用的这种方法,但它跟 ViewController 绑定地过于紧密,一定程度上降低了灵活性。 于是就有了 MGJRouter。

/**
 *  保存了所有已注册的 URL
 *  结构类似 @{@"beauty": @{@":id": {@"_", [block copy]}}}
 */
@property (nonatomic) NSMutableDictionary *routes;
复制代码

MGJRouter是一个单例对象,在其内部维护着一个“URL -> block”格式的注册表,通过这个注册表来保存服务方注册的block。使调用方可以通过URL映射出block,并通过MGJRouter对服务方发起调用。

大概的使用流程如下:

  • 业务组件对外提供一个PublicHeader,在PublicHeader中声明外部可以调用的一系列URL
#ifndef MMCUserUrlDefines_h
#define MMCUserUrlDefines_h

/**
 description 我的个人中心页
 
 @return MMCUserViewController
 */
#define MMCRouterGetUserViewController @"MMC://User/UserCenter"

/**
 description 我的消息列表
 
 @return MMCMessageListViewController
 */
#define MMCRouterGetMessageVC @"MMC://User/MMCMessageListViewController"
复制代码
  • 在组件内部实现block的注册工作,调用方通过URL对block发起调用
+ (void)registerGotoUserVC
{
    [MMCRouter registerURLPattern:MMCRouterGetUserViewController toHandler:^(NSDictionary *params) {
    
    }];
}
复制代码
  • 通过openURL调用,可以通过GET请求的方式在url后面拼接参数,也可以通过param传入一个字典
[MMCRouter openURL:MMCRouterGetUserViewController];
复制代码
  • 除了跳转,MGJRouter还提供了可以返回一个对象的方法
+ (void)registerURLPattern:(NSString *)URLPattern toObjectHandler:(MGJRouterObjectHandler)handler
复制代码

举个例子:这个route就返回了一个控制器,可以交给调用方自行处理。

+(void)registerSearchCarListVC{
    [MMCRouter registerURLPattern:MMCRouterGetSearchCarListController toObjectHandler:^id(NSDictionary *params) {
        NSDictionary *userInfo = [params objectForKey:MMCRouterParameterUserInfo];
        NSString *carType = [userInfo objectForKey:MMCRouterCarType];
        MMCSearchCarListViewController *vc = [[MMCSearchCarListViewController alloc] init];
        vc.strCarType = carType;
        return vc;
    }];
}
复制代码
Protocol-class方案

根据上面介绍的MGJRouter的使用,不难看出存在URL硬编码和参数局限性的问题,为了解决这些问题,蘑菇街又提出来Protocol方案。Protocol方案由两部分组成,进行组件间通信的ModuleManager类以及MGJComponentProtocol协议类。

通过中间件ModuleManager进行消息的调用转发,在ModuleManager内部维护一张映射表,映射表由之前的"URL -> block"变成"Protocol -> Class"。

因为目前手里的项目没有用到这个,所以使用代码就不贴了,感兴趣的可以自行百度。

优缺点

URL注册方案

优点:

  1. 最容易想到的最简单的方式
  2. 可以统一三端的调度
  3. 线上bug动态降级处理

缺点:

  1. 硬编码,URL需要专门管理
  2. URL规则需要提前注册
  3. 有常驻内存,可能发生内存问题

Protocol-Class方案

优点:

  1. 无硬编码
  2. 参数无限制,甚至可以传递model

缺点:

  1. 增加了新的中间件以及很多protocol,调用编码复杂
  2. route的跳转逻辑分散在各个类中,不好维护

Target-Action方案

优点:

  1. 利用runtime实现解耦调用,无需注册
  2. 有一定的安全处理
  3. 统一App外部和组件间的处理

缺点:

  1. 需要为每一个组件另外创建一个category,作者建议category也是一个单独的pod
  2. 调用内部也是硬编码,要求Target_ ,Action_ 命名规则

最后想说的是,没有最好的route,只有最适合你项目的route,根据自己项目的实际情况,分析决定要使用哪一种组件化方案。



这篇关于iOS组件化-路由设计分析的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程