AngularJS路由二三事專題文章,
在AngularJS中,關(guān)于路由的設(shè)計和用法是一個很重要的方面。簡單來說AngularJS的路由其實是一種純前端的解決方案。不同于后端路由,它的本質(zhì)其實是:當(dāng)請求一個url時,根據(jù)路由配置匹配這個url,然后請求模板片段,并插入到ng-view
中去。所以從某種意義上來說,AngularJS的路由更加傾向通過改變url來進行頁面的局部刷新。
AngularJS路由二三事這個專題文章中,我將基于AngularJS 1.5版本,結(jié)合內(nèi)置的ngRoute服務(wù)、ui-router模塊,以及ui-router-extras模塊來詳細(xì)闡述AngularJS中路由的相關(guān)內(nèi)容。
文章中涉及到的示例代碼在我的github上,就是這個倉庫,可供參考。
另外提一點,因為AngularJS的官網(wǎng)在國內(nèi)訪問可能不太穩(wěn)定,所以可能對查閱文檔造成一些干擾。我們可以選擇查閱AngularJS中文站提供的文檔鏡像,但是這個文檔并不是緊跟AngularJS官方的版本號的。另一種途徑就是,我們可以將AngularJS的源代碼clone到本地,然后安裝好所有的依賴之后在本地build一下,然后grunt webserver
就可以在本地起一個AngularJS的官方網(wǎng)站,此時就可以無障礙的查閱相關(guān)文檔了。
ngRoute
本篇文章我們將介紹如何使用AngularJS內(nèi)置的ngRoute
模塊來做前端路由。
我不太記得AngularJS是從哪個版本開始將ngRoute
獨立成一個單獨的module,貌似是1.2之后吧,現(xiàn)在如果要使用ngRoute
需要額外加載這個模塊文件,就像下面這樣,
<script src="../bower_components/angular/angular.js"></script>
<script src="../bower_components/angular-route/angular-route.js"></script>
除了angular-route模塊,還有angular-animate,anglar-aria,angular-cookies等模塊在使用時也需要額外引入相關(guān)文件。這地方有點小坑,大家注意一下就可以了。
ngRoute
模塊中包含以下內(nèi)容,
名稱 | 所屬 | 作用 |
---|---|---|
ngView |
DIRECTIVE | 提供不同路由模板插入的視圖層 |
$routeProvider |
PROVIDER | 提供路由配置 |
$route |
SERVICE | 用于構(gòu)建各個路由的url、view、controller這三者的關(guān)系 |
$routeParams |
SERVICE | 解析返回路由中帶有的參數(shù) |
上表中的每一個組件在路由中都扮演著不可或缺的作用?;旧鲜褂肁ngularJS配置路由的基本流程是這樣的,
ngView
定義一個路由模板的視圖層。不同路由對應(yīng)的模板將會插入到這個ngView
所在的dom元素下。$routeProvider
進行路由配置,包括每一個路由對應(yīng)的url,template以及controller。除了這些基本的配置之外,還會有一些額外的配置,比如resolve
配置等。$routeParams
服務(wù)來獲取路由url上的參數(shù);還可以通過$rootScope
來監(jiān)控$routeChangeStart
和$routeChangeSuccess
事件。在實例代碼倉庫中有一個demo001文件夾,其目錄結(jié)構(gòu)如下,
- index.html
- home.html
- post.html
- about.html
- index.js
其中index.html
是我們的主頁面文件,其內(nèi)容如下,
<body ng-app="demo001" ng-controller="Demo">
<h1>Angular Route Demo</h1>
<hr>
<div>
<a href="#/home">home</a>
<a href="#/post">post</a>
<a href="#/about">about</a>
</div>
<hr>
<div ng-view></div>
</body>
注意,我們的頁面上有一個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
未定義這樣的錯誤。$routeProvider.when
來配置不同路由的具體信息。$routeProvider.when
方法接受2個參數(shù),第一個是路由的url。第二個路由的具體配置,包括對應(yīng)的模板地址,控制器名稱等。$routeProvider.otherwise
可以用于設(shè)置默認(rèn)路由地址。簡單來說就是將未設(shè)置的url自動重定向到此url。在我們補充完各個路由的控制器后,我們打開index.html
就可以預(yù)覽了。在預(yù)覽時,注意點擊不同鏈接時url的變化,還可以觀察瀏覽器的Network行為。所有的子模板默認(rèn)加載一次之后就會被緩存。
在經(jīng)過上面的實例之后,應(yīng)該對AngularJS路由的基本用法有所了解了。現(xiàn)在我們來假定有這樣一個場景,假設(shè)我們的項目比較復(fù)雜,內(nèi)部的模塊很多。此時更優(yōu)的一種方案是,基于AngularJS來做模塊化設(shè)計與開發(fā)。AngularJS的模塊化是以它的module以及依賴注入等行為作為基礎(chǔ)的。
在實例代碼倉庫中有一個demo002的文件夾,其目錄結(jié)構(gòu)如下,
- 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
的設(shè)置。
這里我們就是使用了模塊化的思想,將/home
,/post
,/about
這幾個路由抽象成獨立的module,將他們內(nèi)部的所有邏輯和設(shè)置都封裝在內(nèi)部。比如下面的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機制和依賴注入機制,為模塊化設(shè)計提供了基礎(chǔ)。在稍微復(fù)雜一點的angularjs項目中我是非常推薦使用模塊化開發(fā)的,能夠抽象成獨立module的不僅僅是不同的路由模塊,可以是一個公共組件,也可以是一個公共服務(wù)等等。
在上一個模塊化實例中,我們的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
其實是一個參數(shù),它將匹配類似/post/001
這種url,其中001就是這個:postId
的值。
我們在路由對應(yīng)的控制器中,可以通過$routeParams
參數(shù)來獲取這個url參數(shù)。如下,
.controller('PostIdController', [
'$scope',
'$routeParams',
function ($scope, $routeParams) {
$scope.msg = 'post id: ' + $routeParams.postId;
}
]);
依次類推,我們可以為路由的url設(shè)置多個參數(shù),比如/post/:year/:month/:day/:postName
這樣的路由,它將匹配/post/2015/12/15/angular-router-demo這樣的路徑??刂破髦凶⑷氲?code>$routeParams服務(wù)將會得到類似下面的對象,
{
"year": 2015,
"month": 12,
"day": 15,
"postName": "angular-router-demo"
}
在前面我們已經(jīng)說明,可以使用$routeProvider.when
方法進行路由配置。這個$routeProvider.when
方法接受2個參數(shù),其中第一個是路由的url,第二個是路由的具體配置項目。
關(guān)于$routeProvider.when的具體用法可以參考官方的文檔。
這里我僅僅針對其中的一個配置項resolve
進行一些說明。
我們先來假設(shè)一個場景。
比如我最近上班太累了,想來一場旅行。在旅行之前,我需要拿到一張機票。而旅游網(wǎng)站出票是需要時間的。
將這個場景抽象成AngularJS應(yīng)用就是這樣的:
/home
),一個是拿到機票開始旅行頁面(/trip
)。所以,這個場景中,我們的問題可以總結(jié)成,當(dāng)我從/home
進入/trip
路由之前,必須要拿到一個機票數(shù)據(jù)。
在實例代碼倉庫中有一個demo003文件夾,其目錄結(jié)構(gòu)如下,
- 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跳轉(zhuǎn)完畢的時候,我們就已經(jīng)將/trip
的模板插入到了ng-view
中,但是此時/trip
需要的數(shù)據(jù)還沒有準(zhǔn)備好。
這種場景下,我們一般會有兩種方式去解決這個問題,
/trip
的模板和控制器中做一些視覺等待邏輯。比如在TripController
中進行耗時操作時,我們可以臨時展示一個loading視覺,待耗時操作完畢之后,我們再將這個視覺隱藏即可。resolve
選項。配置resolve
選項意味著,在進入這個路由之前就必須等待resolve
中的數(shù)據(jù)返回。這里我們主要來看看第二種方式。在實例代碼倉庫的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) {
var deferred = $q.defer();
$timeout(function () {
deferred.resolve('上海 -> 澳大利亞');
}, 4000);
return deferred.promise;
}]
}
})
.otherwise('/home');
}
])
.controller('TripController', [
'$scope',
'ticket',
function ($scope, ticket) {
$scope.ticket = ticket;
}
])
注意/trip
路由的配置中,我們設(shè)置了一個resolve
配置項。這個配置項包含了一個叫做ticket
的key,它將返回一個promise(這里采用AngularJS內(nèi)置的$q來實現(xiàn)promise)。其內(nèi)部也是使用定時器做了一個耗時操作的模擬。
當(dāng)我們的路由從/home
->/trip
時,會觸發(fā)resolve
下的所有promise,只有當(dāng)所有的promise都都被正確的resolve之后才會進行路由切換,才會將/trip
的模板插入到ng-view
中。其實此時$route
會拋出一個$routeChangeSuccess
的事件,這個事件會被$rootScope
捕獲到。
若resolve
中只要有一個promise沒有被正確的resolve,那么此時$route
將會拋出一個$routeChangeError
的事件,并且終止路由切換,雖然url中的地址可能的確發(fā)生了變化,但是/trip
的模板并沒有插入到ng-view
,且TripController
也沒有被執(zhí)行。
當(dāng)所有的resolve
配置都返回之后,AngularJS會將resolve
中key作為對應(yīng)控制器的一個依賴注入進去,然后我們在相應(yīng)的controller中就可以使用了。比如,
.controller('TripController', [
'$scope',
'ticket',
function ($scope, ticket) {
$scope.ticket = ticket;
}
])
這里可以看出,上面提到的兩種預(yù)載入數(shù)據(jù)的方案其實是有著本質(zhì)區(qū)別的。前者其實是在跳轉(zhuǎn)的目標(biāo)路由上做一些額外的工作去適配耗時操作的視覺,此時目標(biāo)路由的模板已經(jīng)被載入ng-view
,且相應(yīng)的控制器也被執(zhí)行了。而后者在跳轉(zhuǎn)目標(biāo)路由之前做一些額外工作去預(yù)加載數(shù)據(jù),當(dāng)數(shù)據(jù)準(zhǔn)備妥當(dāng)才會去載入目標(biāo)路由的模板和執(zhí)行相應(yīng)的controller。
關(guān)于在路由中使用resolve
的示例,有興趣可參閱這篇文章以及這篇文章。
更多建議: