轉自 http://blog.gejiawen.com/2015/12/15/routes-solution-on-angular-ng-route/?utm_source=tuicool&utm_medium=referral
前言
在AngularJS中,關于路由的設計和用法是一個很重要的方面。簡單來說AngularJS的路由其實是一種純前端的解決方案。不同于后端路由,它的本質其實是:當請求一個url時,根據路由配置匹配這個url,然后請求模板片段,并插入到ng-view中去。所以從某種意義上來說,AngularJS的路由更加傾向通過改變url來進行頁面的局部刷新。
AngularJS路由二三事這個專題文章中,我將基于AngularJS 1.5版本,結合內置的ngRoute服務、ui-router模塊,以及ui-router-extras模塊來詳細闡述AngularJS中路由的相關內容。
文章中涉及到的示例代碼在我的github上,就是這個倉庫,可供參考。
另外提一點,因為AngularJS的官網在國內訪問可能不太穩定,所以可能對查閱文檔造成一些干擾。我們可以選擇查閱AngularJS中文站提供的文檔鏡像,但是這個文檔并不是緊跟AngularJS官方的版本號的。另一種途徑就是,我們可以將AngularJS的源代碼clone到本地,然后安裝好所有的依賴之后在本地build一下,然后grunt webserver就可以在本地起一個AngularJS的官方網站,此時就可以無障礙的查閱相關文檔了。
ngRoute
本篇文章我們將介紹如何使用AngularJS內置的ngRoute模塊來做前端路由。
我不太記得AngularJS是從哪個版本開始將ngRoute獨立成一個單獨的module,貌似是1.2之后吧,現在如果要使用ngRoute需要額外加載這個模塊文件,就像下面這樣,
除了angular-route模塊,還有angular-animate,anglar-aria,angular-cookies等模塊在使用時也需要額外引入相關文件。這地方有點小坑,大家注意一下就可以了。
使用說明
ngRoute模塊中包含以下內容,
名稱所屬作用
ngViewDIRECTIVE提供不同路由模板插入的視圖層
$routeProviderPROVIDER提供路由配置
$routeSERVICE用于構建各個路由的url、view、controller這三者的關系
$routeParamsSERVICE解析返回路由中帶有的參數
上表中的每一個組件在路由中都扮演著不可或缺的作用。基本上使用AngularJS配置路由的基本流程是這樣的,
在主模板中使用ngView定義一個路由模板的視圖層。不同路由對應的模板將會插入到這個ngView所在的dom元素下。
使用$routeProvider進行路由配置,包括每一個路由對應的url,template以及controller。除了這些基本的配置之外,還會有一些額外的配置,比如resolve配置等。
在每個路由的controller中完成對應的業務邏輯。
可以通過注入$routeParams服務來獲取路由url上的參數;還可以通過$rootScope來監控$routeChangeStart和$routeChangeSuccess事件。
簡單實例
在實例代碼倉庫中有一個demo001文件夾,其目錄結構如下,
-index.html
-home.html
-post.html
-about.html
-index.js
其中index.html是我們的主頁面文件,其內容如下,
Angular Route Demo
home
post
about
注意,我們的頁面上有一個ng-view指令。
在index.js中,我們需要聲明一個AngularJS的module叫做demo001,并且做一些路由配置工作。代碼如下,
angular.module('demo001', ['ngRoute'])
.config([
'$routeProvider',
function($routeProvider){
$routeProvider
.when('/home', {
templateUrl:'home.html',
controller:'HomeController'
})
.when('/post', {
templateUrl:'post.html',
controller:'PostController'
})
.when('/about', {
templateUrl:'about.html',
controller:'AboutController'
})
.otherwise('/home')
}
])
這里有3點需要注意,
在聲明demo001這個module的時候,需要將ngRoute作為依賴。否則報$routeProvider未定義這樣的錯誤。
在module的configuration中,我們調用$routeProvider.when來配置不同路由的具體信息。$routeProvider.when方法接受2個參數,第一個是路由的url。第二個路由的具體配置,包括對應的模板地址,控制器名稱等。
$routeProvider.otherwise可以用于設置默認路由地址。簡單來說就是將未設置的url自動重定向到此url。
在我們補充完各個路由的控制器后,我們打開index.html就可以預覽了。在預覽時,注意點擊不同鏈接時url的變化,還可以觀察瀏覽器的Network行為。所有的子模板默認加載一次之后就會被緩存。
模塊化實例
在經過上面的實例之后,應該對AngularJS路由的基本用法有所了解了。現在我們來假定有這樣一個場景,假設我們的項目比較復雜,內部的模塊很多。此時更優的一種方案是,基于AngularJS來做模塊化設計與開發。AngularJS的模塊化是以它的module以及依賴注入等行為作為基礎的。
在實例代碼倉庫中有一個demo002的文件夾,其目錄結構如下,
-index.html
-index.js
-home.html
-home.js
-post.html
-post-id.html
-post.js
-about.html
-about.js
index.html與上一個實例相比基本沒有變化。然后我們再看一眼index.js,
angular.module('demo002', [
'ngRoute',
'Module.Home',
'Module.Post',
'Module.About'
])
.config([
'$routeProvider',
function($routeProvider){
$routeProvider.otherwise('/home');
}
])
與之前不同的是,我們在聲明demo002這個module時,附加了額外3個module。在路由的配置中,也僅僅只有一個$routeProvider.otherwise的設置。
這里我們就是使用了模塊化的思想,將/home,/post,/about這幾個路由抽象成獨立的module,將他們內部的所有邏輯和設置都封裝在內部。比如下面的home.js
angular.module('Module.Home', ['ngRoute'])
.config([
'$routeProvider',
function($routeProvider){
$routeProvider.when('/home', {
templateUrl:'home.html',
controller:'HomeController'
});
}
])
.controller('HomeController', [
'$scope',
function($scope){
$scope.msg ='This is home page';
}
]);
AngularJS的module機制和依賴注入機制,為模塊化設計提供了基礎。在稍微復雜一點的angularjs項目中我是非常推薦使用模塊化開發的,能夠抽象成獨立module的不僅僅是不同的路由模塊,可以是一個公共組件,也可以是一個公共服務等等。
路由參數
在上一個模塊化實例中,我們的post.js模塊如下,
angular.module('Module.Post', ['ngRoute'])
.config([
'$routeProvider',
function($routerProvider){
$routerProvider
.when('/post', {
templateUrl:'post.html',
controller:'PostController'
})
.when('/post/:postId', {
templateUrl:'post-id.html',
controller:'PostIdController'
})
}
])
.controller('PostController', [
'$scope',
function($scope){
$scope.posts = [
{
name:'post1',
id:'post-001'
}, {
name:'post2',
id:'post-002'
}
]
}
])
.controller('PostIdController', [
'$scope',
'$routeParams',
function($scope, $routeParams){
$scope.msg ='post id: '+ $routeParams.postId;
}
]);
注意這里,我們使用$routeProvider配置的第二個路由是這樣的/post/:postId。路由中的/:postId其實是一個參數,它將匹配類似/post/001這種url,其中001就是這個:postId的值。
我們在路由對應的控制器中,可以通過$routeParams參數來獲取這個url參數。如下,
.controller('PostIdController', [
'$scope',
'$routeParams',
function($scope, $routeParams){
$scope.msg ='post id: '+ $routeParams.postId;
}
]);
依次類推,我們可以為路由的url設置多個參數,比如/post/:year/:month/:day/:postName這樣的路由,它將匹配/post/2015/12/15/angular-router-demo這樣的路徑。控制器中注入的$routeParams服務將會得到類似下面的對象,
{
"year":2015,
"month":12,
"day":15,
"postName":"angular-router-demo"
}
路由中的resolve
在前面我們已經說明,可以使用$routeProvider.when方法進行路由配置。這個$routeProvider.when方法接受2個參數,其中第一個是路由的url,第二個是路由的具體配置項目。
關于$routeProvider.when的具體用法可以參考官方的文檔。
這里我僅僅針對其中的一個配置項resolve進行一些說明。
我們先來假設一個場景。
比如我最近上班太累了,想來一場旅行。在旅行之前,我需要拿到一張機票。而旅游網站出票是需要時間的。
將這個場景抽象成AngularJS應用就是這樣的:
有兩個頁面,一個是上班頁面(/home),一個是拿到機票開始旅行頁面(/trip)。
默認處于上班頁面。可以通過導航到開始旅行頁面。
在進入旅行頁面之前,我們必須要有一張機票。
所以,這個場景中,我們的問題可以總結成,當我從/home進入/trip路由之前,必須要拿到一個機票數據。
在實例代碼倉庫中有一個demo003文件夾,其目錄結構如下,
-index.html
-index.js
- index2.js// resolve方案
- home.html
- trip.html
在index.js中,
.config([
'$routeProvider',
function($routeProvider){
$routeProvider
.when('/home', {
templateUrl:'home.html',
controller:'HomeController'
})
.when('/trip', {
templateUrl:'trip.html',
controller:'TripController'
})
.otherwise('/home');
}
])
.controller('TripController', [
'$scope',
'$timeout',
function($scope, $timeout){
$timeout(function(){
$scope.ticket ='上海 -> 澳大利亞'
},4000);
}
])
這里我們使用定時器$timeout來模擬一個耗時的出票操作。此時我們從/home->/trip時,頁面會白屏4秒鐘。意味著在進行url跳轉完畢的時候,我們就已經將/trip的模板插入到了ng-view中,但是此時/trip需要的數據還沒有準備好。
這種場景下,我們一般會有兩種方式去解決這個問題,
第一種方式:在/trip的模板和控制器中做一些視覺等待邏輯。比如在TripController中進行耗時操作時,我們可以臨時展示一個loading視覺,待耗時操作完畢之后,我們再將這個視覺隱藏即可。
第二種方式:在配置路由時,配置resolve選項。配置resolve選項意味著,在進入這個路由之前就必須等待resolve中的數據返回。
這里我們主要來看看第二種方式。在實例代碼倉庫的demo003文件夾下的index2.js中,我們是怎么做的,
.config([
'$routeProvider',
function($routeProvider){
$routeProvider
.when('/home', {
templateUrl:'home.html',
controller:'HomeController'
})
.when('/trip', {
templateUrl:'trip.html',
controller:'TripController',
resolve: {
ticket: ['$q','$timeout',function($q, $timeout){
vardeferred = $q.defer();
$timeout(function(){
deferred.resolve('上海 -> 澳大利亞');
},4000);
returndeferred.promise;
}]
}
})
.otherwise('/home');
}
])
.controller('TripController', [
'$scope',
'ticket',
function($scope, ticket){
$scope.ticket = ticket;
}
])
注意/trip路由的配置中,我們設置了一個resolve配置項。這個配置項包含了一個叫做ticket的key,它將返回一個promise(這里采用AngularJS內置的$q來實現promise)。其內部也是使用定時器做了一個耗時操作的模擬。
當我們的路由從/home->/trip時,會觸發resolve下的所有promise,只有當所有的promise都都被正確的resolve之后才會進行路由切換,才會將/trip的模板插入到ng-view中。其實此時$route會拋出一個$routeChangeSuccess的事件,這個事件會被$rootScope捕獲到。
若resolve中只要有一個promise沒有被正確的resolve,那么此時$route將會拋出一個$routeChangeError的事件,并且終止路由切換,雖然url中的地址可能的確發生了變化,但是/trip的模板并沒有插入到ng-view,且TripController也沒有被執行。
當所有的resolve配置都返回之后,AngularJS會將resolve中key作為對應控制器的一個依賴注入進去,然后我們在相應的controller中就可以使用了。比如,
.controller('TripController', [
'$scope',
'ticket',
function($scope, ticket){
$scope.ticket = ticket;
}
])
這里可以看出,上面提到的兩種預載入數據的方案其實是有著本質區別的。前者其實是在跳轉的目標路由上做一些額外的工作去適配耗時操作的視覺,此時目標路由的模板已經被載入ng-view,且相應的控制器也被執行了。而后者在跳轉目標路由之前做一些額外工作去預加載數據,當數據準備妥當才會去載入目標路由的模板和執行相應的controller。