iOS组件化(二)-MGJRouter的使用和源码分析
MGJRouter的使用
对于前面的文章,我提及到了组件化其实就是对项目的拆分和组合。在iOS组件化(一)-利用CocoaPods拆分项目和私有化这篇文章提及到了怎么用CocoaPods进行拆分和私有化模块,那剩下的组合该怎么去做呢?方法其实有很多,在这里我选用了蘑菇街的MGJRouter去实现。下面给出一个大概的项目关系图
将工程拆分成多个基础模块和多个子模块,每个子模块都依赖于基础模块。基础模块中继承了与业务无关的代码,像一些网络请求库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实现组件化
- SFMainProject 主工程
- SFRepoCenter 私有索引库
- SFUserModule 用户模块
- SFCommonKit 基础组件模块
clone主工程下来即可看效果
MGJRouter源码分析
那MGJRouter是通过何种方式去实现模块间的调用的呢?核心的就是一个全局字典的匹配
QQ20200515-184038.pngMGJRouter这个库其实内容不太多,实现也只有三百多行,其中心思想很简单,就是把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