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