LARouter-Swift一个用于模块间解耦和通讯,根据Swift协议进行动态懒加载注册路由与翻开路由的东西。一起支撑经过Service-Protocol寻觅对应的模块,并用 protocol进行依靠注入和模块通讯。

Features

LARouter-Swift一个用于模块间解耦和通讯,根据Swift协议进行动态懒加载注册路由与翻开路由的东西。一起支撑经过Service-Protocol寻觅对应的模块,并用 protocol进行依靠注入和模块通讯。

  • 页面导航跳转才干:支撑常规vc或Storyboard的push/present/popToTaget/windowNavRoot/modalDismissBeforePush跳转才干;
  • 主动注册才干:懒加载办法动态注册路由,仅当第一次调用OpenURL时进行动态注册;
  • 硬编码消除:将注册的path转为静态字符串常量供事务运用;
  • 动态化才干:支撑添加重定向,移除重定向、动态添加路由、动态移除路由、拦截器、过错path修复等;
  • 链式编程:支撑链式编程办法拼接URL与参数;
  • 适配Objective-C:OC类能够在Swift中运用承继的办法遵从协议来进行动态注册;
  • 服务调用:支撑本地服务调用与远端服务调用;

背景

随着项目需求的日益添加,开发人员的不断添加,带来了许多问题:

  • 模块划分不明晰,任何开发人员随意调用并修正其他模块的代码完成以满足自己的事务需求。

  • 保护困难,同一组件的不同服务,散落在工程各个地方,不利于一致保护修正替换。

  • 模块负责人无法明晰,导致同一功用多人保护,造成抵触。

Swift路由-LARouter

别的件拆分完之后都上升到远端,那么它们之间本地的代码是没办法相互依靠的,所以就需求经过一种东西,然后去完成透传服务的才干。咱们需求一个中间件去处理这些问题。路由便是将耦合进行转移,经过添加中间层映射关系,处理事务之间的依靠关系。

一个成熟的路由该是什么样子

  1. 事务组件化之后,组件化需求将整个项目的各个模块进行解耦,升级远端之后,界面之间的跳转怎样处理?路由Api

  2. 动态注册路由,无需手动注册。

  3. 端上跳转一致问题怎样处理?运用一致URL映射办法处理

  4. 事务跳转中出现问题,怎样修正跳转逻辑?服务怎样降级? 远端下发装备,修正跳转URL

  5. 事务服务异常,界面改为h5界面。重定向

  6. App跳转出现问题怎样跳转到同一个本地的error界面?一致失利处理

  7. 怎样在跳转前添加强制的事务逻辑处理,比方事务调整,必须先履行某些操作,才干进入。重定向

  8. 事务中有许多需求前置跳转,比方先登录才干去订单列表,怎样完成。拦截器

  9. 怎样测验各个跳转事务是否正常。 路由Path校验

  10. 路由在Swift项目中怎样运用注解装备?怎样动态映射? 无解

  11. 怎样把最频频的事务跳转前置,削减查询次数?添加优先级priority

运用介绍预览

Swift路由-LARouter

怎样集成运用

CocoaPods

Add the following entry in your Podfile:

   pod 'LARouterKit', '0.1.0'

Swift限制版本

 Swift5.0 or above

LARouter 运用办法

  1. 注册

鉴于已经完成了主动注册才干,开发者无需自己添加路由,只需求进行如下操作即可

/// 完成LARouterable协议
extension LARouterController: LARouterable {
    static var patternString: [String] {
        ["scheme://router/demo"]
    }
    static func registerAction(info: [String : Any]) -> Any {
        debugPrint(info)
        let vc =  LARouterController()
        vc.qrResultCallBack = info["clouse"] as? QrScanResultCallBack
        vc.resultLabel.text = info.description
        return vc
    }
}
/// 在AppDelegate中完成懒加载的闭包
LARouter.lazyRegisterRouterHandle { url, userInfo in
   // .LA 是Swift类名的前缀,比方 LARouterKit_Example.LARouterControllerC
   return LARouterManager.addGloableRouter([".LA"], url, userInfo)
}
/// 如果要跳转Web或者进行服务的远端调用,添加装备信息
LARouter.injectRouterServiceConfig(webRouterUrl, serivceHost)

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? LARouterable.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)"
                        LARouter.addRouter(cls.patternString[s], classString: "\(subString)")
                    } else {
                        pagePathMap[cls.patternString[s]] = fullName
                        LARouter.addRouter(cls.patternString[s], classString: fullName)
                    }
                }
            }
        }
