【iOS开源库】JLRoutes源码阅读&原理解析

引子

近期要开新项目,包括iOS&Android。正好是做一款强运营的电商类APP。所以无论如何都是要用到Router的。


参考github上的Router开源库,整体看过来基本JLRoutes用的最多,今天就来掰扯掰扯JLRoutes的实现(JLRoutes 2.1链接)。

组件化思路

先简单说下常用组件化思想和背景,在大公司或者复杂的项目中,常见的方式是需要跳转到某个具体的viewController的时候,#import viewController 然后进行push或者present进行展示。这样会引入两个问题:

  • 模块不清晰,文件之间相互依赖
  • 维护不方便,假如依赖的模块进行修改时,其他跳转的部分都需要同步修改

为了让工程结构清晰、易读和降低维护成本,常用的会进行模块拆分,通过 Router 的概念进行依赖相互之间的跳转。所以可以看到 JLRouter 的Star会很高。

JLRoutes组件说明

JLRoutes 其实只做了一件事情,就是管理对应的 < 路径Block >的映射和管理。然后根据传入的 URI 进行查询和执行对应的 Block 。同时也引入了 * 或者 (/可选路径) 匹配规则 和 优先级设置。由于整体工程较小,说明下每个类的功能(按照从最基础的开始枚举)。

  • JLRParsingUtilities 管理可选路径枚举,下面举个例子就知道了,括号为可选(源码标注中有一处错误,下列描述已经修改):
     /path/:thing/(/a)(/b)(/c)
     
     create the following paths:
     
     /path/:thing/a/b/c
     /path/:thing/a/b
     /path/:thing/a/c
     /path/:thing/b/c
     /path/:thing/a
     /path/:thing/b
     /path/:thing/c
     
     */
  • JLRRouteRequest 提供输入URL的分解,分解为scheme、path、param和fragment等
  • JLRRouteResponse 则是结果的封装,包括匹配的参数、是否匹配等内容
  • JLRRouteDefinition 用固有的规则初始化,去计算 JLRouteRequest 是否匹配自己的规则并输出
  • JLRoutes 进行Route的管理、调度、优先级管理,核心是 NSArray 部分
  • JLRRouteHandler 工具辅助类,不参与主逻辑,可以忽略

类功能说明

JLRParsingUtilities

JLRParsingUtilities 主要提供根据传入的匹配链接生成对应的合规的URI。主要的功能可以查看代码中给的说明:例如路径 /path/:thing/(/a)(/b)(/c) 中包含有可选路径 a b c ,该类主要是提供几个功能模块:

  • 展开可选路径,生成list。主要是如下实现:
+ (NSArray  *)expandOptionalRoutePatternsForPattern:(NSString *)routePattern
{
    /* this method exists to take a route pattern that is known to contain optional params, such as:
     
     /path/:thing/(/a)(/b)(/c)
     
     and create the following paths:
     
     /path/:thing/a/b/c
     /path/:thing/a/b
     /path/:thing/a/c
     /path/:thing/b/a
     /path/:thing/a
     /path/:thing/b
     /path/:thing/c
     
     */
    
    if ([routePattern rangeOfString:@"("].location == NSNotFound) {
        return @[];
    }
    
    // First, parse the route pattern into subpath objects.
    NSArray  *subpaths = [self _routeSubpathsForPattern:routePattern];
    if (subpaths.count == 0) {
        return @[];
    }
    
    // Next, etract out the required subpaths.
    NSSet  *requiredSubpaths = [NSSet setWithArray:[subpaths JLRoutes_filter:^BOOL(JLRParsingUtilities_RouteSubpath *subpath) {
        return !subpath.isOptionalSubpath;
    }]];
    
    // Then, expand the subpath permutations into possible route patterns.
    NSArray  *> *allSubpathCombinations = [subpaths JLRoutes_allOrderedCombinations];
    
    // Finally, we need to filter out any possible route patterns that don't actually satisfy the rules of the route.
    // What this means in practice is throwing out any that do not contain all required subpaths (since those are explicitly not optional).
    NSArray  *> *validSubpathCombinations = [allSubpathCombinations JLRoutes_filter:^BOOL(NSArray  *possibleRouteSubpaths) {
        return [requiredSubpaths isSubsetOfSet:[NSSet setWithArray:possibleRouteSubpaths]];
    }];
    
    // Once we have a filtered list of valid subpaths, we just need to convert them back into string routes that can we registered.
    NSArray  *validSubpathRouteStrings = [validSubpathCombinations JLRoutes_map:^id(NSArray  *subpaths) {
        NSString *routePattern = @"/";
        for (JLRParsingUtilities_RouteSubpath *subpath in subpaths) {
            NSString *subpathString = [subpath.subpathComponents componentsJoinedByString:@"/"];
            routePattern = [routePattern stringByAppendingPathComponent:subpathString];
        }
        return routePattern;
    }];
    
    // Before returning, sort them by length so that the longest and most specific routes are registered first before the less specific shorter ones.
    validSubpathRouteStrings = [validSubpathRouteStrings sortedArrayUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"length" ascending:NO selector:@selector(compare:)]]];
    
    return validSubpathRouteStrings;
}

这里代码首先根据 [self _routeSubpathsForPattern:routePattern] 获取分段Array列表,然后通过fliter 筛选出可选项。
然后通过 NSArray的JLRoutes_Utilities 扩展,生成对应组合的路径。例如通过 @[@"a", @"b", @"c"] 生成如下list:

\a\b\c
\a\b
\a\c
\b\c
\a
\b
\c
\

然后通过过滤部分前缀为空产生的不符合前缀的string,进行排序返回。

JLRRouteRequest & JLRRouteResponse

其实这两个类看名字就知道,主要是提供在有规则的情况下,我们输入一个 URL,例如: http://www.baidu.com/a/b/c 会生成一个 JLRRouteRequest 对象,将协议、域名和后面的路径、参数等都拆分开来。具体的可以参考源码,逻辑比较简单。
JLRRouteResponse 则是在经过规则匹配后,得到的相关参数的一个返回对象。

JLRRouteDefinition

JLRRouteDefinition 是匹配规则的核心类,对应的是输入一个匹配规则,例如:weixin://push/:viewController/:param1/:param2 ,那么先解析需要的参数和协议、前缀、优先级以及callback,得到对应的信息后,暴露匹配接口,这里就用到了上面提到的 JLRRouteRequest 对象。

- (NSDictionary  *)routeVariablesForRequest:(JLRRouteRequest *)request
{
    NSMutableDictionary *routeVariables = [NSMutableDictionary dictionary];
    
    BOOL isMatch = YES;
    NSUInteger index = 0;
    
    for (NSString *patternComponent in self.patternPathComponents) {
        NSString *URLComponent = nil;
        BOOL isPatternComponentWildcard = [patternComponent isEqualToString:@"*"];
        
        if (index < [request.pathComponents count]) {
            URLComponent = request.pathComponents[index];
        } else if (!isPatternComponentWildcard) {
            // URLComponent is not a wildcard and index is >= request.pathComponents.count, so bail
            isMatch = NO;
            break;
        }
        
        if ([patternComponent hasPrefix:@":"]) {
            // this is a variable, set it in the params
            NSAssert(URLComponent != nil, @"URLComponent cannot be nil");
            NSString *variableName = [self routeVariableNameForValue:patternComponent];
            NSString *variableValue = [self routeVariableValueForValue:URLComponent];
            
            // Consult the parsing utilities as well to do any other standard variable transformations
            BOOL decodePlusSymbols = ((request.options & JLRRouteRequestOptionDecodePlusSymbols) == JLRRouteRequestOptionDecodePlusSymbols);
            variableValue = [JLRParsingUtilities variableValueFrom:variableValue decodePlusSymbols:decodePlusSymbols];
            
            routeVariables[variableName] = variableValue;
        } else if (isPatternComponentWildcard) {
            // match wildcards
            NSUInteger minRequiredParams = index;
            if (request.pathComponents.count >= minRequiredParams) {
                // match: /a/b/c/* has to be matched by at least /a/b/c
                routeVariables[JLRouteWildcardComponentsKey] = [request.pathComponents subarrayWithRange:NSMakeRange(index, request.pathComponents.count - index)];
                isMatch = YES;
            } else {
                // not a match: /a/b/c/* cannot be matched by URL /a/b/
                isMatch = NO;
            }
            break;
        } else if (![patternComponent isEqualToString:URLComponent]) {
            // break if this is a static component and it isn't a match
            isMatch = NO;
            break;
        }
        index++;
    }
    
    if (!isMatch) {
        // Return nil to indicate that there was not a match
        routeVariables = nil;
    }
    
    return [routeVariables copy];
}

该部分逻辑比较简单:

  • 判断是否是参数或者有通配符,如果没有,那么现在就判断路径是否相等
  • 如果匹配参数,那么记录key-value对
  • 如果有通配符,主要是匹配后面的参数数量等
    得到参数后,返回到 JLRoutes 模块进行 JLRRouteResponse 对象封装出去

JLRoutes

JLRoutes 提供对外的接口,以及Routes的管理。
主要有几个层级:

  • Scheme,可以默认使用global的,也可以添加自定义的,方便APP扩展不同scheme
  • 管理规则对象JLRRouteDefinition , 根据得到的一系列 JLRRouteDefinition 对象,在传入路径时,根据优先级遍历去判断是否符合当前规则,如果符合则进入相关的callback。

整体逻辑如下,由于JLRoutes 主要是提供路由功能,所以功能模块都是偏向于对URI进行解析和分解。所以匹配到的具体逻辑操作,都是依赖给定的callback去进行处理。

整个的Routes模块做的事情比较简单,也可以了解下大公司做的组件化方案,他们可能实现方式上会不同,但用的Routes的原理上是一致的。


可以关注个人公众号联系我,欢迎大家一起沟通交流。

可以关注个人公众号联系我,欢迎大家一起沟通交流。

你可能感兴趣的:(【iOS开源库】JLRoutes源码阅读&原理解析)