布景
- 跟着社区对支撑Swift的需求日益增多,Swift5.0二进制库也具有更好的稳定性和兼容性体现,货拉拉技能团队依据社区反应及内部讨论,决定开源内部事务运用的Swift版别路由组件,与2023年8月份已发布的Objective-C版别路由组件组成一个完整处理方案。
- TheRouter开源团队将把重心放在保护和晋级Swift版别的TheRouter上,一起也会继续支撑Objective-C版别的TheRouter,并欢迎社区贡献。
- 关于运用Objective-C版别TheRouter的用户,建议将版别固定为1.0.0版以确保稳定性。
Features
TheRouterSwift是货拉拉TheRouter系列开源结构的Swift版别,为日益增多的Swift开发者供给一高可用路由结构。TheRouterSwift用于模块间解耦和通信,基于Swift协议进行动态懒加载注册路由与翻开路由的东西。一起支撑经过Service-Protocol寻觅对应的模块,并用 protocol进行依靠注入和模块通信。 Github传送门>>
首要功用如下:
- 页面导航跳转才干:支撑常规vc或Storyboard的push/present/popToTaget/windowNavRoot/modalDismissBeforePush跳转才干;
- 路由主动注册才干:懒加载方法动态注册路由,仅当第一次调用OpenURL时进行动态注册;
- 路由映射文件导出:支撑将工程中的路由映射联系导出为文档,支撑JSON、Plist格局,便当开发者进行双端的汇总比对、记载等;
- 服务主动注册才干:动态注册服务,运用runtime方法主动注入;
- 动态化才干:支撑添加重定向,移除重定向、动态添加路由、动态移除路由、阻拦器、过错path修正等;
- 链式编程:支撑链式编程方法拼接URL与参数;
- 适配Objective-C:OC类能够在Swift中运用承继的方法遵从协议来进行动态注册;
- 服务调用:支撑本地服务调用与远端服务调用;
具体功用如下:
功用描述 | 事例代码及注释 |
---|---|
懒加载路由 | lazyRegisterRouterHandle 仅当第一次调用OpenURL时进行动态注册 |
发挥Swift特性,面向协议编程 | TheRouterServiceProtocol TheRouterableProtocol |
动态注册,无需手动注册 | TheRouterManager.addGloableRouter([“.LA”], url, userInfo) |
支撑依靠注入与服务的主动注册 | TheRouter.registerServices() TheRouterServiceManager.registerService(serviceName) |
服务的动态注册与协议方法调用 | TheRouter.fetchService(AppConfigServiceProtocol.self) |
支撑路由映射文件导出 | TheRouter.writeRouterMapToFile |
支撑重定向,移除重定向、动态添加路由、动态移除路由,过错path修正 | TheRouter.addRelocationHandle |
支撑阻拦器 | TheRouter.addRouterInterceptor |
支撑Swift项目中调用OC路由 | OC类能够在Swift中运用承继的方法遵从协议来进行动态注册 |
支撑大局失利监控 | TheRouter.addGlobalMatchFailedHandel |
支撑路由与服务日志回调 | TheRouter.routerLogHandle(_ url: String, _ logType: TheRouterLogType, _ errorMsg: String) |
支撑路由注册期的安全查看 | TheRouterManager.routerForceRecheck() 客户端强制校验,是否匹配,不匹配触发断语 |
支撑后端对客户端服务的调用 | TheRouter.openURL() 服务接口下发,MQTT,JSBridge |
支撑链式调用 | TheRouterBuilder.build(“scheme://router/demo”).withInt(key: “intValue”, value: 2).navigation() |
支撑链式调用翻开路由回调闭包 | TheRouterBuilder.build(“scheme://router/demo”).withInt(key: “intValue”, value: 2).navigation(_ handler: complateHandler = nil) |
支撑非链式调用翻开路由回调闭包 | TheRouter.openURL(“therouter.cn/” ) { param, instance in } |
布景
跟着项目需求的日益添加,开发人员的不断添加,带来了很多问题:
- 模块区分不明晰,任何开发人员随意调用并修正其他模块的代码完成以满意自己的事务需求。
- 保护困难,同一组件的不同服务,散落在工程各个地方,不利于一致保护修正替换。
- 模块负责人无法明晰,导致同一功用多人保护,形成抵触。
另外件拆分完之后都上升到远端,那么它们之间本地的代码是没办法相互依靠的,所以就需求经过一种东西,然后去完成透传服务的才干。咱们需求一个中间件去处理这些问题。路由便是将耦合进行转移,经过添加中间层映射联系,处理事务之间的依靠联系。
一个老练的 路由 该是什么样子
- 事务组件化之后,组件化需求将整个项目的各个模块进行解耦,晋级远端之后,界面之间的跳转怎样处理?路由Api
- 动态注册路由,无需手动注册。服务的动态注册,无需手动注册。
- 端上跳转一致问题怎样处理?运用一致 URL 映射方法处理
- 事务跳转中出现问题,怎样修正跳转逻辑?服务怎样降级? 远端下发装备,修正跳转 URL
- 事务服务反常,界面改为 h5 界面。重定向
- App 跳转出现问题怎样跳转到同一个本地的 error 界面?一致失利处理
- 怎样在跳转前添加强制的事务逻辑处理,比如事务调整,必须先履行某些操作,才干进入。重定向
- 事务中有很多需求前置跳转,比如先登录才干去订单列表,怎样完成。阻拦器
- 怎样测试各个跳转事务是否正常。 路由 Path 校验
- 怎样把最频频的事务跳转前置,削减查询次数?添加优先级 priority
- 本地服务经过路由调用,远端服务经过路由调用 支撑服务调用
全体规划思路
为了和Android端保持一致,运用了URL,class注册的方法完成。经过URL匹配方法查询数组中保存的模版信息,找到履行获取对应实例,履行跳转操作。
运用介绍预览
怎样集成运用
CocoaPods
Add the following entry in your Podfile:
ruby
pod 'TheRouter', '1.1.0'
Swift限制版别
ruby
Swift5.0 or above
TheRouterSwift 运用方法
注册
鉴于现已完成了主动注册才干,开发者无需自己添加路由,只需求进行如下操作即可
/// 完成TheRouterable协议
extension TheRouterController: TheRouterable {
static var patternString: [String] {
["scheme://router/demo"]
}
static func registerAction(info: [String : Any]) -> Any {
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 动态注册路由该怎样处理呢,咱们运用 runtime 遍历工程里的方法找到遵从了路由协议的类进行主动注册。
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](), handler: complateHandler = nil) -> 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]), handler: complateHandler = nil) -> Any? {
if !shareInstance.isLoaded {
return shareInstance.lazyRegisterHandleBlock?(uriTuple.0, uriTuple.1)
} else {
return openCacheRouter(uriTuple)
}
}
public class func openCacheRouter(_ uriTuple: (String, [String: Any]), handler: complateHandler = nil) -> 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", desc: "简略注册,直接调用TheRouter", params: ["key1": 1]))
// 字典形式
TheRouter.addRouterItem(["scheme://router/demo?&desc=简略注册,直接调用TheRouter.addRouterItem()注册即可": "TheRouterSwift_Example.TheRouterController"])
// 常量参数形式
TheRouter.addRouterItem("scheme://router/demo?&desc=简略注册", classString: "TheRouterSwift_Example.TheRouterController")
// 协议形式, TheRouterApi完成了 CustomRouterInfo协议
TheRouter.addRouterItem(TheRouterApi.patternString, classString: TheRouterApi.routerClass)
一起支撑手动批量注册
TheRouter.addRouterItem(["scheme://router/demo": "TheRouterSwift_Example.TheRouterController", "scheme://router/demo1": "TheRouterSwift_Example.TheRouterControllerA"])
移除
TheRouter.removeRouter(TheRouterViewCApi.patternString)
翻开
声明晰不同的方法,首要用于明显的区分,内部一致调用 openURL
便当结构器链式翻开路由
let model = TheRouterModel.init(name: "AKyS", age: 18)
TheRouterBuilder.build("scheme://router/demo")
.withInt(key: "intValue", value: 2)
.withString(key: "stringValue", value: "2222")
.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=直接调用TheRouter.addRouterItem()注册即可,支撑单个注册,批量注册,动态注册,懒加载动态注册", ["descs": "追加参数"]))
参数传递方法
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]))
大局失利映射
TheRouter.globalOpenFailedHandler { info in
debugPrint(info)
}
阻拦
比如在未登录状况下一致阻拦:跳转音讯列表之前先去登录,登录成功之后跳转到音讯列表等。
let login = TheRouterLoginApi.templateString
TheRouter.addRouterInterceptor([login], priority: 0) { (info) -> Bool in
if LALoginManger.shared.isLogin {
return true
} else {
TheRouter.openURL(TheRouterLoginApi().build)
return false
}
}
登录成功之后删去阻拦器即可。
路由 Path 与类正确安全校验
// 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是否匹配")
}
踩坑 路由 注册-KVO
在进行 classes 本地校验时遇到了类名不匹配问题。
排查原因: 是由于为了防止路由在发动时就注册,影响发动速度,选用了懒加载的方法即第一次翻开路由界面的时候才先进行注册然后跳转。但是在咱们动态注册之前,某个类由于添加了 KVO (Key-Value Observing 键值监听),这个类在遍历时 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: Decodable {
public init() {}
public var targetPath: String?
public var orginPath: String?
public var routerType: Int = 0 // 1: 表明替换或许修正客户端代码path过错 2: 新增路由path 3:删去路由 4: 重置路由
public var path: String? // 新的路由地址
public var className: String? // 路由地址对应的界面
public var params: [String: Any]?
enum CodingKeys: String, CodingKey {
case targetPath
case orginPath
case path
case className
case routerType
case params
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
targetPath = try container.decodeIfPresent(String.self, forKey: CodingKeys.targetPath)
orginPath = try container.decodeIfPresent(String.self, forKey: CodingKeys.orginPath)
routerType = try container.decode(Int.self, forKey: CodingKeys.routerType)
path = try container.decodeIfPresent(String.self, forKey: CodingKeys.path)
className = try container.decodeIfPresent(String.self, forKey: CodingKeys.className)
params = try container.decodeIfPresent(Dictionary<String, Any>.self, forKey: CodingKeys.params)
}
}
经过远端下发重定向数据,原本跳转到白色界面的事务逻辑改为跳转到黄色界面
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=跳转白色界面被重定向到了黄色界面之后,依据下发数据又康复到跳转白色界面")
路由 Path 动态修正
在实践开发中,开发人员由于大意写错了路由 Path,上线之后无法进行正常的事务跳转,此时就需求经过远端下发路由进行匹配跳转了。scheme://router/demo3 是正确 path,但是本地写错的路由 path 为 scheme://router/demo33,那么需求新增一个 path 进行映射。
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-Path
在实践开发中,一旦运用 URI 这种方法,牵扯到双端,就能够存在双端不一致的问题,那么怎样处理呢,能够经过本地新增多路由 path 处理,也能够经过远端下发新路由处理。
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
}
}
let relocationMap = ["routerType": 2, "className": "TheRouterSwift_Example.TheRouterControllerD", "path": "scheme://router/demo5"] as NSDictionary
TheRouterManager.addRelocationHandle(routerMapList: [relocationMap])
TheRouter.openURL("scheme://router/demo2Android?desc=demo5是Android一个界面的path,为了双端一致,咱们动态添加一个path,这样远端下发时demo5也就能跳转了")
服务的动态注册与调用
怎样声明服务及完成服务
@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: [:])
}
服务运用了runtime动态注册,所以你不必担心服务没有注册的问题。只需像上述事例一样运用即可。
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())
}
}
}
路由 远端调用本地服务:服务接口下发,MQTT,JSBridge
let dict = ["ivar1": ["key":"value"]]
let url = "scheme://services?protocol=AppConfigLAServiceProtocol&method=openMiniProgramWithInfo:&resultType=0"
TheRouter.openURL((url, dict))
是否考虑Swift5.9 Macros?
从现在的完成方法来看,懒加载加上动态注册,现已处理了注册时的功能问题。即便需求遍历全工程的类,然后处理相关逻辑,也不会超越0.2s。之所以能够经过Class获得path,由于给类声明晰静态变量。
/// 完成TheRouterable协议
extension TheRouterController: TheRouterable {
static var patternString: [String] {
["scheme://router/demo"]
}
static func registerAction(info: [String : Any]) -> Any {
let vc = TheRouterController()
vc.qrResultCallBack = info["clouse"] as? QrScanResultCallBack
vc.resultLabel.text = info.description
return vc
}
}
关于作者
货拉拉移动端技能团队
开源协议
TheRouterSwift 选用Apache2.0协议,概况参考LICENSE
反应交流群