开源库信息

之前有写了一篇博客《iOS 基于JLRoutes 改造的路由》那时候比较忙所以只是简单地画了一个大致的框图,这段时间有时间所以抽空再写一篇针对JRLRoute源码解析的文章。
JLRoute代码量不大,建议大家可以跟着这篇博客简单过下整个代码:

源码解析

在对源码进行分析之前需要先了解下JLRoutes的大致结构,JLRoutes通过sheme将整个路由空间分隔开,每个sheme相当于一个命名空间,每个sheme对应一个JLRoutes,每个JLRoutes有一个属性mutableRoutes 用于存放它所能匹配的JLRRouteDefinition。每个JLRRouteDefinition针对一个路由匹配规则,负责路由的匹配。每个路由匹配都被封装为JLRRouteRequest,交给JLRRouteDefinition进行匹配,JLRRouteDefinition会针对该request 输出一个匹配结果JLRRouteResponse。里面包含了是否匹配的信息。大体就是这么一个流程。

我们下面将针对上面介绍的结构一步一步进行介绍:

通过sheme分隔路由空间

+ (instancetype)routesForScheme:(NSString *)scheme {
JLRoutes *routesController = nil;

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
JLRGlobal_routeControllersMap = [[NSMutableDictionary alloc] init];
});
//如果之前没有注册当前scheme,则创建JLRoutes
if (!JLRGlobal_routeControllersMap[scheme]) {
routesController = [[self alloc] init];
routesController.scheme = scheme;
JLRGlobal_routeControllersMap[scheme] = routesController;
}
return JLRGlobal_routeControllersMap[scheme];;
}

+ (instancetype)globalRoutes {
return [self routesForScheme:JLRoutesGlobalRoutesScheme];
}

每个路由都要被添加到指定的scheme空间,存储在JLRGlobal_routeControllersMap中,JLRGlobal_routeControllersMap是一个字典类型,key为scheme,value为指定的JLRoutes。默认的情况下JLRoutes帮我们提供了一个全局的命名空间globalRoutes。

注册指定的路由匹配规则

- (void)addRoute:(NSString *)routePattern handler:(BOOL (^)(NSDictionary<NSString *, id> *parameters))handlerBlock {
[self addRoute:routePattern priority:0 handler:handlerBlock];
}

- (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];
}
return;
}
[self _registerRoute:route];
}

我们以****/path/:thing/(/a)(/b)(/c)**** 为例子,这里有固定path /path/ 带参数的path **/:thing/*,可选的path **/(/a)(/b)(/c)。首先我们会通过expandOptionalRoutePatternsForPattern** 将可选的部分转换成固定的或者带参数的path。比如上面的*/path/:thing/(/a)(/b)(/c)** 会转换为如下几种:

/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

然后再通过_registerRoute注册上面的routePattern。

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

所有的routePattern 都会被封装到JLRRouteDefinition 然后按照优先级添加到mutableRoutes中。

路由匹配

- (BOOL)routeURL:(NSURL *)URL withParameters:(NSDictionary *)parameters {
return [self _routeURL:URL withParameters:parameters executeRouteBlock:YES];
}
- (BOOL)_routeURL:(NSURL *)URL withParameters:(NSDictionary *)parameters executeRouteBlock:(BOOL)executeRouteBlock {

//.....

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;
}
//匹配了但是强制不执行直接返回
if (!executeRouteBlock) {
return YES;
}
//调用路由block传入参数,进行处理
didRoute = [route callHandlerBlockWithParameters:response.parameters];
if (didRoute) {
//已经处理退出循环
break;
}
}

//没有匹配
if (!didRoute) {
[self _verboseLog:@"Could not find a matching route"];
}

//如果没有找到匹配的,并且允许转到Globle路由则调用全局路由
if (!didRoute && self.shouldFallbackToGlobalRoutes && ![self _isGlobalRoutesController]) {
didRoute = [[JLRoutes globalRoutes] _routeURL:URL withParameters:parameters executeRouteBlock:executeRouteBlock];
}

// 如果没有则调用unmatchedURLHandler
if (!didRoute && executeRouteBlock && self.unmatchedURLHandler) {
self.unmatchedURLHandler(self, URL, parameters);
}
return didRoute;
}

如果说路由规则是一个固定的内容,那么路由匹配则是拿着一个个具体的URL去规则里面去匹配:

路由匹配有几个阶段:


1. 将路由URL以及参数封装成一个JLRRouteRequest
2. 使用已经注册好的JLRRouteDefinition去和JLRRouteRequest进行匹配,并生成一个JLRRouteResponse,如果匹配,则调用相应的回调。
3. 如果不匹配则根据我们的需求选择是否允许转到Globle路由去匹配,如果还没找到则选择是否调用unmatchedURLHandler

JLRRouteRequest路由请求

- (instancetype)initWithURL:(NSURL *)URL options:(JLRRouteRequestOptions)options additionalParameters:(nullable NSDictionary *)additionalParameters {
if ((self = [super init])) {
self.URL = URL; // URL
self.options = options; // options
self.additionalParameters = additionalParameters; // 额外参数

BOOL treatsHostAsPathComponent = ((options & JLRRouteRequestOptionTreatHostAsPathComponent) == JLRRouteRequestOptionTreatHostAsPathComponent);

//取出路径的各个部分
NSURLComponents *components = [NSURLComponents componentsWithString:[self.URL absoluteString]];

//是否将host作为path
if (components.host.length > 0
&& (treatsHostAsPathComponent || (![components.host isEqualToString:@"localhost"] && [components.host rangeOfString:@"."].location == NSNotFound))) {
// convert the host to "/" so that the host is considered a path component
NSString *host = [components.percentEncodedHost copy];
components.host = @"/";
components.percentEncodedPath = [host stringByAppendingPathComponent:(components.percentEncodedPath ?: @"")];
}

//格式化后的路径
NSString *path = [components percentEncodedPath];

// handle fragment if needed
if (components.fragment != nil) {
BOOL fragmentContainsQueryParams = NO;
NSURLComponents *fragmentComponents = [NSURLComponents componentsWithString:components.percentEncodedFragment];

if (fragmentComponents.query == nil && fragmentComponents.path != nil) {
fragmentComponents.query = fragmentComponents.path;
}

if (fragmentComponents.queryItems.count > 0) {
// determine if this fragment is only valid query params and nothing else
fragmentContainsQueryParams = fragmentComponents.queryItems.firstObject.value.length > 0;
}

if (fragmentContainsQueryParams) {
// include fragment query params in with the standard set
components.queryItems = [(components.queryItems ?: @[]) arrayByAddingObjectsFromArray:fragmentComponents.queryItems];
}

if (fragmentComponents.path != nil && (!fragmentContainsQueryParams || ![fragmentComponents.path isEqualToString:fragmentComponents.query])) {
// handle fragment by include fragment path as part of the main path
path = [path stringByAppendingString:[NSString stringWithFormat:@"#%@", fragmentComponents.percentEncodedPath]];
}
}

// strip off leading slash so that we don't have an empty first path component
if (path.length > 0 && [path characterAtIndex:0] == '/') {
path = [path substringFromIndex:1];
}

// strip off trailing slash for the same reason
if (path.length > 0 && [path characterAtIndex:path.length - 1] == '/') {
path = [path substringToIndex:path.length - 1];
}

// split apart into path components
self.pathComponents = [path componentsSeparatedByString:@"/"];

// convert query items into a dictionary
NSArray <NSURLQueryItem *> *queryItems = [components queryItems] ?: @[];
NSMutableDictionary *queryParams = [NSMutableDictionary dictionary];
for (NSURLQueryItem *item in queryItems) {
if (item.value == nil) {
continue;
}

if (queryParams[item.name] == nil) {
// first time seeing a param with this name, set it
queryParams[item.name] = item.value;
} else if ([queryParams[item.name] isKindOfClass:[NSArray class]]) {
// already an array of these items, append it
NSArray *values = (NSArray *)(queryParams[item.name]);
queryParams[item.name] = [values arrayByAddingObject:item.value];
} else {
// existing non-array value for this key, create an array
id existingValue = queryParams[item.name];
queryParams[item.name] = @[existingValue, item.value];
}
}

self.queryParams = [queryParams copy];
}
return self;
}
@interface JLRRouteRequest : NSObject

/// The URL being routed. 需要被路由的URL
@property (nonatomic, copy, readonly) NSURL *URL;
/// The URL's path components. URL的路径部分
@property (nonatomic, strong, readonly) NSArray *pathComponents;
/// The URL's query parameters. URL的请求参数部分
@property (nonatomic, strong, readonly) NSDictionary *queryParams;
/// Route request options, generally configured from the framework global options. 请求的可选部分
@property (nonatomic, assign, readonly) JLRRouteRequestOptions options;
/// Additional parameters to pass through as part of the match parameters dictionary. 额外参数
@property (nonatomic, copy, nullable, readonly) NSDictionary *additionalParameters;

@end

每个JLRRouteRequest包含了URL它是JLRRouteRequest其他部分的来源,pathComponents是URL中的path部分,queryParams是URL中的query部分,options是外部注入的选项配置,additionalParameters是外部注入的额外参数。进入下一阶段匹配的时候都是将这些参数与JLRRouteDefinition中定义的模版进行匹配。

JLRRouteDefinition 路由规则定义

我们接下来看下JLRRouts中的路由规则定义部分,以及路由匹配过程:

在JLRoutes中每个路由规则被定义为一个JLRRouteDefinition对象,我们先来看下它的结构:

/// 当前route 的 URL scheme
@property (nonatomic, copy, readonly) NSString *scheme;
/// 当前route匹配模版.
@property (nonatomic, copy, readonly) NSString *pattern;
/// 当前路由的优先级
@property (nonatomic, assign, readonly) NSUInteger priority;
/// 当前路由的匹配模版分隔后的Components,它通过‘/’对pattern字符串进行分隔后存到patternPathComponents
@property (nonatomic, copy, readonly) NSArray <NSString *> *patternPathComponents;
/// 在每次匹配的时候都会触发这个handlerBlock
@property (nonatomic, copy, readonly) BOOL (^handlerBlock)(NSDictionary *parameters);
- (instancetype)initWithPattern:(NSString *)pattern priority:(NSUInteger)priority handlerBlock:(BOOL (^)(NSDictionary *parameters))handlerBlock {
NSParameterAssert(pattern != nil);

if ((self = [super init])) {
self.pattern = pattern;
self.priority = priority;
self.handlerBlock = handlerBlock;
if ([pattern characterAtIndex:0] == '/') {
pattern = [pattern substringFromIndex:1];
}

self.patternPathComponents = [pattern componentsSeparatedByString:@"/"];
}
return self;
}

pattern 是当前路由的匹配规则比如/path/:things/a/b/c,而patternPathComponents则是通过‘/’对pattern字符串进行分隔后的子pathPattern。我们接下来看下最重要的路由匹配过程。

- (JLRRouteResponse *)routeResponseForRequest:(JLRRouteRequest *)request {
BOOL patternContainsWildcard = [self.patternPathComponents containsObject:@"*"];

//不包含通配符的情况下传入的request与当前的pathComponents长度不相等,这种情况确定是不匹配的
if (request.pathComponents.count != self.patternPathComponents.count && !patternContainsWildcard) {
return [JLRRouteResponse invalidMatchResponse];
}

//通过patternPathComponents模版定义从request pathComponents取出具体的参数值来填充routeVariables,routeVariables就是匹配出的变量值
NSDictionary *routeVariables = [self routeVariablesForRequest:request];

if (routeVariables != nil) {
// It's a match, set up the param dictionary and create a valid match response
//匹配的情况下将所有的参数放到JLRRouteResponse返回
NSDictionary *matchParams = [self matchParametersForRequest:request routeVariables:routeVariables];
return [JLRRouteResponse validMatchResponseWithParameters:matchParams];
} else {
// nil variables indicates no match, so return an invalid match response
return [JLRRouteResponse invalidMatchResponse];
}
}

routeResponseForRequest 中最关键的部分在于routeVariablesForRequest,它会在提取path中变量的过程中,一边针对path进行匹配,一边提取,一旦发现不匹配就直接返回。所以我们重点来看下routeVariablesForRequest部分:

- (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:@"*"];

// 1. 取出request中同样位置的pathComponents
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;
}

//从patternPathComponent看当前的path是否是带参数的path,带参数的path之前都是 :xxx这种以冒号开头
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];

//将变量名为key,值为value 存在 routeVariables中
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]中
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];
}

首先我们会遍历JLRRouteDefinition中的patternPathComponents,将它的每个元素取出来,如果发现当前的patternPathComponent是以冒号开头,那么表示,当前的path是一个变量,就从request中的pathComponents对应位置取出pathComponent。再将patternPathComponen开始的冒号以及结尾如果是#的去掉作为key,将requst pathComponent 作为value添加到routeVariables。

如果当前的匹配规则patternPathComponent是通配符,并且request的pathComponents长度大于当前的index,那么将request pathComponents中剩余的值都赋给routeVariables,这时候key为通配符符号*。
为什么能够这样做,以为我们规定通配符要放在路径的最后面。

除了变量,通配符之外只剩下静态component,这种只要判断是否相等就可以了。不相等就表示直接不匹配。

所以routeVariablesForRequest其实是要完成两部分任务,一部分负责匹配,一部分负责将path中的变量提取出来。

我们再回到routeResponseForRequest,提取完path中的全部变量后会执行下面这部分代码:

NSDictionary *matchParams = [self matchParametersForRequest:request routeVariables:routeVariables];

它其实是将上面从path提取出来的参数以及request.additionalParameters,request.queryParams合并起来作为最终的参数,并且在这最终的参数中添加request.URL,以及self.scheme。我们知道JLRRoutes的参数可以通过path指定,通过request.queryParams,通过request.additionalParameters 这三个地方指定。这里就是将这写参数合并起来,传递下去。后续的处理block拿到这些参数进行对应的逻辑处理。

- (NSDictionary *)matchParametersForRequest:(JLRRouteRequest *)request routeVariables:(NSDictionary <NSString *, NSString *> *)routeVariables {
NSMutableDictionary *matchParams = [NSMutableDictionary dictionary];

// Add the parsed query parameters ('?a=b&c=d'). Also includes fragment.
// 添加query参数
BOOL decodePlusSymbols = ((request.options & JLRRouteRequestOptionDecodePlusSymbols) == JLRRouteRequestOptionDecodePlusSymbols);
[matchParams addEntriesFromDictionary:[JLRParsingUtilities queryParams:request.queryParams decodePlusSymbols:decodePlusSymbols]];

// Add the actual parsed route variables (the items in the route prefixed with ':').
// 添加路径中的参数
[matchParams addEntriesFromDictionary:routeVariables];

// Add the additional parameters, if any were specified in the request.
// 添加request中附带的额外的参数
if (request.additionalParameters != nil) {
[matchParams addEntriesFromDictionary:request.additionalParameters];
}

// Finally, add the base parameters. This is done last so that these cannot be overriden by using the same key in your route or query.
// 添加request.URL ,self.scheme这些公共参数
[matchParams addEntriesFromDictionary:[self defaultMatchParametersForRequest:request]];

return [matchParams copy];
}

- (NSDictionary *)defaultMatchParametersForRequest:(JLRRouteRequest *)request {
return @{JLRoutePatternKey: self.pattern ?: [NSNull null], JLRouteURLKey: request.URL ?: [NSNull null], JLRouteSchemeKey: self.scheme ?: [NSNull null]};
}

有了参数之后就将参数封装到JLRRouteResponse中传递给处理block:

[JLRRouteResponse validMatchResponseWithParameters:matchParams]

JLRRouteResponse 十分简单就两个属性,一个用于标记是否匹配,另一个是当前路由的所有请求:

@interface JLRRouteResponse : NSObject <NSCopying>
/// 是否匹配
@property (nonatomic, assign, readonly, getter=isMatch) BOOL match;
/// 匹配的参数
@property (nonatomic, copy, readonly, nullable) NSDictionary *parameters;
@end

我们前面在介绍_routeURL的时候看到最终的response是传递到JLRRouteDefinition中的callHandlerBlockWithParameters方法中:

didRoute = [route callHandlerBlockWithParameters:response.parameters];

而callHandlerBlockWithParameters则是调用了handlerBlock将参数传递出去,在业务层进行处理:

- (BOOL)callHandlerBlockWithParameters:(NSDictionary *)parameters {
if (self.handlerBlock == nil) {
return YES;
}

return self.handlerBlock(parameters);
}

所以整个路由的过程是这样的:

  1. 在使用路由之前先注册一系列路由规则JLRRouteDefinition,每个路由规则有对应的匹配规则pathPattern,以及如果有对应的路由请求匹配这条规则后的处理。
  2. 将匹配规则封装成一个JLRRouteRequest请求,并拿它与所属的schema中的一系列JLRRouteDefinition进行匹配,并尝试按照JLRRouteDefinition中的规则提取对应的参数。
  3. 如果匹配了哪条规则,就会触发对应规则的handlerBlock,并将所有参数传递进去。我们在handlerBlock中就可以自定义我们的业务处理。
Contents
  1. 1. 开源库信息
  2. 2. 源码解析