iOS(Swift) 路由设计

2022-02-17 更新url 编码

最近在整理 flutter 面试的内容,顺便也回顾swift 的框架,面试题等等,然后就看到了我以前封装的 Get系列,还记得我之前提过一个 MVP模式微变种的文章吗,那个是模仿flutter Get框架写的状态管理框架,Get 另外一个好用的地方在于它的路由框架写的很好,所以我同时我另外其实也了一个 iOS 的路由框架,不过没有参照Get 的思想,只不过也归到我定义的Get框架中去了,这里顺便也抛出来

设计思路

  • 是否需要注册路由
    我设计的路由框架,是有注册这一个环节的,其实使用硬编码,是可以做到不需要注册,直接生成类,然后用 mirror 取到需要赋值的参数,赋值并跳转的,这种写法很流批,也很炫,封装好了,业务代码中一句路由都不需要注册写,但是,他很不容易阅读.代码不止是给机器看的,还是需要给人看的,在远程路由中,你确实是可以告诉前端,路由的类名和参数,但是,这太麻烦了,你最终还是得维护一份路由表,来提供给前端使用,而且由于使用的是 NSClassFromString,没有做过映射,到时候路由就会变得像这样这么丑
    native://?ios_class=PersonEditInfoViewController
    甚至还会增加被逆向的风险,当你为了隐藏类名做一层映射表的时候,你会发现,其实硬编码还不如手动注册一份路由表来的简单直接.
  • 远程路由和本地路由
    远程路由和本地路由需要统一一致,这也是我在踩坑的过程中总结出来的,有些人可能会为了传参方便,直接不使用路由,或者另外写一份本地路由,第一种做法是在腐蚀框架,第二种做法大大增加维护的成本,要知道,使用路由,第一大目的,就是为了解耦,这也是组件化的基础,还有,为了传参方便,维护本地路由,最后肯定是得不偿失的,这块其实很简单,只需要对本地路由,拼接成远程路由,或者远程路由,解析成本地路由,我自己的方案,是远程解析成本地,毕竟本地拼接成远程,最后还是要解析成本地的.

代码分析

拋出去给外部的方法,这里我暂时没有承担掉 pop 的职责,原因只是因为懒,后续肯定需要做的.back,backToRoot.removeClass等等.

///  注册路由处理
    static func registerHandler(_ handler: GetRouterHandlerDelegate) {
        to.handler = handler
    }

注册其实就是抛出去路由给业务处理,这个等会儿再说,先从路由解析开始

外部调用

假设banner 的点击事件是

GetRouter.router(name: "native://www.taoqu.com?ios_path=/edit_info&userId=200944533&targetId=222222222", arguments: nil)

这个路径是符合我们 app 业务路径的,为什么我没有用url中的 path 来区分跳转路径呢,因为有可能安卓路径设计跟iOS 是不一样的,所以我们用固定参数 ios_path 来取跳转路径,参数名自己设计即可,这里的路径可以设计成全部是一级路径,或者一级+二级路径,这看你们自己想怎么设计解析的方案都行.
scheme 和 host 可以用作权限验证,非 native:// 和 www.taoqu.com 的拒绝跳转,另外也可以另外增加 http://和 https://协议的识别跳转

路由参数解析

    /// 路由方法
    static func router(name: String?, arguments: Dict?) {
        guard let name = name else { return }
        if let urlComponents = URLComponents(string: name) {
            // url 路由,包括协议 native, http, https 比如 native://www.taoqu.com?ios_path=/mine/setting&userId=200944533
            to._parse(urlComponents: urlComponents, argument: arguments)
        } else {
            // 本地路由, 比如 name=/mine/setting
            to.handler?.handler(path: name, arguments: arguments)
        }
    }

这里我只做了一件事,就是不管远程路由和本地路由,参数都整合到一个 map 中.然后继续

执行路由

private func _doRouter(urlComponents: URLComponents, argument: Dict?) {
        guard let scheme = urlComponents.scheme else {
            return
        }
        switch scheme {
        case GetRouterConfig.native.scheme:
            guard let ios_path = argument?["ios_path"] as? String else { return }
            handler?.handler(path: ios_path, arguments: argument)
        case GetRouterConfig.http.scheme, GetRouterConfig.https.scheme:
            handler?.handler(path: "http", arguments: argument)
        default:
            getllog("协议头错误")
        }
    }

抛给业务解析路由

我们 GetRouter 框架细看也只是做了一点事儿,就是把 urlString 中的参数和 本地argument参数整合到了一起,然后分出路径,抛出给业务.
这里就讲到业务路由如何解析了,原本我是想着按模块区分路径的,比如个人模块自己管理自己的 class,甚至直接用 enum 来匹配路径,这样很Swift,但是有问题,1.我无法一次性全部告诉web 端我所实现的所有路由,2.我无法实现一二级路径匹配,这个问题还好,我自己做路径分隔,然后按模块填入,这样也能实现 enum 匹配路径,但是这更增加了1的难度,就像下面这样

