TheRouterSwift
功能
TheRouterSwift:用于解除模块耦合和通信的工具,基于 Swift 协议进行注册路由和打开路由的动态懒加载。此外,它支持通过 Service-Protocol 寻找对应的模块,并使用协议进行依赖注入和模块通信。
- 1. 页面导航跳转能力:支持常规vc或Storyboard push/present/popToTaget/windowNavRoot/modalDismissBeforePush 跳转能力;
- 2. 自动注册路由能力:懒加载动态注册路由,只有在首次调用 OpenURL 动态注册时;
- 3. 导出路由映射文件:支持将项目中的路由映射关系导出为文档,支持 JSON 和 Plist 格式,方便开发者进行双端汇总、对比和记录;
- 4. 自动注册服务能力:动态注册服务,使用运行时自动注入;
- 5. 去除硬编码:将注册的路径转换为业务使用的静态字符串常量;
- 6. 动态能力:支持添加重定向方向、移除重定向、动态添加路由、动态移除路由、拦截器、错误路径修复等;
- 7. 链式编程:支持链式编程以连接 URL 和参数;
- 8. 兼容 Objective-C:OC 类可以使用继承在 Swift 中动态注册以遵循协议;
- 9. 服务调用:支持本地服务调用和远程服务调用;
编号 | 描述 | 实例代码 |
---|---|---|
1 | 支持懒加载路由 | lazyRegisterRouter 只是当首次调用 OpenURL 时进行动态注册 |
2 | 支持利用 Swift 特性进行面向协议的编程 | TheRouterServiceProtocol TheRouterableProtocol |
3 | 支持无需手动注册的动态注册 | TheRouterManager.addGloableRouter([".LA"], url, userInfo) |
4 | 支持依赖注入和服务的自动注册 | 注册服务() TheRouterServiceManager注册服务(serviceName) |
5 | 支持服务的动态注册和协议调用 | TheRouter.fetchService(AppConfigServiceProtocol.self) |
6 | 支持单个模块的独立初始化 | ModuleProtocol moduleSetUp() |
7 | 支持导出路由映射文件 | TheRouter.writeRouterMapToFile |
8 | 支持重定向、取消重定向、动态添加路由、动态删除路由和修复错误路径 | TheRouter.addRelocationHandle |
9 | 支持拦截器 | TheRouter.addRouterInterceptor |
10 | 支持在Swift项目中调用OC路由 | 使用继承遵循协议的方式,OC类可以在Swift中动态注册 |
11 | 支持全局故障监控 | TheRouter.globalOpenFailedHandler |
12 | 支持路由和服务日志回调和 | TheRouter.logcat(_ url: String, _ logType: TheRouterLogType, _ errorMsg: String) |
13 | 在路由注册期间支持安全检查 | TheRouterManager.routerForceRecheck() 客户端强制验证,是否匹配,如果不匹配则触发断言 |
14 | 支持后端调用客户端服务 | TheRouter.openURL() 服务接口分发,MQTT,JSBridge |
15 | 支持链式调用 | TheRouterBuilder.build("scheme://router/demo").withInt(key: "intValue", value: 2).navigation() |
16 | 支持打开路由回调闭包 | TheRouterBuilder.build("scheme://router/demo").withInt(key: "intValue", value: 2).navigation(_ complateHandler: ComplateHandler = nil) |
17 | 支持非链式调用打开路由回调闭包 | TheRouter.openURL("https://therouter.cn/" ) { param, instance in } |
背景
随着项目的需求不断增加,越来越多的开发者带来了许多问题
-
模块划分不明确,任何开发者都会调用并修改其他模块的代码实现以满足自己的业务需求。
-
难以维护。同一组件的不同服务分散在项目的不同部分,不利于统一维护、修改和替换。
-
无法明确模块归属,导致同一功能的多个维护,导致冲突。
当端到端拆分后,本地代码之间无法相互依赖,因此需要通过工具实现透明服务的能力。我们需要一个中间件来处理这些问题。通过添加中间层映射关系,路由转移耦合并解决了服务间的依赖关系。
成熟路由是什么样的?
-
在服务组件化后,项目的每个模块都需要解耦。在远端升级后,如何解决接口跳转的问题?《路由API》
-
动态注册路由。无需手动注册,无需手动注册服务。
-
如何解决上行统一问题?《使用统一URL映射处理》
-
如果服务转发期间发生故障,如何修改服务转发逻辑?如何进行服务降级?《远程交付配置,修改转发URL》
-
服务异常,显示H5接口。《重定向》
-
如果应用重定向失败,我应该如何访问相同的本地错误页面?
统一错误处理
-
如何在跳转前添加强制业务逻辑处理,例如服务调整,你必须在进入前执行一些操作。
重定向
-
业务流程中有许多前置操作,例如登录后才能访问订单列表。如何实现这一功能?
拦截器
-
如何测试每个服务是否正常向前转发?
路由路径验证
-
如何将最常调用的服务优先转发以减少查询次数?
提高优先级
-
本地服务通过路由调用,远程服务也通过路由调用。
支持服务调用
整体设计思路
为了与Android端保持一致,使用URL和类注册实现。通过URL匹配查询存储在数组中的模板信息,找到并获取相应的实例,并执行跳转操作。
使用介绍预览
如何安装
CocoaPods
在你的Podfile中添加以下条目
pod 'TheRouterSwift', '0.1.1'
Swift 限制版本
Swift5.0 or above
TheRouterSwift 使用方法
-
注册
自从自动注册功能实现后,开发者不需要自己添加路由,只需做以下操作。
/// 实现TheRouterable协议
extension TheRouterController: TheRouterable {
static var patternString: [String] {
["scheme://router/demo"]
}
static func registerAction(info: [String : Any]) -> Any {
debugPrint(info)
let vc = TheRouterController()
vc.qrResultCallBack = info["clouse"] as? QrScanResultCallBack
vc.resultLabel.text = info.description
return vc
}
}
/// 在AppDelegate中实现懒加载的闭包
// 路由懒加载注册
TheRouter.lazyRegisterRouterHandle { url, userInfo in
TheRouterManager.injectRouterServiceConfig(webRouterUrl, serivceHost)
return TheRouterManager.addGloableRouter([".The"], url, userInfo)
}
// 动态注册服务
TheRouterManager.registerServices()
// 日志回调,可以监控线上路由运行情况
TheRouter.logcat { url, logType, errorMsg in
debugPrint("TheRouter: logMsg- \(url) \(logType.rawValue) \(errorMsg)")
}
OC 注释的形式
以下是 OC 使用注释的方式列表,Swift 由于缺乏动态性不支持这些方式。
//使用注解
@page(@"home/main")
- (UIViewController *)homePage{
// Do stuff...
}
Swift 注册路由
在 Swift 中,我们都知道 Swift 不支持注释,那么如何解决 Swift 的动态注册路由问题,我们使用项目的运行时遍历来找到一个注册协议来自动注册类。
public class func registerRouterMap(_ registerClassPrifxArray: [String], _ urlPath: String, _ userInfo: [String: Any]) -> Any? {
let expectedClassCount = objc_getClassList(nil, 0)
let allClasses = UnsafeMutablePointer<AnyClass>.allocate(capacity: Int(expectedClassCount))
let autoreleasingAllClasses = AutoreleasingUnsafeMutablePointer<AnyClass>(allClasses)
let actualClassCount: Int32 = objc_getClassList(autoreleasingAllClasses, expectedClassCount)
var resultXLClass = [AnyClass]()
for i in 0 ..< actualClassCount {
let currentClass: AnyClass = allClasses[Int(i)]
let fullClassName: String = NSStringFromClass(currentClass.self)
for value in registerClassPrifxArray {
if (fullClassName.containsSubString(substring: value)) {
if currentClass is UIViewController.Type {
resultXLClass.append(currentClass)
}
#if DEBUG
if let clss = currentClass as? CustomRouterInfo.Type {
assert(clss.patternString.hasPrefix("scheme://"), "URL非scheme://开头,请重新确认")
apiArray.append(clss.patternString)
classMapArray.append(clss.routerClass)
}
#endif
}
}
}
for i in 0 ..< resultXLClass.count {
let currentClass: AnyClass = resultXLClass[i]
if let cls = currentClass as? TheRouterable.Type {
let fullName: String = NSStringFromClass(currentClass.self)
for s in 0 ..< cls.patternString.count {
if fullName.hasPrefix(NSKVONotifyingPrefix) {
let range = fullName.index(fullName.startIndex, offsetBy: NSKVONotifyingPrefix.count)..<fullName.endIndex
let subString = fullName[range]
pagePathMap[cls.patternString[s]] = "\(subString)"
TheRouter.addRouterItem(cls.patternString[s], classString: "\(subString)")
} else {
pagePathMap[cls.patternString[s]] = fullName
TheRouter.addRouterItem(cls.patternString[s], classString: fullName)
}
}
}
}
#if DEBUG
debugPrint(pagePathMap)
routerForceRecheck()
#endif
TheRouter.routerLoadStatus(true)
return TheRouter.openURL(urlPath, userInfo: userInfo)
}
为了避免无效遍历,我们传递registerClassPrifxArray指定遍历包含这些前缀的类。一旦是UIViewController.Type,就将其存储并检查它是否遵守TheRouterable协议。如果是,它将自动注册。无需手动注册。
路由注册的懒加载
动态注册的一个缺点是在启动时动态注册。TheRouter中注册的时间被延迟了,并且它是在App第一次调用TheRouter.openURL()时注册的。它将确定路由是否已加载,如果没有则加载它,然后打开该路由。
@discardableResult
public class func openURL(_ urlString: String, userInfo: [String: Any] = [String: Any]()) -> Any? {
if urlString.isEmpty {
return nil
}
if !shareInstance.isLoaded {
return shareInstance.lazyRegisterHandleBlock?(urlString, userInfo)
} else {
return openCacheRouter((urlString, userInfo))
}
}
// MARK: - Public method
@discardableResult
public class func openURL(_ uriTuple: (String, [String: Any])) -> Any? {
if !shareInstance.isLoaded {
return shareInstance.lazyRegisterHandleBlock?(uriTuple.0, uriTuple.1)
} else {
return openCacheRouter(uriTuple)
}
}
public class func openCacheRouter(_ uriTuple: (String, [String: Any])) -> Any? {
if uriTuple.0.isEmpty {
return nil
}
if uriTuple.0.contains(shareInstance.serviceHost) {
return routerService(uriTuple)
} else {
return routerJump(uriTuple)
}
}
OC类如何也使用Swift路由
这是一个OC类接口,路由跳转的实现需要继承OC类,并实现TheRouterAble协议
@interface TheRouterBController : UIViewController
@property (nonatomic, strong) UILabel *desLabel;
@end
@interface TheRouterBController ()
@end
@implementation TheRouterBController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor yellowColor];
[self.view addSubview:self.desLabel];
// Do any additional setup after loading the view.
}
@end
public class TheRouterControllerB: TheRouterBController, TheRouterable {
public static var patternString: [String] {
["scheme://router/demo2",
"scheme://router/demo2-Android"]
}
public static func registerAction(info: [String : Any]) -> Any {
let vc = TheRouterBController()
vc.desLabel.text = info.description
return vc
}
}
单独注册
TheRouter.addRouterItem(RouteItem(path: "scheme://router/demo?&desc=简单注册,直接调用TheRouter.addRouterItem()注册即可", className: "TheRouterSwift_Example.TheRouterController"))
TheRouter.addRouterItem(["scheme://router/demo?&desc= Simple registration, directly call TheRouter.addRouterItem() registration ": "TheRouterSwift_Example.TheRouterController"])
TheRouter.addRouterItem("scheme://router/demo? & desc = simple registration ", classString: "TheRouterSwift_Example. TheRouterController")
TheRouter.addRouterItem(TheRouterApi.patternString, classString: TheRouterApi.routerClass)
TheRouter.addRouterItem(TheRouterAApi.patternString, classString: TheRouterAApi.routerClass)
批量注册
TheRouter.addRouterItem(["scheme://router/demo": "TheRouterSwift_Example.TheRouterController",
"scheme://router/demo1": "TheRouterSwift_Example.TheRouterControllerA"])
2. 删除
TheRouter.removeRouter(TheRouterViewCApi.patternString)
3. 打开
声明了不同的方法,主要是为了明显的区分,内部统一调用openURL
便利构造函数链路打开路由
let model = TheRouterModel.init(name: "AKyS", age: 18)
TheRouterBuilder.build("scheme://router/demo")
.withInt(key: "intValue", value: 2)
.withBool(key: "boolValue", value: false)
.withFloat(key: "floatValue", value: 3.1415)
.withBool(key: "boolValue", value: false)
.withDouble(key: "doubleValue", value: 2.0)
.withAny(key: "any", value: model)
.navigation()
TheRouterBuilder.build("scheme://router/demo")
.withInt(key: "intValue", value: 2)
.withString(key: "stringValue", value: "sdsd")
.withFloat(key: "floatValue", value: 3.1415)
.withBool(key: "boolValue", value: false)
.withDouble(key: "doubleValue", value: 2.0)
.withAny(key: "any", value: model)
.navigation { params, instance in
}
启用常用路由模式
public class TheRouterApi: CustomRouterInfo {
public static var patternString = "scheme://router/demo"
public static var routerClass = "TheRouterSwift_Example.TheRouterController"
public var params: [String: Any] { return [:] }
public var jumpType: LAJumpType = .push
public init() {}
}
public class TheRouterAApi: CustomRouterInfo {
public static var patternString = "scheme://router/demo1"
public static var routerClass = "TheRouterSwift_Example.TheRouterControllerA"
public var params: [String: Any] { return [:] }
public var jumpType: LAJumpType = .push
public init() {}
}
TheRouter.openURL(TheRouterCApi.init().requiredURL)
TheRouter.openWebURL("https://xxxxxxxx")
@discardableResult
public class func openWebURL(_ uriTuple: (String, [String: Any])) -> Any? {
return TheRouter.openURL(uriTuple)
}
@discardableResult
public class func openWebURL(_ urlString: String,
userInfo: [String: Any] = [String: Any]()) -> Any? {
TheRouter.openURL((urlString, userInfo))
}
原始形式入站路由和附加参数
TheRouter.openURL(("scheme://router/demo1? id=2&value=3&name=AKyS&desc= Directly call TheRouter.addRouterItem() registration can be, support single registration, batch registration, dynamic registration, lazy loading dynamic registration ", ["descs": "Add parameters "]))
参数传递模式
let clouse = { (qrResult: String, qrStatus: Bool) in
print("\(qrResult) \(qrStatus)")
self.view.makeToast("\(qrResult) \(qrStatus)")
}
let model = TheRouterModel.init(name: "AKyS", age: 18)
TheRouter.openURL(("scheme://router/demo?id=2&value=3&name=AKyS", ["model": model, "clouse": clouse]))
4. 全局故障映射
TheRouter.globalOpenFailedHandler { info in
debugPrint(info)
}
5. 拦截
例如,无登录情况下的统一拦截:登录前跳转消息列表,登录成功后跳转到消息列表。
let login = TheRouterLoginApi.parttingString
TheRouter.addRouterInterceptor([login], priority: 0) { (info) -> Bool in
if LALoginManger.shared.isLogin {
return true
} else {
TheRouter.openURL(TheRouterLoginApi().build)
return false
}
}
登录成功后,删除拦截器。
6. 路径和类正确且安全验证
// MARK: - 客户端强制校验,是否匹配
public static func routerForceRecheck() {
let patternArray = Set(pagePathMap.keys)
let apiPathArray = Set(apiArray)
let diffArray = patternArray.symmetricDifference(apiPathArray)
debugPrint("URL差集:\(diffArray)")
debugPrint("pagePathMap:\(pagePathMap)")
assert(diffArray.count == 0, "URL 拼写错误,请确认差集中的url是否匹配")
let patternValueArray = Set(pagePathMap.values)
let classPathArray = Set(classMapArray)
let diffClassesArray = patternValueArray.symmetricDifference(classPathArray)
debugPrint("classes差集:\(diffClassesArray)")
assert(diffClassesArray.count == 0, "classes 拼写错误,请确认差集中的class是否匹配")
}
7. 地下隧道路径注册 -KVO
类名在本地验证类时发生不匹配。检查原因:为了避免启动时的路径注册影响启动速度,采用懒加载方法,即路径接口注册后再首次打开时跳转。然而,在我们动态注册之前,由于在该类中添加了KVO(键值观察),这个类在传递过程中将其className改变为NSKVONotifying_xxx。我们需要特殊处理,如下:
/// 对于KVO监听,动态创建子类,需要特殊处理
public let NSKVONotifyingPrefix = "NSKVONotifying_"
if fullName.hasPrefix(NSKVONotifyingPrefix) {
let range = fullName.index(fullName.startIndex, offsetBy: NSKVONotifyingPrefix.count)..<fullName.endIndex
let subString = fullName[range]
pagePathMap[cls.patternString[s]] = "\(subString)"
TheRouter.addRouterItem(cls.patternString[s], classString: "\(subString)")
}
动态调用路由
在上述路由能力下,我们希望App能够动态添加路由、删除路由、重定向路由,通过路由调优本地服务,并通过路由远程调优App服务,然后进行动态扩展。
重定向功能
定义路由传递模型数据结构
public struct TheRouterInfo {
public init() {}
public var targetPath: String = ""
public var orginPath: String = ""
// 1: 表示替换或者修复客户端代码path错误 2: 新增路由 3:删除路由, 4: 重置路由
public var routerType: TheRouterReloadMapEnum = .none
public var path: String = "" // 新的路由地址
public var className: String = "" // 路由地址对应的界面
public var params: [String: Any] = [:]
}
当重定向数据远程传输时,将切换到白屏的服务逻辑切换到黄屏。
let relocationMap = ["routerType": 1, "targetPath": "scheme://router/demo1", "orginPath": "scheme://router/demo"] as NSDictionary
TheRouterManager.addRelocationHandle(routerMapList: [relocationMap])
TheRouter.openURL("scheme://router/demo?desc=跳转白色界面被重定向到了黄色界面")
重定向恢复
在业务中,业务调整很常见,因此如果需要在重定向后恢复,则需要删除重定向。
let relocationMap = ["routerType": 4, "targetPath": "scheme://router/demo", "orginPath": "scheme://router/demo"] as NSDictionary
TheRouterManager.addRelocationHandle(routerMapList: [relocationMap])
TheRouter.openURL("scheme://router/demo?desc=跳转白色界面被重定向到了黄色界面之后,根据下发数据又恢复到跳转白色界面")
路由路径被动态恢复。
开发过程中,开发者不小心输入了错误的路由路径,上线后正常服务转发无法执行。在这种情况下,需要远程路由进行匹配。《code>scheme://router/demo3是正确的路径,但错误编写的路由路径是scheme://router/demo33
。在这种情况下,需要创建一个新的映射路径。
let relocationMap = ["routerType": 2, "className": "TheRouterSwift_Example.TheRouterControllerC", "path": "scheme://router/demo33"] as NSDictionary
TheRouterManager.addRelocationHandle(routerMapList: [relocationMap])
let value = TheRouterCApi.init().requiredURL
TheRouter.openURL(value)
路由匹配不同的基于Android的路径
在实际开发中,一旦使用该URI涉及两端,则两端可能存在不一致的问题,那么如何解决,可以通过在本地添加新的路由路径或通过发送新的路由远程来解决。
public class TheRouterControllerB: TheRouterBController, TheRouterable {
public static var patternString: [String] {
["scheme://router/demo2",
"scheme://router/demo2Android"]
}
public static func registerAction(info: [String : Any]) -> Any {
let vc = TheRouterBController()
vc.desLabel.text = info.description
return vc
}
}
如何声明和实现服务
服务使用运行时动态注册,因此你不必担心服务未注册。只需像上述案例那样使用即可。
public class func registerServices() {
let expectedClassCount = objc_getClassList(nil, 0)
let allClasses = UnsafeMutablePointer<AnyClass>.allocate(capacity: Int(expectedClassCount))
let autoreleasingAllClasses = AutoreleasingUnsafeMutablePointer<AnyClass>(allClasses)
let actualClassCount: Int32 = objc_getClassList(autoreleasingAllClasses, expectedClassCount)
var resultXLClass = [AnyClass]()
for i in 0 ..< actualClassCount {
let currentClass: AnyClass = allClasses[Int(i)]
if (class_getInstanceMethod(currentClass, NSSelectorFromString("methodSignatureForSelector:")) != nil),
(class_getInstanceMethod(currentClass, NSSelectorFromString("doesNotRecognizeSelector:")) != nil),
let cls = currentClass as? TheRouterServiceProtocol.Type {
print(currentClass)
resultXLClass.append(cls)
TheRouterServiceManager.default.registerService(named: cls.seriverName, lazyCreator: (cls as! NSObject.Type).init())
}
}
}
路由调用本地服务
如何声明和实现服务
@objc
public protocol AppConfigServiceProtocol: TheRouterServiceProtocol {
// 打开小程序
func openMiniProgram(info: [String: Any])
}
final class ConfigModuleService: NSObject, AppConfigServiceProtocol {
static var seriverName: String {
String(describing: AppConfigServiceProtocol.self)
}
func openMiniProgram(info: [String : Any]) {
if let window = UIApplication.shared.delegate?.window {
window?.makeToast("打开微信小程序", duration: 1, position: window?.center)
}
}
}
调用服务
if let appConfigService = TheRouter.fetchService(AppConfigServiceProtocol.self){
appConfigService.openMiniProgram(info: [:])
}
路由远程端调用本地服务:服务接口交付,MQTT,JSBridge
let dict = ["ivar1": ["key":"value"]]
let url = "scheme://services?protocol=AppConfigServiceProtocol&method=openMiniProgramWithInfo:&resultType=0"
TheRouter.openURL((url, dict))
public class func routerService(_ uriTuple: (String, [String: Any])) -> Any? {
let request = TheRouterRequest.init(uriTuple.0)
let queries = request.queries
guard let protocols = queries["protocol"] as? String,
let methods = queries["method"] as? String else {
assert(queries["protocol"] != nil, "The protocol name is empty")
assert(queries["method"] != nil, "The method name is empty")
shareInstance.logcat?(uriTuple.0, .logError, "protocol or method is empty,Unable to initiate service")
return nil
}
// 为了使用方便,针对1个参数或2个参数,依旧可以按照ivar1,ivar2进行传递,自动匹配。对于没有ivar1参数的,但是方法中必须有参数的,将queries赋值作为ivar1。
shareInstance.logcat?(uriTuple.0, .logNormal, "")
if let functionResultType = uriTuple.1[TheRouterFunctionResultKey] as? Int {
if functionResultType == TheRouterFunctionResultType.voidType.rawValue {
self.performTargetVoidType(protocolName: protocols,
actionName: methods,
param: uriTuple.1[TheRouterIvar1Key],
otherParam: uriTuple.1[TheRouterIvar2Key])
return nil
} else if functionResultType == TheRouterFunctionResultType.valueType.rawValue {
let exectueResult = self.performTarget(protocolName: protocols,
actionName: methods,
param: uriTuple.1[TheRouterIvar1Key],
otherParam: uriTuple.1[TheRouterIvar2Key])
return exectueResult?.takeUnretainedValue()
} else if functionResultType == TheRouterFunctionResultType.referenceType.rawValue {
let exectueResult = self.performTarget(protocolName: protocols,
actionName: methods,
param: uriTuple.1[TheRouterIvar1Key],
otherParam: uriTuple.1[TheRouterIvar2Key])
return exectueResult?.takeRetainedValue()
}
}
return nil
}
您是否考虑Swift5.9宏?
从当前实现架构来看,懒加载结合动态注册已解决了注册过程中的性能问题。即使需要遍历整个工程类和执行相关逻辑,也不会超过0.2秒。能够通过Class获取路径的原因是因为类中声明了静态变量。
/// 实现TheRouterable协议
extension TheRouterController: TheRouterable {
static var patternString: [String] {
["scheme://router/demo"]
}
static func registerAction(info: [String : Any]) -> Any {
debugPrint(info)
let vc = TheRouterController()
vc.qrResultCallBack = info["clouse"] as? QrScanResultCallBack
vc.resultLabel.text = info.description
return vc
}
}
作者
许可协议
TheRouterSwift在Apache2.0许可下可用。有关更多信息,请参阅LICENSE文件。