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=帅奇
转码参数可以自己定制,我这里给出了系统提供的,和自定义的,感兴趣的自己搜搜.