
TheRouter-iOS 轻量化路由中间件
TheRouter是货拉拉打造的一款同时支持 Android 及 iOS 的轻量级路由中间件,在iOS端吸取了其他其他语言的特性,支持 注解 功能,极大提升了路由在iOS端的使用体感。摒弃了传统ioser的target-action或protocol理念,向更广的后台或Android应用对齐。
Github传送门>>
一、Why TheRouter
随着应用需求复杂度愈来愈高,开发人员将App架构由原来简单的MVC变成MVP,MVVM等复杂架构并按业务拆分独立服务。那么该如何解耦各层,解耦各个界面和各个组件,不管多么复杂的情况下都能保持“高内聚,低耦合”的特点?随着痛点愈发突出,在iOS领域内产生了几种常见的中间件:
可以发现目前iOS领域并无一个统一的组件间通讯中间件,由于各个中间件实现底层原理都不相同造成了优劣点明显,互相之间难以迁移,并且很难和Android、H5、苹果自身的Universal Links形成一个通用式通讯组件。因此,TheRouter-iOS它来了!
TheRouter 四大核心功能:
- 依赖注入:实现类似Java注解功能,在vc类或任意方法上标注即可完成路由注册;
- 硬编码消除:内置脚本会自动将注册的path转为静态字符串常量供业务使用;
- 动态化能力 :支持添加重定向、拦截器等,也可以使用后台配置动态添加;
- 页面导航跳转能力:支持常规vc或Storyboard的push/present跳转能力;
二、实现原理
2.1 模块描述
├── Classes
│ ├── TheRouter+Annotation.h
│ ├── TheRouter+Annotation.m // 路由注解器及Path功能扩展
│ ├── TheRouter.h
│ └── TheRouter.m // 路由库核心代码(增删改查,重定向/拦截器)
└── Resources
└── scan.py // 注解扫描及硬编码处理脚本(该脚本只会被引用不会参与编译和打包)
2.2 存储结构
在路由表中一条路由的存储会转为KeyPath
,在后续查找时通过valueForKeyPath
方法会更加高效。
例如:test://abc.com/login 在组件中存储如下:
@{
@"test":@{
@"abc-com":@{
@"login":@{
@"_handler":block, // 打开回调
@"_placeholder":array, // 占位符信息
@"_interceptor":block, // 拦截器回调
@"_redirectUrl":string, // 重定向链接
}
}
}
}
在打开该路由时流程如下:
2.3 注解式依赖注入
其实早在BeeHive
库中其实就已经有针对iOS注解实现的方式,BeeHive中其实是利用了__attribute((used, section("__DATA,"#sectname" ")))
将映射关系写入了Mach-O文件中,在启动时读取Mach-O文件再注册到路由表。但是这样做会产生大量的全局变量也会造成Mach-O文件过大,因此TheRouter做法是将映射关系存入提前建好的plist文件,注解宏定义只做一个标识,供给脚本扫描使用,后续再读取该plist将路由注册。
注解宏定义:
/// VC annotators, equivalent to call registController: clazzName storyBoard: nil path: routerPath
#define TheRouterController(routerPath,clazzName)
/// Storyboard VC annotators, equivalent to call registController: clazzName Storyboard: sbName path: routerPath
#define TheRouterStoryboardController(routerPath,sbName,clazzName)
/// Method annotator, equivalent to calling registSelector:selName targetClass:clazzName path:routerPath
#define TheRouterSelector(routerPath,selName,clazzName)
这里分为了三种注解:
- TheRouterController:vc类型
- routerPath:子路径
- clazzName:vc类名
- TheRouterStoryboardController:支持storyboard中的vc
- routerPath:子路径
- sbName:storyboard文件名
- clazzName:vc类名
(注:这里storyboardID必须与类名一致)
- TheRouterSelector:事件类型
- routerPath:子路径
- selName:方法名
- clazzName:事件方法所在类名
vc类型/storyboard vc类型
通过vc类型注解的vc会自动创建该vc的实例,如果路由中有参数的话会自动通过kvc方式给vc对应属性赋值。
vc类型注解在调用时也可以指定跳转类型,路由中会自动获取当前合适的navigationController进行push、present或presentInNavition等操作。
事件类型
通过事件类型注解的方法对方法名不做任何要求,但需要保证方法只有一个入参并且入参必须是TheRouterInfo
,该类中包含了本次调用路由时的所有信息,例如路由参数、回调block等。
事件类型注解的方法可以有返回值也可以没有,该返回值其实就是调用openURLString
或openPath
方法时所返回的值,可以根据业务场景同步返回或者通过TheRouterInfo
对象中的openCompleteHandler
异步返回结果。
事件类型注解的方法可以是类方法也可以是实例方法,如果是实例方法那么在每次调用时都会自动创建一个实例。后续会支持指定实例返回,业务将可以更灵活的指定实现实例。
整体注解发现/注册流程如下:
2.4 Path硬编码处理
使用路由组件难免会产生大量的硬编码字符串,对读写都有很大的麻烦,因此在TheRouter中会将注册的路由Path
根据规则转化为静态常量
存放在特定的头文件中,有些类似R.swift。
后续会支持通过注解添加路由相关注释文档,并写入该头文件中方便查看路由的定义
整体如下流程:
三、使用介绍
Cocoapods 引入
pod 'TheRouter'
3.1 注解使用
step1
创建TheRouterAnnotation.plist文件,必须在MainBundle下。
step2
为项目创建一个Aggregate类型的target:
step3
在新建的target添加脚本:
图中实例脚本参数含义:
python3 $SRCROOT/../TheRouter/Resources/scan.py # 脚本路径
$SRCROOT/ # 参数1:扫描路径,一般为项目根目录
$SRCROOT/TheRouter/ # 参数2:路径定义头文件存放目录 一般为存放至公共模块
$SRCROOT/TheRouter/TheRouterAnnotation.plist # 参数3:TheRouterAnnotation文件路径
step4
在应用加载完成时注册host,在想要跳转的VC类上添加路由注解或创建对应模块的Service类,在Service中的方法上添加注解即可,例如:
注册该项目的host:
[TheRouter.shared registPathAnnotationsWithHost:@"hd://com.therouter.test"];
注册ViewController类型路由:
TheRouterController(test/vc, TestViewController)
@interface TestViewController : UIViewController
@end
注册事件类型路由:
#import "TestService.h"
#import "TheRouter_Mappings.h"
#import <TheRouter/TheRouter+Annotation.h>
@implementation TestService
TheRouterSelector(test/jump, jumpToTestVC, TestService)
+ (id)jumpToTestVC:(TheRouterInfo *)routerInfo {
UIViewController *vc = [TheRouter.shared openVCPath:kRouterPathTestVcVC
cmd:TheRouterOpenCMDPush
withParams:@{@"title":@"123"}
hanlder:^(NSString * _Nonnull tag, NSDictionary * _Nullable result) {
!routerInfo.openCompleteHandler ?: routerInfo.openCompleteHandler(tag, result);
}];
return vc;
}
@end
step5
在以后的开发中对路由进行增删改时编译一次创建好的target,会自动向TheRouterAnnotation.plist文件写入信息,并在指定的目录下生成TheRouter_Mappings.h文件,将此文件拖入对应模块即可:
3.2 拦截器和重定向
拦截器:
拦截器支持全路径或通配符设置,如果返回YES
那么对应的路由事件可以正常执行,反之则会被拦截不会执行路由事件,如果该拦截器中需要执行异步操作也可以先返回NO
,等待异步任务完成后再调用continueHandle
即可
// 只要访问hd://com.therouter.test或其子路径 (hd://com.therouter.test/xxx) 都会进入该回调
[TheRouter.shared registInterceptorForURLString:@"hd://com.therouter.test/*" handler:^BOOL(TheRouterInfo * _Nonnull router, id _Nullable (^ _Nonnull continueHandle)(void)) {
NSLog(@"will execute router %@", router.URLString);
return YES;
}];
重定向:
重定向可以用来迁移老路径或线上遇到问题时可快速更改至其他页面承接业务,可以通过后台下发映射关系来动态添加。
// 访问 hd://test.com/test 时会走 hd://test.com/test/vc的事件
[TheRouter.shared registRedirect:@"hd://test.com/test" to:@"hd://test.com/test/vc"];
3.3 执行路由事件
路由支持同步
和异步
返回,可根据业务灵活使用。在异步返回中业务也可以返回不同的tag来标记多种业务返回场景。
UIViewController *vc = [TheRouter.shared openVCPath:kRouterPathTestVcVC // 传入Path
cmd:TheRouterOpenCMDPush // 指定打开命令
withParams:@{@"title":@"123"} // 指定参数,这里支持对kvc赋值
hanlder:^(NSString * _Nonnull tag, NSDictionary * _Nullable result) {
!routerInfo.openCompleteHandler ?: routerInfo.openCompleteHandler(tag, result);
}];
四、写在最后
TheRouter
是目前iOS中一个功能比较全面的轻量级路由,结合了目前行业中各通讯组件的优点,也摒弃了大部分缺陷,做成一个统一多场景组件。在使用中更符合现代编程习惯,提升书写愉悦度的同时降低业务耦合度。
TheRouter
所使用的注解方式彻底解决了路由在iOS中手动注册的痛点,只能说一直注解一直爽,并提供了重定向
和拦截器
等动态接口,可以结合业务后台做业务逻辑降级操作或者动态跳转映射。
最后欢迎大家在Github
issue
中提出需求,我们评估后会尽快支持,也欢迎任何人提供 Pull Requests
,一起壮大我们的TheRouter。
关于作者:王恺靖,货拉拉iOS司机端资深开发工程师,负责主要核心业务及基础架构工作。