原文html
前言前端
随着用户的需求愈来愈多,对App的用户体验也变的要求愈来愈高。为了更好的应对各类需求,开发人员从软件工程的角度,将App架构由原来简单的MVC变成MVVM,VIPER等复杂架构。更换适合业务的架构,是为了后期能更好的维护项目。react
可是用户依旧不满意,继续对开发人员提出了更多更高的要求,不只须要高质量的用户体验,还要求快速迭代,最好一天出一个新功能,并且用户还要求不更新就能体验到新功能。为了知足用户需求,因而开发人员就用H5,ReactNative,Weex等技术对已有的项目进行改造。项目架构也变得更加的复杂,纵向的会进行分层,网络层,UI层,数据持久层。每一层横向的也会根据业务进行组件化。尽管这样作了之后会让开发更加有效率,更加好维护,可是如何解耦各层,解耦各个界面和各个组件,下降各个组件之间的耦合度,如何能让整个系统无论多么复杂的状况下都能保持“高内聚,低耦合”的特色?这一系列的问题都摆在开发人员面前,亟待解决。今天就来谈谈解决这个问题的一些思路。android
目录ios
1.引子git
2.App路由能解决哪些问题github
3.App之间跳转实现api
4.App内组件间路由设计数组
5.各个方案优缺点浏览器
6.最好的方案
1、引子
大前端发展这么多年了,相信也必定会遇到类似的问题。近两年SPA发展极其迅猛,React 和 Vue一直处于风口浪尖,那咱们就看看他们是如何处理好这一问题的。
在SPA单页面应用,路由起到了很关键的做用。路由的做用主要是保证视图和 URL 的同步。在前端的眼里看来,视图是被当作是资源的一种表现。当用户在页面中进行操做时,应用会在若干个交互状态中切换,路由则能够记录下某些重要的状态,好比用户查看一个网站,用户是否登陆、在访问网站的哪个页面。而这些变化一样会被记录在浏览器的历史中,用户能够经过浏览器的前进、后退按钮切换状态。总的来讲,用户能够经过手动输入或者与页面进行交互来改变 URL,而后经过同步或者异步的方式向服务端发送请求获取资源,成功后从新绘制 UI,原理以下图所示:
react-router经过传入的location到最终渲染新的UI,流程以下:
location的来源有2种,一种是浏览器的回退和前进,另一种是直接点了一个连接。新的 location 对象后,路由内部的 matchRoutes 方法会匹配出 Route 组件树中与当前 location 对象匹配的一个子集,而且获得了 nextState,在this.setState(nextState) 时就能够实现从新渲染 Router 组件。
大前端的作法大概是这样的,咱们能够把这些思想借鉴到iOS这边来。上图中的Back / Forward 在iOS这边不少状况下均可以被UINavgation所管理。因此iOS的Router主要处理绿色的那一块。
2、App路由能解决哪些问题
既然前端能在SPA上解决URL和UI的同步问题,那这种思想能够在App上解决哪些问题呢?
思考以下的问题,平时咱们开发中是如何优雅的解决的:
1.3D-Touch功能或者点击推送消息,要求外部跳转到App内部一个很深层次的一个界面。
好比微信的3D-Touch能够直接跳转到“个人二维码”。“个人二维码”界面在个人里面的第三级界面。或者再极端一点,产品需求给了更加变态的需求,要求跳转到App内部第十层的界面,怎么处理?
2.自家的一系列App之间如何相互跳转?
若是本身App有几个,相互之间还想相互跳转,怎么处理?
3.如何解除App组件之间和App页面之间的耦合性?
随着项目愈来愈复杂,各个组件,各个页面之间的跳转逻辑关联性愈来愈多,如何能优雅的解除各个组件和页面之间的耦合性?
4.如何能统一iOS和Android两端的页面跳转逻辑?甚至如何能统一三端的请求资源的方式?
项目里面某些模块会混合ReactNative,Weex,H5界面,这些界面还会调用Native的界面,以及Native的组件。那么,如何能统一Web端和Native端请求资源的方式?
5.若是使用了动态下发配置文件来配置App的跳转逻辑,那么若是作到iOS和Android两边只要共用一套配置文件?
6.若是App出现bug了,如何不用JSPatch,就能作到简单的热修复功能?
好比App上线忽然遇到了紧急bug,可否把页面动态降级成H5,ReactNative,Weex?或者是直接换成一个本地的错误界面?
7.如何在每一个组件间调用和页面跳转时都进行埋点统计?每一个跳转的地方都手写代码埋点?利用Runtime AOP ?
8.如何在每一个组件间调用的过程当中,加入调用的逻辑检查,令牌机制,配合灰度进行风控逻辑?
9.如何在App任何界面均可以调用同一个界面或者同一个组件?只能在AppDelegate里面注册单例来实现?
好比App出现问题了,用户可能在任何界面,如何随时随地的让用户强制登出?或者强制都跳转到同一个本地的error界面?或者跳转到相应的H5,ReactNative,Weex界面?如何让用户在任何界面,随时随地的弹出一个View ?
以上这些问题其实均可以经过在App端设计一个路由来解决。那么咱们怎么设计一个路由呢?
3、App之间跳转实现
在谈App内部的路由以前,先来谈谈在iOS系统间,不一样App之间是怎么实现跳转的。
1. URL Scheme方式
iOS系统是默认支持URL Scheme的,具体见官方文档。
好比说,在iPhone的Safari浏览器上面输入以下的命令,会自动打开一些App:
在iOS 9 以前只要在App的info.plist里面添加URL types - URL Schemes,以下图:
这里就添加了一个com.ios.Qhomer的Scheme。这样就能够在iPhone的Safari浏览器上面输入:
就能够直接打开这个App了。
关于其余一些常见的App,能够从iTunes里面下载到它的ipa文件,解压,显示包内容里面能够找到info.plist文件,打开它,在里面就能够相应的URL Scheme。
固然了,某些App对于调用URL Scheme比较敏感,它们不但愿其余的App随意的就调用本身。
若是待调用的App已经运行了,那么它的生命周期以下:
若是待调用的App在后台,那么它的生命周期以下:
明白了上面的生命周期以后,咱们就能够经过调用application:openURL:sourceApplication:annotation:这个方法,来阻止一些App的随意调用。
如上图,饿了么App容许经过URL Scheme调用,那么咱们能够在Safari里面调用到饿了么App。手机QQ不容许调用,咱们在Safari里面也就无法跳转过去。
关于App间的跳转问题,感兴趣的能够查看官方文档Inter-App Communication。
App也是能够直接跳转到系统设置的。好比有些需求要求检测用户有没有开启某些系统权限,若是没有开启就弹框提示,点击弹框的按钮直接跳转到系统设置里面对应的设置界面。
2. Universal Links方式
虽然在微信内部开网页会禁止全部的Scheme,可是iOS 9.0新增长了一项功能是Universal Links,使用这个功能可使咱们的App经过HTTP连接来启动App。
1.若是安装过App,无论在微信里面http连接仍是在Safari浏览器,仍是其余第三方浏览器,均可以打开App。
2.若是没有安装过App,就会打开网页。
具体设置须要3步:
1.App须要开启Associated Domains服务,并设置Domains,注意必需要applinks:开头。
2.域名必需要支持HTTPS。
3.上传内容是Json格式的文件,文件名为apple-app-site-association到本身域名的根目录下,或者.well-known目录下。iOS自动会去读取这个文件。具体的文件内容请查看官方文档。
若是App支持了Universal Links方式,那么能够在其余App里面直接跳转到咱们本身的App里面。以下图,点击连接,因为该连接会Matcher到咱们设置的连接,因此菜单里面会显示用咱们的App打开。
在浏览器里面也是同样的效果,若是是支持了Universal Links方式,访问相应的URL,会有不一样的效果。以下图:
以上就是iOS系统中App间跳转的二种方式。
从iOS 系统里面支持的URL Scheme方式,咱们能够看出,对于一个资源的访问,苹果也是用URI的方式来访问的。
统一资源标识符(英语:Uniform Resource Identifier,或URI)是一个用于标识某一互联网资源名称的字符串。 该种标识容许用户对网络中(通常指万维网)的资源经过特定的协议进行交互操做。URI的最多见的形式是统一资源定位符(URL)。
举个例子:
这是一段URI,每一段都表明了对应的含义。对方接收到了这样一串字符串,按照规则解析出来,就能获取到全部的有用信息。
这个能给咱们设计App组件间的路由带来一些思路么?若是咱们想要定义一个三端(iOS,Android,H5)的统一访问资源的方式,能用URI的这种方式实现么?
4、App内组件间路由设计
上一章节中咱们介绍了iOS系统中,系统是如何帮咱们处理App间跳转逻辑的。这一章节咱们着重讨论一下,App内部,各个组件之间的路由应该怎么设计。关于App内部的路由设计,主要须要解决2个问题:
1.各个页面和组件之间的跳转问题。
2.各个组件之间相互调用。
先来分析一下这两个问题。
1. 关于页面跳转
在iOS开发的过程当中,常常会遇到如下的场景,点击按钮跳转Push到另一个界面,或者点击一个cell Present一个新的ViewController。在MVC模式中,通常都是新建一个VC,而后Push / Present到下一个VC。可是在MVVM中,会有一些不合适的状况。
众所周知,MVVM把MVC拆成了上图演示的样子,原来View对应的与数据相关的代码都移到ViewModel中,相应的C也变瘦了,演变成了M-VM-C-V的结构。这里的C里面的代码能够只剩下页面跳转相关的逻辑。若是用代码表示就是下面这样子:
假设一个按钮的执行逻辑都封装成了command。
上述的代码自己没啥问题,可是可能会弱化MVVM框架的一个重要做用。
MVVM框架的目的除去解耦之外,还有2个很重要的目的:
代码高复用率
方便进行单元测试
若是须要测试一个业务是否正确,咱们只要对ViewModel进行单元测试便可。前提是假定咱们使用ReactiveCocoa进行UI绑定的过程是准确无误的。目前绑定是正确的。因此咱们只须要单元测试到ViewModel便可完成业务逻辑的测试。
页面跳转也属于业务逻辑,因此应该放在ViewModel中一块儿单元测试,保证业务逻辑测试的覆盖率。
把页面跳转放到ViewModel中,有2种作法,第一种就是用路由来实现,第二种因为和路由没有关系,因此这里就很少阐述,有兴趣的能够看lpd-mvvm-kit这个库关于页面跳转的具体实现。
页面跳转相互的耦合性也就体现出来了:
1.因为pushViewController或者presentViewController,后面都须要带一个待操做的ViewController,那么就必需要引入该类,import头文件也就引入了耦合性。
2.因为跳转这里写死了跳转操做,若是线上一旦出现了bug,这里是不受咱们控制的。
3.推送消息或者是3D-Touch需求,要求直接跳转到内部第10级界面,那么就须要写一个入口跳转到指定界面。
2. 关于组件间调用
关于组件间的调用,也须要解耦。随着业务愈来愈复杂,咱们封装的组件愈来愈多,要是封装的粒度拿捏不许,就会出现大量组件之间耦合度高的问题。组件的粒度能够随着业务的调整,不断的调整组件职责的划分。可是组件之间的调用依旧不可避免,相互调用对方组件暴露的接口。如何减小各个组件之间的耦合度,是一个设计优秀的路由的职责所在。
3. 如何设计一个路由
如何设计一个能完美解决上述2个问题的路由,让咱们先来看看GitHub上优秀开源库的设计思路。如下是我从Github上面找的一些路由方案,按照Star从高到低排列。依次来分析一下它们各自的设计思路。
(1)JLRoutes Star 3189
JLRoutes在整个Github上面Star最多,那就来从它来分析分析它的具体设计思路。
首先JLRoutes是受URL Scheme思路的影响。它把全部对资源的请求当作是一个URI。
首先来熟悉一下NSURLComponent的各个字段:
Note
The URLs employed by the NSURL
class are described in RFC 1808, RFC 1738, and RFC 2732.
JLRoutes会传入每一个字符串,都按照上面的样子进行切分处理,分别根据RFC的标准定义,取到各个NSURLComponent。
JLRoutes全局会保存一个Map,这个Map会以scheme为Key,JLRoutes为Value。因此在routeControllerMap里面每一个scheme都是惟一的。
至于为什么有这么多条路由,笔者认为,若是路由按照业务线进行划分的话,每一个业务线可能会有不相同的逻辑,即便每一个业务里面的组件名字可能相同,可是因为业务线不一样,会有不一样的路由规则。
举个例子:若是滴滴按照每一个城市的打车业务进行组件化拆分,那么每一个城市就对应着这里的每一个scheme。每一个城市的打车业务都有叫车,付款……等业务,可是因为每一个城市的地方法规不相同,因此这些组件即便名字相同,可是里面的功能也许千差万别。因此这里划分出了多个route,也能够理解为不一样的命名空间。
在每一个JLRoutes里面都保存了一个数组,这个数组里面保存了每一个路由规则JLRRouteDefinition里面会保存外部传进来的block闭包,pattern,和拆分以后的pattern。
在每一个JLRoutes的数组里面,会按照路由的优先级进行排列,优先级高的排列在前面。
因为这个数组里面的路由是一个单调队列,因此查找优先级的时候只用从高往低遍历便可。
具体查找路由的过程以下:
首先根据外部传进来的URL初始化一个JLRRouteRequest,而后用这个JLRRouteRequest在当前的路由数组里面依次request,每一个规则都会生成一个response,可是只有符合条件的response才会match,最后取出匹配的JLRRouteResponse拿出其字典parameters里面对应的参数就能够了。查找和匹配过程当中重要的代码以下:
举个例子:
咱们先注册一个Router,规则以下:
咱们传入一个URL,让Router进行处理。
匹配成功以后,咱们会获得下面这样一个字典:
把上述过程图解出来,见下图:
JLRoutes还能够支持Optional的路由规则,假如定义一条路由规则:
JLRoutes 会帮咱们默认注册以下4条路由规则:
(2)routable-ios Star 1415
Routable路由是用在in-app native端的 URL router, 它能够用在iOS上也能够用在Android上。
UPRouter里面保存了2个字典。routes字典里面存储的Key是路由规则,Value存储的是UPRouterOptions。cachedRoutes里面存储的Key是最终的URL,带传参的,Value存储的是RouterParams。RouterParams里面会包含在routes匹配的到的UPRouterOptions,还有额外的打开参数openParams和一些额外参数extraParams。
这一段代码里面重点在干一件事情,遍历routes字典,而后找到参数匹配的字符串,封装成RouterParams返回。
上面这段函数,第一个参数是外部传进来URL带有各个入参的分割数组。第二个参数是路由规则分割开的数组。routerComponent因为规定:号后面才是参数,因此routerComponent的第1个位置就是对应的参数名。params字典里面以参数名为Key,参数为Value。
最后经过RouterParams的初始化方法,把路由规则对应的UPRouterOptions,上一步封装好的参数字典givenParams,还有
routerParamsForUrl: extraParams: 方法的第二个入参,这3个参数做为初始化参数,生成了一个RouterParams。
最后一步self.cachedRoutes的字典里面Key为带参数的URL,Value是RouterParams。
最后将匹配封装出来的RouterParams转换成对应的Controller。
若是Controller是一个类,那么就调用allocWithRouterParams:方法去初始化。若是Controller已是一个实例了,那么就调用initWithRouterParams:方法去初始化。
将Routable的大体流程图解以下:
(3)HHRouter Star 1277
这是布丁动画的一个Router,灵感来自于 ABRouter 和 Routable iOS。
先来看看HHRouter的Api。它提供的方法很是清晰。
ViewController提供了2个方法。map是用来设置路由规则,matchController是用来匹配路由规则的,匹配争取以后返回对应的UIViewController。
block闭包提供了三个方法,map也是设置路由规则,matchBlock:是用来匹配路由,找到指定的block,可是不会调用该block。callBlock:是找到指定的block,找到之后就当即调用。
matchBlock:和callBlock:的区别就在于前者不会自动调用闭包。因此matchBlock:方法找到对应的block以后,若是想调用,须要手动调用一次。
除去上面这些方法,HHRouter还为咱们提供了一个特殊的方法。
这个方法就是用来找到执行路由规则对应的RouteType,RouteType总共就3种:
再来看看HHRouter是如何管理路由规则的。整个HHRouter就是由一个NSMutableDictionary *routes控制的。
别看只有这一个看似“简单”的字典数据结构,可是HHRouter路由设计的仍是很精妙的。
上面两个方法分别是block闭包和ViewController设置路由规则调用的方法实体。无论是ViewController仍是block闭包,设置规则的时候都会调用subRoutesToRoute:方法。
上面这段函数就是来构造路由匹配规则的字典。
举个例子:
设置3条规则之后,按照上面构造路由匹配规则的字典的方法,该路由规则字典就会变成这个样子:
路由规则字典生成以后,等到匹配的时候就会遍历这个字典。
假设这时候有一条路由过来:
HHRouter对这条路由的处理方式是先匹配前面的scheme,若是连scheme都不正确的话,会直接致使后面匹配失败。
而后再进行路由匹配,最后生成的参数字典以下:
具体的路由参数匹配的函数在
这个方法里面实现的。这个方法就是按照路由匹配规则,把传进来的URL的参数都一一解析出来,带?号的也都会解析成字典。这个方法没什么难度,就不在赘述了。
ViewController 的字典里面默认还会加上2项:
route里面都会保存传过来的完整的URL。
若是传进来的路由后面带访问字符串呢?那咱们再来看看:
那么解析出全部的参数字典会是下面的样子:
同理,若是是一个block闭包的状况呢?
仍是先添加一条block闭包的路由规则:
这条规则对应的会生成一个路由规则的字典。
注意”_”后面跟着是一个block。
匹配block闭包的方式有两种。
匹配出来的参数字典是以下:
block的字典里面会默认加上下面这2项:
route里面都会保存传过来的完整的URL。
生成的参数字典最终会被绑定到ViewController的Associated Object关联对象上。
这个绑定的过程是在match匹配完成的时候进行的。
最终获得的ViewController也是咱们想要的。相应的参数都在它绑定的params属性的字典里面。
将上述过程图解出来,以下:
(4)MGJRouter Star 633
这是蘑菇街的一个路由的方法。
这个库的由来:
JLRoutes 的问题主要在于查找 URL 的实现不够高效,经过遍历而不是匹配。还有就是功能偏多。
HHRouter 的 URL 查找是基于匹配,因此会更高效,MGJRouter 也是采用的这种方法,但它跟 ViewController 绑定地过于紧密,必定程度上下降了灵活性。
因而就有了 MGJRouter。
从数据结构来看,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会匹配成功,那么生成的参数字典结构以下:
5.能够统一管理URL
这个功能很是有用。
URL 的处理一不当心,就容易散落在项目的各个角落,不容易管理。好比注册时的 pattern 是 mgj://beauty/:id,而后 open 时就是 mgj://beauty/123,这样到时候 url 有改动,处理起来就会很麻烦,很差统一管理。
因此 MGJRouter 提供了一个类方法来处理这个问题。
generateURLWithPattern:函数会对咱们定义的宏里面的全部的:进行替换,替换成后面的字符串数组,依次赋值。
将上述过程图解出来,以下:
蘑菇街为了区分开页面间调用和组件间调用,因而想出了一种新的方法。用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 Star 803
再来讲说@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的一个方案。
Uber在发现MVC的一些弊端以后:好比动辄上万行巨胖无比的VC,没法进行单元测试等缺点后,因而考虑把架构换成VIPER。可是VIPER也有必定的弊端。由于它的iOS特定的结构,意味着iOS必须为Android作出一些妥协的权衡。以视图为驱动的应用程序逻辑,表明应用程序状态由视图驱动,整个应用程序都锁定在视图树上。由操做应用程序状态所关联的业务逻辑的改变,就必须通过Presenter。所以会暴露业务逻辑。最终致使了视图树和业务树进行了牢牢的耦合。这样想实现一个牢牢只有业务逻辑的Node节点或者牢牢只有视图逻辑的Node节点就很是的困难了。
经过改进VIPER架构,吸取其优秀的特色,改进其缺点,就造成了Uber 骑手App的全新架构——Riblets(肋骨)。
在这个新的架构中,即便是类似的逻辑也会被区分红很小很小,相互独立,能够单独进行测试的组件。每一个组件都有很是明确的用途。使用这些一小块一小块的Riblets(肋骨),最终把整个App拼接成一颗Riblets(肋骨)树。
经过抽象,一个Riblets(肋骨)被定义成一下6个更小的组件,这些组件各自有各自的职责。经过一个Riblets(肋骨)进一步的抽象业务逻辑和视图逻辑。
一个Riblets(肋骨)被设计成这样,那和以前的VIPER和MVC有什么区别呢?最大的区别在路由上面。
Riblets(肋骨)内的Router再也不是视图逻辑驱动的,如今变成了业务逻辑驱动。这一重大改变就致使了整个App再也不是由表现形式驱动,如今变成了由数据流驱动。
每个Riblet都是由一个路由Router,一个关联器Interactor,一个构造器Builder和它们相关的组件构成的。因此它的命名(Router - Interactor - Builder,Rib)也由此得来。固然还能够有可选的展现器Presenter和视图View。路由Router和关联器Interactor处理业务逻辑,展现器Presenter和视图View处理视图逻辑。
重点分析一下Riblet里面路由的职责。
1.路由的职责
在整个App的结构树中,路由的职责是用来关联和取消关联其余子Riblet的。至于决定是由关联器Interactor传递过来的。在状态转换过程当中,关联和取消关联子Riblet的时候,路由也会影响到关联器Interactor的生命周期。路由只包含2个业务逻辑:
提供关联和取消关联其余路由的方法。
在多个孩子之间决定最终状态的状态转换逻辑。
2.拼装
每个Riblets只有一对Router路由和Interactor关联器。可是它们能够有多对视图。Riblets只处理业务逻辑,不处理视图相关的部分。Riblets能够拥有单一的视图(一个Presenter展现器和一个View视图),也能够拥有多个视图(一个Presenter展现器和多个View视图,或者多个Presenter展现器和多个View视图),甚至也能够能没有视图(没有Presenter展现器也没有View视图)。这种设计能够有助于业务逻辑树的构建,也能够和视图树作到很好的分离。
举个例子,骑手的Riblet是一个没有视图的Riblet,它用来检查当前用户是否有一个激活的路线。若是骑手肯定了路线,那么这个Riblet就会关联到路线的Riblet上面。路线的Riblet会在地图上显示出路线图。若是没有肯定路线,骑手的Riblet就会被关联到请求的Riblet上。请求的Riblet会在屏幕上显示等待被呼叫。像骑手的Riblet这样没有任何视图逻辑的Riblet,它分开了业务逻辑,在驱动App和支撑模块化架构起了重大做用。
3.Riblets是如何工做的
Riblet中的数据流
在这个新的架构中,数据流动是单向的。Data数据流从service服务流到Model Stream生成Model流。Model流再从Model Stream流动到Interactor关联器。Interactor关联器,scheduler调度器,远程推送均可以想Service触发变化来引发Model Stream的改动。Model Stream生成不可改动的models。这个强制的要求就致使关联器只能经过Service层改变App的状态。
举两个例子:
1.数据从后台到视图View上
一个状态的改变,引发服务器后台触发推送到App。数据就被Push到App,而后生成不可变的数据流。关联器收到model以后,把它传递给展现器Presenter。展现器Presenter把model转换成view model传递给视图View。
2.数据从视图到服务器后台
当用户点击了一个按钮,好比登陆按钮。视图View就会触发UI事件传递给展现器Presenter。展现器Presenter调用关联器Interactor登陆方法。关联器Interactor又会调用Service call的实际登陆方法。请求网络以后会把数据pull到后台服务器。
Riblet间的数据流
当一个关联器Interactor在处理业务逻辑的工程中,须要调用其余Riblet的事件的时候,关联器Interactor须要和子关联器Interactor进行关联。见上图5个步骤。
若是调用方法是从子调用父类,父类的Interactor的接口一般被定义成监听者listener。若是调用方法是从父类调用到子类,那么子类的接口一般是一个delegate,实现父类的一些Protocol。
在Riblet的方案中,路由Router仅仅只是用来维护一个树型关系,而关联器Interactor才担当的是用来决定触发组件间的逻辑跳转的角色。
5、各个方案优缺点
通过上面的分析,能够发现,路由的设计思路是从URLRoute ->Protocol-class ->Target-Action一步步的深刻的过程。这也是逐渐深刻本质的过程。
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四处,维护起来实在有点麻烦,虽然蘑菇街想到了用宏统一管理这些连接,可是仍是解决不了硬编码的问题。
真正一个好的路由是在无形当中服务整个App的,是一个无感知的过程,从这一点来讲,略有点缺失。
最后一个缺点是,对于传递NSObject的参数,URL是不够友好的,它最可能是传递一个字典。
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的方法。这种把一个功能所有都划分红登陆组件的话,划分粒度就稍微粗一点。
若是仅仅把登陆状态的细小功能划分红一个元组件,那么外面想获取登陆状态就直接调用这个组件就好。这种划分的粒度就很是细了。这样就会致使组件个数巨多。
因此在进行拆分组件的时候,也许当时业务并不复杂的时候,拆分红组件,相互耦合也不大。可是随着业务无论变化,以前划分的组件间耦合性愈来愈大,因而就会考虑继续把以前的组件再进行拆分。也许有些业务砍掉了,以前一些小的组件也许还会被组合到一块儿。总之,在业务没有彻底固定下来以前,组件的划分可能一直进行时。
6、最好的方案
关于架构,我以为抛开业务谈架构是没有意义的。由于架构是为了业务服务的,空谈架构只是一种理想的状态。因此没有最好的方案,只有最适合的方案。
最适合本身公司业务的方案才是最好的方案。分而治之,针对不一样业务选择不一样的方案才是最优的解决方案。若是非要笼统的采用一种方案,不一样业务之间须要同一种方案,须要妥协牺牲的东西太多就很差了。
但愿本文能抛砖引玉,帮助你们选择出最适合自家业务的路由方案。固然确定会有更加优秀的方案,但愿你们能多多指点我。
References: