#MGJRouter的使用 對於前面的文章,我說起到了組件化其實就是對項目的拆分和組合。在iOS組件化(一)-利用CocoaPods拆分項目和私有化這篇文章說起到了怎麼用CocoaPods進行拆分和私有化模塊,那剩下的組合該怎麼去作呢?方法其實有不少,在這裏我選用了蘑菇街的MGJRouter去實現。下面給出一個大概的項目關係圖 html
將工程拆分紅多個基礎模塊和多個子模塊,每一個子模塊都依賴於基礎模塊。基礎模塊中繼承了與業務無關的代碼,像一些網絡請求庫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是經過何種方式去實現模塊間的調用的呢?核心的就是一個全局字典的匹配
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
// 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
複製代碼