前言
组件化, Router 这些概念可能在几年前还是比较新颖的概念, 至今相信绝大多数同学都对这些名词已耳熟能详, 笔者在真正接触到 Router 并在项目使用之前, 也有读过一些 组件化, Router 进行解耦的思想和框架的文章, 但是由于自己修行不够, 加上没能真正将其运用到项目进行实践。 导致每次读完文章之后, 所理解的知识没能真正转化为可以解决问题的技能, 笔者有幸在项目中接触并运用著名开源库 JLRouter 来解决 App 内外所有页面之间的跳转逻辑, 经过这几年的学习和使用, 将其记录一边巩固知识, 写出来跟大家一起学习, 加上看到网上分享关于使用组件化-Router 相关文章偏于理论, 很少有完整详细Demo, 具体在项目中使用还需进一步深入研究, 所以有了此篇文章, 有什么不对或需要补充的, 望大家多多指教。
此篇文章偏向实战, 想深入学习 Router 思想的推荐霜神写的 iOS 组件化 —— 路由设计思路分析。
==Demo 在文章最👇==
为什么 Router
路由基础三问, 每次接触新颖思想框架时, 我都会不禁的问自己这几个问题, 希望通过下面几个简要的概括, 能很好的帮助大家理解 Router;
- 路由是什么,解决了什么问题
上面一幅图很形象的展示了项目中各个控制器模块之间错综复杂的关系, 当我们在处理不当的情况下可能更加糟糕.
使用 Router 之后大概是这样的;
打个比方, Router 就是跟我们日常使用的路由器一样, App 内每个控制器可以想象成已经连接了这台路由器的不同设备, 当然连接路由器时, 一般需要输入密码, Router 同样的, 使用前需要每台设备进行一次注册, Router 在内部保存每台设备的 URL, 不同设备之间需要交互时, 将消息发送到路由器中统一处理;
当控制器之间需要交互跳转时, 只需要将对应的 URL 地址发送到 Router 里, Router 根据其注册的 URL 来寻址到对方信息, 然后负责实例化对象, 并传参, 进行跳转等工作, 各个控制器之间不需要相互依赖对方, 完美解决不同模块之间耦合!
-
为什么要用路由来实现 VC 跳转
Router 能做的事情很多, 首先我们用它来解决棘手的控制器耦合关系,是一种非常有效的解决方案;
在 App 中控制器跳转普遍分为 3 种, 模态跳转Modal(presented/dismiss), 导航控制器跳转(Push/pop), Storyboard 跳转(Segue), 还有 UITabBarVC 主控制器 Index 切换;
除了常规的控制器之间跳转之外, 还会有 3D Touch 指定跳转到某个控制器中;
App 之间跳转: URL Scheme, Universal Links方式;
可想而知 App 内不管是页面切换, 外部调用, 都会涉及到控制器的跳转, 切换等等;
下面引用常见场景来举个栗子:
Router 前 伪代码:
假如在没有引入 Router 之前, 实现 A Push B, B Modal C 的场景: 一般做法都是在 A 中引入B, B 中引入 C, 然后在每次跳转前都需要来一段硬编码,
//A Push B A 页面跳转至 B页面, 并且设置相应 @perpeoty, callback 等;
#import "B"
B* BVC = [B new];
BVC.delegate = A;
BVC.name = @"jersey";
BVC.callback = ^ (id data) {
};
...
...
... 对 b 设置一些业务相关参数, delegate, callback 等等;
[A.nav pushVC: BVC animation: true];
// B -> C
#import "C"
C* CVC = [C new];
[B presentVC: CVC];
[B presentVC: CVC animation: true completion: nil];
==Router 后 伪代码:==
在引用了 Router 之后, 相同的场景下, 我们的代码是这样的; 在需要做跳转的控制器引入我们封装好的 ==JSDVCRouter(是针对 JLRouter 进行的一层封装, 专门用于管理 App 跳转的类, 在文章后面会详细讲解)== 即可.
// A Push B;
#import "JSDVCRouter"
[JSDVCRouter openURL: BVCPath info: @{@"delegate":self,@"name":@"jersey",@"callback":callback}];
// BVCPath: 表示我们对 B 控制器定义的路径, 一般保存在全局 Map 里面, 每个 Path 映射当前控制器 Map 包含相关 title, class, needLog, 等参数;
// B Modal C
[JSDVCRouter openURL: C info: {kJSDRouteSegue: @"Modal"}]; // 控制器之间跳转默认以 Push 实现, 当需要 Modal 时, 则传递一个参数;
看到这里相信认真阅读的同学们已看出使用 Router 的好处:
1. 耦合度降低: A 控制器不需要知道 B 控制器的存在, 只需要 import "JSDRouter", 由其去进行相应跳转逻辑, 以及赋值等等;
2. 代码阅读性提高: 当然在刚刚接触时, 看着会不大不习惯, 等接触一段时间之后, 不仅减少了代码行数, 同时可读性还是很高的, 跟 push/pop, present/dismiss 说再见吧;
3. 提高代码复用性: 每次控制器之间跳转和赋值等操作, 都需要重复性的 code 一次(严重违背了: 可复用性原则), 通过 JSDRouter 将跳转和赋值等逻辑封装起来, 一次 code, 终生受用;
4. 易于维护: 写到这一点有点儿纠结, 当项目随着公司规模不断壮大时, 控制器数量, 跳转变得越加复杂, 跳转方法和逻辑很容易变得越来越混乱, 后期管理起来比较困难。 使用 JSDVCRouter 单一职责的原则来专门负责 App 内所有的跳转, 能非常有效的提高测试及后期维护, 当然成本是需要维护 RouterMap 同时完善 JSDVCRouter 内部逻辑;
5. 动态化及灵活性: 使用 Router 时可以配合后台响应传递响应的 Key 来决定真正跳转的页面, 而不是硬编码的方式来进行跳转;
6. 待补充:
-
实现 Router 完成控制器跳转, 至少需要几个步骤?
首次将控制器跳转转成 Router 方案
很简单只有 3个步骤, 如何需求变动不大的话, 几乎一劳永逸;
- Map 表创建: 其是一个全局 Map, App 内相应的控制器定义好 Path, Router 可以根据 Path 映射相应控制器制定的 Map 内, Map 里面最少包含当前控制器的参数如: {@"Class": @"控制器类名"}。相当于调用这个路由时,得到一组其绑定的 Map 作为参数, 通过 Class 来初始化实例;
代码结构如:
+ (NSDictionary *)configInfo
return @{ JSDRouteHomeCenter: @{
@"class": @"JSDAHomeCenterVC",
@"name": @"首页",
@"parameter": @"",
@"needLogin": @"0", },
JSDRouteUserLogin: @{
@"class": @"JSDUserLoginVC",
@"name": @"登陆",
@"parameter": @"",
@"needLogin": @"0", },
};
- 封装 JLRouter; 为了方便使用,管理,以及后期迁移等!类似使用 AFNetwork, SDWebImage, MJRefresh 等有名的开源库一样, 由于开源库提供功能非常丰富, 但是可能我们实际使用到的只是它一两个主要的功能来解决项目中存在的问题, 大家都会根据公司具体的业务场景或者使用习惯, 来对其进行一层甚至多层封装一样, 使其能更加适合实际要求;
笔者对其进行了一层封装 + Category 的形式: JSDVCRouter,JSDVCRouter + Add;
JSDVCRouter: 主要用于声明 Router 调用接口;
JSDVCRouter + Handle: 主要用于实现 Router 注册, 处理控制器之间跳转和参数赋值代码; - 根据约定 Path 进行跳转: 上面 1 2 都准备好之后, 即可轻松的进行控制器跳转 [JSDVCRouter openURL:BVC];
业务变更后期维护
- Map 维护: 随着业务发展, 当有新的页面加入时, 对 Map 添加一个指定的 Path 和绑定的相应参数;
- JSDVCRouter 维护: 其包含着真正对控制器初始化跳转和赋值的代码这里一般很少进行修改; 比如后期需支持跳转到 H5, 处理 3D Touch, Universal Links 时来这里进行维护;
实战 Code!
写到这里, 笔者不知道上面讲的对 Router 实现控制器跳转的简要介绍, 是否起到帮助初步接触 Router 时的同学们, 希望下面通过 Code 的方式能让大家更好的理解和使用起来!
下面详细介绍笔者封装 JLRoutes 实现控制器跳转的三个类:
JSDVCRouterConfig
这个文件主要用于管理所有 Router 映射到指定控制器类名(class), 以及相关参数的配置文件(title,needLogin等), 具体配置根据实际项目需求进行即可;
- 为了编译期能更好的检查到错误, 使用 extern NSString* const 声明, 配合 NSString* const 实现指定 Router URL, 使用的时候直接通过外部声明的常量字符串来指定跳转即可;
- 这样管理 Router URL 能更加方便阅读和维护, 如果直接使用 @"/login" 的方式来进行绑定可读性差, 很容易出现粗心大意导致的错误;
代码如下:
//App 内所有控制器
extern NSString* const JSDVCRouteWebview;
extern NSString* const JSDVCRouteLogin;
@interface JSDVCRouterConfig : NSObject
+ (NSDictionary *)configMapInfo;
@end
//App 内相关控制器
NSString* const JSDVCRouteWebview = @"/webView";
NSString* const JSDVCRouteLogin = @"/login";
@implementation JSDVCRouterConfig
+ (NSDictionary *)configMapInfo {
return @{
JSDVCRouteWebview: @{@"class": @"JSDWebViewVC",
@"title": @"WebView",
@"flags": @"",
@"needLogin": @"",
},
JSDVCRouteLogin: @{@"class": @"JSDLoginVC",
@"title": @"登录",
@"flags": @"",
@"needLogin": @"",
},
};
@end
JSDVCRouter
这个类内部实现的事情非常简单, 继承自 NSObject, 对外提供 注册和调用 Router 接口, 在内部调用 JLRoutes 提供的接口;
在项目中所有跳转均使用此类提供的接口来调用 Router;
一个是默认不带任何参数
另一个可以携带我们需要的参数(NSDictionary);
[JSDVCRouter openURL:JSDVCRouteAppear]; //push 到 AppearVC;
[JSDVCRouter openURL:JSDVCRouteAppear parameters:@{kJSDVCRouteSegue: kJSDVCRouteSegueModal, @"name": @"jersey"}]; // Modal 到 Appear VC 并携带参数 name;
单独封装一个 JSDVCRouter 好处:
防止三方库入侵. 其继承自 NSObject 并不直接依赖于 JLRouter, 这样在后期如果考虑更换三方库, 或者自己封装一套类似 JLRouter 提供的功能时, 只需要对其修改即可, 其他地方均无需修改;
接口隔离保持统一, 可读性更高;
@interface JSDVCRouter : NSObject
+ (BOOL)openURL:(NSString *)url;//调用 Router;
+ (BOOL)openURL:(NSString *)url parameters:(NSDictionary *)parameters;
+ (void)addRoute:(NSString* )route handler:(BOOL (^)(NSDictionary *parameters))handlerBlock;//注册 Router,调用 Router 时会触发回调;
@end
#define JSDRouterURL(string) [NSURL URLWithString:string]
@implementation JSDVCRouter
+ (BOOL)openURL:(NSString *)url {
return [self routeURL:url parameters:nil];
}
+ (BOOL)openURL:(NSString *)url parameters:(NSDictionary *)parameters {
return [self routeURL:url parameters:parameters];
}
+ (void)addRoute:(NSString *)route handler:(BOOL (^)(NSDictionary * _Nonnull parameters))handlerBlock {
[JLRoutes addRoute:route handler:handlerBlock];
}
#pragma mark - mark JLRouter
+ (BOOL)routeURL:(NSString*)url parameters:(NSDictionary *)parameters{
return [JLRoutes routeURL:JSDRouterURL(url) withParameters:parameters];
}
@end
JSDVCRouter+Handle
真正注册和调用 Router 时处理回调控制器跳转和参数赋值逻辑实现放在这里。
注册 Router : 对控制器内所有 Router 一一进行注册以及 TabBarIndex 切换和 处理返回 Router, 将回调统一转发到定义的方法里头。
处理 Router: 也就是注册好 Router 之后, 调用相应 Router 时, 我们在注册时写得回调方法, 这里是执行控制器跳转和传参的逻辑。
关于控制器跳转: 在触发 Router 时, 我们能拿到 Router 映射到的 Map, 获取到其 Class, 在通过 Class 来进行初始初始化实例, 这里通过对 UIViewController Category 找到当前 visibleVC 来进行 Push 或 Modal, 我们也可以根据业务方传递过来的参数来决定进行 Push 或 Modal 以及是否需要执行动画等等;
关于传参: 传递过来的参数是字典的数据结构, 所以我们先检测实例 VC 是否包含这个属性, [vc respondsToSelector:NSSelectorFromString(key)], 如果 VC 有这个属性则直接使用 KVC 的方式来进行赋值, 为了防止在开发时, 传入的字典 Key 与 VC 属性不匹配导致一些 Bug, 添加一层 NSAssert,这样能在开发过程中更快找到问题!
笔者自行封装的控制器跳转逻辑可能有考虑不周的地方, 主要还得根据具体业务需求来做具体判断;
下面分别是注册 Router 和匹配到 Router 之后回调处理代码, 有点长请耐心阅读
Router 注册, 将三种类型回调处理统一
@implementation JSDVCRouter (Handle)
//注册 Router, 控制器的跳转 + UITabBarIndex 切换 + 页面返回
+ (void)load {
[self performSelectorOnMainThread:@selector(registerRouter) withObject:nil waitUntilDone:false];
}
+ (void)registerRouter {
//获取全局 RouterMapInfo
NSDictionary* routerMapInfo = [JSDVCRouterConfig configMapInfo];
// router 对应控制器路径, 使用其来注册 Route, 当调用当前 Route 时会执行回调; 回调参数 parameters: 在执行 Route 时传入的参数;
for (NSString* router in routerMapInfo.allKeys) {
NSDictionary* routerMap = routerMapInfo[router];
NSString* className = routerMap[kJSDVCRouteClassName];
if (JSDIsString(className)) {
/*注册所有控制器 Router, 使用 [JSDVCRouter openURL:JSDVCRouteAppear]; push 到 AppearVC;
[JSDVCRouter openURL:JSDVCRouteAppear parameters:@{kJSDVCRouteSegue: kJSDVCRouteSegueModal, @"name": @"jersey"}]; Modal 到 Appear VC 并携带参数 name;
*/
[self addRoute:router handler:^BOOL(NSDictionary * _Nonnull parameters) {
//执行路由匹配成功之后,跳转逻辑回调;
/*执行 Route 回调; 处理控制器跳转 + 传参;
** routerMap: 当前 route 映射的 routeMap; 我们在 RouterConfig 配置的 Map;
** parameters: 调用 route 时, 传入的参数;
*/
return [self executeRouterClassName:className routerMap:routerMap parameters:parameters];
}];
}
}
// 注册 Router 到指定TabBar Index; 使用 [JSDVCRouter openURL:JSDVCRouteCafeTab] 切换到 Cafe Index
[self addRoute:@"/rootTab/:index" handler:^BOOL(NSDictionary * _Nonnull parameters) {
NSInteger index = [parameters[@"index"] integerValue];
// 处理 UITabBarControllerIndex 切换;
UITabBarController* tabBarVC = (UITabBarController* )[UIViewController jsd_rootViewController];
if ([tabBarVC isKindOfClass:[UITabBarController class]] && index >= 0 && tabBarVC.viewControllers.count >= index) {
UIViewController* indexVC = tabBarVC.viewControllers[index];
if ([indexVC isKindOfClass:[UINavigationController class]]) {
indexVC = ((UINavigationController *)indexVC).topViewController;
}
//传参
[self setupParameters:parameters forViewController:indexVC];
tabBarVC.selectedIndex = index;
return YES;
} else {
return NO;
}
}];
// 注册返回上层页面 Router, 使用 [JSDVCRouter openURL:kJSDVCRouteSegueBack] 返回上一页 或 [JSDVCRouter openURL:kJSDVCRouteSegueBack parameters:@{kJSDVCRouteBackIndex: @(2)}] 返回前两页
[self addRoute:kJSDVCRouteSegueBack handler:^BOOL(NSDictionary * _Nonnull parameters) {
return [self executeBackRouterParameters:parameters];
}];
}
Router 匹配到之后回调: 实例化控制器, 参数赋值, 页面跳转
#pragma mark - execute Router VC
// 当查找到指定 Router 时, 触发路由回调逻辑; 找不到已注册 Router 则直接返回 NO; 如需要的话, 也可以在这里注册一个全局未匹配到 Router 执行的回调进行异常处理;
+ (BOOL)executeRouterClassName:(NSString *)className routerMap:(NSDictionary* )routerMap parameters:(NSDictionary* )parameters {
// 拦截 Router 映射参数,是否需要登录才可跳转;
BOOL needLogin = [routerMap[kJSDVCRouteClassNeedLogin] boolValue];
if (needLogin && !userIsLogin) {
[JSDVCRouter openURL:JSDVCRouteLogin];
return NO;
}
//统一初始化控制器,传参和跳转;
UIViewController* vc = [self viewControllerWithClassName:className routerMap:routerMap parameters: parameters];
if (vc) {
[self gotoViewController:vc parameters:parameters];
return YES;
} else {
return NO;
}
}
// 根据 Router 映射到的类名实例化控制器;
+ (UIViewController *)viewControllerWithClassName:(NSString *)className routerMap:(NSDictionary *)routerMap parameters:(NSDictionary* )parameters {
id vc = [[NSClassFromString(className) alloc] init];
if (![vc isKindOfClass:[UIViewController class]]) {
vc = nil;
}
#if DEBUG
//vc不是UIViewController
NSAssert(vc, @"%s: %@ is not kind of UIViewController class, routerMap: %@",__func__ ,className, routerMap);
#endif
//参数赋值
[self setupParameters:parameters forViewController:vc];
return vc;
}
// 对 VC 参数赋值
+ (void)setupParameters:(NSDictionary *)params forViewController:(UIViewController* )vc {
for (NSString *key in params.allKeys) {
BOOL hasKey = [vc respondsToSelector:NSSelectorFromString(key)];
BOOL notNil = params[key] != nil;
if (hasKey && notNil) {
[vc setValue:params[key] forKey:key];
}
#if DEBUG
//vc没有相应属性,但却传了值
if ([key hasPrefix:@"JLRoute"]==NO &&
[key hasPrefix:@"JSDVCRoute"]==NO && [params[@"JLRoutePattern"] rangeOfString:[NSString stringWithFormat:@":%@",key]].location==NSNotFound) {
NSAssert(hasKey == YES, @"%s: %@ is not property for the key %@",__func__ ,vc,key);
}
#endif
};
}
// 跳转和参数设置;
+ (void)gotoViewController:(UIViewController *)vc parameters:(NSDictionary *)parameters {
UIViewController* currentVC = [UIViewController jsd_findVisibleViewController];
NSString *segue = parameters[kJSDVCRouteSegue] ? parameters[kJSDVCRouteSegue] : kJSDVCRouteSeguePush; // 决定 present 或者 Push; 默认值 Push
BOOL animated = parameters[kJSDVCRouteAnimated] ? [parameters[kJSDVCRouteAnimated] boolValue] : YES; // 转场动画;
NSLog(@"%s 跳转: %@ %@ %@",__func__ ,currentVC, segue,vc);
if ([segue isEqualToString:kJSDVCRouteSeguePush]) { //PUSH
if (currentVC.navigationController) {
NSString *backIndexString = [NSString stringWithFormat:@"%@",parameters[kJSDVCRouteBackIndex]];
UINavigationController* nav = currentVC.navigationController;
if ([backIndexString isEqualToString:kJSDVCRouteIndexRoot]) {
NSMutableArray *vcs = [NSMutableArray arrayWithObject:nav.viewControllers.firstObject];
[vcs addObject:vc];
[nav setViewControllers:vcs animated:animated];
} else if ([backIndexString integerValue] && [backIndexString integerValue] < nav.viewControllers.count) {
//移除掉指定数量的 VC, 在Push;
NSMutableArray *vcs = [nav.viewControllers mutableCopy];
[vcs removeObjectsInRange:NSMakeRange(vcs.count - [backIndexString integerValue], [backIndexString integerValue])];
nav.viewControllers = vcs;
[nav pushViewController:vc animated:YES];
} else {
[nav pushViewController:vc animated:animated];
}
}
else { //由于无导航栏, 直接执行 Modal
BOOL needNavigation = parameters[kJSDVCRouteSegueNeedNavigation] ? NO : YES;
if (needNavigation) {
UINavigationController* navigationVC = [[UINavigationController alloc] initWithRootViewController:vc];
//vc.modalPresentationStyle = UIModalPresentationFullScreen;
[currentVC presentViewController:navigationVC animated:YES completion:nil];
}
else {
//vc.modalPresentationStyle = UIModalPresentationFullScreen;
[currentVC presentViewController:vc animated:animated completion:nil];
}
}
}
else { //Modal
BOOL needNavigation = parameters[kJSDVCRouteSegueNeedNavigation] ? parameters[kJSDVCRouteSegueNeedNavigation] : NO;
if (needNavigation) {
UINavigationController* navigationVC = [[UINavigationController alloc] initWithRootViewController:vc];
//vc.modalPresentationStyle = UIModalPresentationFullScreen;
[currentVC presentViewController:navigationVC animated:animated completion:nil];
}
else {
//vc.modalPresentationStyle = UIModalPresentationFullScreen;
[currentVC presentViewController:vc animated:animated completion:nil];
}
}
}
能坚持看到这里, 应该对 Router 进行控制器跳转已经有了个不错的理解!
待补充
App 内部跳转除了, 频繁的控制器之间切换外, 还有比如跳转到 H5, 或者跳转到 WebView 等;
App 外跳转则包含 Scheme 启动, 3D Touch, UniversalLink, 点击通知等都会触发;
这些包含跳转, 页面切换的我们均可以统一使用 Router 来进行有效的管理, 使 App 变得更加动态化, 模块之间耦合度更低;
- 支持 H5 跳转
- 外部 Scheme 启动 App
- UniversalLink
- 3D Touch Shortcut
- 支持后台动态下发 RouterMap 配置
最后
希望此篇文章对您有所帮助,如有不对的地方,希望大家能留言指出纠正。
emmmmm,每次看到这么长一串代码, 要么是直接跳过, 要么就是认认真真看完之后马上启动 Xcode Coding 一遍, Commond + R 实践一遍, 为了方便大家理解献上
Demo 如果觉得对你有帮助, 麻烦大家给个 Star 谢谢😆!!!!!
学习的路上,与君共勉!!!
参考-链接
iOS 组件化 —— 路由设计思路分析
iOS架构实践干货
iOS应用架构谈 组件化方案
蘑菇街 App 的组件化之路
实战Demo 如果觉得对你有帮助, 麻烦大家给个 Star 谢谢😆!!!!!