Skip to content
/ FJRouter Public

正则、参数解析匹配、路由(重定向、回调、子路由)、资源中心、事件总线

License

Notifications You must be signed in to change notification settings

zgjff/FJRouter

Repository files navigation

FJRouter

简介

理解中的路由应该包含路由页面跳转管理、通过url获取对应位置的资源

框架包含三大模块:

基础设施: url的参数正则解析

通过提前注册url pattern规则/path/:{路径参数}, 通过正则表达式解析并匹配其中的所有参数。eg:

  • 单个参数: 注册路径/users/:userId, 解析出此url路径的参数数组为[userId], 可以匹配/users/..., 如/users/123, 可以匹配成功并解析出userId123;

  • 多个参数: 注册路径/city/school/:sname/class/:cid, 解析出此url路径的参数数组为[sname, cid], 可以匹配/city/school/xxx/class/xxx, 如/city/school/清华大学/class/123, 可以匹配成功并解析出参数为["sname": "清华大学", "cid": "123"];

  • 无参数: 注册路径/settings/detail, 可以匹配/settings/detail,以及携带查询参数的url, 如/settings/detail?q=a/settings/detail?p=1&q=2...

路由页面跳转管理

页面跳转FJRouterJumpable协议

1: 路由页面跳转是定义为FJRouterJumpable的协议, 可以通过FJRouter.jump()获取框架内跳转管理中心对象.

2: 支持通过路由路径和已经注册的路由名称进行对应操作

当然在这里, 建议通过路由名称相关的api进行操作, 如goNamed, viewController(name...)方法; why:

当路由路径比较复杂,且含有参数的时候, 如果通过硬编码的方法直接手写路径, 可能会造成拼写错误,参数位置错误等错误

在实际app中, 路由的URL格式可能会随着时间而改变, 但是一般路由名称不会去更改

通过路由路径获取对应的控制器: 
let vc = try await FJRouter.jump().viewController(byLocation: "/five", extra: nil)

通过路由名称获取对应的控制器:
let fvc = try await FJRouter.jump().viewController(byName: "five", params: [:], queryParams: [:], extra: nil)

通过路由路径导航至对应控制器:
try FJRouter.jump().go(location: "/second", extra: nil, from: self, ignoreError: true)

通过路由名称导航至对应控制器:
try FJRouter.jump().goNamed("second", params: [:], queryParams: [:], extra: nil, from: self, ignoreError: true)

3: 支持路由回调

路由回调是使用Combine框架实现的

只支持跳转的使用使用async异步, 且返回值是AnyPublisher<FJRouter.CallbackItem, FJRouter.MatchError>go方法

不需要提前注册回调方法, 只需要在收到Combine事件流中区分对应的事件

let callback = await FJRouter.jump().goNamed(FJRouter.GoByNameParams.init(name: "second"))
callback.sink(receiveCompletion: { cop in
    print("cop----全部", cop)
}, receiveValue: { item in
    print("value----全部", item)
}).store(in: &cancels)


触发:需要viewController方调用
try? dispatchFJRouterCallBack(name: "haha", value: ())
dismiss(animated: true, completion: { [weak self] in
    try? self?.dispatchFJRouterCallBack(name: "completion", value: 123)
})

路由模型model:FJRoute

每一个路由都通过FJRoute对象进行配置.

路由匹配路径path

1: 如果是起始父路由, 其path必须以/为前缀

2: 支持路径参数, 路由参数将被解析并储存在JJRouterState中, 用于builderredirect

路由的名称: name

如果赋值, 必须提供唯一的字符串名称, 且不能为空

构建路由方式: builder

1: 此参数是block形式的类型别名

public typealias Builder = (@MainActor @Sendable (_ info: BuilderInfo) -> UIViewController)

2: 此参数可以为nil, 但是为nil时, 重定向参数redirect不能为nil

显示路由控制器的方式: animator

1: 此参数是block形式的类型别名

public typealias Animator = (@MainActor @Sendable (_ info: AnimatorInfo) -> any FJRouteAnimator)

2: 用于在使用gogoNamed方法进行跳转时, 页面的展现方式。

3: 可以使用框架已内置的动画方式对象, 或者可以使用自己实现的协议对象, 具体参考FJRouteAnimator

4: 没有pushpresent的概念; 所有的动画细节均隐藏在协议对象里:

FJRoute.SystemPushAnimator对应push

FJRoute.SystemPresentAnimator对应present

路由拦截器: redirect

1: 有些路由地址需要拦截器,例如对于没有登录的用户,有些页面就无法访问.eg:

let route = try FJRoute(path: "/", name: "root", builder: nil, redirect: FJRouteCommonRedirector(redirect: { state in
    switch AppUser.shared.hasLogin {
        case true:
            if let app = await UIApplication.shared.versionkKeyWindow?.rootViewController, app is AppTabBarViewController {
                return nil // 返回nil代表不需要进行重定向
            }
            return "/app"
        case false:
            if let app = await UIApplication.shared.versionkKeyWindow?.rootViewController, !(app is AppTabBarViewController) {
                return nil // 返回nil代表不需要进行重定向
            }
            return try? await FJRouter.shared.convertLocationBy(name: "loginAccount")
    }
}))

2: 此参数可以为nil, 但是为nil时, 构建控制器参数builder不能为nil

关联的子路由: routes

1: 所谓子路由就是: 一个大的路由页面下面的多个相关联的路由。如设置页面下的设置项a,b,c,d...

2: 子路由还一个好处是, 可以减少拼写相同的path路径。如

设置项:FJRoute(path: "/user/settings", name: "user_settings", xxx)
设置项a:FJRoute(path: "/user/settings/a", name: "user_settings_a", xxx)
设置项b:FJRoute(path: "/user/settings/b", name: "user_settings_a", xxx)
设置项c:FJRoute(path: "/user/settings/c", name: "user_settings_c", xxx)
...

上述所有路由可以总结为一个设置项路由:
try FJRoute(path: "/user/settings", name: "user_settings", xxx, routes: [
    FJRoute(path: "a", name: "user_settings_a", xxx),
    FJRoute(path: "b", name: "user_settings_b", xxx),
    FJRoute(path: "c", name: "user_settings_c", xxx),
    ...
])

3: 只路由也支持此当前路由的参数

4: 强烈建议子路由的path不要以/为开头

let route = try FJRoute(path: "/play/:id", builder: ({ _  in ViewControllerPlay() }), routes: [
    FJRoute(path: "feature1", builder: ({ _  in ViewControllerPlay1() })),
    FJRoute(path: "feature2", name: "bfeature2", builder: ({ _ in ViewControllerPlay2() })),
    FJRoute(path: "feature3/:name", builder: ({ _  in ViewControllerPlay3() })),
    FJRoute(path: "feature3", name: "bfeature3", builder: ({ _  in ViewControllerPlay4() })),
    FJRoute(path: "feature3", name: "bfeature3-1", builder: ({ _  in ViewControllerPlay5() })),
    FJRoute(path: "feature4/:name", name: "feature4", builder: ({ _  in ViewControllerPlay6() })),
])

路由显示动画协议: FJRouteAnimator

1: 此协议只有一个方法

/// 开始路由转场动画
/// - Parameters:
///   - fromVC: 要跳转到的源控制器
///   - toVC: 匹配到的路由指向的控制器
///   - matchState: 匹配到的路由信息
@MainActor func startAnimatedTransitioning(from fromVC: UIViewController?, to toVC: UIViewController, state matchState: FJRouterState)

2: 内置动画显示方式:

FJRoute.xxxxAnimator为开头的实例对象

  • 设置app window的rootViewController: FJRoute.AppRootControllerAnimator

  • 系统present动画进行显示: FJRoute.SystemPresentAnimator

  • 系统push进行显示: FJRoute.SystemPushAnimator

  • 根据情况自动选择动画方式: FJRoute.AutomaticAnimator

  • 自定义custompresent转场动画: FJRoute.CustomPresentationAnimator

  • 自定义转场动画进行push: FJRoute.CustomPushAnimator

  • 自定义fullScreenpresent转场动画: FJRoute.FullScreenPresentAnimator

  • 系统push/pop动画风格的present/dismiss转场动画, 支持侧滑dismiss: FJRoute.PresentSameAsPushAnimator

  • 刷新与匹配控制器相同类型的上一个控制器动画: FJRoute.RefreshSamePreviousAnimator

      try await FJRouter.shared.registerRoute(FJRoute(path: "/four", name: "four", builder: { sourceController, state in
          FourViewController()
      }, animator: { info in
          if let pvc = info.fromVC, pvc is FourViewController { // 或者其它判断条件
              return FJRoute.RefreshSamePreviousAnimator { @Sendable previousVC, state in
                  previousVC.view.backgroundColor = .random()
                  previousVC.updatexxxx()
              }
          }
          return FJRoute.SystemPushAnimator()
      }))

路由重定向协议: FJRouteRedirector

1: 此协议只有一个方法: 根据匹配状态进行判断返回对应路由的url路径

/// 指向需要重定向的路由路径。返回`nil`, 则代表不需要重定向
func redirectRoute(state: FJRouterState) async throws -> String?

2: app已经内置了一个通用的重定向: FJRouteCommonRedirector

3: 路由循环重定向, 框架内部在进行路由匹配的时候, 如果检测到巡航重定向, 则会抛出错误。如:

a->b->c->d->a

4: 最大重定向次数: 框架内部在进行单个路由匹配的时候, 会记录重定向次数, 如果次数大于设定的值, 会抛出错误。默认最大重定向次数为5, 可以通过如下代码进行设置修改:

await FJRouter.jump().setRedirectLimit(50)

注册、跳转事例代码:

注册
static func register() async throws {
    try await FJRouter.jump().registerRoute(FJRoute(path: "/", name: "root", builder: { @MainActor @Sendable _ in ViewController()}, animator: { info in
            FJRoute.AutomaticAnimator(navigationController: UINavigationController())
        }))
        
    try await FJRouter.jump().registerRoute(FJRoute(path: "/first", name: "first", builder: { _ in FViewController() }, animator: { _ in FJRoute.SystemPushAnimator() }))
        
    try await FJRouter.jump().registerRoute(FJRoute(path: "/second", name: "second", builder: { _ in SViewController() }, animator: { _ in
            FJRoute.CustomPresentationAnimator(navigationController: UINavigationController())
        }))
        
    try await FJRouter.jump().registerRoute(FJRoute(path: "/third", name: "third", builder: { _ in TViewController() }, animator: { _ in FJRoute.SystemPresentAnimator(fullScreen: true, navigationController: UINavigationController()) }))
        
    try await FJRouter.jump().registerRoute(FJRoute(path: "/four", name: "four", builder: { _ in FourViewController() }, animator: { info in
        if let pvc = info.fromVC, pvc is FourViewController {
            return FJRoute.RefreshSamePreviousAnimator { @Sendable previousVC, state in
                previousVC.view.backgroundColor = .random()
            }
        }
        return FJRoute.SystemPushAnimator()
    }))
        
    try await FJRouter.jump().registerRoute(FJRoute(path: "/five", name: "five", builder: { _ in FiveViewController() }, animator: { _ in FJRoute.  CustomPresentationAnimator(navigationController: UINavigationController()) { @Sendable ctx in
            ctx.usingBottomPresentation()
    }}))
        
    try await FJRouter.jump().registerRoute(FJRoute(path: "/six", name: "six", builder: { _ in SixViewController() }, animator: { info in
        return FJRoute.PresentSameAsPushAnimator(navigationController: UINavigationController())
    }))
        
    try await FJRouter.jump().registerRoute(FJRoute(path: "/seven", name: "seven", builder: { _ in SevenViewController() }, animator: { info in
        return FJRoute.PresentSameAsPushAnimator(navigationController: UINavigationController())
    }))
        
    try await FJRouter.jump().registerRoute(FJRoute(path: "/eight", name: "eight", builder: { _ in EightViewController() }, animator: { info in
        return FJRoute.AutomaticAnimator()
    }))
        
    try await FJRouter.jump().registerRoute(FJRoute(path: "/nine", name: "nine", builder: { _ in NineViewController() }, animator: { info in
        return FJRoute.SystemPushAnimator()
    })) 
}
跳转
try? FJRouter.jump().goNamed(FJRouter.GoByNameParams.init(name: "first"))

let callback = await FJRouter.jump().goNamed(FJRouter.GoByNameParams.init(name: "second"))
callback.sink(receiveCompletion: { cop in
    print("cop----全部", cop)
}, receiveValue: { item in
    print("value----全部", item)
}).store(in: &cancels)

try? FJRouter.jump().go(location: FJRouter.GoByLocationParams.init(location: "/six"))

资源管理中心

资源管理FJRouterResourceable协议:

1: 资源管理是定义为FJRouterResourceable的协议, 可以通过FJRouter.resource()获取框架资源管理中心对象.

2: 建议使用try FJRouterResource(path: "/xxx", name: "xxx", value: xxx), get(name: xxx)方法进行相关操作。

当资源路径比较复杂,且含有参数的时候, 如果通过硬编码的方法直接手写路径, 可能会造成拼写错误,参数位置错误等错误

在实际app中, 资源的URL格式可能会随着时间而改变, 但是一般资源名称不会去更改

存放资源:

func put(_ resource: FJRouterResource) async throws

1: 资源可以是int, string, enum, uiview, uiviewcontroller, protocol...

2: 资源必须是Sendable修饰的对象

3: 存放资源的时候可以携带参数

4: 适用于全局只会存放一次的资源: 如单例中或者application:didFinishLaunchingWithOptions中, 或者存放的资源内部具体的值是个固定值, 不会随着时间/操作更改

5: 如果每次存放资源可能会更改, 建议使用put(_ resource: FJRouterResource, uniquingPathWith: xxx)方法

6: 事例代码:

let r1 = try FJRouterResource(path: "/intvalue1", name: "intvalue1", value: { _ in 1 })
try await FJRouter.resource().put(r1)

let r3 = try FJRouterResource(path: "/intOptionalvalue2", name: "intOptionalvalue2") { @Sendable info -> Int? in
    return nil
}
try await FJRouter.resource().put(r3)

let r5 = try FJRouterResource(path: "/intOptionalvalue3/:optional", name: "intOptionalvalue3", value: { @Sendable info -> Int? in
    let isOptional = info.pathParameters["optional"] == "1"
    return isOptional ? nil : 1
})
try await FJRouter.resource().put(r5)

存放协议
let r6 = try FJRouterResource(path: "/protocolATest/:isA", name: "protocolATest", value: { @Sendable info -> ATestable in
    let isA = info.pathParameters["isA"] == "1"
    return isA ? AModel() : BModel()
})
try await FJRouter.resource().put(r6)

根据策略存放资源

func put(_ resource: FJRouterResource, uniquingPathWith combine: @Sendable (_ current: @escaping FJRouterResource.Value, _ new: @escaping FJRouterResource.Value) -> FJRouterResource.Value) async

1: 如果已经存放过相同path的资源, 不会抛出FJRouter.PutResourceError.exist错误, 会按照combine策略进行合并

2: 适用于可能多处/多处存放: 如某个viewController, 出现的时候才去存储资源, 但是因为viewController可能会多次进入, 而且每次存放的资源的具体值均不相同, 使用此方法可以有效的存储, 不会抛出FJRouter.PutResourceError.exist错误

3: 资源的名称会优先使用新的资源name, 如果新的资源name为nil, 才会使用旧资源name

4: 事例代码

// 使用旧值
await FJRouter.resource().put(r) { (currnet, _) in currnet }
// 使用新值
await FJRouter.resource().put(r) { (_, new) in new }

获取资源

1: 可以通过路径和资源名称获取资源

func get<Value>(_ location: String, inMainActor mainActor: Bool) async throws -> Value where Value: Sendable
func get<Value>(name: String, params: [String : String], queryParams: [String : String], inMainActor mainActor: Bool) async throws -> Value where Value: Sendable

