【iOS开源库】JLRoutes源码阅读&原理解析
引子
近期要开新项目,包括iOS&Android。正好是做一款强运营的电商类APP。所以无论如何都是要用到Router的。
</br>
参考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<JLRRouteDefinition*>
部分 -
JLRRouteHandler
工具辅助类,不参与主逻辑,可以忽略
类功能说明
JLRParsingUtilities
JLRParsingUtilities 主要提供根据传入的匹配链接生成对应的合规的URI。主要的功能可以查看代码中给的说明:例如路径 /path/:thing/(/a)(/b)(/c)
中包含有可选路径 a
b
c
,该类主要是提供几个功能模块:
- 展开可选路径,生成list。主要是如下实现:
+ (NSArray <NSString *> *)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 <JLRParsingUtilities_RouteSubpath *> *subpaths = [self _routeSubpathsForPattern:routePattern];
if (subpaths.count == 0) {
return @[];
}
// Next, etract out the required subpaths.
NSSet <JLRParsingUtilities_RouteSubpath *> *requiredSubpaths = [NSSet setWithArray:[subpaths JLRoutes_filter:^BOOL(JLRParsingUtilities_RouteSubpath *subpath) {
return !subpath.isOptionalSubpath;
}]];
// Then, expand the subpath permutations into possible route patterns.
NSArray <NSArray <JLRParsingUtilities_RouteSubpath *> *> *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 <NSArray <JLRParsingUtilities_RouteSubpath *> *> *validSubpathCombinations = [allSubpathCombinations JLRoutes_filter:^BOOL(NSArray <JLRParsingUtilities_RouteSubpath *> *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 <NSString *> *validSubpathRouteStrings = [validSubpathCombinations JLRoutes_map:^id(NSArray <JLRParsingUtilities_RouteSubpath *> *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 <NSString *, NSString *> *)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的原理上是一致的。
可以关注个人公众号联系我,欢迎大家一起沟通交流。
可以关注个人公众号联系我,欢迎大家一起沟通交流。