乐趣区

关于前端:产品经理点这里我要跳到任何我想跳的页面-解耦提效神器统跳路由



1. 背景

咱们晓得前端畛域以路由来定位页面,要跳转到对应页面只需拜访对应路由即可,非常不便。可始终以来 iOS 畛域没有路由这个概念,跳转一个页面须要创立出指标页面的实例,而后通过导航控制器进行跳转,非常繁琐。有这么个需要:当点击推送音讯 (或点击某个区域) 时须要跳转到任意一个可能的页面。乍一听我是抗拒的,但这个需要仿佛又荒诞不经。作为一群有谋求的开发者咱们正式启动了 百瓶统跳路由 我的项目。

统跳:即通过统跳路由 SDK 的 open() 办法,达到跳转至任意页面的目标(指标页面实现形式反对但不限于 Native、Flutter、HTML5、微信小程序、零碎利用和其余第三方利用)。

2. 解决哪些问题

依照常规,启动一个我的项目咱们先梳理痛点,整顿出要解决的问题,进而确定需要。

2.1 页面跳转

页面跳转只需提供目标页面路由给统跳路由 SDK,统跳路由 SDK 就能准确无误地跳转到目标页面,而不是像传统形式那样创立出页面实例后通过导航控制器跳转。

2.2 入参携带

页面跳转时须要携带一些参数给指标页面实例,以供指标页面正确处理逻辑。

2.3 返回值回传

有时咱们须要在指标页面敞开时回传一些咱们感兴趣的值。如:抉择收货地址页面,在抉择完地址后须要把刚选中的地址信息回传给上一个页面。

2.4 回退到指定路由

有时咱们须要回退到指定的页面。以公布短视频为例:首先从首页(M)进入视频拍摄页面(A),拍摄实现进入视频编辑页面(B),编辑实现进入发布页面(C),公布完视频应回退到进入拍摄页面(A)之前的页面(M)而不是视频编辑页面(B)。

2.5 对立的路由规定

基本思路

  • 一个路由对应一个页面
  • 路由应是一个有意义的字符串
  • 路由规定应实用于各端
  • 各端应把路由与页面做绑定

计划

由以上思路很容易想到咱们的路由标准沿用前端路由标准即可,既合乎 Restful 的 URI。

