iOS

iOS组件化(二)-MGJRouter的使用和源码分析

2020-05-15  本文已影响0人  samstring

MGJRouter的使用

对于前面的文章,我提及到了组件化其实就是对项目的拆分和组合。在iOS组件化(一)-利用CocoaPods拆分项目和私有化这篇文章提及到了怎么用CocoaPods进行拆分和私有化模块,那剩下的组合该怎么去做呢?方法其实有很多,在这里我选用了蘑菇街的MGJRouter去实现。下面给出一个大概的项目关系图

QQ20200515-160414.png

将工程拆分成多个基础模块和多个子模块,每个子模块都依赖于基础模块。基础模块中继承了与业务无关的代码,像一些网络请求库AFNetworking和SDWebImage等,其中也包括了MGJRouter。

每一个模块向MGJRouter注册URL和回调,当另外一个模块去open URL时候,就会去MGJRouter 就会去匹配URL,如果找到对应的URL,就执行相应的回调,以达到模块间的调用问题。举个例子

模块A文章模块中点击用户头像需要跳转到模块B中的用户信息界面时候,由于模块和模块间没有直接引用,所以不能直接跳转页面。涉及到了模块间的交互问题,这时候如果要实现跳转到模块B中的用户信息界面的时候,就需要
模块B中向URL注册一个URL和回调,在回调中去执行跳转代码

在模块B中

 [MGJRouter registerURLPattern:@"sf_user://SFUserInfoViewController" toHandler:^(NSDictionary *routerParameters) {
            //获取导航控制器
          UINavigationController *nav = [routerParameters[MGJRouterParameterUserInfo] objectForKey:@"nav"];
          //跳转模块B中页面
          [nav pushViewController:[[SFUserInfoViewController alloc] initWithNibName:@"SFUserInfoViewController" bundle:[NSBundle bundleForClass:NSClassFromString(@"SFUserInfoViewController")]] animated:YES];
      }];

在模块A中

NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
    [userInfo setObject:self.navigationController forKey:@"nav"];
    [MGJRouter openURL:@"sf_user://SFUserInfoViewController" withUserInfo:userInfo completion:^(id result) {
        
    }];

注意这里模块A中和模块B中没有直接的引用,而是通过MGJRouter去调用 。模块A中openURL的时候就会去执行Handler去执行跳转逻辑。

然后的是这里openURL的时候可以传参数,这里的参数可以是一个对象,也可是一个block(block 本质上也是一个对象),handler里面也不一定是要跳转页面,也可以是方法的调用。具体的用法参考MGJRouter的GitHub,这里只是阐述一下组件化和MGJRouter之间的调用关系。

这里给出demo,基于MGJRouter实现组件化

clone主工程下来即可看效果


MGJRouter源码分析

那MGJRouter是通过何种方式去实现模块间的调用的呢?核心的就是一个全局字典的匹配

QQ20200515-184038.png

MGJRouter这个库其实内容不太多,实现也只有三百多行,其中心思想很简单,就是把URL按照一定的规则解析出来字符串数组,再逐级的解析的内容数组存放到全局字典里面,匹配URL的时候,再按照规则去解析出来字符串数组,再按照规则判断全局字典里面有没有想对应的URL和回调,如果有,则执行回调。

这里讲述一下最主要的两个方法的实现思路,分别是注册URL和打开URL

-addURLPattern:(NSString *)URLPattern andObjectHandler:(MGJRouterObjectHandler)handler

1 按规则将URL分割成一个一个字符串数组

如test1://test2/test3 会被分割成[@"test1",@"test2",@"test3"]

2 循环数组里的字符串,判断每一个字典里是否存在以该字符串为key的字典,如果不存在,则创建。如果存在,则继续循环到下一个元素。最后生成的字典如下

{
      @"test1":{
          @"test2":{
               @"test3":{

               }
           }
       }
   }

3 上述步骤成功后,会将handler以 下划线“_”为key,存储到最后一个匹配的字典中,如

{
      @"test1":{
          @"test2":{
               @"test3":{
                   @"_": handler
               }
           }
       }
   }

-(void)openURL:(NSString *)URL withUserInfo:(NSDictionary *)userInfo completion:(void (^)(id result))completion

1 按照与添加相同的规则去分割字符串,得出字符串数组

2 遍历数组,用字符串数组里值的为key,查找全局字典,判断是否有匹配的字典,如果没有,则匹配上一级的路由 如test1://test2/test3/test4 这个对应的字典找不到,则会匹配test1://test2/test3 的字典(这是一个容错机制,如果不能完全匹配URL,则匹配上一级的URL)

3 拿到刚才匹配的字典,把openURL时候传递的userInfo, completion复制给字典

4 拿到字典后,执行字典里面存储的handle。

对于匹配规则,解析规则,可以查看MGJRouter的源码


蘑菇街路由方案MGJRouter源码如下,对其中的一些关键的方法加入了一些自己的注释

MGJRouter.h
//
//  MGJRouter.h
//  MGJFoundation
//
//  Created by limboy on 12/9/14.
//  Copyright (c) 2014 juangua. All rights reserved.
//

#import <Foundation/Foundation.h>

extern NSString *const MGJRouterParameterURL;
extern NSString *const MGJRouterParameterCompletion;
extern NSString *const MGJRouterParameterUserInfo;

/**
 *  routerParameters 里内置的几个参数会用到上面定义的 string
 */
typedef void (^MGJRouterHandler)(NSDictionary *routerParameters);

/**
 *  需要返回一个 object,配合 objectForURL: 使用
 */
typedef id (^MGJRouterObjectHandler)(NSDictionary *routerParameters);

@interface MGJRouter : NSObject

/**
 *  注册 URLPattern 对应的 Handler,在 handler 中可以初始化 VC,然后对 VC 做各种操作
 *
 *  @param URLPattern 带上 scheme,如 mgj://beauty/:id
 *  @param handler    该 block 会传一个字典,包含了注册的 URL 中对应的变量。
 *                    假如注册的 URL 为 mgj://beauty/:id 那么,就会传一个 @{@"id": 4} 这样的字典过来
 */
+ (void)registerURLPattern:(NSString *)URLPattern toHandler:(MGJRouterHandler)handler;

/**
 *  注册 URLPattern 对应的 ObjectHandler,需要返回一个 object 给调用方
 *
 *  @param URLPattern 带上 scheme,如 mgj://beauty/:id
 *  @param handler    该 block 会传一个字典,包含了注册的 URL 中对应的变量。
 *                    假如注册的 URL 为 mgj://beauty/:id 那么,就会传一个 @{@"id": 4} 这样的字典过来
 *                    自带的 key 为 @"url" 和 @"completion" (如果有的话)
 */
+ (void)registerURLPattern:(NSString *)URLPattern toObjectHandler:(MGJRouterObjectHandler)handler;

/**
 *  取消注册某个 URL Pattern
 *
 *  @param URLPattern URLPattern
 */
+ (void)deregisterURLPattern:(NSString *)URLPattern;

/**
 *  打开此 URL
 *  会在已注册的 URL -> Handler 中寻找,如果找到,则执行 Handler
 *
 *  @param URL 带 Scheme,如 mgj://beauty/3
 */
+ (void)openURL:(NSString *)URL;

/**
 *  打开此 URL,同时当操作完成时,执行额外的代码
 *
 *  @param URL        带 Scheme 的 URL,如 mgj://beauty/4
 *  @param completion URL 处理完成后的 callback,完成的判定跟具体的业务相关
 */
+ (void)openURL:(NSString *)URL completion:(void (^)(id result))completion;

/**
 *  打开此 URL,带上附加信息,同时当操作完成时,执行额外的代码
 *
 *  @param URL        带 Scheme 的 URL,如 mgj://beauty/4
 *  @param userInfo 附加参数
 *  @param completion URL 处理完成后的 callback,完成的判定跟具体的业务相关
 */
