移動端基于動態(tài)路由的架構(gòu)設(shè)計
好久好久沒寫過文章了,一是最近項目太忙了,沒時間寫。二是也沒有時間學(xué)習(xí)新的東西,想寫點什么卻又無從下筆。一味的去寫這個API怎么用,那個新技術(shù)怎么用,又顯的沒意思。沒有項目經(jīng)驗總結(jié)的技術(shù)知識講解,總感覺有些蒼白。
最近在做混合App開發(fā)這塊,從開始的ionic 框架,到后來的mui框架,讓我在混合開發(fā)這塊有了更深的理解,如果在這塊要寫點什么無非漫天蓋地的這個指令怎么用,那個模版怎么用,數(shù)據(jù)怎么進(jìn)行雙向綁定,等等,但是這些網(wǎng)上已經(jīng)很多資料了,等不太忙了,我想我會總結(jié)一篇這些框架的使用心得吧。但是我今天不講這個,我們來談一談在原生app中(iOS android)如何使用動態(tài)路由機(jī)制來搭建整個app的框架。
路由機(jī)制在web開發(fā)中是比較常見的,app開發(fā)中還是很少聽到這種概念的,目前有些大公司采用的組件化開發(fā)(手淘,攜程,蘑菇街等),倒是跟我們講的有很多相同之處,不過它們的比較復(fù)雜,而且網(wǎng)上很多組件化開發(fā),路由機(jī)制,沒有一個能給出完整代碼示例的,看著讓人云里霧里的。索性自己就借鑒它們的思想,加上一點個人的理解,搞出了一個簡單實用的可行性demo出來。我們主要介紹以下三方面內(nèi)容:
1 什么是動態(tài)路由
2 它能解決我們什么問題
3 如何在項目中實現(xiàn)
一 什么是動態(tài)路由
原生開發(fā)沒這概念,我們借助angular路由機(jī)制來解釋這一概念,所謂路由,就是一套路徑跳轉(zhuǎn)機(jī)制,事先通過配置文件定義好一個路徑映射文件,跳轉(zhuǎn)時根據(jù)key去找到具體頁面,當(dāng)然angular會做一些緩存,頁面棧的管理等等一些操作,它大致的定義是這樣的
- angular.module('app',[])
- .config('$routeProvider',function ($routeProvider) {
- $routeProvider
- .when('/',{
- templateUrl:'view/home.html',
- controller:'homeCtrl'
- }
- )
- .when('/',{
- templateUrl:'view/home.html',
- controller:'homeCtrl'
- }
- )
- .when('/',{
- templateUrl:'view/home.html',
- controller:'homeCtrl'
- }
- )
- .ontherwise({
- redirective:'/'
- })
- })
config函數(shù)是一個配置函數(shù)。在使用
$routeProvider這樣的一個服務(wù)。when:代表當(dāng)你訪問這個“/”根目錄的時候 去訪問 templateUrl中的那個模板。 controller可想已知,就是我們配套的controller,就是應(yīng)用于根目錄的這個 模板時的controller。
ontherwise 就是當(dāng)你路徑訪問錯誤時,找不到。***跳到這個默認(rèn)的 頁面。
為此我們可以總結(jié)一下幾個特點:
1 一個映射配置文件
2 路徑出錯處理機(jī)制
這就是路由的基本意思,我們看看,在原生開發(fā)中,采用此種方式,他能解決我們什么問題。
二 它能解決我們什么問題
首先我們來比較一下我們以前的結(jié)構(gòu)模式以及與 加入路由機(jī)制后的項目結(jié)構(gòu),實現(xiàn)路由機(jī)制,不僅需要一個映射文件,還需要一套路由管理機(jī)制,那么采用路由機(jī)制,我們的項目架構(gòu)就跟原來不一樣了,如下圖:
原先業(yè)務(wù)之間的調(diào)用關(guān)系.png
加入路由后的頁面調(diào)用關(guān)系.png
接下來我們看一下平時我們采用的頁面跳轉(zhuǎn)方法:
iOS 下
- [self presentViewController:controller animated:YES completion:nil];
- [self.navigationController pushViewController:controller animated:YES];
android 下
- Intent intent = new Intent(this, A.class); startActivity(intent); startActivityForResult(Intent intent, Int requestCode)
我們看一下它有哪些缺點:
(1)都要在當(dāng)前頁面引入要跳轉(zhuǎn)頁面的class 類。這就造成了頁面的耦合性很高。
(2)遇到重大bug,不能夠及時的修復(fù)問題,需要等待更新發(fā)版后才能解決。
(3)推送消息,如果入口沒有關(guān)與頁面的引入處理,則不能跳轉(zhuǎn)到指定頁面。
引入路由機(jī)制后我們能否解決這些問題呢?
試想一下,如果我們通過一個配置文件來映射頁面跳轉(zhuǎn)關(guān)系,而且通過反射機(jī)制來取消頭文件的引入問題,是不是我們就可以解決以上那些弊端了呢,比如,我們線上應(yīng)用出現(xiàn)bug, 導(dǎo)致某個頁面一打開,app就跪了,那我們是不是就可以通過更新路由配置文件,把它映射到另一個頁面去:一個錯誤提示文件,或者一個線上H5能實現(xiàn)相同功能的頁面。這樣的話,原生app也具有了一定的動態(tài)更新能力,其實想想還有很多好處,比如項目功能太多原生開發(fā)要很長時間,但是領(lǐng)導(dǎo)又急著要上線,那么我們是不是就可以先開發(fā)一個網(wǎng)頁版的模塊,app路由映射到這個web頁面,讓用戶先用著,等我們原生開發(fā)完了,然后再改一下映射文件,沒升級的依舊用H5的路由,升級的就用原生的路由,如果H5頁面我們要廢棄了,那我們整體就可以路由到一個升級提升的頁面去了。
總結(jié)一下路由機(jī)制能解決我們哪些問題:
1 避免引入頭文件,是頁面之間的依賴大大變少了(通過反射動態(tài)生成頁面實例)。
2 線上出現(xiàn)重大bug,給我們提供了一個及時修補(bǔ)的入口
3 網(wǎng)頁和原生切換更方便,更自由。
4 可以跳轉(zhuǎn)任意頁面 例如我們常用的推送,要打開指定的頁面,以前我們怎么做的,各種啟動判斷,現(xiàn)在呢,我們只要給發(fā)送消息配個路由路徑就行了,打開消息,就能夠跳轉(zhuǎn)到我們指定的頁面。
等等,其它好處自行發(fā)掘。
三 如何在項目中實現(xiàn)
說了這么多概念性問題,下面我們就用代碼來實現(xiàn)我們的構(gòu)想, 下面先以IOS平臺為例:
我們先看一下demo結(jié)構(gòu)
iOS demo結(jié)構(gòu)圖.png
說明:WXRouter 路由管理文件
demo 路由使用示例
urlMap.plist 路由配置文件
我們主要講解一下 WXRouter里面的幾個文件,以及ViewController文件,還有urlmap.plist文件,其他請下載示例demo,文末我會給出demo地址。
- #import
- #import
- @interface WXRouter : NSObject
- +(id)sharedInstance;
- -(UIViewController *)getViewController:(NSString *)stringVCName;
- -(UIViewController *)getViewController:(NSString *)stringVCName withParam:(NSDictionary *)paramdic;
- @end
- #import "WXRouter.h"
- #import "webView.h"
- #import "RouterError.h"
- #import "PlistReadUtil.h"
- #define SuppressPerformSelectorLeakWarning(Stuff) \
- do {
- _Pragma("clang diagnostic push") \
- _Pragma("clang diagnostic ignored \"-Warc-performSelector-leaks\"") \
- Stuff; \
- _Pragma("clang diagnostic pop") \
- }
- while (0)
- @implementation WXRouter
- +(id)sharedInstance {
- static dispatch_once_t onceToken;
- static WXRouter * router;
- dispatch_once(&onceToken,^{
- router = [[WXRouter alloc] init];
- });
- return router;
- }
- -(UIViewController *)controller:(UIViewController *)controller withParam:(NSDictionary *)paramdic andVcname:(NSString *)vcName {
- SEL selector = NSSelectorFromString(@"iniViewControllerParam:");
- if(![controller respondsToSelector: selector]){ //如果沒定義初始化參數(shù)方法,直接返回,沒必要在往下做設(shè)置參數(shù)的方法
- NSLog(@"目標(biāo)類:%@未定義:%@方法",controller,@"iniViewControllerParam:");
- return controller;
- }
- if(paramdic == nil) {
- //如果參數(shù)為空 URLKEY 頁面唯一路徑標(biāo)識別
- paramdic = [[NSMutableDictionary alloc] init];
- [paramdic setValue: vcName forKey:@"URLKEY"];
- SuppressPerformSelectorLeakWarning([controller performSelector: selector withObject:paramdic]);
- }
- else {
- [paramdic setValue: vcName forKey:@"URLKEY"];
- }
- SuppressPerformSelectorLeakWarning( [controller performSelector:selector withObject:paramdic]);
- return controller;
- }
- -(UIViewController *)getViewController:(NSString *)stringVCName {
- NSString *viewControllerName = [PlistReadUtil plistValueForKey: stringVCName];
- Class class = NSClassFromString(viewControllerName);
- UIViewController *controller = [[class alloc] init];
- if(controller == nil){ //此處可以跳轉(zhuǎn)到一個錯誤提示頁面
- NSLog(@"未定義此類:%@",viewControllerName);
- return nil;
- }
- return controller;
- }
- -(UIViewController *)getViewController:(NSString *)stringVCName withParam:(NSDictionary *)paramdic {
- UIViewController *controller = [self getViewController: stringVCName];
- if(controller != nil){
- controller = [self controller: controller withParam:paramdic andVcname:stringVCName];
- }
- else {
- //異常處理 可以跳轉(zhuǎn)指定的錯誤頁面
- controller = [[RouterError sharedInstance] getErrorController];
- }
- return controller;
- }
- @end
說明:通過反射機(jī)制根據(jù)傳入的string來獲取 viewcontroller實例,實現(xiàn)了兩個方法,一個是不需要傳入?yún)?shù)的,一個是需要傳入?yún)?shù)的,當(dāng)跳轉(zhuǎn)到第二個頁面需要傳值 就使用第二個帶參數(shù)的方法,所傳的值通過NSDictionary來進(jìn)行封裝,跳轉(zhuǎn)后的頁面通過實現(xiàn)
-(void)iniViewControllerParam:(NSDictionary *)dic 方法來獲取傳過來的參數(shù)。
- #import
- @interface PlistReadUtil : NSObject
- @property(nonatomic,strong) NSMutableDictionary *plistdata;
- +(id)sharedInstanceWithFileName:(NSString *)plistfileName;
- +(NSString *)plistValueForKey:(NSString *)key;
- @end
- #import "PlistReadUtil.h"
- @implementation PlistReadUtil
- +(id)sharedInstanceWithFileName:(NSString *)plistfileName {
- static dispatch_once_t onceToken;
- static PlistReadUtil * plistUtil;
- dispatch_once(&onceToken,^{
- plistUtil = [[PlistReadUtil alloc] init];
- NSString *plistPath = [[NSBundle mainBundle] pathForResource: plistfileName ofType:@"plist"];
- plistUtil.plistdata = [[NSMutableDictionary alloc] initWithContentsOfFile: plistPath];
- });
- return plistUtil;
- }
- +(NSString *)plistValueForKey:(NSString *)key {
- PlistReadUtil *plist = [PlistReadUtil sharedInstanceWithFileName: @"urlMap"];
- return [plist.plistdata objectForKey: key];
- }
- @end
說明:路由配置文件讀取工具類,我這里讀取的是plist 文件,我這里也可以讀取json,或則訪問網(wǎng)絡(luò)獲取后臺服務(wù)器上的路由配置文件,這個根據(jù)我們業(yè)務(wù)需求的不同,可以添加不同的讀取方法。
- #import <Foundation/Foundation.h>
- #import <UIKit/UIKit.h>
- @interface RouterError : NSObject
- +(id)sharedInstance;
- -(UIViewController *)getErrorController;
- @end
- #import "RouterError.h"
- #import "WXRouter.h"
- @implementation RouterError
- +(id)sharedInstance {
- static dispatch_once_t onceToken;
- static RouterError * routerError;
- dispatch_once(&onceToken,^{
- routerError = [[RouterError alloc] init];
- });
- return routerError;
- }
- #pragma mark 自定義錯誤頁面 此頁面一定確保能夠找到,否則會進(jìn)入死循環(huán)
- -(UIViewController *)getErrorController {
- NSDictionary *diction = [[NSMutableDictionary alloc] init];
- [diction setValue: @"https://themeforest.net/item/octopus-error-template/2562783" forKey:@"url"];
- UIViewController *errorController = [[WXRouter sharedInstance] getViewController: @"MSG003" withParam:diction];
- return errorController;
- }
- @end
說明:在讀取配置文件時如果沒有讀到相應(yīng)的路徑,或者未定義相應(yīng)的class,我們可以在這里處理,我這邊處理的是如果出現(xiàn)錯誤,就返回一個webview頁面,我們可以在項目里寫一個統(tǒng)一的錯誤處理webview頁面,其實每個頁面默認(rèn)都添加了一個參數(shù)[paramdic setValue:vcName forKey:@"URLKEY"]; 就是這個URLKEY,這個key標(biāo)示配置文件中每個跳轉(zhuǎn)動作的key,這個key是唯一的,我們可以根據(jù)不同的URLKEY然后通過后臺統(tǒng)一的一個接口來判斷跳轉(zhuǎn)到不同的錯誤處理H5頁面。
- #import "ViewController.h"
- #import "view2.h"
- #import "WXRouter.h"
- #import "PlistReadUtil.h"
- @interface ViewController ()
- @end
- @implementation ViewController
- -(void)viewDidLoad {
- [super viewDidLoad];
- UILabel *lable = [[UILabel alloc] initWithFrame: CGRectMake(0, 0, 100, 50)];
- lable.textColor = [UIColor blueColor];
- lable.text =@"hello word";
- [self.view addSubview: lable];
- UIButton *button = [[UIButton alloc] initWithFrame: CGRectMake(0, 50, 200, 50)];
- [button setTitle: @"訪問view1" forState:UIControlStateNormal];
- [button setTitleColor: [UIColor blackColor] forState:UIControlStateNormal];
- button.tag = 1;
- [button addTarget: self action:@selector(back:) forControlEvents:UIControlEventTouchUpInside];
- [self.view addSubview: button];
- UIButton *button2 = [[UIButton alloc] initWithFrame: CGRectMake(0, 110, 200, 50)];
- [button2 setTitle: @"訪問view3" forState:UIControlStateNormal];
- [button2 setTitleColor: [UIColor blackColor] forState:UIControlStateNormal];
- button2.tag = 2;
- [button2 addTarget: self action:@selector(back:) forControlEvents:UIControlEventTouchUpInside];
- [self.view addSubview: button2];
- UIButton *butto3 = [[UIButton alloc] initWithFrame: CGRectMake(0, 170, 200, 50)];
- [butto3 setTitle: @"訪問webview" forState:UIControlStateNormal];
- [butto3 setTitleColor: [UIColor blackColor] forState:UIControlStateNormal];
- butto3.tag = 3;
- [butto3 addTarget: self action:@selector(back:) forControlEvents:UIControlEventTouchUpInside];
- [self.view addSubview: butto3];
- UIButton *button4 = [[UIButton alloc] initWithFrame: CGRectMake(0, 230, 200, 50)];
- [button4 setTitle: @"訪問wei定義的頁面" forState:UIControlStateNormal];
- [button4 setTitleColor: [UIColor blackColor] forState:UIControlStateNormal];
- button4.tag = 4;
- [button4 addTarget: self action:@selector(back:) forControlEvents:UIControlEventTouchUpInside];
- [self.view addSubview: button4];
- }
- -(void)back:(UIButton *)btn {
- switch (btn.tag) {
- case 1: {
- NSMutableDictionary *dic = [[NSMutableDictionary alloc] init];
- [dic setValue: @"nihao shijie" forKey:@"title"];
- UIViewController *controller = [[WXRouter sharedInstance] getViewController: @"MSG001" withParam:dic];
- [self presentViewController: controller animated:YES completion:nil];
- }
- break;
- case 2: {
- NSMutableDictionary *dic = [[NSMutableDictionary alloc] init];
- [dic setValue: @"nihao shijie" forKey:@"title"];
- UIViewController *controller = [[WXRouter sharedInstance] getViewController: @"MSG002" withParam:dic];
- [self presentViewController: controller animated:YES completion:nil];
- }
- break;
- case 3: {
- NSMutableDictionary *dic = [[NSMutableDictionary alloc] init];
- [dic setValue: @"https://www.baidu.com" forKey:@"url"];
- UIViewController *controller = [[WXRouter sharedInstance] getViewController: @"MSG003" withParam:dic];
- [self presentViewController: controller animated:YES completion:nil];
- }
- break;
- case 4: {
- UIViewController *controller = [[WXRouter sharedInstance] getViewController: @"MSG005" withParam:nil];
- [self presentViewController: controller animated:YES completion:nil];
- }
- default:
- break;
- }
- }
- -(void)didReceiveMemoryWarning {
- [super didReceiveMemoryWarning];
- // Dispose of any resources that can be recreated.
- }
- @end
說明:這個是使用示例,為了獲取***的靈活性,這里我并沒有把跳轉(zhuǎn)動作presentViewcontroller,pushViewController,以及參數(shù)的組裝封裝在路由管理類里。看過很多大神寫的路由庫,有些也通過url schema的方式。類似于:xml:id/123/name/xu,這樣的路徑方式,但是個人感覺,如果界面之間傳遞圖片對象,或者傳嵌套的類對象,就有點麻煩了。因為怕麻煩,所以就先寫個簡單的吧。
- #import "view3.h"
- @interface view3 ()
- @end
- @implementation view3
- - (void)viewDidLoad {
- [super viewDidLoad];
- UILabel *lable = [[UILabel alloc] initWithFrame: CGRectMake(0, 0, 100, 50)];
- lable.textColor = [UIColor blueColor];
- lable.text =@"我是view3";
- [self.view addSubview: lable];
- UIButton *button = [[UIButton alloc] initWithFrame: CGRectMake(200, 200, 200, 200)];
- [button setTitle: @"back" forState:UIControlStateNormal];
- [button setTitleColor: [UIColor blackColor] forState:UIControlStateNormal];
- [button addTarget: self action:@selector(back) forControlEvents:UIControlEventTouchUpInside];
- [self.view addSubview: button];
- }
- -(void) back {
- [self dismissViewControllerAnimated: YES completion:nil];
- }
- -(void)iniViewControllerParam:(NSDictionary *)dic {
- self.title = [dic objectForKey: @"title"];
- }
說明:這個是要跳轉(zhuǎn)的頁面我們可以通過iniViewControllerParam:(NSDictionary *)dic方法獲取上一個界面?zhèn)鬟^來的參數(shù)。
urlMap.plist
說明:路由配置文件,key:value的形式,頁面里的每個跳轉(zhuǎn)動作都會對應(yīng)一個唯一的key,這里如果兩個頁面都跳轉(zhuǎn)到同一個頁面,就會產(chǎn)生不同的key 對應(yīng)相同的value,感覺是有點冗余了,如果有更好的優(yōu)化,我會更新下文章的,這里的配置文件我們可以怎么玩,由于我在android的這塊的描述已經(jīng)很詳細(xì)了,所以這里就不再贅述。只是android的配置有點坑,類前需要加上包名,這點就沒有iOS方便靈活了,至此iOS示例我就講完了。
總結(jié):代碼是簡陋的,只是簡單的實現(xiàn)了自己的構(gòu)想,還有很多值得細(xì)細(xì)琢磨的地方,關(guān)鍵是架構(gòu)思路,通過中間路由根據(jù)下發(fā)的路由配置文件來動態(tài)跳轉(zhuǎn)頁面,解決原生開發(fā)的遇到的一些問題,不同的項目有不同的業(yè)務(wù)邏輯,這種思路有什么缺陷,或者解決不了什么問題,大家一起討論分享?;谶@種思路搭建架子的話,對于將來的組件化開發(fā),應(yīng)該也會很方便轉(zhuǎn)換吧。😊
demo地址
android:https://github.com/lerpo/WXAndroidRouter.git
iOS :https://github.com/lerpo/WXiOSRouter.git