本文首发地址:开源实践网:大前端通用路由设计原理篇(一)
一、什么是路由?
在我们整个大前端(包含前端,安卓,iOS)的开发中,经常会遇到『路由』的概念。那么,到底什么是路由?简单来说,路由就是URL到函数的映射。
首先我们来学习三个单词(route,routes,router):
route:首先它是个单数,译为路由,即我们可以理解为单个路由或者某一个路由;
routes:它是个复数,表示多个的集合才能为复数;即我们可以理解为多个路由的集合,JS中表示多种不同状态的集合的形式只有数组和对象两种,事实上官方定义routes是一个数组;所以我们记住了,routes表示多个数组的集合;
router:译为路由器,上面都是路由,这个是路由器,我们可以理解为一个容器包含上述两个或者说它是一个管理者,负责管理上述两个;举个常见的场景的例子:当用户在页面上点击按钮的时候,这个时候router就会去routes中去查找route,就是说路由器会去路由集合中找对应的路由;
二、路由的历史和各端路由简介
简单举例说明,假如我们有一台提供 Web 服务的服务器的网络地址是:10.0.0.1,而该 Web 服务又提供了三个可供用户访问的页面,其页面 URI 分别是:
http://10.0.0.1/
http://10.0.0.1/ about
http://10.0.0.1/ concat
那么其路径就分别是 /,/about,/concat。
当用户使用 http://10.0.0.1/ about 来访问该页面时,Web 服务会接收到这个请求,然后会解析 URL 中的路径 /about,在 Web 服务的程序中,该路径对应着相应的处理逻辑,程序会把请求交给路径所对应的处理逻辑,这样就完成了一次「路由分发」,这个分发就是通过「路由」来完成的。
以前路由都是后台做的,通过用户请求的url导航到具体的html页面,前端路由就是通过配置js文件,把这个工作拿到前端来做。
当时的服务端路由,就是是根据不同的 url 地址展示不同的内容或页面
2.1、前端的路由
随着前端页面越来越复杂,并且伴随着前端的工程化发展,前端SPA(web单页面应用)的使用,较为流行的前端框架将路由也搬到前端中,这里我重点介绍一下vue中的路由
Vue Router 是 Vue.js 官方的路由管理器。它和 Vue.js 的核心深度集成,让构建单页面应用变得易如反掌。
在Vue Router 中,单个route被定义为path和component的映射
const routes = [
{path:'/page1',component:page1},
{path:"/page2",component:page2}
]
<template>
<div id="app">
<img src="./assets/logo.png">
<div>
//router-link定义页面中点击触发部分
<router-link to="/page1">Page1</router-link>
<router-link to="/page2">Page2</router-link>
</div>
//router-view定义页面中显示部分
<router-view></router-view>
</div>
</template>
<script>
export default {
name: 'App'
}
</script>
如上图所示,用户输入url,经过Vue Router的路由解析出对应的视图加载(上面代码中component中指定)后渲染,用户在页面中点击<router-link>标签,又通过路由解析加载对应的页面形成一个功能上的闭环
Vue Router就是通过上图所示的流程,将前端的各个功能页面封装为单独的component(也就是视图),然后通过path来跳转指定的功能页面。
2.2、安卓中的路由
提到安卓中的路由,就不得不说大名鼎鼎,出身名门(阿里巴巴开源)的ARouter
ARouter的核心功能:
支持直接解析标准URL进行跳转,并自动注入参数到目标页面中
支持多模块工程使用
支持添加多个拦截器,自定义拦截顺序
支持依赖注入,可单独作为依赖注入框架使用
...等等(略很多吊炸天的功能,具体参考文档https://github.com/alibaba/ARouter/blob/master/README_CN.md)
相信安卓的大佬已经很熟悉,这边简单的描述一下流程
1.首先是router的声明
// 在支持路由的页面上添加注解(必选)
// 这里的路径需要注意的是至少需要有两级,/xx/xx
@Route(path = "/test/activity")
public class YourActivity extend Activity {
...
}
2.然后是 router调用
// 1. 应用内简单的跳转(通过URL跳转在'进阶用法'中)
ARouter.getInstance().build("/test/activity").navigation();
// 2. 跳转并携带参数
ARouter.getInstance().build("/test/1")
.withLong("key1", 666L)
.withString("key3", "888")
.withObject("key4", new Test("Jack", "Rose"))
.navigation();
3.高级功能 通过依赖注入解耦:服务管理(一) 暴露服务
// 声明接口,其他组件通过接口来调用服务
public interface HelloService extends IProvider {
String sayHello(String name);
}
// 实现接口
@Route(path = "/yourservicegroupname/hello", name = "测试服务")
public class HelloServiceImpl implements HelloService {
@Override
public String sayHello(String name) {
return "hello, " + name;
}
@Override
public void init(Context context) {
}
}
4.高级功能 通过依赖注入解耦:服务管理(二) 发现服务
public class Test {
@Autowired
HelloService helloService;
@Autowired(name = "/yourservicegroupname/hello")
HelloService helloService2;
HelloService helloService3;
HelloService helloService4;
public Test() {
ARouter.getInstance().inject(this);
}
public void testService() {
// 1. (推荐)使用依赖注入的方式发现服务,通过注解标注字段,即可使用,无需主动获取
// Autowired注解中标注name之后,将会使用byName的方式注入对应的字段,不设置name属性,会默认使用byType的方式发现服务(当同一接口有多个实现的时候,必须使用byName的方式发现服务)
helloService.sayHello("Vergil");
helloService2.sayHello("Vergil");
}
}
综上,安卓中的Aouter是一个功能强大的路由,他通过注解和依赖注入的方式,提供了页面跳转,和调用服务的方法。
2.3 iOS中的路由
iOS中的路由,JLRoutes在Github上面Star最多,下面我就它为例,讲解iOS中的路。
1.JLRoutes是如何做到URL到方法的映射呢 ?
首先,JLRoutes会有一个全局的字典,key是URL,value是对应的block(ps:就是代码片段,类似匿名函数),当打开一个URL时,JLRoutes 就可以遍历这个全局的字典,通过 URL 来执行对应的 block。具体层次图如下
具体理解:
- routeControllersMap 是全局的单例可变字典
- 这个字典的 key 值对应一个标识,源码中称之为 scheme,为了不混淆,咱们就叫其为 JLRoutes 对象标识。这个标识对应的value 值为 routesController(JLRoutes类的对象:JLRoutes *routesController)。
- JLRoutes的对象(routesController)有很多属性,常用的有两个属性:
- NSString *scheme:也就是上面所说的 JLRoutes对象标识,也就是说,此 value 值记录了自己的 key 值。
NSMutableArray *routes:此数组中存放了JLRRouteDefinition 对象。 - JLRRouteDefinition 对象为最终的具体模型,也就是说你注册的跳转逻辑的所有信息,都存在于这个模型中,包括要实施操作的handlerBlock(执行操作的block代码块)、scheme(JLRoutes对象标识)、pattern(模式)、priority(优先级)。
2.JLRoutes的注册
// 全局JLRoutes注册
[[JLRoutes globalRoutes] addRoute:@"取url内容值的标识" handler:^BOOL(NSDictionary<NSString *,id> * _Nonnull parameters) {
return YES; // 一旦匹配,立即返回 YES
}];
// 自定义命名空间注册
[[JLRoutes routesForScheme:@"第一模块的标识"] addRoute:@"取url内容值的标识" handler:^BOOL(NSDictionary<NSString *,id> * _Nonnull parameters) {
return YES; // 一旦匹配,立即返回 YES
}];
// 定义优先级注册
[[JLRoutes globalRoutes] addRoute:@"取url内容值的标识" priority:1 handler:^BOOL(NSDictionary<NSString *,id> * _Nonnull parameters) {
return YES; // 一旦匹配,立即返回 YES
}];
3.JLRoutes的调用
- (BOOL)application:(UIApplication *)app openURL:(nonnull NSURL *)url options:(nonnull NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
NSLog(@"options: %@", options);
NSLog(@"Calling Application Bundle ID: %@", [options objectForKey:@"UIApplicationOpenURLOptionsSourceApplicationKey"]);
NSLog(@"URL scheme: %@", [url scheme]);
NSLog(@"URL host : %@", [url host]);
NSLog(@"URL query: %@", [url query]);
// 从浏览器打开时候会自动全部转成小写,而从应用内调用的话大小写不会变化
// 为了方便判断所以统一转成小写来判断
NSString *urlSchemeStr = [[url scheme] lowercaseString]; // url scheme 转换为小写的字符串
NSLog(@"urlSchemeStr: %@",urlSchemeStr);
if ([urlSchemeStr isEqualToString:@"jlrouteschemeone"]) {
// 要和 info.plist 的 URL types 里面的一致
return [[JLRoutes routesForScheme:@"JLRouteSchemeOne"]routeURL:url];
} else if ([urlSchemeStr isEqualToString:@"jlrouteschemetwo"]) {
// 要和 info.plist 的 URL types 里面的一致
return [[JLRoutes routesForScheme:@"JLRouteSchemeTwo"]routeURL:url];
}
return YES;
}
4.JLRoutes的跳转
- (void)clickBtn {
NSString *customURL = @"JLRouteSchemeOne://OneDetailViewController";
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:customURL]];
}
- (void)clickBtn {
NSString *customURL = @"JLRouteSchemeTwo://TwoDetailViewController";
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:customURL]];
}
到这里,我们已经已经将前端,安卓,iOS上路由实现的具体的原理和对应具体的代码做了一些简单的讲解。可以看到,在前端和安卓平台上,对应的路由组件功能比强大,而在iOS这边,路由功能只剩下了URL到block的映射,所以我这边会从零开始实现一个iOS端的路由组件,来讲解我这次的主题。
三、我们需要路由解决什么问题?
1.页面之间的跳转问题
随着App越做越大,里面的页面越来越多,并且在天朝,有些超级app甚至包含了自己都不能列举完的功能页面,所以但我们在进行页面跳转的时候,会有以下比较难处理的问题。
2 页面之间的耦合问题
在iOS中,点击按钮跳转Push到另外一个界面,或者点击一个cell Present一个新的ViewController。我们的做法,一般都是新建一个VC,然后Push / Present到下一个VC。但我们新建一个ViewController的时候,我们就需要引入该对象,当我们的页面跳转比较复杂的时候,就会出现上图中A,B,C, D互相耦合的情况。那我们如何做到通过一个路由中心,跳转到任意app界面呢?
// ViewControllerA代码
#improt "ViewControllerB" // 这里对ViewControllerB进行了依赖
...
- (void)btnClick:(UIButton)btn {
ViewControllerB *bVc = [[ViewControllerB alloc] init];
[self.navigationController pushViewController:targetViewController animated:YES];
}
3 页面入口不收敛的问题
当我们使用上面代码跳转方式,整个app中就会存在大量的类似的页面跳转代码,并且业务还会别出心裁的各种风骚的页面跳转,使用不同的方法初始化vc,设置vc不同的属性,当我们改动页面的逻辑时,我们恰巧忘记兼容某个方法,并且恰巧qa没有测到,就这样一个新鲜的bug就这样巧无声息的跑到了线上,然后结果大家懂的都懂,无需我多言。那么,我们如何做到统一页面的跳转逻辑,做到每个页面有一个统一的入口呢?
4 3D-Touch功能或者点击推送消息,要求外部跳转到App内部一个很深层次的一个界面
比如微信的3D-Touch可以直接跳转到“我的二维码”。“我的二维码”界面在我的里面的第三级界面。或者再极端一点,产品需求给了更加变态的需求,要求跳转到App内部第十层的界面,怎么处理?
5 统一安卓和IOS甚至整个大前端的跳转逻辑
项目里面某些模块会混合ReactNative,Weex,H5界面,这些界面还会调用Native的界面,以及Native的组件。那么,如何能统一Web端和Native端请求资源的方式?当我们在手机上浏览h5页面,如何做到每个h5页面都能跳转到app内部相同的功能页面?
6 如果App出现bug了,如何不用JSPatch,就能做到简单的热修复功能?
比如公司App上线突然遇到了紧急bug,能否把页面动态降级成H5,ReactNative,Weex?或者是直接换成一个本地的错误界面?
7 如何解决app组件化后,同层功能组件页面跳转的问题
app组件化,会将app分拆成各个功能模块,比如美团中的外面和电影模块,但是从层级上说,外面和电影模块属于同一层的功能模块,不存在谁依赖谁,所以外卖和电影如果模块化以后,都不能调用对方,但是如果产品提了从外面跳转到电影卖票界面,如何优雅的解决?使用rumtime跳转?
四、使用路由的缺点?如何避免?
缺点1 路由解决了依赖问题,却没有编译检查优势
原因: 路由的最核心的功能点之一就是实现模块之间的依赖解耦,势必是会以track(注:iOS中使用runtime)的方式跳过编译的检查,但是这样就不会有编译上的检查,如果这个调用模块不存在,势必会有异常。
解决思路: 在app的debug模式下,对路由调研的所有方法进行检查,如果没有该模块,使用断言尽早反馈给调用方
缺点2 需要将路由模块进行注册,以便路由识别调用
原因: 路由调用对应的组件,肯定路由需要知道组件,不然无法调用,所以需要注册
解决思路: 在实现上,使用方便使用的宏,或者app启动自动注入的方式,让组件的注册流程对业务透明
缺点3 需要集成路由组件,对业务有侵入
原因: 因为路由组件涉及到页面跳转和方法调用,所以不可避免的需要业务的支持或者改造。
解决思路: 学习java注解的方式,使用宏定义注册方法,尽可能地减少业务的接入成本
五、总结,我们的目标是
在native(包含安卓,iOS)和前端(h5)上实现一套通用的路由逻辑,能够实现跟平台无关的页面调用,将整个大前端的页面彻底统一起来,实现无缝调用