+ (void)openURL:(NSString *)URL withUserInfo:(NSDictionary *)userInfo completion:(void (^)(id result))completion;

/**
 * 查找谁对某个 URL 感兴趣,如果有的话,返回一个 object
 *
 *  @param URL 带 Scheme,如 mgj://beauty/3
 */
+ (id)objectForURL:(NSString *)URL;

/**
 * 查找谁对某个 URL 感兴趣,如果有的话,返回一个 object
 *
 *  @param URL 带 Scheme,如 mgj://beauty/3
 *  @param userInfo 附加参数
 */
+ (id)objectForURL:(NSString *)URL withUserInfo:(NSDictionary *)userInfo;

/**
 *  是否可以打开URL
 *
 *  @param URL 带 Scheme,如 mgj://beauty/3
 *
 *  @return 返回BOOL值
 */
+ (BOOL)canOpenURL:(NSString *)URL;
+ (BOOL)canOpenURL:(NSString *)URL matchExactly:(BOOL)exactly;

/**
 *  调用此方法来拼接 urlpattern 和 parameters
 *
 *  #define MGJ_ROUTE_BEAUTY @"beauty/:id"
 *  [MGJRouter generateURLWithPattern:MGJ_ROUTE_BEAUTY, @[@13]];
 *
 *
 *  @param pattern    url pattern 比如 @"beauty/:id"
 *  @param parameters 一个数组,数量要跟 pattern 里的变量一致
 *
 *  @return 返回生成的URL String
 */
+ (NSString *)generateURLWithPattern:(NSString *)pattern parameters:(NSArray *)parameters;
@end

MGJRouter.m
//
//  MGJRouter.m
//  MGJFoundation
//
//  Created by limboy on 12/9/14.
//  Copyright (c) 2014 juangua. All rights reserved.
//

#import "MGJRouter.h"
#import <objc/runtime.h>

static NSString * const MGJ_ROUTER_WILDCARD_CHARACTER = @"~";
static NSString *specialCharacters = @"/?&.";

NSString *const MGJRouterParameterURL = @"MGJRouterParameterURL";
NSString *const MGJRouterParameterCompletion = @"MGJRouterParameterCompletion";
NSString *const MGJRouterParameterUserInfo = @"MGJRouterParameterUserInfo";


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

@implementation MGJRouter

+ (instancetype)sharedInstance
{
    static MGJRouter *instance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[self alloc] init];
    });
    return instance;
}

+ (void)registerURLPattern:(NSString *)URLPattern toHandler:(MGJRouterHandler)handler
{
    [[self sharedInstance] addURLPattern:URLPattern andHandler:handler];
}

+ (void)deregisterURLPattern:(NSString *)URLPattern
{
    [[self sharedInstance] removeURLPattern:URLPattern];
}

+ (void)openURL:(NSString *)URL
{
    [self openURL:URL completion:nil];
}

+ (void)openURL:(NSString *)URL completion:(void (^)(id result))completion
{
    [self openURL:URL withUserInfo:nil completion:completion];
}

