大前端通用路由设计原理篇(一)

本文首发地址:开源实践网:大前端通用路由设计原理篇(一)

一、什么是路由?

在我们整个大前端(包含前端,安卓,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。具体层次图如下

JLRoutes 层次解析图

具体理解:

  1. routeControllersMap 是全局的单例可变字典
  2. 这个字典的 key 值对应一个标识,源码中称之为 scheme,为了不混淆,咱们就叫其为 JLRoutes 对象标识。这个标识对应的value 值为 routesController(JLRoutes类的对象:JLRoutes *routesController)。
  3. JLRoutes的对象(routesController)有很多属性,常用的有两个属性:
  4. NSString *scheme:也就是上面所说的 JLRoutes对象标识,也就是说,此 value 值记录了自己的 key 值。
    NSMutableArray *routes:此数组中存放了JLRRouteDefinition 对象。
  5. 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)上实现一套通用的路由逻辑,能够实现跟平台无关的页面调用,将整个大前端的页面彻底统一起来,实现无缝调用

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

推荐阅读更多精彩内容