源码解读 JLRoutes

GithubStar 最多的路由方案是 JLRoutes , 该方案基于 URL Scheme 方式跳转的!我们来分析下它的具体设计思路!

JLRoutes 设计思路
1、路由模型 JLRRouteDefinition 的注册
路由模型 JLRRouteDefinition
@implementation JLRoutes

/** 注册路由:
 * @param routePattern 在注册阶段已经进行赋值,不需要别的操作
 * @param priority 优先级,默认为 0;用途在存入数组中排队顺序,数值越大,在数组中排的位置越靠前。
 * @param handler 处理路由时间,返回值表示是否处理
 *                返回一个BOOL值,表示 handlerBlock 是否真的处理了该路由。如果返回NO, JLRoutes将继续寻找匹配的路由
 * @param routeDefinition 定制路由逻辑:将每一条注册数据(pattern、priority、handler)封装在JLRouteDefinition对象中
 */
- (void)addRoute:(JLRRouteDefinition *)routeDefinition;
- (void)addRoute:(NSString *)routePattern handler:(BOOL (^__nullable)(NSDictionary<NSString *, id> *parameters))handlerBlock;
- (void)addRoutes:(NSArray<NSString *> *)routePatterns handler:(BOOL (^__nullable)(NSDictionary<NSString *, id> *parameters))handlerBlock;
- (void)addRoute:(NSString *)routePattern priority:(NSUInteger)priority handler:(BOOL (^__nullable)(NSDictionary<NSString *, id> *parameters))handlerBlock{

    //1、将 routePattern 展开为可选路由模式,如:@"/path/:thing/(/a)(/b)(/c)"
    NSArray <NSString *> *optionalRoutePatterns = [JLRParsingUtilities expandOptionalRoutePatternsForPattern:routePattern];
    
    // 2、根据 routePattern、priority、handlerBlock 封装一个路由模型 JLRRouteDefinition
    JLRRouteDefinition *route = [[JLRGlobal_routeDefinitionClass alloc] initWithPattern:routePattern priority:priority handlerBlock:handlerBlock];
    
    // 3、如果有可选路由模式,则注册可选路由;并结束不再向下执行
    if (optionalRoutePatterns.count > 0) {
    /// 有可选参数,需要解析和添加它们
        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];
        }
        // 如果有可选路由模式,则不需注册 routePattern
        return;
    }

    // 4、如果没有可选路由,则注册 routePattern 对应的路由模型 JLRRouteDefinition
    [self _registerRoute:route];
}

@end

注册路由的方法,大致可以归纳为四件事:

  • 1、将 routePattern 展开为可选路由模式,如:@"/path/:thing/(/a)(/b)(/c)"
  • 2、根据 routePattern、priority、handlerBlock 封装一个路由模型 JLRRouteDefinition
  • 3、如果有可选路由模式,则注册可选路由;并结束不再向下执行
  • 4、如果没有可选路由,则注册 routePattern 对应的路由模型 JLRRouteDefinition

可选的路由模式,这里不多赘述!详细探究下封装的路由模型 JLRRouteDefinition

@implementation JLRRouteDefinition

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

@end
2、通过 URL 调起已注册的 JLRRouteDefinition
@implementation JLRoutes

/// 如果提供的 ULR 可以成功为当前scheme匹配任一个已注册的路由,则返回YES。否则返回NO。
- (BOOL)canRouteURL:(nullable NSURL *)URL{
    return [self _routeURL:URL withParameters:nil executeRouteBlock:NO];
}

/** 在特定scheme内路由一个URL,为与此URL相匹配的模式调用 handlerBlock,直到找到了相匹配的模式,返回YES。
 *  如果没有找到匹配的路由,将调用提前设置的 unmatchedURLHandler
 *  @param parameters  一些参数信息,传送至匹配的 route block
 */
- (BOOL)routeURL:(nullable NSURL *)URL;
- (BOOL)routeURL:(nullable NSURL *)URL withParameters:(nullable NSDictionary<NSString *, id> *)parameters{
    return [self _routeURL:URL withParameters:parameters executeRouteBlock:YES];
}