既:URI = scheme:[//authority]path[?query][#fragment]

参见:RFC3986

咱们给不同畛域的路由定义了对应的 scheme,即 Native: native,Flutter:flutter,http/https:http/https,小程序:wxmp,三方利用:tp(third party)等。

2.6 跨模块 API 调用

有时咱们须要像向 WebServer 发动 GET 申请的形式一样拜访其余模块提供的办法。传统模式下,跨模块办法互调须要援用指标模块,很繁琐,有时还会产生循环援用。

2.7 行为路由

有时咱们须要为页面某个区域配置点击行为(非页面跳转行为)。如:有个 HTML5 页面须要在点击页面上某个区域时调起 Native 的分享行为。

2.8 路由拦截器

有时咱们须要对路由进行鉴权、参数重整、打断点、重定向等需要。因而为统跳路由 SDK 增加拦截器性能是一个很好的解决方案。

2.9 分组路由拦截器

有时咱们须要对某组路由进行鉴权、参数重整、打断点、防沉迷等需要。因而咱们为统跳路由 SDK 增加分组拦截器性能。

2.10 路由重定向

随着一直迭代和技术的更新,很多页面会被其余更适合的技术重写,此时历史代码还在用老路由,如果要把老的代码都改为新路由则要思考版本控制和改变老本以及可能引入的危险。有了重定向性能则能够无老本切入新路由。

2.11 不同技术栈之间互不烦扰(放弃优雅)

当把一个路由传入统跳路由 SDK 时,统跳路由 SDK 应能辨别把以后路由散发至哪个路由调度器进行调度。如:Native、Flutter、HTML5 各自实现的页面应交给各自的路由调度器。

3. 提供哪些 API

上面是 BBRouter 要害 API 的设计


NS_ASSUME_NONNULL_BEGIN

@interface BBRouter : NSObject <BBNativeRouter, BBBlockRouter>

@property (nonatomic, strong, class, readonly) BBRouterConfig *routerConfig;

/// 设置跳转未定义路由时的对立回调
/// @param undefinedRouteHandle 未定义路由时的对立回调
+ (void)setUndefinedRouteHandle:(void (^)(BBRouterParameter *))undefinedRouteHandle;

/// 设置将要关上指定页面的回调
+ (void)setWillOpenBlock:(void (^)(BBRouterParameter *))willOpenBlock;

/// 设置曾经关上指定页面的回调
+ (void)setDidOpenBlock:(void (^)(BBRouterParameter *))didOpenBlock;

#pragma mark - 注册路由调度器 Dispatcher

/// 注册路由调度器
/// @param dispatcher 调度器
/// @param scheme scheme
+ (BOOL)registerRouterDispatcher:(id<BBRouterDispatcher>)dispatcher scheme:(NSString *)scheme;

#pragma mark - BBBlockRouter
/// 注册路由的实现 block
/// @param path 路由
/// @param action 实现
+ (BOOL)registerTask:(NSString *)path action:(BBBlockDispatcherAction)action;

/// 移除已注册的 block
/// @param path 对应的路由
+ (BOOL)removeTask:(NSString *)path;

#pragma mark - 路由跳转 API
/// 路由到指定页面(该办法为底层办法,不倡议间接应用,请应用上面的👇便捷办法)/// @param parameter 参数
+ (void)routeWithRouterParameter:(BBRouterParameter *)parameter;

#pragma mark - 曾经存在 URL 的状况 页面跳转

/// 判断是否关上指定 URI
/// @param url 页面 URI
+ (BOOL)canOpen:(NSString *)url;

/// 关上页面
/// @param url 页面 URI
+ (void)open:(NSString *)url;

/// 关上页面并携带参数
/// @param url 页面 URI
/// @param urlParams 携带的参数 json 可序列化的数据类型(当 scheme 为 native 时 能够传递简单数据结构)+ (void)open:(NSString *)url urlParams:(NSDictionary * __nullable)urlParams;

/// 关上页面并携带参数且反对数据回传
/// @param url url
/// @param urlParams 携带的参数 json 可序列化的数据类型(当 scheme 为 native 时 能够传递简单数据结构)/// @param resultCallback 当须要回调后果时 通过该回调 block 实现
+ (void)open:(NSString *)url urlParams:(NSDictionary * __nullable)urlParams onPageFinished:(void (^ __nullable)(NSDictionary *result))resultCallback;

/// 关上页面并携带参数且反对数据回传
/// @param url url
/// @param urlParams 携带的参数 json 可序列化的数据类型(当 scheme 为 native 时 能够传递简单数据结构)/// @param exts 额定参数
/// @param resultCallback 当须要回调后果时通过该回调 block 实现
+ (void)open:(NSString *)url urlParams:(NSDictionary * __nullable)urlParams exts:(NSDictionary * __nullable)exts onPageFinished:(void (^ __nullable)(NSDictionary *result))resultCallback;

/// 关上页面并携带参数且反对数据回传
/// @param url url
/// @param urlParams 携带的参数 json 可序列化的数据类型(当 scheme 为 native 时 能够传递简单数据结构)/// @param exts 额定参数 animated: 是否有过渡动画
/// @param routerStyle 过渡动画
/// @param resultCallback 当须要回调后果时通过该回调 block 实现
+ (void)open:(NSString *)url urlParams:(NSDictionary * __nullable)urlParams exts:(NSDictionary * __nullable)exts routerStyle:(KBBRouterStyle)routerStyle  onPageFinished:(void (^ __nullable)(NSDictionary *result))resultCallback;

#pragma - mark BBNativeRouter

/// 通过类注册视图控制器,path 为标识
+ (BOOL)registerClass:(Class)cls withPath:(NSString *)path;

/// 通过类名注册视图控制器 SDK,path 为标识
+ (BOOL)registerWithClassName:(NSString *)className andPath:(NSString *)path;

/// 通过类名注册视图控制器,path 为标识,确认参数是否匹配该路由
+ (BOOL)registerWithClassName:(NSString *)className andPath:(NSString *)path verifyBlock:(BOOL(^ __nullable)(NSString *path ,BBRouterParameter *routerParameter))verifyBlock;

/// 删除注册的视图控制器
+ (BOOL)removeRegisteredPath:(NSString *)path;

#pragma mark - BlockDispatcher 相干实现

/// 执行已注册的 block,同步返回
/// @param url url
/// @param urlParams 入参
+ (id _Nullable)invokeTaskWithUrl:(NSString *)url urlParams:(NSDictionary *)urlParams;

/// 执行已注册的 block,同步返回
/// @param url url
/// @param urlParams 入参
/// @param error 出错信息
+ (id _Nullable)invokeTaskWithUrl:(NSString *)url urlParams:(NSDictionary *)urlParams error:(NSError **)error;

#pragma mark - 分组

/// 增加 path 到指定分组
/// @param path 路由门路
/// @param group 分组名称
+ (BOOL)addPath:(NSString *)path toGroup:(NSString *)group;

/// 增加一组门路到指定分组
/// @param paths 门路分组
/// @param group 分组名称
+ (void)addPaths:(NSArray<NSString *> *)paths toGroup:(NSString *)group;

/// 指定分组下所有路由
/// @param group 分组名
+ (NSArray<NSString *> *)pathsInGroup:(NSString *)group;

/// 配置分组的回调函数 用以解决分组逻辑
/// @param group 分组名称
/// @param verifyBlock 回调闭包
+ (void)configGroup:(NSString *)group verifyBlock:(BOOL(^ __nullable)(NSString *path ,BBRouterParameter *routerParameter))verifyBlock;

/// 回退
+ (UIViewController * _Nullable)backwardCompletion:(void (^ __nullable)(void))completion;

/// 回退
/// @param animated 是否动画
+ (UIViewController * _Nullable)backwardAnimated:(BOOL)animated completion: (void (^ __nullable)(void))completion;

/// 回退,无动画
/// @param count 回退级数
+ (void)backwardCount:(NSInteger)count completion: (void (^ __nullable)(void))completion;

/// 关上指定新视图控制器
/// @param vc 新的视图控制器
+ (void)openVC:(UIViewController *)vc routerParameter:(BBRouterParameter *)parameter;

/// 以后是否存在路由指定的视图控制器实例
/// @param path 路由门路
+ (UIViewController * _Nullable)containsRouteObjectByPath:(NSString *)path;

/// 返回到指定页面的上一级
/// @param vc 指定的视图控制器实例
/// @param animatedBlock 是否须要动画
/// @param completion 实现
+ (UIViewController * _Nullable)backwardVC:(UIViewController *)vc animatedBlock:(BOOL(^)(NSString *toppath))animatedBlock completion: (void (^__nullable)(void))completion;
@end

NS_ASSUME_NONNULL_END

3.1 注册 / 移除路由调度器

路由调度器用来隔离不同畛域的路由,便于解除耦合。开发者应用 统跳路由 SDK 时能够十分不便的定义本人的路由调度器,实现本人的路由逻辑。

Native 页面、Flutter 页面和 HTML5 页面跳转逻辑必定存在差别,此时应有对应的路由调度器实现跳转行为。当然「行为路由」也有对应的路由调度器。

总之你能够施展设想,纵情施展 统跳路由 SDK 的能力,你只需定义一个适宜你的路由调度器。

3.2 注册 / 移除路由与页面的绑定关系

咱们须要把路由与页面的绑定关系注册给 统跳路由 SDK 以容许 统跳路由 SDK 对路由进行动静解析,动静生成页面实例并实现主动跳转。

应该留神:这个注册应该容许更新,以实现路由表动静更新。

注册的不肯定非要是一个页面,也能够是某个服务(Service)。如:指标页面是某个第三方提供的,只能通过调用对应 SDK 的某个办法关上,想间接注册页面是做不到的。此时咱们能够注册一个服务做直达,在该服务被调用时,咱们再调用 SDK 的对应办法即可轻松实现以上需要。

3.3 关上某个路由并获取回传值

统跳路由 SDK 应提供一个办法关上某个页面(或调用某个办法),并提供获取返回值的回调。

入参应有以下几个

  • uri:路由
  • parameters: 要携带的入参
  • exts:其余参数(非业务参数,如:指定转场动画形式)
  • callback:回调函数指针

3.4 回退到上个页面

统跳路由 SDK 应提供返回上个页面的办法。

3.5 回退到指定页面

统跳路由 SDK 应提供回退到回退栈内指定路由的办法并返回指定路由的实例。
统跳路由 SDK 应提供回退 N 层路由的办法。

3.6 路由未找到的解决

当音讯推送了新版本特有的页面时,老版本应进入路由未找到的对立进口,能够在此处做一个重定向或提醒用户降级到最新版本的操作。

4. 要害思路和标准

需要咱们曾经理顺了,紧接着就是设计 统跳路由 SDK 的架构。

4.1 如何实现高可扩大

正当形象和性能拆分是实现高可扩大的根底。

咱们设计了路由调度器这个形象的概念,路由调度器用来隔离不同畛域的路由。

Native 路由、Flutter 路由、HTML5 路由、小程序路由等,别离有对应的路由调度器实现调度。行为路由 也由对应的路由调度器实现调度。

4.2 如何防止侵入和耦合

统跳路由 SDK 立项时咱们的我的项目曾经有了肯定规模,如果接入 统跳路由 SDK 须要批改已有业务代码则无疑是个劫难。因而咱们必须完满兼容传统开发方式,防止引入额定工作量和成员学习老本。

如你所想,咱们 近乎完满地兼容了传统的开发方式 ,详见 统跳路由 SDK(iOS 端实现)

4.3 科学管理路由表

  • 路由表集中管理
  • 版本治理(利用于动静下发路由表)
  • 路由表应标注路由名称、用处形容、入参、出参、其余额定限度(如要进入该页面须要的权限)

4.3.1 应用体验优化

通过脚本生成各端代码

防止硬编码:路由表映射为一个构造体,每个路由是一个属性,通过这种形式防止硬编码。

入参结构器:入参是一个字典,咱们能够依据路由定义时的入参生成字典对应的结构器。

出参:出参是一个字典,咱们能够依据路由表主动生成字典的关联属性。

版本治理:路由表仓库打 tag 后主动执行脚本生成各端代码(本文不开展)。

4.4 路由表动静下发

配置核心提供更新路由表能力,各端按约定的策略更新路由表。

5. 统跳路由 SDK(iOS 端实现)

5.1 兼容原生开发方式

以 iOS 传统开发方式为例,跳转一个新页面须要以下步骤

  1. 创立指标 ViewController 实例
  2. 入参以 ViewController 实例属性赋值形式传递
  3. 获取适合的 NavigationController 实例(若转场形式为模态,则需获取适合的 ViewController 实例)
  4. NavigationController 实例以 push 形式跳转新页面(或 ViewController 以模态形式跳转新页面)
  5. 以 block 或 delegate 形式回传值

以上形式曾经能满足绝大部分场景,上面咱们思考下如何以优雅的形式实现以上步骤

  1. 以键值对的形式实现 URI 与 ViewController 类的绑定,借助 Objective-C runtime 动静生成 ViewController 实例。
  2. URI 以 Query 形式携带入参(统跳路由 SDK 外部会把入参解析为 Dictionary),key 为 ViewController 属性(或实例变量)名,借助 Objective-C runtime 判断该 ViewController 类是否蕴含该属性或实例变量,并判断数据类型是否合乎,如果合乎则通过 Objective-C KVC 形式为该属性或实例变量赋值,从而实现入参传递。
  3. 通过遍历主 Window(未必是 keyWindow 要看理论状况)上的路由回退栈能够获取适合的 NavigationController 实例(present 时是栈顶 ViewController 实例)。
  4. 以上条件都具备了,此时能很容易实现页面跳转。
  5. 对于数据回传,咱们能够通过 ViewController 被移除时回传(肯定不能是 dealloc 时,因为 dealloc 在内存泄露时不会调用,而内存泄露又偶然会产生)。

以上思路清晰可执行,可如果想更灵便易用还需奇妙的使 ViewController 实例与路由相干参数建立联系。

咱们把路由相干参数封装为类 RouterParameter,构造如下

@interface RouterParameter : NSObject

/// 路由所属畛域(由哪个路由调度器调度)@property (nonatomic, copy) NSString *scheme;
/// 路由门路(不蕴含 query 和 fragment 局部)@property (nonatomic, copy) NSString *fullPath;
/// URI query 局部
@property (nonatomic, copy) NSString *query;
/// URI fragment 局部
@property (nonatomic, copy) NSString *fragment;
/// 页面跳转形式(push/present)@property (nonatomic, assign) KBBRouterStyle routerStyle;
/// 残缺 URI(会把 addition 拼接入 query)@property (nonatomic, copy) NSString *url;
/// 路由入参
@property (nonatomic, strong, readonly) NSMutableDictionary *addition;
/// 额定参数(路由行为参数,如:是否开启转场动画)@property (nonatomic, strong, readonly) NSMutableDictionary *exts;
/// 回调值(code、message、data)@property (nonatomic, strong) NSDictionary *response;
/// 回传值应用的回调函数
@property (nonatomic, copy) void (^__nullable callBackBlock)(NSDictionary *result);

把统跳 + (void)open:(NSString *)url urlParams:(NSDictionary * __nullable)urlParams exts:(NSDictionary * __nullable)exts routerStyle:(KBBRouterStyle)routerStyle onPageFinished:(void (^ __nullable)(NSDictionary *result))resultCallback 办法携带的相干参数转换为 RouterParameter 实例在统跳路由 SDK 外部传递,通过 UIViewController 分类(Category)和 Objective-C「关联对象」的形式为 UIViewController 增加属性 routerParameter

此刻咱们会发现下面的「思路」曾经被落实了,思路清晰易懂,并且完满兼容原生开发模式。从而能够使 传统模式无痛渐进地切换到「路由模式」

5.2 架构

6. 如何应用

疾速浏览 Demo 能更直观地理解一个框架,咱们一起来看下惯例用法和用处。

6.1 整体流程

初始化阶段

  • 加载路由表
  • 注册路由拦截器
  • 原生路由注册
  • 非页面路由注册
  • 分组拦截器注册

就绪阶段:此时统跳路由 SDK 已准备就绪。

6.2 页面类路由调用

自有 Native 页面

路由注册


// 注册 Objective-C 实现的 ViewController
BBRouter.register(withClassName: "MomentsViewController", andPath: BBRouterPaths.moments)

// 注册 Swift 实现的 ViewController(留神命名空间)BBRouter.register(withClassName: swiftClassFullName("MomentsViewController", "Community"), andPath: BBRouterPaths.moments)

Flutter/HTML5 实现的页面不在此处注册,由 Flutter/HTML 5 我的项目本人治理

路由跳转


// 无返还值路由跳转
[BBRouter open:BBRouterPaths.moments urlParams:@{@"momentId":@"11223344"}];

// 有返回值路由跳转(BBRouterPaths.selectAlcohol 这个页面可能是任意一种技术实现的如:Native[Swift\Objective-C]、Flutter、HTML5 等)[BBRouter open:BBRouterPaths.selectAlcohol urlParams:@{@"alcoholId":@"112233"} onPageFinished:^(NSDictionary * _Nonnull result) {
    // r_data 是通过 Objective-C 的 Category 和关联对象形式为 NSDictionary 增加的属性,从而干掉硬编码。DEBUGLog(@"%@", [NSString stringWithFormat:@"%@", result.r_data]);
}];

