移动端基于动态路由的架构设计

好久好久没写过文章了,一是最近项目太忙了,没时间写。二是也没有时间学习新的东西,想写点什么却又无从下笔。一味的去写这个API怎么用,那个新技术怎么用,又显的没意思。没有项目经验总结的技术知识讲解,总感觉有些苍白。

最近在做混合App开发这块,从开始的ionic 框架,到后来的mui框架,让我在混合开发这块有了更深的理解,如果在这块要写点什么无非漫天盖地的这个指令怎么用,那个模版怎么用,数据怎么进行双向绑定,等等,但是这些网上已经很多资料了,等不太忙了,我想我会总结一篇这些框架的使用心得吧。但是我今天不讲这个,我们来谈一谈在原生app中(iOS android)如何使用动态路由机制来搭建整个app的框架。

路由机制在web开发中是比较常见的,app开发中还是很少听到这种概念的,目前有些大公司采用的组件化开发(手淘,携程,蘑菇街等),倒是跟我们讲的有很多相同之处,不过它们的比较复杂,而且网上很多组件化开发,路由机制,没有一个能给出完整代码示例的,看着让人云里雾里的。索性自己就借鉴它们的思想,加上一点个人的理解,搞出了一个简单实用的可行性demo出来。我们主要介绍以下三方面内容:

1 什么是动态路由

2 它能解决我们什么问题

3 如何在项目中实现

一 什么是动态路由

原生开发没这概念,我们借助angular路由机制来解释这一概念,所谓路由,就是一套路径跳转机制,事先通过配置文件定义好一个路径映射文件,跳转时根据key去找到具体页面,当然angular会做一些缓存,页面栈的管理等等一些操作,它大致的定义是这样的

 
  
  1. angular.module('app',[]) 
  2.            .config('$routeProvider',function  ($routeProvider) { 
  3.                $routeProvider 
  4.                    .when('/',{ 
  5.                        templateUrl:'view/home.html'
  6.                        controller:'homeCtrl' 
  7.                        } 
  8.                        ) 
  9.                    .when('/',{ 
  10.                        templateUrl:'view/home.html'
  11.                        controller:'homeCtrl' 
  12.                        } 
  13.                        ) 
  14.                    .when('/',{ 
  15.                        templateUrl:'view/home.html'
  16.                        controller:'homeCtrl' 
  17.                        } 
  18.                        ) 
  19.                    .ontherwise({ 
  20.                        redirective:'/' 
  21.                    }) 
  22.            })  

config函数是一个配置函数。在使用

$routeProvider这样的一个服务。when:代表当你访问这个“/”根目录的时候 去访问 templateUrl中的那个模板。 controller可想已知,就是我们配套的controller,就是应用于根目录的这个 模板时的controller。

ontherwise 就是当你路径访问错误时,找不到。最后跳到这个默认的 页面。

为此我们可以总结一下几个特点:

1 一个映射配置文件

2 路径出错处理机制

这就是路由的基本意思,我们看看,在原生开发中,采用此种方式,他能解决我们什么问题。

二 它能解决我们什么问题

首先我们来比较一下我们以前的结构模式以及与 加入路由机制后的项目结构,实现路由机制,不仅需要一个映射文件,还需要一套路由管理机制,那么采用路由机制,我们的项目架构就跟原来不一样了,如下图:

原先业务之间的调用关系

原先业务之间的调用关系.png

加入路由后的页面调用关系

加入路由后的页面调用关系.png

接下来我们看一下平时我们采用的页面跳转方法:

iOS 下

 
  
  1. [self presentViewController:controller animated:YES completion:nil]; 
  2. [self.navigationController pushViewController:controller animated:YES];  

android 下

 
  
  1. Intent intent = new Intent(this, A.class); startActivity(intent); startActivityForResult(Intent intent, Int requestCode) 

我们看一下它有哪些缺点:

(1)都要在当前页面引入要跳转页面的class 类。这就造成了页面的耦合性很高。

(2)遇到重大bug,不能够及时的修复问题,需要等待更新发版后才能解决。

