AngularJS路由二三事(一):ngRoute

2018-06-07 18:45 更新

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-ariaangular-cookies等模塊在使用時(shí)也需要額外引入相關(guān)文件。這地方有點(diǎn)小坑,大家注意一下就可以了。

使用說(shuō)明

ngRoute模塊中包含以下內(nèi)容,

名稱 所屬 作用
ngView DIRECTIVE 提供不同路由模板插入的視圖層
$routeProvider PROVIDER 提供路由配置
$route SERVICE 用于構(gòu)建各個(gè)路由的url、view、controller這三者的關(guān)系
$routeParams SERVICE 解析返回路由中帶有的參數(shù)

上表中的每一個(gè)組件在路由中都扮演著不可或缺的作用?;旧鲜褂肁ngularJS配置路由的基本流程是這樣的,

  1. 在主模板中使用ngView定義一個(gè)路由模板的視圖層。不同路由對(duì)應(yīng)的模板將會(huì)插入到這個(gè)ngView所在的dom元素下。
  2. 使用$routeProvider進(jìn)行路由配置,包括每一個(gè)路由對(duì)應(yīng)的url,template以及controller。除了這些基本的配置之外,還會(huì)有一些額外的配置,比如resolve配置等。
  3. 在每個(gè)路由的controller中完成對(duì)應(yīng)的業(yè)務(wù)邏輯。
  4. 可以通過(guò)注入$routeParams服務(wù)來(lái)獲取路由url上的參數(shù);還可以通過(guò)$rootScope來(lái)監(jiān)控$routeChangeStart$routeChangeSuccess事件。

簡(jiǎn)單實(shí)例

在實(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)需要注意,

  1. 在聲明demo001這個(gè)module的時(shí)候,需要將ngRoute作為依賴。否則報(bào)$routeProvider未定義這樣的錯(cuò)誤。
  2. 在module的configuration中,我們調(diào)用$routeProvider.when來(lái)配置不同路由的具體信息。$routeProvider.when方法接受2個(gè)參數(shù),第一個(gè)是路由的url。第二個(gè)路由的具體配置,包括對(duì)應(yīng)的模板地址,控制器名稱等。
  3. $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ì)被緩存。

模塊化實(shí)例

在經(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ù)等等。

路由參數(shù)

在上一個(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"
}

路由中的resolve

在前面我們已經(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)用就是這樣的:

  1. 有兩個(gè)頁(yè)面,一個(gè)是上班頁(yè)面(/home),一個(gè)是拿到機(jī)票開始旅行頁(yè)面(/trip)。
  2. 默認(rèn)處于上班頁(yè)面??梢酝ㄟ^(guò)導(dǎo)航到開始旅行頁(yè)面。
  3. 在進(jìn)入旅行頁(yè)面之前,我們必須要有一張機(jī)票。

所以,這個(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)題,

  1. 第一種方式:在/trip的模板和控制器中做一些視覺等待邏輯。比如在TripController中進(jìn)行耗時(shí)操作時(shí),我們可以臨時(shí)展示一個(gè)loading視覺,待耗時(shí)操作完畢之后,我們?cè)賹⑦@個(gè)視覺隱藏即可。
  2. 第二種方式:在配置路由時(shí),配置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的示例,有興趣可參閱這篇文章以及這篇文章。


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)