iOS組件化(二)-MGJRouter的使用和源碼分析

#MGJRouter的使用 對於前面的文章,我說起到了組件化其實就是對項目的拆分和組合。在iOS組件化(一)-利用CocoaPods拆分項目和私有化這篇文章說起到了怎麼用CocoaPods進行拆分和私有化模塊,那剩下的組合該怎麼去作呢?方法其實有不少,在這裏我選用了蘑菇街的MGJRouter去實現。下面給出一個大概的項目關係圖 QQ20200515-160414.pnghtml

將工程拆分紅多個基礎模塊和多個子模塊,每一個子模塊都依賴於基礎模塊。基礎模塊中繼承了與業務無關的代碼,像一些網絡請求庫AFNetworking和SDWebImage等,其中也包括了MGJRouter。git

每個模塊向MGJRouter註冊URL和回調,當另一個模塊去open URL時候,就會去MGJRouter 就會去匹配URL,若是找到對應的URL,就執行相應的回調,以達到模塊間的調用問題。舉個例子github

模塊A文章模塊中點擊用戶頭像須要跳轉到模塊B中的用戶信息界面時候,因爲模塊和模塊間沒有直接引用,因此不能直接跳轉頁面。涉及到了模塊間的交互問題,這時候若是要實現跳轉到模塊B中的用戶信息界面的時候,就須要 模塊B中向URL註冊一個URL和回調,在回調中去執行跳轉代碼數組

在模塊B中markdown

[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去執行跳轉邏輯。oop

而後的是這裏openURL的時候能夠傳參數,這裏的參數能夠是一個對象,也但是一個block(block 本質上也是一個對象),handler裏面也不必定是要跳轉頁面,也能夠是方法的調用。具體的用法參考MGJRouter的GitHub,這裏只是闡述一下組件化和MGJRouter之間的調用關係。源碼分析

這裏給出demo,基於MGJRouter實現組件化組件化

clone主工程下來便可看效果atom


#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

複製代碼
相關文章
相關標籤/搜索