#if DEBUG
        debugPrint(pagePathMap)
        routerForceRecheck()
#endif
        LARouter.routerLoadStatus(true)
        return LARouter.openURL(urlPath, userInfo: userInfo)
}

为了避免无效遍历,咱们经过传入 registerClassPrifxArray 指定咱们遍历包含这些前缀的类即可。一旦是 UIViewController.Type 类型就进行存储,然后再进行校验是否遵从 LARouterable 协议,遵从则主动注册。无需手动注册。

路由注册的懒加载

采用动态注册有一个不好的情况就是在启动时就去动态注册,在 LARouter 中注册的时机被延后了,放在了 App 第一次经过 LARouter.openUrl()时进行注册,会判断当前 map 中是否已经存在路由,不存在即注册路由,然后翻开路由。

@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 类,并完成 LARouterAble 协议即可

@interface LARouterBController : UIViewController
@property (nonatomic, strong) UILabel *desLabel;
@end
@interface LARouterBController ()
@end
@implementation LARouterBController
- (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 LARouterControllerB: LARouterBController, LARouterable {
    public static var patternString: [String] {
        ["scheme://router/demo2",
         "scheme://router/demo2-Android"]
    }
    public static func registerAction(info: [String : Any]) -> Any {
        let vc =  LARouterBController()
        vc.desLabel.text = info.description
        return vc
    }
}

一起支撑手动单个注册


LARouter.addRouter(["scheme://router/demo?&desc=简略注册,直接调用LARouter.addRouter()注册即可": "LARouterKit_Example.LARouterController"])
LARouter.addRouter("scheme://router/demo?&desc=简略注册", classString: "LARouterKit_Example.LARouterController")
LARouter.addRouter(LARouterApi.patternString, classString: LARouterApi.routerClass)
LARouter.addRouter(LARouterAApi.patternString, classString: LARouterAApi.routerClass)

一起支撑手动批量注册

LARouter.addRouter(["scheme://router/demo": "LARouterKit_Example.LARouterController",
                    "scheme://router/demo1": "LARouterKit_Example.LARouterControllerA"])
  1. 移除

LARouter.removeRouter(LARouterViewCApi.patternString)
  1. 翻开

声明了不同的办法,首要用于明显的区别,内部一致调用 openURL

便当结构器链式翻开路由

let model = LARouterModel.init(name: "AKyS", age: 18)
LARouterBuilder()
    .buildPath(path: "scheme://router/demo?dynamic=3&desc=链式编程结构器模式LARouterBuilder().buildPath.buildInt.buildString.buildFloat.buildBool.buildDouble.buildAny")
    .buildInt(key: "intValue", value: 2)
    .buildString(key: "stringValue", value: "AKyS")
    .buildFloat(key: "floatValue", value: 3.1415)
    .buildBool(key: "boolValue", value: false)
    .buildDouble(key: "doubleValue", value: 2.0)
    .buildAny(key: "any", value: model)
    .navigation()

翻开路由常用办法

public class LARouterApi: CustomRouterInfo {
    public static var patternString = "scheme://router/demo"
    public static var routerClass = "LARouterKit_Example.LARouterController"
    public var params: [String: Any] { return [:] }
    public var jumpType: LAJumpType = .push
    public init() {}
}
public class LARouterAApi: CustomRouterInfo {
    public static var patternString = "scheme://router/demo1"
    public static var routerClass = "LARouterKit_Example.LARouterControllerA"
    public var params: [String: Any] { return [:] }
    public var jumpType: LAJumpType = .push
    public init() {}
}
LARouter.openURL(LARouterCApi.init().requiredURL)
LARouter.openWebURL("https://xxxxxxxx")

@discardableResult
public class func openWebURL(_ uriTuple: (String, [String: Any])) -> Any? {
    return LARouter.openURL(uriTuple)
}
@discardableResult
public class func openWebURL(_ urlString: String,
                             userInfo: [String: Any] = [String: Any]()) -> Any? {
    LARouter.openURL((urlString, userInfo))
}

元祖办法传入路由与追加参数

LARouter.openURL(("scheme://router/demo1?id=2&value=3&name=AKyS&desc=直接调用LARouter.addRouter()注册即可,支撑单个注册,批量注册,动态注册,懒加载动态注册", ["descs": "追加参数"]))

参数传递办法

let clouse = { (qrResult: String, qrStatus: Bool) in
    print("\(qrResult) \(qrStatus)")
    self.view.makeToast("\(qrResult) \(qrStatus)")
}
let model = LARouterModel.init(name: "AKyS", age: 18)
LARouter.openURL(("scheme://router/demo?id=2&value=3&name=AKyS", ["model": model, "clouse": clouse]))

4.大局失利映射

LARouter.registerGlobalMatchFailedHandel { info in
   Dlog(info)
}

5.拦截

比方在未登录情况下一致拦截:跳转音讯列表之前先去登录,登录成功之后跳转到音讯列表等。

let login = LARouterLoginApi.templateString
 LARouter.registerInterceptor([login], priority: 0) { (info) -> Bool in
       if LALoginManger.shared.isLogin {
             return true
       } else {
             LARouter.openURL(LARouterLoginApi().build)
             return false
       }
 }

登录成功之后删去拦截器即可。

6. 路由 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是否匹配")
}

