github上的优秀开源库的设计思路,Star从高到底:
(1)JLRoutes
受URL Scheme思路的影响。它把所有对资源的请求看成是一个URI。
(2)routable-ios
(3)HHRouter
这是布丁动画APP的一个Router,灵感来自于ABRouter 和 Routable iOS。
(4)MGJRouter
这是蘑菇街的一个路由的方法,这个库的由来:
1.JLRoutes 的问题主要在于查找 URL 的实现不够高效,通过遍历而不是匹配。还有就是功能偏多。
2.HHRouter 的 URL 查找是基于匹配,所以会更高效,MGJRouter 也是采用的这种方法,但它跟 ViewController 绑定地过于紧密,一定程度上降低了灵活性。
从数据结构来看,MGJRouter还是和HHRouter一模一样的。
对HHRouter所做的优化:
1.**MGJRouter 支持 openURL 时,可以传一些 userinfo 过去
这个对比HHRouter,仅仅只是写法上的一个语法糖,在HHRouter中虽然不支持带字典的参数,但是在URL后面可以用URL Query Parameter来弥补。
MGJRouter对userInfo的处理是:直接把它封装到Key = MGJRouterParameterUserInfo对应的Value里面。
2.支持中文的URL。
这里就是需要注意一下编码。
3.定义一个全局的 URL Pattern 作为 Fallback。
这一点是模仿的JLRoutes的匹配不到会自动降级到global的思想。
parameters字典里面会先存储下一个路由规则,存在block闭包中,在匹配的时候会取出这个handler,降级匹配到这个闭包中,进行最终的处理。
4.当 OpenURL 结束时,可以执行 Completion Block。
在MGJRouter里面,作者对原来的HHRouter字典里面存储的路由规则的结构进行了改造。
这3个key会分别保存一些信息:
MGJRouterParameterURL保存的传进来的完整的URL信息。
MGJRouterParameterCompletion保存的是completion闭包。
MGJRouterParameterUserInfo保存的是UserInfo字典。
举个例子:
上面的URL会匹配成功,那么生成的参数字典结构如下:
注意,额外的age和name,一个是注册时候的参数,一个是调用时候url中?后面的。
5.可以统一管理URL
这个功能非常有用。
URL 的处理一不小心,就容易散落在项目的各个角落,不容易管理。比如注册时的 pattern 是 mgj://beauty/:id,然后 open 时就是 mgj://beauty/123,这样到时候 url 有改动,处理起来就会很麻烦,不好统一管理。
所以 MGJRouter 提供了一个类方法来处理这个问题。
generateURLWithPattern:函数会对我们定义的宏里面的所有的:进行替换,替换成后面的字符串数组,依次赋值。
将上述过程图解出来,如下:
以下部分可以忽略了,貌似MGJRouter中没有。
--- MGJRouter中貌似没有体现出要区分开页面间调用和组件间调用 ---
--- 以下的应该是用代理代替了block ---
蘑菇街为了区分开页面间调用和组件间调用,于是想出了一种新的方法。用Protocol的方法来进行组件间的调用。
每个组件之间都有一个 Entry,这个 Entry,主要做了三件事:
- 注册这个组件关心的 URL
- 注册这个组件能够被调用的方法/属性
- 在 App 生命周期的不同阶段做不同的响应
页面间的openURL调用就是如下的样子:
每个组件间都会向MGJRouter注册,组件间相互调用或者是其他的App都可以通过openURL:方法打开一个界面或者调用一个组件。
在组件间的调用,蘑菇街采用了Protocol的方式。
[ModuleManager registerClass:ClassA forProtocol:ProtocolA] 的结果就是在 MM 内部维护的 dict 里新加了一个映射关系。
[ModuleManager classForProtocol:ProtocolA] 的返回结果就是之前在 MM 内部 dict 里 protocol 对应的 class,使用方不需要关心这个 class 是个什么东东,反正实现了 ProtocolA 协议,拿来用就行。
这里需要有一个公共的地方来容纳这些 public protocl,也就是图中的 PublicProtocl.h。
我猜测,大概实现可能是下面的样子:
然后这个是一个单例,在里面注册各个协议:
在ModuleProtocolManager中用一个字典保存每个注册的protocol。现在再来猜猜ModuleEntry的实现。
然后每个模块内都有一个和暴露到外面的协议相连接的“接头”。
在它的实现中,需要引入3个外部文件,一个是ModuleProtocolManager,一个是DetailModuleEntryProtocol,最后一个是所在模块需要跳转或者调用的组件或者页面。
至此基于Protocol的方案就完成了。如果需要调用某个组件或者跳转某个页面,只要先从ModuleProtocolManager的字典里面根据对应的ModuleEntryProtocol找到对应的DetailModuleEntry,找到了DetailModuleEntry就是找到了组件或者页面的“入口”了。再把参数传进去即可。
这样就可以调用到组件或者界面了。
如果组件之间有相同的接口,那么还可以进一步的把这些接口都抽离出来。这些抽离出来的接口变成“元接口”,它们是可以足够支撑起整个组件一层的。
(5)CTMediator
说说casatwy的方案,这方案是基于Mediator的。
传统的中间人Mediator的模式是这样的:
这种模式每个页面或者组件都会依赖中间者,各个组件之间互相不再依赖,组件间调用只依赖中间者Mediator,Mediator还是会依赖其他组件。那么这是最终方案了么?
看看@casatwy是怎么继续优化的。
主要思想是利用了Target-Action简单粗暴的思想,利用Runtime解决解耦的问题。
targetName就是调用接口的Object,actionName就是调用方法的SEL,params是参数,shouldCacheTarget代表是否需要缓存,如果需要缓存就把target存起来,Key是targetClassString,Value是target。
通过这种方式进行改造的,外面调用的方法都很统一,都是调用performTarget: action: params: shouldCacheTarget:。第三个参数是一个字典,这个字典里面可以传很多参数,只要Key-Value写好就可以了。处理错误的方式也统一在一个地方了,target没有,或者是target无法响应相应的方法,都可以在Mediator这里进行统一出错处理。
但是在实际开发过程中,不管是界面调用,组件间调用,在Mediator中需要定义很多方法。于是做作者又想出了建议我们用Category的方法,对Mediator的所有方法进行拆分,这样就就可以不会导致Mediator这个类过于庞大了。
把这些具体的方法一个个的都写在Category里面就好了,调用的方式都非常的一致,都是调用performTarget: action: params: shouldCacheTarget:方法。
最终去掉了中间者Mediator对组件的依赖,各个组件之间互相不再依赖,组件间调用只依赖中间者Mediator,Mediator不依赖其他任何组件。
(6)一些并没有开源的方案
Uber 骑手App的一个方案。。。。
各个方案优缺点
1. URLRoute注册方案的优缺点
首先URLRoute也许是借鉴前端Router和系统App内跳转的方式想出来的方法。它通过URL来请求资源。不管是H5,RN,Weex,iOS界面或者组件请求资源的方式就都统一了。URL里面也会带上参数,这样调用什么界面或者组件都可以。所以这种方式是最容易,也是最先可以想到的。
URLRoute的优点很多,最大的优点就是:服务器可以动态的控制页面跳转,可以统一处理页面出问题之后的错误处理,可以统一三端,iOS,Android,H5 / RN / Weex 的请求方式。
但是这种方式也需要看不同公司的需求。如果公司里面已经完成了服务器端动态下发的脚手架工具,前端也完成了Native端如果出现错误了,可以随时替换相同业务界面的需求,那么这个时候可能选择URLRoute的几率会更大。
但是如果公司里面H5没有做相关出现问题后能替换的界面,H5开发人员觉得这是给他们增添负担。如果公司也没有完成服务器动态下发路由规则的那套系统,那么公司可能就不会采用URLRoute的方式。因为URLRoute带来的少量动态性,公司是可以用JSPatch来做到。线上出现bug了,可以立即用JSPatch修掉,而不采用URLRoute去做。
所以选择URLRoute这种方案,也要看公司的发展情况和人员分配,技术选型方面。
URLRoute方案也是存在一些缺点:
首先URL的map规则是需要注册的,它们会在load方法里面写。写在load方法里面是会影响App启动速度的。
其次是大量的硬编码。URL链接里面关于组件和页面的名字都是硬编码,参数也都是硬编码。而且每个URL参数字段都必须要一个文档进行维护,这个对于业务开发人员也是一个负担。而且URL短连接散落在整个App四处,维护起来实在有点麻烦,虽然蘑菇街想到了用宏统一管理这些链接,但是还是解决不了硬编码的问题。
最后一个缺点是,对于传递NSObject的参数,URL是不够友好的,它最多是传递一个字典。
真正一个好的路由是在无形当中服务整个App的,是一个无感知的过程,从这一点来说,略有点缺失。
2. Protocol-Class注册方案的优缺点
Protocol-Class方案的优点,这个方案没有硬编码。
Protocol-Class方案也是存在一些缺点的,每个Protocol都要向ModuleManager进行注册。
这种方案ModuleEntry是同时需要依赖ModuleManager和组件里面的页面或者组件两者的。当然ModuleEntry也是会依赖ModuleEntryProtocol的,但是这个依赖是可以去掉的,比如用Runtime的方法NSProtocolFromString,加上硬编码是可以去掉对Protocol的依赖的。但是考虑到硬编码的方式对出现bug,后期维护都是不友好的,所以对Protocol的依赖还是不要去除。
最后一个缺点是组件方法的调用是分散在各处的,没有统一的入口,也就没法做组件不存在时或者出现错误时的统一处理。
3. Target-Action方案的优缺点
Target-Action方案的优点,充分的利用Runtime的特性,无需注册这一步。Target-Action方案只有存在组件依赖Mediator这一层依赖关系。在Mediator中维护针对Mediator的Category,每个category对应一个Target,Categroy中的方法对应Action场景。Target-Action方案也统一了所有组件间调用入口。
Target-Action方案也能有一定的安全保证,它对url中进行Native前缀进行验证。
Target-Action方案的缺点,Target_Action在Category中将常规参数打包成字典,在Target处再把字典拆包成常规参数,这就造成了一部分的硬编码。
4. 组件如何拆分?
这个问题其实应该是在打算实施组件化之前就应该考虑的问题。为何还要放在这里说呢?因为组件的拆分每个公司都有属于自己的拆分方案,按照业务线拆?按照最细小的业务功能模块拆?还是按照一个完成的功能进行拆分?这个就牵扯到了拆分粗细度的问题了。组件拆分的粗细度就会直接关系到未来路由需要解耦的程度。
假设,把登录的所有流程封装成一个组件,由于登录里面会涉及到多个页面,那么这些页面都会打包在一个组件里面。那么其他模块需要调用登录状态的时候,这时候就需要用到登录组件暴露在外面可以获取登录状态的接口。那么这个时候就可以考虑把这些接口写到Protocol里面,暴露给外面使用。或者用Target-Action的方法。这种把一个功能全部都划分成登录组件的话,划分粒度就稍微粗一点。
如果仅仅把登录状态的细小功能划分成一个元组件,那么外面想获取登录状态就直接调用这个组件就好。这种划分的粒度就非常细了。这样就会导致组件个数巨多。
所以在进行拆分组件的时候,也许当时业务并不复杂的时候,拆分成组件,相互耦合也不大。但是随着业务不管变化,之前划分的组件间耦合性越来越大,于是就会考虑继续把之前的组件再进行拆分。也许有些业务砍掉了,之前一些小的组件也许还会被组合到一起。总之,在业务没有完全固定下来之前,组件的划分可能一直进行时。
最好的方案:
关于架构,我觉得抛开业务谈架构是没有意义的。因为架构是为了业务服务的,空谈架构只是一种理想的状态。所以没有最好的方案,只有最适合的方案。
最适合自己公司业务的方案才是最好的方案。分而治之,针对不同业务选择不同的方案才是最优的解决方案。如果非要笼统的采用一种方案,不同业务之间需要同一种方案,需要妥协牺牲的东西太多就不好了。
References:iOS 组件化 —— 路由设计思路分析
以下部分是最开始写的。
中间件是一种设计思想,两个组件并不是直接交互,而是通过一个第三平台.
蘑菇街:mgjroute(mgj路由器)
各个需要交互的组件和宿主工程都要依赖中间件mgjroute;
mgjroute的内部在维护一张表(url-block,即一个url字符串对应一个block),这个表是各个组件共享的。
示例1:如果想给外界提供一个VC控制器,[registerURL:@"xiazai://VC" withBlock:{ reture VC }];写在下载听;url就是@"xiazai://VC",block就是{ reture VC }。
示例2:播放器想拿到下载听的控制器VC,[mgjroute openURL:@"xiazai://VC"],返回一个VC
弊端:
1、每个组件都要依赖mgjroute中间件;
2、url如果特别多,会越来越难维护;
3、传一些参数还要硬解码,把里面的url翻译过来,把参数取出来。
target-action
中间件用perform target action,target可以通过运行时来获取,action也可以通过运行时来获取;
各个组件无需依赖target-action
建议完成MainModuleAPI
无论选择哪个方案,都建议做好统一披露API,即完成MainModuleAPI.m、MainModuleAPI.h。