+ (void)openURL:(NSString *)URL withUserInfo:(NSDictionary *)userInfo completion:(void (^)(id result))completion
{
    URL = [URL stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
    NSMutableDictionary *parameters = [[self sharedInstance] extractParametersFromURL:URL matchExactly:NO];
    
    [parameters enumerateKeysAndObjectsUsingBlock:^(id key, NSString *obj, BOOL *stop) {
        if ([obj isKindOfClass:[NSString class]]) {
            parameters[key] = [obj stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
        }
    }];
    
    //往路由字典里添加用户信息并执行block
    if (parameters) {
        MGJRouterHandler handler = parameters[@"block"];
        if (completion) {
            parameters[MGJRouterParameterCompletion] = completion;
        }
        if (userInfo) {
            parameters[MGJRouterParameterUserInfo] = userInfo;
        }
        if (handler) {
            [parameters removeObjectForKey:@"block"];
            handler(parameters);
        }
    }
}

+ (BOOL)canOpenURL:(NSString *)URL
{
    return [[self sharedInstance] extractParametersFromURL:URL matchExactly:NO] ? YES : NO;
}

+ (BOOL)canOpenURL:(NSString *)URL matchExactly:(BOOL)exactly {
    return [[self sharedInstance] extractParametersFromURL:URL matchExactly:YES] ? YES : NO;
}


///  这个方法是用于将值 替换 url参数的占位符
/// @param pattern 带有占位符url
/// @param parameters 替换成URL里面的占位符
+ (NSString *)generateURLWithPattern:(NSString *)pattern parameters:(NSArray *)parameters
{
    NSInteger startIndexOfColon = 0;//冒号出现时候的index
    
    NSMutableArray *placeholders = [NSMutableArray array];
    
    for (int i = 0; i < pattern.length; i++) {
        //将url拆分成单个字符
        NSString *character = [NSString stringWithFormat:@"%c", [pattern characterAtIndex:i]];
        //如果出现了,则url解析开始
        if ([character isEqualToString:@":"]) {
            startIndexOfColon = I;
        }
        //判断特殊符号的出现,然后分割字符串,并把分割出来的字符串添加到placeholders数组里(这样做的目的是为了取出。。。。)
        if ([specialCharacters rangeOfString:character].location != NSNotFound && i > (startIndexOfColon + 1) && startIndexOfColon) {
            //获取冒号:与特殊字符串/?&.之间的 内容
            NSRange range = NSMakeRange(startIndexOfColon, i - startIndexOfColon);
            NSString *placeholder = [pattern substringWithRange:range];
            //如果placehoder里面没有特殊字符次,则添加到placeholder数组里,并重置冒号:出现的index为0
            if (![self checkIfContainsSpecialCharacter:placeholder]) {
                [placeholders addObject:placeholder];
                startIndexOfColon = 0;//将startIndexOfColon重置为0是为了方便下面条件判断是否成立
            }
        }
        //如果遍历到patter的尽头了,出现了冒号但是没有出现特殊字符串,则把冒号往后的内容作为一个占位符放到placeholders里面
        if (i == pattern.length - 1 && startIndexOfColon) {
            NSRange range = NSMakeRange(startIndexOfColon, i - startIndexOfColon + 1);
            NSString *placeholder = [pattern substringWithRange:range];
            if (![self checkIfContainsSpecialCharacter:placeholder]) {
                [placeholders addObject:placeholder];
            }
        }
    }
    
    __block NSString *parsedResult = pattern;
    
    //将placeholders里面的占位符替换成实际的值(可以看到这里的parameters是一个字符串,所以处理对象的时候会比较麻烦)
    [placeholders enumerateObjectsUsingBlock:^(NSString *obj, NSUInteger idx, BOOL * _Nonnull stop) {
        idx = parameters.count > idx ? idx : parameters.count - 1;
        parsedResult = [parsedResult stringByReplacingOccurrencesOfString:obj withString:parameters[idx]];
    }];
    
    return parsedResult;
}

+ (id)objectForURL:(NSString *)URL withUserInfo:(NSDictionary *)userInfo
{
    MGJRouter *router = [MGJRouter sharedInstance];
    
    URL = [URL stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
    NSMutableDictionary *parameters = [router extractParametersFromURL:URL matchExactly:NO];
    //处理block
    MGJRouterObjectHandler handler = parameters[@"block"];
    //如果有block,则调用block(block里面传入userInfo参数),处理完block以后将参数移除
    if (handler) {
        if (userInfo) {
            parameters[MGJRouterParameterUserInfo] = userInfo;
        }
        [parameters removeObjectForKey:@"block"];
        return handler(parameters);
    }
    return nil;
}

+ (id)objectForURL:(NSString *)URL
{
    return [self objectForURL:URL withUserInfo:nil];
}

+ (void)registerURLPattern:(NSString *)URLPattern toObjectHandler:(MGJRouterObjectHandler)handler
{
    [[self sharedInstance] addURLPattern:URLPattern andObjectHandler:handler];
}

- (void)addURLPattern:(NSString *)URLPattern andHandler:(MGJRouterHandler)handler
{
    NSMutableDictionary *subRoutes = [self addURLPattern:URLPattern];
//    容错机制,就是当url多出来的时候(如注册时候是mgj://abc,但是openUrl是mgj://abc/d),会调用这个handler
    if (handler && subRoutes) {
        subRoutes[@"_"] = [handler copy];
    }
}

- (void)addURLPattern:(NSString *)URLPattern andObjectHandler:(MGJRouterObjectHandler)handler
{
    NSMutableDictionary *subRoutes = [self addURLPattern:URLPattern];
    if (handler && subRoutes) {
        subRoutes[@"_"] = [handler copy];//?
    }
}


///  分割url,并根据分割出来的数组逐级生成字典
/// @param URLPattern <#URLPattern description#>
- (NSMutableDictionary *)addURLPattern:(NSString *)URLPattern
{
    //获取数组里面的值
    NSArray *pathComponents = [self pathComponentsFromURL:URLPattern];
    //获取路由
    NSMutableDictionary* subRoutes = self.routes;
    //下面的代码逻辑如下
    //根据pathComponent里面第i个值去的值判断字典是否存在,
    //如果不存在,则创建字典,并把字典作为以i-1为key的字典的值
    //如 @["test1","test2","test3"]生成的字典如下
//    {
//        @"test1":{
//            @"test2":{
//                @"test3":{
//
//                }
//
//
//            }
//        }
//    }
    for (NSString* pathComponent in pathComponents) {
        if (![subRoutes objectForKey:pathComponent]) {
            subRoutes[pathComponent] = [[NSMutableDictionary alloc] init];
        }
        
        
        subRoutes = subRoutes[pathComponent];
    }
    return subRoutes;
}

#pragma mark - Utils

/// 将url生成分割成一个数组,再将数组作为key,逐级比对路由字典
/// @param url <#url description#>
/// @param exactly <#exactly description#>
- (NSMutableDictionary *)extractParametersFromURL:(NSString *)url matchExactly:(BOOL)exactly
{
    NSMutableDictionary* parameters = [NSMutableDictionary dictionary];
    
    parameters[MGJRouterParameterURL] = url;
    
    NSMutableDictionary* subRoutes = self.routes;
    NSArray* pathComponents = [self pathComponentsFromURL:url];
    
    BOOL found = NO;
    // borrowed from HHRouter(https://github.com/Huohua/HHRouter)
    for (NSString* pathComponent in pathComponents) {
        
        // 对 key 进行排序,这样可以把 ~ 放到最后
        NSArray *subRoutesKeys =[subRoutes.allKeys sortedArrayUsingComparator:^NSComparisonResult(NSString *obj1, NSString *obj2) {
            return [obj1 compare:obj2];
        }];
        
        //通过循环找到route中对应的路由字典
        for (NSString* key in subRoutesKeys) {
            if ([key isEqualToString:pathComponent] || [key isEqualToString:MGJ_ROUTER_WILDCARD_CHARACTER]) {
                found = YES;
                subRoutes = subRoutes[key];
                break;
            } else if ([key hasPrefix:@":"]) {
                found = YES;
                subRoutes = subRoutes[key];
                NSString *newKey = [key substringFromIndex:1];
                NSString *newPathComponent = pathComponent;
                // 再做一下特殊处理,比如 :id.html -> :id
                if ([self.class checkIfContainsSpecialCharacter:key]) {
                    NSCharacterSet *specialCharacterSet = [NSCharacterSet characterSetWithCharactersInString:specialCharacters];
                    NSRange range = [key rangeOfCharacterFromSet:specialCharacterSet];
                    if (range.location != NSNotFound) {
                        // 把 pathComponent 后面的部分也去掉
                        newKey = [newKey substringToIndex:range.location - 1];
                        NSString *suffixToStrip = [key substringFromIndex:range.location];
                        newPathComponent = [newPathComponent stringByReplacingOccurrencesOfString:suffixToStrip withString:@""];
                    }
                }
                parameters[newKey] = newPathComponent;
                break;
            } else if (exactly) {
                found = NO;
            }
        }
        
        // 如果没有找到该 pathComponent 对应的 handler,则以上一层的 handler 作为 fallback
        if (!found && !subRoutes[@"_"]) {
            return nil;
        }
    }
    
    // Extract Params From Query.
    NSArray<NSURLQueryItem *> *queryItems = [[NSURLComponents alloc] initWithURL:[[NSURL alloc] initWithString:url] resolvingAgainstBaseURL:false].queryItems;
    
    for (NSURLQueryItem *item in queryItems) {
        parameters[item.name] = item.value;
    }

    
    if (subRoutes[@"_"]) {
        parameters[@"block"] = [subRoutes[@"_"] copy];
    }
    
    return parameters;
}



/// @param URLPattern <#URLPattern description#>
- (void)removeURLPattern:(NSString *)URLPattern
{
    NSMutableArray *pathComponents = [NSMutableArray arrayWithArray:[self pathComponentsFromURL:URLPattern]];
    
    // 只删除该 pattern 的最后一级
    if (pathComponents.count >= 1) {
        // 假如 URLPattern 为 a/b/c, components 就是 @"a.b.c" 正好可以作为 KVC 的 key
        NSString *components = [pathComponents componentsJoinedByString:@"."];
        NSMutableDictionary *route = [self.routes valueForKeyPath:components];
        //如果存在匹配,则删除
        if (route.count >= 1) {
            NSString *lastComponent = [pathComponents lastObject];
            [pathComponents removeLastObject];
            
            // 有可能是根 key,这样就是 self.routes 了
            route = self.routes;
            if (pathComponents.count) {
                NSString *componentsWithoutLast = [pathComponents componentsJoinedByString:@"."];
                route = [self.routes valueForKeyPath:componentsWithoutLast];
            }
            
            [route removeObjectForKey:lastComponent];
        }
    }
}

/// 将url按/分割成一个字符数组
/// @param URL <#URL description#>
- (NSArray*)pathComponentsFromURL:(NSString*)URL
{

    NSMutableArray *pathComponents = [NSMutableArray array];
    // 分割协议和URL,如果url为空,则用~替代
    if ([URL rangeOfString:@"://"].location != NSNotFound) {
        NSArray *pathSegments = [URL componentsSeparatedByString:@"://"];
        // 如果 URL 包含协议,那么把协议作为第一个元素放进去
        [pathComponents addObject:pathSegments[0]];
        
        // 如果只有协议,那么放一个占位符
        URL = pathSegments.lastObject;
        if (!URL.length) {
            [pathComponents addObject:MGJ_ROUTER_WILDCARD_CHARACTER];
        }
    }

    //去除参数,保留参数前的内容
    for (NSString *pathComponent in [[NSURL URLWithString:URL] pathComponents]) {
        if ([pathComponent isEqualToString:@"/"]) continue;
        if ([[pathComponent substringToIndex:1] isEqualToString:@"?"]) break;
        [pathComponents addObject:pathComponent];
    }
    return [pathComponents copy];
}

- (NSMutableDictionary *)routes
{
    if (!_routes) {
        _routes = [[NSMutableDictionary alloc] init];
    }
    return _routes;
}

#pragma mark - Utils

/// 检查是否有特殊字符
/// @param checkedString <#checkedString description#>
+ (BOOL)checkIfContainsSpecialCharacter:(NSString *)checkedString {
    NSCharacterSet *specialCharactersSet = [NSCharacterSet characterSetWithCharactersInString:specialCharacters];
    return [checkedString rangeOfCharacterFromSet:specialCharactersSet].location != NSNotFound;
}

@end

上一篇下一篇

猜你喜欢

热点阅读