BBRouterPaths.selectAlcohol:应用这种形式把路由硬编码干掉。间接硬编码无奈应用编译器查看,保护老本高。统跳路由 SDK 的设计指标之一就是 毁灭硬编码

6.2 办法 / 行为类路由调用


// 注册行为
[BBRouter registerTask:@"action://xxx.com/yyy/zzz" action:^id _Nullable(BBRouterParameter * _Nonnull routerParameter) {return routerParameter.addition;}];

// 办法异步调用(统跳对立办法进行路由,不辨别路由所属畛域)[BBRouter open:@"action://xxx.com/yyy/zzz" urlParams:@{@"name":@"xiaoming"} onPageFinished:^(NSDictionary * _Nonnull result) {DEBUGLog(@"%@", [NSString stringWithFormat:@"%@", result]);
}];

// 办法同步调用(事件专用办法进行路由)NSError *error = nil;
id result = [BBRouter invokeTaskWithUrl:@"action://xxx.com/yyy/zzz" urlParams:@{@"name":@"xiaoming"} error:&error];
DEBUGLog(@"%@", [NSString stringWithFormat:@"%@", result]);

三方利用指定页面

解包淘宝和天猫的 .ipa 文件,剖析了他们的路由表和调用规定,抱着试一试的态度发现咱们的统跳路由 SDK 也完满反对。