/// 公共路由
struct RouterGeneralHandler: GetRouterHandlerDelegate {
    enum RouterGeneralEnum: String {
        case http,https
    }
    func handler(path: String, arguments: Dict?) -> Bool {
        switch path {
        case RouterGeneralEnum.http.rawValue:
            getllog("处理了")
        default:
            return false
        }
        return true
    }
}

/// 个人模块路由
struct RouterPersonHandler: GetRouterHandlerDelegate {
    enum RouterPersonEnum: String {
        case edit_info
        case setting
    }
    func handler(path: String, arguments: Dict?) -> Bool {
        switch path {
        case RouterPersonEnum.edit_info.rawValue:
            getllog(arguments?.toJSONString())
            CompleteInfoVC.push()
        default:
            return false
        }
        return true
    }
}

所以, 最后决定,业务层路由解析修改成这样

业务路由解析

// 路由名管理,这个类可以直接复制给 web,相当于所有已维护的路由表
enum GetRouterName: String {
    // MARK: - --------------------------------------公共
    case http = "http"
    case https = "https"
    // MARK: - --------------------------------------发现
    
    // MARK: - --------------------------------------我的
    case mine_setting = "mine_setting"
    // 路由名可用_也可用/,这个主要是强调统一规范
    case mine_complete = "/mine/complete"
    // MARK: - --------------------------------------帖子
    case post_detail = "post_detail"
}

路由解析


struct RougerHandler: GetRouterHandlerDelegate {
    let subHandlers: [GetRouterHandlerDelegate] = [
        RouterGeneralHandler(),
        RouterPersonHandler(),
    ]
    
    func handler(path: String, arguments: Dict?) -> Bool {
        for handler in subHandlers {
            if handler.handler(path: path, arguments: arguments) {
                return true
            } else {
                continue
            }
        }
        getllog("路由无法被解析")
        return false
    }
}


/// 公共路由
struct RouterGeneralHandler: GetRouterHandlerDelegate {
    func handler(path: String, arguments: Dict?) -> Bool {
        switch path {
        case GetRouterName.http.rawValue:
            getllog("处理了")
        default:
            return false
        }
        return true
    }
}

/// 个人模块路由
struct RouterPersonHandler: GetRouterHandlerDelegate {
    func handler(path: String, arguments: Dict?) -> Bool {
        switch path {
        case GetRouterName.mine_setting.rawValue:
            getllog("跳转个人设置")
            SettingVC.push()
        case GetRouterName.mine_complete.rawValue:
            getllog("跳转完善资料")
            CompleteInfoVC.push()
        default:
            return false
        }
        return true
    }
}

这块儿,就是由共同的业务开发一起维护,至于权限校验什么的,可以根据实际跳转的路径,具体的策略看你们自己想要怎么维护,加中间件可以在这里加.

参数解析

我在flutter 架构设计(基于 getx 的路由)中有写过一点

本地路由使用arguments还是parameters我有点忘了,但是这两种其中有一种肯定与上面的url 拼接参数方式取参的方式是一样的,直接可以用同一种方式取出来,之前的 demo 我测试过了,不过现在忘了,大家做个测试即可.

这里注意,对于带参的路由,由于远程路由解析下来的参数,key-value 的 value 是 string,也为了解决参数接收问题,我个人建议本地路由,传参也都使用 string 传参,这样,你就不需要在进行两种判断了,或者你可以手动判断类型,进行值的获取.虽然json 传参有可能会牺牲点编解码的性能. 这个东西见仁见智了,看你们自己判断.

我目前的 iOS 项目路由设计太过复杂了,为了兼容老版本路由,新版本路由,设计了很多策略,同事还另外写了一套本地路由,路由框架显得很复杂,所以这东西,在一开始,一定要定好规范,不要随意更改.
最后,贴一下主要的代码吧

//
//  GetRouter.swift
//  spsd
//
//  Created by suyikun on 2022/1/7.
//  Copyright © 2022 未来. All rights reserved.
//

import Foundation

class GetRouter {
    // MARK: - --------------------------------------singleton
    private static let to = GetRouter()
    private init() {}
    // MARK: - --------------------------------------property
    /// 路由处理
    private var handler: GetRouterHandlerDelegate?
    // MARK: - --------------------------------------func

    ///  注册路由处理
    static func registerHandler(_ handler: GetRouterHandlerDelegate) {
        to.handler = handler
    }

    /// 路由方法
    static func router(name: String?, arguments: Dict?) {
        guard let name = name else { return }
        if let urlComponents = URLComponents(string: name) {
            // url 路由,包括协议 native, http, https 比如 native://www.taoqu.com?ios_path=/mine/setting&userId=200944533
            to._parse(urlComponents: urlComponents, argument: arguments)
        } else {
            // 本地路由, 比如 name=/mine/setting
            to.handler?.handler(path: name, arguments: arguments)
        }
    }