/** 调起路由,执行 handlerBlock */
- (BOOL)_routeURL:(NSURL *)URL withParameters:(NSDictionary *)parameters executeRouteBlock:(BOOL)executeRouteBlock{
    if (!URL) {
        return NO;
    }    
    BOOL didRoute = NO;/// 标记是否已经路由
    JLRRouteRequestOptions options = [self _routeRequestOptions];
    
    /// 1、根据 URL 创建一个请求 JLRRouteRequest
    JLRRouteRequest *request = [[JLRRouteRequest alloc] initWithURL:URL options:options additionalParameters:parameters];
    
    /// 2、遍历已注册路由,查找能匹配的路由,执行 handlerBlock
    for (JLRRouteDefinition *route in [self.mutableRoutes copy]) {
        // 检查每个路由是否有匹配的响应
        JLRRouteResponse *response = [route routeResponseForRequest:request];
        if (!response.isMatch) {
            continue;
        }
                
        // 没有执行block立即返回
        if (!executeRouteBlock) {
            return YES;
        }
                
        // 调用路由模型对象 handlerBlock
        didRoute = [route callHandlerBlockWithParameters:response.parameters];
        if (didRoute) {
            break;/// 如果成功路由,中断循环
        }
    }
    
    /// 3、如果找不到匹配的路由,尝试去全局路由来匹配
    if (!didRoute && self.shouldFallbackToGlobalRoutes && ![self _isGlobalRoutesController]) {
        [self _verboseLog:@"Falling back to global routes..."];
        didRoute = [[JLRoutes globalRoutes] _routeURL:URL withParameters:parameters executeRouteBlock:executeRouteBlock];
    }
    
    /// 4、如果还是找不到匹配的路由,回调 unmatchedURLHandler()
    if (!didRoute && executeRouteBlock && self.unmatchedURLHandler) {
        self.unmatchedURLHandler(self, URL, parameters);
    }
    
    // 5、返回是否已路由
    return didRoute;
}


@end

调用路由的方法,大致可以归纳为几件事:

  • 1、根据 URL 创建一个请求 JLRRouteRequest
  • 2、在路由器的数组 mutableRoutes 中匹配已注册的对应路由,
    如果不匹配,中断当前循环,进入下一轮查询
    如果匹配,但没有执行 executeRouteBlock 则立即返回结果
    如果匹配,执行 handlerBlock;中断循环!
  • 3、如果找不到匹配的路由,尝试去全局路由来匹配
  • 4、如果还是找不到匹配的路由,回调 unmatchedURLHandler()
  • 5、返回路由结果
Ⅰ、封装请求 JLRRouteRequest
JLRRouteRequest的初始化
@implementation JLRRouteRequest

/** JLRRouteRequest 的初始化方法
 *  1、使用 NSURLComponents 将一个 URL 拆分为 scheme、host、port、path、query、fragment 等;
 *  2、 将 components.host 拼接到 path 中 ?
 *          条件一:components.host.length > 0;
 *          条件二:(components.host 不是 localhost 并且 components.host 不包含 .) || 配置项
 *     如果将 components.host 拼接到 path 中,则 JLRRouteRequest.pathComponents 包含 host 并且 包含 path
 *  3、 将 URL 的附带参数 components.queryItems 转为字典格式 JLRRouteRequest.queryParams
 */
- (instancetype)initWithURL:(NSURL *)URL options:(JLRRouteRequestOptions)options additionalParameters:(nullable NSDictionary *)additionalParameters{
    if ((self = [super init])) {
        self.URL = URL;
        self.options = options;
        self.additionalParameters = additionalParameters;
        
        BOOL treatsHostAsPathComponent = ((options & JLRRouteRequestOptionTreatHostAsPathComponent) == JLRRouteRequestOptionTreatHostAsPathComponent);
        
        NSURLComponents *components = [NSURLComponents componentsWithString:[self.URL absoluteString]];
        
        /// 将 host 拼接到 path 中
        // host 不是 localhost 且 host 不包含 .
        if (components.host.length > 0 &&
            (treatsHostAsPathComponent ||
             (![components.host isEqualToString:@"localhost"] && [components.host rangeOfString:@"."].location == NSNotFound))) {
            // 将 host 转为一个路径组件
            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]];
            }
        }
        
        // 去掉开头的斜杠,这样第一个路径组件不会为空
        if (path.length > 0 && [path characterAtIndex:0] == '/') {
            path = [path substringFromIndex:1];
        }
        
        // 去掉结尾的斜杠,这样最后一个路径组件不会为空
        if (path.length > 0 && [path characterAtIndex:path.length - 1] == '/') {
            path = [path substringToIndex:path.length - 1];
        }
        // 分割 path
        self.pathComponents = [path componentsSeparatedByString:@"/"];
        
        // 将 URL 的附带参数转为字典格式
        NSArray <NSURLQueryItem *> *queryItems = [components queryItems] ?: @[];
        NSMutableDictionary *queryParams = [NSMutableDictionary dictionary];
        for (NSURLQueryItem *item in queryItems) {
            if (item.value == nil) {
                continue;
            }
            if (queryParams[item.name] == nil) {
                // 第一次设置键值
                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 {
                // 再次遇见该键,将多组值组成一个数组
                id existingValue = queryParams[item.name];
                queryParams[item.name] = @[existingValue, item.value];
            }
        }
        self.queryParams = [queryParams copy];
    }
    return self;
}

@end

JLRRouteRequest 的初始化方法,大致做了以下几件事:

  • 1、使用 NSURLComponents 将一个 URL 拆分为scheme、host、port、path、query、fragment 等;
  • 2、 将 components.host 拼接到 path 中 ?
    条件一:components.host.length > 0
    条件二:(components.host 不是 localhost 并且 components.host 不包含 .) || 配置项
    如果将 components.host 拼接到 path 中,则 JLRRouteRequest.pathComponents 包含 host 并且 包含 path
  • 3、 将 URL 的附带参数 components.queryItems 转为字典格式 JLRRouteRequest.queryParams