2: 事例代码

let intvalue1: Int = try await FJRouter.resource().get("/intvalue1", inMainActor: false)
let intvalue3: Int? = try await FJRouter.resource().get("/intvalue1", inMainActor: true)
let intOptionalvalue3: Int? = try await FJRouter.resource().get("/intOptionalvalue2", inMainActor: false)
let stringvalue1: String = try await FJRouter.resource().get("/stringvalue1", inMainActor: false)
let aTestable1: ATestable = try await FJRouter.resource().get("/protocolATest/1", inMainActor: false)
let aTestable2: ATestable = try await FJRouter.resource().get("/protocolATest/0", inMainActor: true)
let aTestable3: BModel = try await FJRouter.resource().get("/protocolATest/0", inMainActor: false)

更新资源

1: 可以通过路径和资源名称更新资源

func update(byPath path: String, value: @escaping FJRouterResource.Value) async throws

func update(byName name: String, value: @escaping FJRouterResource.Value) async throws

2: 如果没有存放过相同path的资源, 会抛出FJRouter.GetResourceError.notFind错误

3: 事例代码

try await impl.update(byName: "sintvalue1", value: { _ in 66 })

try await FJRouter.resource().delete(byPath: "adfasdf")

删除资源

func delete(byPath path: String) async throws

func delete(byName name: String) async throws

1: 必须是已经存放过的资源, 删除不存在的资源会抛出FJRouter.GetResourceError.notFind错误

2: 可以根据名称或者路由删除

3: 事例代码:

try await FJRouter.resource().delete(byPath: "/intvalue1")
try await FJRouter.resource().delete(byName: "intOptionalvalue1")

事件总线

事件总线FJRouterEventable协议

1: 事件总线协议定义为FJRouterEventable的协议, 可以通过FJRouter.event()获取事件总线管理对象.

2: 建议使用onReceive(path: "xxx", name: "xxx"), emit(name: xxx)方法进行相关操作。

当事件路径比较复杂,且含有参数的时候, 如果通过硬编码的方法直接手写路径, 可能会造成拼写错误,参数位置错误等错误

在实际app中, 事件的URL格式可能会随着时间而改变, 但是一般事件名称不会去更改

监听事件

1: 监听动作是通过系统Combine框架进行响应, 不持有监听者

2: 可以一对多的进行监听, 即可以在多处监听

3: 事例代码

无参
let seekSuccess = try await FJRouter.event().onReceive(path: "/seek/success", name: "onSeekSuccess")
seekSuccess.receive(on: OperationQueue.main)
.sink(receiveValue: { info in
    print("onSeekSuccess=>", info)
}).store(in: &self.cancels)

有参
let seekProgress = try await FJRouter.event().onReceive(path: "/seek/:progress", name: "onSeekProgress")
seekProgress.receive(on: OperationQueue.main)
.sink(receiveValue: { info in
    print("onSeekProgress=>", info)
}).store(in: &self.cancels)

触发事件

1: 可以通过事件路径和名称进行触发

2: 事例代码

通过事件url路径触发事件
//无参
try await FJRouter.event().emit("/seek/success", extra: 5)
//有参: 1就是监听"/seek/:progress"中的progress字段
try await FJRouter.event().emit("/seek/1", extra: nil)

通过事件名称触发事件
//无参
try await FJRouter.event().emit(name: "onSeekSuccess", params: [:], queryParams: [:], extra: 5)
// 有参
try await FJRouter.event().emit(name: "onSeekProgress", params: ["progress": "1"], queryParams: [:], extra: nil)

安装

从2.0.2分支开始, 要求swfitVersion>=6, 即必须使用xcode 16.0版本以上

Swift Package Manager

使用 Swift PM 的最简单的方式是找到 Project Setting -> Swift Packages

搜索 https://github.com/zgjff/FJRouter 并添加

CocoaPods

在 Podfile 文件中添加 FJRouter:

pod 'FJRouter'

然后运行 pod install

About

正则、参数解析匹配、路由(重定向、回调、子路由)、资源中心、事件总线

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published