7.踩坑路由注册-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)"
    LARouter.addRouter(cls.patternString[s], classString: "\(subString)")
}

动态调用路由

在之上的路由才干下,咱们期望 App 能够支撑动态添加路由,删去路由,重定向路由、经过路由调起本地服务、远端经过路由调起 App 服务才干,随即进行了动态化的扩展。

  1. 重定向功用

定义路由下发模型数据结构

public struct LARouterInfo: HandyJSON {
    public init() {}
    public var targetPath: String = ""
    public var orginPath: String = ""
    // 1: 表示替换或者修复客户端代码path过错 2: 新增路由path 3:删去路由
    public var routerType: LARouterReloadMapEnum = .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
LARouterManager.addRelocationHandle(routerMapList: [relocationMap])
LARouter.openURL("scheme://router/demo?desc=跳转白色界面被重定向到了黄色界面")
  1. 重定向康复

在事务中,通常会进行事务调整,那么重定向之后需求康复的话,就需求移除重定向

let relocationMap = ["routerType": 4, "targetPath": "scheme://router/demo", "orginPath": "scheme://router/demo"] as NSDictionary
LARouterManager.addRelocationHandle(routerMapList: [relocationMap])
LARouter.openURL("scheme://router/demo?desc=跳转白色界面被重定向到了黄色界面之后,根据下发数据又康复到跳转白色界面")
  1. 路由 Path 动态修复

在实际开发中,开发人员由于马虎写错了路由 Path,上线之后无法进行正常的事务跳转,此时就需求经过远端下发路由进行匹配跳转了。scheme://router/demo3 是正确 path,可是本地写错的路由 path 为 scheme://router/demo33,那么需求新增一个 path 进行映射。

let relocationMap = ["routerType": 2, "className": "LARouterKit_Example.LARouterControllerC", "path": "scheme://router/demo33"] as NSDictionary
LARouterManager.addRelocationHandle(routerMapList: [relocationMap])
let value = LARouterCApi.init().requiredURL
LARouter.openURL(value)

4.路由适配不同的 Android-Path

在实际开发中,一旦运用 URI 这种办法,牵扯到双端,就能够存在双端不一致的问题,那么怎样处理呢,能够经过本地新增多路由 path 处理,也能够经过远端下发新路由处理。

public class LARouterControllerB: LARouterBController, LARouterable {
    public static var patternString: [String] {
        ["scheme://router/demo2",
         "scheme://router/demo2Android"]
    }
    public static func registerAction(info: [String : Any]) -> Any {
        let vc =  LARouterBController()
        vc.desLabel.text = info.description
        return vc
    }
}
let relocationMap = ["routerType": 2, "className": "LARouterKit_Example.LARouterControllerD", "path": "scheme://router/demo5"] as NSDictionary
LARouterManager.addRelocationHandle(routerMapList: [relocationMap])
LARouter.openURL("scheme://router/demo2Android?desc=demo5是Android一个界面的path,为了双端一致,咱们动态添加一个path,这样远端下发时demo5也就能跳转了")

5.路由调用本地服务

例如需求经过路由翻开小程序

 if let appConfigService = LARouter.fetchService(AppConfigLAServiceProtocol.self){
     appConfigService.openMiniProgram(info: [:])
}

6.路由远端调用本地服务:服务接口下发,MQTT,JSBridge

let dict = ["ivar1": ["key":"value"]]
let url = "scheme://services?protocol=AppConfigLAServiceProtocol&method=openMiniProgramWithInfo:&resultType=0"
LARouter.openURL((url, dict))