基于路由的iOS模块间通信方案

引入

在一个业务较为复杂的应用系统中,对于模块间通信,传递的参数不同,返回的数据也不同,返回的方式也有可能不同,定义一个统一的抽象似乎比较困难。这时我们想到客户端对服务器接口的调用,这个是完全分离的,而且是通过一套统一的网络框架进行的,基于此我们可以仿照网络框架来设计这个模块间通信方案。
    对于模块间的通信数据大致可以分为UI界面和逻辑运算数据。在iOS中UI界面通常为UIViewController。在客户端系统中,通过路由进行页面跳转是常见的选择,但一般的路由方案对于逻辑数据交互的支持能力较弱,所以本文的实现方案对一般的路由方案进行了改造,以加强其通用数据的通信能力。
    网络请求在客户端和服务器之间进行。客户端发起请求,期望得到数据,为数据请求者;服务器接受并处理请求,为数据返回者。模块间通信也是如此,任何一个模块都可以是客户端和服务器,当一个模块作为请求数据者的时候,它就是客户端,当它作为数据返回者时,它就是服务器。

实现

主体结构

结构.png

一般在应用启动的时候,程序从配置文件读取url和处理者名称,然后通过RouterCenter注册RouterHandlerProtocol,并存储在内存中。程序运行过程中,路由请求者发起请求RouterRequestProtocol,RouterCenter查找到匹配的RouterHandlerProtocol,并运行处理,将RouterResponseProtocol返回给路由请求者。

主要类解析

该实现包含的主要类有

  • RouterResponseProtocol:模块之间通信数据的接口
  • RouterRequestProtocol:路由请求接口
  • RouterHandlerProtocol:路由处理者接口
  • RouterCenter: 负责注册RouterHandlerProtocol,存储路由节点,查询匹配路由节点,并接受路由请求。
  • RouterHandlerLoader:负责从配置文件读取url和handler,并注册到RouterCenter中(这个类不是必须的)

RouterResponseProtocol

所有返回的数据都要遵从该协议,包含一个任意类型的返回数据,一般为字典类型:[String:Any],对于UI页面类型,data为UIViewController,实现如下:

protocol RouterResponseProtocol {
        var data:Any?; //返回的有效数据
        var error:Error?; //错误
    }

RouterRequestProtocol

请求协议,包含一个url,对应一个RouterHandlerProtocol;parameters为参数,可为nil;responseClosure为数据返回的回调。

protocol RouterRequestProtocol {
        var url:String;
        var parameters:[String:Any]?
        var responseClosure:((RouterResponseProtocol) -> Void)?
    }

RouterHandlerProtocol

  • 通用的路由处理者协议
public protocol RouterHandlerProtocol {
        func handleRouter(url:String, parameters:[String:Any]?, completion:((RouterResponseProtocol) -> Void)?)
}
  • 静态路由处理者协议,有的时候路由处理者可能不想实例化,只需要一个静态方法,如下定义一个拥有静态方法的协议
public protocol RouterStaticHandlerProtocol {
        static func handleRouter(url:String, parameters:[String:Any]?, completion:((RouterResponseProtocol) -> Void)?)
}
  • UI路由处理者协议,对于UI页面类的数据,可以通过以上2个协议实现,将UIViewController作为RouterResponseProtocol中的data传递。但是定义一个特殊的协议RouterViewControllerProtocol应该是一个更好的选择,当UIViewController初始化需要额外的参数时可以实现这个协议,当然也可以不实现,那就只能调用基类的init()方法了。
public protocol RouterViewControllerProtocol {
        init?(url:String, parameters:[String:Any]?);
}

RouterCenter

路由处理中心,主要包含存储路由节点,注册路由处理者,处理路由请求等功能。

存储路由节点

存储规则按照 scheme-host-path3个级别存储,path支持参数设置,以冒号:为标识,如http://anyrouter/some/path/:id,则该path包含一个参数名是id的键值。
存储结构为,如下,key存储scheme、host或path的值,handlerObject, handlerClass, viewControllerClass为路由处理者,3者有其一即可,children为下一级的Node,scheme包含若干host,host包含若干path,只有最后一级的Path,才持有处理者的引用,如下