// 淘宝商品详情页
[BBRouter open:BBRouterPaths.threeSides urlParams:@{@"i":@"taobao://item.taobao.com/item.htm?id=554418184878"}];

// 天猫商品详情页
[BBRouter open:BBRouterPaths.threeSides urlParams:@{@"i":@"tmall://page.tm/itemDetail?itemID=551101867384"}];

6.3 路由拦截器简略演示

参数重整:Objective-C 里 id 是关键字,但其余语言能够失常应用,为了兼容这种场景能够在拦截器里做一个入参重新整理的操作。

BBRouter.register(withClassName: "XXXViewController", andPath: BBRouterPaths.xxx, verifyBlock: { path, routerParameter in
    routerParameter.addition["ID"] = routerParameter.addition["id"]
    return true
})

重定向:页面应用新的技术重构,新版本应跳转新页面,借助重定向能力,咱们就不必批改已有代码了。即便老代码跳的还是老的路由,运行时也会被重定向到新页面。

BBRouter.register(withClassName: "XXXViewController", andPath: BBRouterPaths.xxx, verifyBlock: { path, routerParameter in
    let newParameter = BBRouterParameter(byURI: BBRouterPaths.yyy, addition: routerParameter.addition.copy() as! [String : Any])
    newParameter.actionBlock = routerParameter.actionBlock
    newParameter.routerStyle = routerParameter.routerStyle
    newParameter.exts.addEntries(from: routerParameter.exts as! [String : Any])
    BBRouter.route(with: newParameter)

    return false
})