    /// 路由解析, 将远程或者本地的路由整合到一个 Map 中
    /// - Parameters:
    ///   - name: 全路由
    ///   - argument: 参数
    private func _parse(urlComponents: URLComponents, argument: Dict?) {
        // 拼接参数,合并参数
        var dict = argument ?? Dict()
        if let queryItems = urlComponents.queryItems {
            for queryItem in queryItems {
                if let value = queryItem.value {
                    dict[queryItem.name] = value
                }
            }
        }
        /// 执行路由
        _doRouter(urlComponents: urlComponents, argument: dict)
    }

    private func _doRouter(urlComponents: URLComponents, argument: Dict?) {
        guard let scheme = urlComponents.scheme else {
            return
        }
        switch scheme {
        case GetRouterConfig.native.scheme:
            guard let ios_path = argument?["ios_path"] as? String else { return }
            handler?.handler(path: ios_path, arguments: argument)
        case GetRouterConfig.http.scheme, GetRouterConfig.https.scheme:
            handler?.handler(path: "http", arguments: argument)
        default:
            getllog("协议头错误")
        }
    }
}


struct RougerHandler: GetRouterHandlerDelegate {
    let subHandlers: [GetRouterHandlerDelegate] = [
        RouterGeneralHandler(),
        RouterPersonHandler(),
    ]

    func handler(path: String, arguments: Dict?) -> Bool {
        for handler in subHandlers {
            if handler.handler(path: path, arguments: arguments) {
                return true
            } else {
                continue
            }
        }
        getllog("路由无法被解析")
        return false
    }
}


/// 公共路由
struct RouterGeneralHandler: GetRouterHandlerDelegate {
    func handler(path: String, arguments: Dict?) -> Bool {
        switch path {
        case GetRouterName.http.rawValue:
            getllog("处理了")
        default:
            return false
        }
        return true
    }
}

/// 社区路由
struct RouterCommunityHandler: GetRouterHandlerDelegate {
    func handler(path: String, arguments: Dict?) -> Bool {
        switch path {
        case GetRouterName.community_post_detail.rawValue:
            getllog("跳转community_post_detail")
            if let postId = arguments?["postId"] as? String {
                PostDetailController(postId: postId).push()
            }
        default:
            return false
        }
        return true
    }
}

/// 个人模块路由
struct RouterPersonHandler: GetRouterHandlerDelegate {
    func handler(path: String, arguments: Dict?) -> Bool {
        switch path {
        case GetRouterName.mine_setting.rawValue:
            getllog("跳转个人设置")
            SettingVC.push()
        case GetRouterName.mine_complete.rawValue:
            getllog("跳转完善资料")
            CompleteInfoVC.push()
        default:
            return false
        }
        return true
    }
}

2022-02-17 更新url 编码

/// 路由方法
    static func router(name: String?, arguments: Dict?) {
        guard var name = name else { return }
        
        name = to._urlEncoding(name)
        
        if let urlComponents = URLComponents(string: name) {
            // url 路由,包括协议 native, http, https 比如 native://www.taoqu.com?ios_path=/mine/setting&userId=200944533
            to._parse(urlComponents: urlComponents, argument: arguments)
        } else {
            // 本地路由, 比如 name=/mine/setting
            to.handler?.handler(path: name, arguments: arguments)
        }
    }
private func _urlEncoding(_ url: String) -> String {
//        CharacterSet.urlHostAllowed: 被转义的字符有  "#%/<>?@\^`{|}
//        CharacterSet.urlPathAllowed: 被转义的字符有  "#%;<>?[\]^`{|}
//        CharacterSet.urlUserAllowed: 被转义的字符有   "#%/:<>?@[\]^`
//        CharacterSet.urlQueryAllowed: 被转义的字符有  "#%<>[\]^`{|}
//        CharacterSet.urlPasswordAllowed 被转义的字符有 "#%/:<>?@[\]^`{|}
//        CharacterSet.urlFragmentAllowed 被转义的字符有 "#%<>[\]^`{|}
//        let characterSet = CharacterSet(charactersIn: "#").inverted
        getllog(url)
        let characterSet = CharacterSet.urlQueryAllowed
        let encodingUrl = url.addingPercentEncoding(withAllowedCharacters: characterSet) ?? url
        getllog(encodingUrl)
        return encodingUrl
    }

主要是解决参数中有可能存在 vue 开发模式#home 这种路由路径参数,导致 URLComponents 编码失败,也方便 wkwebview 加载,顺便对中文转个码等等
例:native://www.taoqu.com?ios_path=http&url=www.baidu.com#home?user_id=222222&postId=33333&key=帅奇
转码参数可以自己定制,我这里给出了系统提供的,和自定义的,感兴趣的自己搜搜.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容