(3)推送消息,如果入口没有关与页面的引入处理,则不能跳转到指定页面。

引入路由机制后我们能否解决这些问题呢?

试想一下,如果我们通过一个配置文件来映射页面跳转关系,而且通过反射机制来取消头文件的引入问题,是不是我们就可以解决以上那些弊端了呢,比如,我们线上应用出现bug, 导致某个页面一打开,app就跪了,那我们是不是就可以通过更新路由配置文件,把它映射到另一个页面去:一个错误提示文件,或者一个线上H5能实现相同功能的页面。这样的话,原生app也具有了一定的动态更新能力,其实想想还有很多好处,比如项目功能太多原生开发要很长时间,但是领导又急着要上线,那么我们是不是就可以先开发一个网页版的模块,app路由映射到这个web页面,让用户先用着,等我们原生开发完了,然后再改一下映射文件,没升级的依旧用H5的路由,升级的就用原生的路由,如果H5页面我们要废弃了,那我们整体就可以路由到一个升级提升的页面去了。

总结一下路由机制能解决我们哪些问题:

1 避免引入头文件,是页面之间的依赖大大变少了(通过反射动态生成页面实例)。

2 线上出现重大bug,给我们提供了一个及时修补的入口

3 网页和原生切换更方便,更自由。

4 可以跳转任意页面 例如我们常用的推送,要打开指定的页面,以前我们怎么做的,各种启动判断,现在呢,我们只要给发送消息配个路由路径就行了,打开消息,就能够跳转到我们指定的页面。

等等,其它好处自行发掘。

三 如何在项目中实现

说了这么多概念性问题,下面我们就用代码来实现我们的构想, 下面先以IOS平台为例:

我们先看一下demo结构

iOS demo结构图

iOS demo结构图.png

说明:WXRouter 路由管理文件

demo 路由使用示例

urlMap.plist 路由配置文件

我们主要讲解一下 WXRouter里面的几个文件,以及ViewController文件,还有urlmap.plist文件,其他请下载示例demo,文末我会给出demo地址。

 
  
  1. #import 
  2. #import 
  3. @interface WXRouter : NSObject 
  4. +(id)sharedInstance; 
  5. -(UIViewController *)getViewController:(NSString *)stringVCName; 
  6. -(UIViewController *)getViewController:(NSString *)stringVCName withParam:(NSDictionary *)paramdic; 
  7. @end 
  8.  
  9. #import "WXRouter.h" 
  10. #import "webView.h" 
  11. #import "RouterError.h" 
  12. #import "PlistReadUtil.h" 
  13. #define SuppressPerformSelectorLeakWarning(Stuff) \ 
  14. do { 
  15. _Pragma("clang diagnostic push") \ 
  16. _Pragma("clang diagnostic ignored \"-Warc-performSelector-leaks\"") \ 
  17. Stuff; \ 
  18. _Pragma("clang diagnostic pop") \ 
  19. while (0) 
  20. @implementation WXRouter 
  21. +(id)sharedInstance { 
  22.     static dispatch_once_t onceToken; 
  23.     static WXRouter * router; 
  24.     dispatch_once(&onceToken,^{ 
  25.         router = [[WXRouter alloc] init]; 
  26. }); 
  27. return router; 
  28. -(UIViewController *)controller:(UIViewController *)controller withParam:(NSDictionary *)paramdic andVcname:(NSString *)vcName { 
  29. SEL selector = NSSelectorFromString(@"iniViewControllerParam:"); 
  30.     if(![controller respondsToSelector: selector]){  //如果没定义初始化参数方法,直接返回,没必要在往下做设置参数的方法 
  31.         NSLog(@"目标类:%@未定义:%@方法",controller,@"iniViewControllerParam:"); 
  32. return controller; 
  33. if(paramdic == nil) { 
  34. //如果参数为空 URLKEY 页面唯一路径标识别 
  35.         paramdic = [[NSMutableDictionary alloc] init]; 
  36.         [paramdic setValue: vcName forKey:@"URLKEY"]; 
  37. SuppressPerformSelectorLeakWarning([controller performSelector: selector withObject:paramdic]); 
  38. else { 
  39. [paramdic setValue: vcName forKey:@"URLKEY"]; 
  40. SuppressPerformSelectorLeakWarning( [controller performSelector:selector withObject:paramdic]); 
  41.     return controller; 
  42. -(UIViewController *)getViewController:(NSString *)stringVCName { 
  43. NSString *viewControllerName = [PlistReadUtil plistValueForKey: stringVCName]; 
  44. Class class = NSClassFromString(viewControllerName); 
  45.     UIViewController *controller = [[class alloc] init]; 
  46.     if(controller == nil){  //此处可以跳转到一个错误提示页面 
  47.         NSLog(@"未定义此类:%@",viewControllerName); 
  48.         return nil; 
  49. return controller; 
  50. -(UIViewController *)getViewController:(NSString *)stringVCName withParam:(NSDictionary *)paramdic { 
  51. UIViewController *controller = [self getViewController: stringVCName]; 
  52. if(controller != nil){ 
  53.         controller = [self controller: controller withParam:paramdic andVcname:stringVCName]; 
  54. else { 
  55. //异常处理  可以跳转指定的错误页面 
  56.         controller = [[RouterError sharedInstance] getErrorController]; 
  57. return controller; 
  58. @end  

说明:通过反射机制根据传入的string来获取 viewcontroller实例,实现了两个方法,一个是不需要传入参数的,一个是需要传入参数的,当跳转到第二个页面需要传值 就使用第二个带参数的方法,所传的值通过NSDictionary来进行封装,跳转后的页面通过实现

-(void)iniViewControllerParam:(NSDictionary *)dic 方法来获取传过来的参数。

 
  
  1. #import 
  2. @interface PlistReadUtil : NSObject 
  3. @property(nonatomic,strong) NSMutableDictionary *plistdata; 
  4. +(id)sharedInstanceWithFileName:(NSString *)plistfileName; 
  5. +(NSString *)plistValueForKey:(NSString *)key
  6. @end 
  7.  
  8. #import "PlistReadUtil.h" 
  9. @implementation PlistReadUtil 
  10. +(id)sharedInstanceWithFileName:(NSString *)plistfileName { 
  11.     static dispatch_once_t onceToken; 
  12.     static PlistReadUtil * plistUtil; 
  13.     dispatch_once(&onceToken,^{ 
  14.         plistUtil = [[PlistReadUtil alloc] init]; 
  15.         NSString *plistPath = [[NSBundle mainBundle] pathForResource: plistfileName ofType:@"plist"]; 
  16.     plistUtil.plistdata = [[NSMutableDictionary alloc] initWithContentsOfFile: plistPath]; 
  17. }); 
  18. return plistUtil; 
  19. +(NSString *)plistValueForKey:(NSString *)key { 
  20. PlistReadUtil *plist =  [PlistReadUtil sharedInstanceWithFileName: @"urlMap"]; 
  21. return [plist.plistdata objectForKey: key]; 
  22. @end  

说明:路由配置文件读取工具类,我这里读取的是plist 文件,我这里也可以读取json,或则访问网络获取后台服务器上的路由配置文件,这个根据我们业务需求的不同,可以添加不同的读取方法。

 
  
  1. #import <Foundation/Foundation.h> 
  2. #import <UIKit/UIKit.h> 
  3. @interface RouterError : NSObject 
  4. +(id)sharedInstance; 
  5. -(UIViewController *)getErrorController; 
  6. @end 
  7.  
  8. #import "RouterError.h" 
  9. #import "WXRouter.h" 
  10. @implementation RouterError 
  11. +(id)sharedInstance { 
  12.     static dispatch_once_t onceToken; 
  13.     static RouterError * routerError; 
  14.     dispatch_once(&onceToken,^{ 
  15.         routerError = [[RouterError alloc] init]; 
  16. }); 
  17. return routerError; 
  18. #pragma mark  自定义错误页面 此页面一定确保能够找到,否则会进入死循环 
  19. -(UIViewController *)getErrorController { 
  20. NSDictionary *diction = [[NSMutableDictionary alloc] init]; 
  21.     [diction setValue: @"https://themeforest.net/item/octopus-error-template/2562783" forKey:@"url"]; 
  22. UIViewController *errorController = [[WXRouter sharedInstance] getViewController: @"MSG003" withParam:diction]; 
  23. return errorController; 
  24. @end  

说明:在读取配置文件时如果没有读到相应的路径,或者未定义相应的class,我们可以在这里处理,我这边处理的是如果出现错误,就返回一个webview页面,我们可以在项目里写一个统一的错误处理webview页面,其实每个页面默认都添加了一个参数[paramdic setValue:vcName forKey:@"URLKEY"]; 就是这个URLKEY,这个key标示配置文件中每个跳转动作的key,这个key是唯一的,我们可以根据不同的URLKEY然后通过后台统一的一个接口来判断跳转到不同的错误处理H5页面。

 
  
  1. #import "ViewController.h" 
  2. #import "view2.h" 
  3. #import "WXRouter.h" 
  4. #import "PlistReadUtil.h" 
  5. @interface ViewController () 
  6. @end 
  7. @implementation ViewController 
  8. -(void)viewDidLoad { 
  9.     [super viewDidLoad]; 
  10.     UILabel *lable = [[UILabel alloc] initWithFrame: CGRectMake(0, 0, 100, 50)]; 
  11.     lable.textColor = [UIColor blueColor]; 
  12.     lable.text =@"hello word"
  13.     [self.view addSubview: lable]; 
  14.     UIButton *button = [[UIButton alloc] initWithFrame: CGRectMake(0, 50, 200, 50)]; 
  15.     [button setTitle: @"访问view1" forState:UIControlStateNormal]; 
  16.     [button setTitleColor: [UIColor blackColor] forState:UIControlStateNormal]; 
  17.     button.tag = 1; 
  18.     [button addTarget: self action:@selector(back:) forControlEvents:UIControlEventTouchUpInside]; 
  19.     [self.view addSubview: button]; 
  20.     UIButton *button2 = [[UIButton alloc] initWithFrame: CGRectMake(0, 110, 200, 50)]; 
  21.     [button2 setTitle: @"访问view3" forState:UIControlStateNormal]; 
  22.     [button2 setTitleColor: [UIColor blackColor] forState:UIControlStateNormal]; 
  23.     button2.tag = 2; 
  24.     [button2 addTarget: self action:@selector(back:) forControlEvents:UIControlEventTouchUpInside]; 
  25.     [self.view addSubview: button2]; 
  26.     UIButton *butto3 = [[UIButton alloc] initWithFrame: CGRectMake(0, 170, 200, 50)]; 
  27.     [butto3 setTitle: @"访问webview" forState:UIControlStateNormal]; 
  28.     [butto3 setTitleColor: [UIColor blackColor] forState:UIControlStateNormal]; 
  29.     butto3.tag = 3; 
  30.     [butto3 addTarget: self action:@selector(back:) forControlEvents:UIControlEventTouchUpInside]; 
  31.     [self.view addSubview: butto3]; 
  32.     UIButton *button4 = [[UIButton alloc] initWithFrame: CGRectMake(0, 230, 200, 50)]; 
  33.     [button4 setTitle: @"访问wei定义的页面" forState:UIControlStateNormal]; 
  34.     [button4 setTitleColor: [UIColor blackColor] forState:UIControlStateNormal]; 
  35.     button4.tag = 4; 
  36.     [button4 addTarget: self action:@selector(back:) forControlEvents:UIControlEventTouchUpInside]; 
  37.     [self.view addSubview: button4]; 
  38. -(void)back:(UIButton *)btn { 
  39.     switch (btn.tag) { 
  40.         case 1: { 
  41.             NSMutableDictionary *dic = [[NSMutableDictionary alloc] init]; 
  42.     [dic setValue: @"nihao shijie" forKey:@"title"]; 
  43.     UIViewController *controller = [[WXRouter sharedInstance] getViewController: @"MSG001" withParam:dic]; 
  44.     [self presentViewController: controller animated:YES completion:nil]; 
  45. break; 
  46.         case 2: { 
  47.     NSMutableDictionary *dic = [[NSMutableDictionary alloc] init]; 
  48.             [dic setValue: @"nihao shijie" forKey:@"title"]; 
  49.     UIViewController *controller = [[WXRouter sharedInstance] getViewController: @"MSG002"  withParam:dic]; 
  50.     [self presentViewController: controller animated:YES completion:nil]; 
  51. break; 
  52.         case 3: { 
  53.     NSMutableDictionary *dic = [[NSMutableDictionary alloc] init]; 
  54.             [dic setValue: @"https://www.baidu.com" forKey:@"url"]; 
  55.     UIViewController *controller = [[WXRouter sharedInstance] getViewController: @"MSG003" withParam:dic]; 
  56.     [self presentViewController: controller animated:YES completion:nil]; 
  57. break; 
  58.         case 4: { 
  59.     UIViewController *controller = [[WXRouter sharedInstance] getViewController: @"MSG005"  withParam:nil]; 
  60.     [self presentViewController: controller animated:YES completion:nil]; 
  61. default
  62.             break; 
  63. -(void)didReceiveMemoryWarning { 
  64. [super didReceiveMemoryWarning]; 
  65.     // Dispose of any resources that can be recreated. 
  66. @end  

说明:这个是使用示例,为了获取最大的灵活性,这里我并没有把跳转动作presentViewcontroller,pushViewController,以及参数的组装封装在路由管理类里。看过很多大神写的路由库,有些也通过url schema的方式。类似于:xml:id/123/name/xu,这样的路径方式,但是个人感觉,如果界面之间传递图片对象,或者传嵌套的类对象,就有点麻烦了。因为怕麻烦,所以就先写个简单的吧。

 
  
  1. #import "view3.h" 
  2. @interface view3 () 
  3. @end 
  4. @implementation view3 
  5. - (void)viewDidLoad { 
  6.     [super viewDidLoad]; 
  7.     UILabel *lable = [[UILabel alloc] initWithFrame: CGRectMake(0, 0, 100, 50)]; 
  8.     lable.textColor = [UIColor blueColor]; 
  9.     lable.text =@"我是view3"
  10.     [self.view addSubview: lable]; 
  11.     UIButton *button = [[UIButton alloc] initWithFrame: CGRectMake(200, 200, 200, 200)]; 
  12.     [button setTitle: @"back" forState:UIControlStateNormal]; 
  13.     [button setTitleColor: [UIColor blackColor] forState:UIControlStateNormal]; 
  14.     [button addTarget: self action:@selector(back) forControlEvents:UIControlEventTouchUpInside]; 
  15.     [self.view addSubview: button]; 
  16. -(void) back { 
  17.     [self dismissViewControllerAnimated: YES completion:nil]; 
  18. -(void)iniViewControllerParam:(NSDictionary *)dic { 
  19.     self.title = [dic objectForKey: @"title"]; 
  20.  

说明:这个是要跳转的页面我们可以通过iniViewControllerParam:(NSDictionary *)dic方法获取上一个界面传过来的参数。

urlMap.plist

urlMap.plist

说明:路由配置文件,key:value的形式,页面里的每个跳转动作都会对应一个唯一的key,这里如果两个页面都跳转到同一个页面,就会产生不同的key 对应相同的value,感觉是有点冗余了,如果有更好的优化,我会更新下文章的,这里的配置文件我们可以怎么玩,由于我在android的这块的描述已经很详细了,所以这里就不再赘述。只是android的配置有点坑,类前需要加上包名,这点就没有iOS方便灵活了,至此iOS示例我就讲完了。

总结:代码是简陋的,只是简单的实现了自己的构想,还有很多值得细细琢磨的地方,关键是架构思路,通过中间路由根据下发的路由配置文件来动态跳转页面,解决原生开发的遇到的一些问题,不同的项目有不同的业务逻辑,这种思路有什么缺陷,或者解决不了什么问题,大家一起讨论分享。基于这种思路搭建架子的话,对于将来的组件化开发,应该也会很方便转换吧。




本文作者:佚名
来源:51CTO
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值