AngularJS路由二三事(一):ngRoute

2018-06-07 18:45 更新

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配置路由的基本流程是這樣的,

  1. 在主模板中使用ngView定義一個路由模板的視圖層。不同路由對應(yīng)的模板將會插入到這個ngView所在的dom元素下。
  2. 使用$routeProvider進行路由配置,包括每一個路由對應(yīng)的url,template以及controller。除了這些基本的配置之外,還會有一些額外的配置,比如resolve配置等。
  3. 在每個路由的controller中完成對應(yīng)的業(yè)務(wù)邏輯。
  4. 可以通過注入$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點需要注意,

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

路由參數(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配置的第二個路由是這樣的/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"
}

路由中的resolve

在前面我們已經(jīng)說明,可以使用$routeProvider.when方法進行路由配置。這個$routeProvider.when方法接受2個參數(shù),其中第一個是路由的url,第二個是路由的具體配置項目。

關(guān)于$routeProvider.when的具體用法可以參考官方的文檔。

這里我僅僅針對其中的一個配置項resolve進行一些說明。

我們先來假設(shè)一個場景。

比如我最近上班太累了,想來一場旅行。在旅行之前,我需要拿到一張機票。而旅游網(wǎng)站出票是需要時間的。

將這個場景抽象成AngularJS應(yīng)用就是這樣的:

  1. 有兩個頁面,一個是上班頁面(/home),一個是拿到機票開始旅行頁面(/trip)。
  2. 默認(rèn)處于上班頁面。可以通過導(dǎo)航到開始旅行頁面。
  3. 在進入旅行頁面之前,我們必須要有一張機票。

所以,這個場景中,我們的問題可以總結(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)備好。

這種場景下,我們一般會有兩種方式去解決這個問題,

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


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

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號