Ⅱ、如何匹配路由?

路由的匹配,交给了 JLRRouteDefinition 去判断:

@implementation JLRRouteDefinition

/// 匹配阶段通过对注册内容进行查找,找到匹配项。并对匹配内容进行拼接,完成匹配pattern的匹配和变量赋值的操作
- (JLRRouteResponse *)routeResponseForRequest:(JLRRouteRequest *)request{
    /// 是否包含通配符 '*'
    BOOL patternContainsWildcard = [self.patternPathComponents containsObject:@"*"];
    
    /// 1、不包含通配符,路径组件的数量又不一样,返回一个无效的响应
    if (request.pathComponents.count != self.patternPathComponents.count && !patternContainsWildcard) {
        return [JLRRouteResponse invalidMatchResponse];
    }
    
    /// 2、判断 request.pathComponents 与 RouteDefinition.patternPathComponents 相对位置的路径是否一致
    ///   如果一致,截取 URL 中的变量,
    ///   如果不一致,则返回 routeVariables = nil ;表示不匹配
    NSDictionary *routeVariables = [self routeVariablesForRequest:request];
    
    if (routeVariables != nil) {
        // 如果匹配,将 request.url 的请求附加参数、变量参数、request.additionalParameters 合并为一个字典
        NSDictionary *matchParams = [self matchParametersForRequest:request routeVariables:routeVariables];
        return [JLRRouteResponse validMatchResponseWithParameters:matchParams];
    } else { /// 没有匹配的变量,返回一个无效响应
        return [JLRRouteResponse invalidMatchResponse];
    }
}

@end

匹配有效的响应,基于以下几个判断:

  • 1、不包含通配符,路径组件的数量又不一样,返回一个无效的响应;
  • 2、判断 request.pathComponentsRouteDefinition.patternPathComponents 相对位置的路径是否一致:
  • 如果一致,截取 URL 中的变量,
  • 如果不一致,则返回 routeVariables = nil ;表示不匹配
  • 3、将 request.url 的请求附加参数、变量参数、request.additionalParameters 合并为一个字典,封装一个有效的响应
Ⅲ、如何匹配路由?URL 请求中还有变量?
@implementation JLRRouteDefinition

/** 解析并返回指定请求的路由变量
 * 注册路由时的路径,如果使用 : 开头,如 mainTabBar/:name
 *               则一个请求走到这里,会尝试取出该请求的对应参数 mainTabBar/user
 *               @{@"name":@"user"}
 * @note 注册路由模型时,路径组件仅仅使用 \ 分割;
 *       而请求 request 的路径组件使用 NSURLComponents 处理的附加的参数!
 *       所以,注册路由时的 URL 一定不能包含参数,否则永远不可能匹配到有效响应
 */
- (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 不是通配符 并且 index 又对 request.pathComponents 越界
            NSLog(@"URLComponent 不是通配符 并且 index 又对 request.pathComponents 越界");
            isMatch = NO;
            break;
        }
        if ([patternComponent hasPrefix:@":"]) {
            // : 开头的路径是一个变量, 将该变量设置到参数 params 中
            NSAssert(URLComponent != nil, @"URLComponent cannot be nil");
            
            ///当字符串长度大于 1 时,去掉字符串开头的 ':' 与 字符串结尾的 '#'
            NSString *variableName = [self routeVariableNameForValue:patternComponent];
            ///对 URLComponent 解码,去掉字符串结尾的 '#'
            NSString *variableValue = [self routeVariableValueForValue:URLComponent];
            BOOL decodePlusSymbols = ((request.options & JLRRouteRequestOptionDecodePlusSymbols) == JLRRouteRequestOptionDecodePlusSymbols);
            variableValue = [JLRParsingUtilities variableValueFrom:variableValue decodePlusSymbols:decodePlusSymbols];
            
            routeVariables[variableName] = variableValue;/// 将该变量设置到参数 params 中
        } else if (isPatternComponentWildcard) {
            // 匹配通配符
            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]) {
            //路径组件 request.pathComponents 与 JLRRouteDefinition.patternPathComponents 相对位置的路径不一致,则终断循环
            isMatch = NO;
            break;
        }
        index++;
    }
    
    if (!isMatch) {
        // 如果没有匹配项,返回 nil
        routeVariables = nil;
    }
    return [routeVariables copy];
}

@end
Ⅳ、匹配到有效响应

如果匹配到有效的响应,可以去执行回调 JLRRouteDefinition.handlerBlock() 处理一些跳转逻辑

@implementation JLRRouteDefinition

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

@end

Demo 与 源码注释

参考文章

iOS 组件化 —— 路由设计思路分析
解读 iOS 组件化与路由的本质

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容