class Node { 
        var key:String 
        var handlerObject:RouterHandlerProtocol? 
        var handlerClass:RouterStaticHandlerProtocol.Type? 
        var viewControllerClass:UIViewController.Type? 
        var children:[Node]? 
        ……
    }
url节点存储.png
注册路由处理者

可以注册3种处理者协议,注册实例化处理者RouterHandlerProtocol,静态处理者RouterStaticHandlerProtocol,页面响应者UIViewController。

class RouterCenter {
        func register(url:String) -> Node {
            //查询节点……
            return node;
        }
        public func register(url: String, handlerObject: RouterHandlerProtocol) { 
            let node = register(url: url) 
            node.handlerObject = handlerObject; 
        } 
        public func register(url: String, handlerClass: RouterStaticHandlerProtocol.Type) { 
            let node = register(url: url) 
            node.handlerClass = handlerClass 
        } 
        public func register(url:String, viewController:UIViewController.Type) { 
            let node = register(url: url) 
            node.viewControllerClass = viewController 
        }
    }
处理路由请求
extension RouterCenter {
        func matchedNode(url:String) -> (node:Node, pathArgs:[String:String]?)? {
            //查询节点
            return (node, args)
        }
        func startRequest(_ request:RouterRequestProtocol) {
            if let (node, args) = matchedNode(url:request.url) {
                if node.handlerObject != nil {
                    node.handleObject!.handleRouter(...)
                }else if node.handlerClass != nil {
                    node.handleClass!.handleRouter(...)
                }else if node.viewControllerClass != nil {
                    var vcObject:UIViewController?
                    if node.viewControllerClass! as? RouterViewControllerProtocol {
                        vcObject = node.ViewControllerClass.init(url:parameters:)
                    }  
                    if vcObject == nil {
                        vcObject = node.ViewControllerClass.init()
                    }
                    if vcObject != nil {
                        let response = RouterResponse(data: vcObject!)
                        request.responseClosure?(response)
                    }
                }
            }    
        }
    }

其中matchedNode(url:String)方法,返回一个节点node, pathArgs表示path中可能存在的参数键值对。其中对UI页面的处理,路由系统并不负责打开页面,只是负责将UIViewController构建出来,具体是使用push还是present,由发起请求者决定。

RouterHandlerLoader

各个模块,可以通过手动添加的方式注册到RouterCenter,但是这种方式比较低效,尤其是数量众多的UIViewController,各个模块可以通过配置plist文件来注册路由处理者,如下:

public struct RouterHandlerLoader { 
    public static func loadFile(_ fileUrl:URL, from bundle:Bundle) {
    }
}

plist文件格式如下:


router_plist.png

podspec

由2个子pod组成,开发时候可以只使用AnyRouter/Core部分,Loader只包含RouterHandlerLoader

s.subspec 'Core' do |ss|
    ss.source_files = 'AnyRouter/Classes/Core/**/*.swift'
  end
  s.subspec 'Loader' do |ss|
    ss.source_files = 'AnyRouter/Classes/Loader/**/*.swift'
    ss.dependency 'AnyRouter/Core'
  end

举例

在一个类似微信的社交应用中,微信好友详情页中需要访问朋友圈中前几张图片。好友详情页作为请求发起者,朋友圈作为请求处理者。

  1. 朋友圈模块初始化的时候,注册路由处理者:
struct FriendPhotos : RouterHandlerProtocol {
    func handleRouter(url:String, parameters:[String:Any]?, completion:((RouterResponseProtocol) -> Void)?)
    if let userId = parameters?[“user_id”] as? String {
        //查询到图片数据
        let response = RouteResponse(data:[photos]);
        completion?(response);
    }
}

注册到路由中心

RouterCenter.register(url:”http://friend_timeline/photos/:user_id”, handlerObject:friendPhotoInstance);
  1. 微信好友详情页面,发起请求
let request = SimpleRouterRequest(url:”http://friend_timeline/photos/1234abc”)
request.responseClosure = { response in
    if let photos = response.data as? [photoUrl] {
        //得到照片url并显示
    }
}
RouterCenter.startRequest(request);

写在最后

系统SDK是我们一个很好的参照,其中的类型结构划分,命名方法都值得我们借鉴。
git主页

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

推荐阅读更多精彩内容