6.4 路由分组拦截器性能简略演示

这里应用分组拦截器实现一组页面须要先胜利登录能力拜访的需要,且实现了用户操作的连贯性。


let isAuthed = "isAuthed"
BBRouter.addPaths(needAuthedPaths, toGroup: isAuthed);
BBRouter.configGroup(isAuthed) {(path, routerParameter) -> Bool in
    if (memberId.isEmpty) {BBRouter.open(BBRouterPaths.login, urlParams: Dictionary(), exts: Dictionary()) {(result) in
            if (!memberId.isEmpty) {// 如果已登录 则持续之前的操作
                BBRouter.route(with: routerParameter)
            }
        }
        return false;
    }
    return true
}

6.5 路由未注册解决

// 能够在这里把未注册的路由信息交给 HTML5 落地页,此时就很灵便了,能够做重定向也能够提醒用户降级。


BBRouter.setUndefinedRouteHandle {(parameter) in
    let url = parameter.url
    BBRouter.open(BBRouterPaths.routerNotFound, urlParams: ["url":url])
}

小结

百瓶统跳路由 SDK 使统跳成为事实,也为页面可视化搭建奠定了根底。到目前为止已交付使用一年左右,对组件化 / 模块化过程有重要的推动作用,很好的实现了立项时「解耦提效」的指标。更可喜的是 iOS 端能 无痛渐进地 从传统模式切换到「路由模式」,接入过程近乎零老本。

因为篇幅无限,很多重要地实现细节没有提到,许多利用场景也没有提到。另一方面也不心愿把细节说的太透,省得先入为主,影响大家思考。

最初,真挚心愿各位能指出计划的有余,并提出新的优化倡议。大家如有疑难可在文章下方留言,咱们会尽快回复。如果本文能对你有一点点启发也请棘手点个喜爱,如果能分享给你的敌人那就更感激了。

更多精彩请关注咱们的公众号「百瓶技术」,有不定期福利呦!

退出移动版