1. 程式人生 > >AngularJS之備忘與訣竅

AngularJS之備忘與訣竅

slist ins 控制器 boot template amp 數據驗證 條件 yui

譯自:《angularjs》

備忘與訣竅

目前為止,之前的章節已經覆蓋了Angular所有功能結構中的大多數,包括指令,服務,控制器,資源以及其它內容.但是我們知道有時候僅僅閱讀是不夠的.有時候,我們並不在乎那些功能機制是如果運行的,我們僅僅想知道如何用AngularJS去做實現一個具體功能。

在這一章中,我麽視圖給出完整的樣例代碼,並且對這些樣例代碼僅僅給出少量的信息和解釋,這些代碼解決是我們在大多數Web應用中碰到的通用問題.這些代碼沒有具體的先後次序,你盡可以跳到你關心的小節先睹為快或者按著本書內容順序閱讀,選那種閱讀方式由你決定.

這一章中我們將要給出代碼樣例包括以下這些:

1、封裝一個jQuery日期選擇器(DatePicker) 2、團隊成員列表應用:過濾器和控制器之間的通信 3、angularjs中的文件上傳 4、使用socket.IO 5、一個簡單的分頁服務. 6、與服務器後端的協作

封裝一個jQuery日期選擇器

這個樣例代碼文件可以在我們GitHub頁面的chatper8/datepicker目錄中找到.

在我們開始實際代碼之前,我們不得不做出一個設計決策:我們的這個組件的外觀顯示和交互設計應該是什麽樣子,假設我們想定義的的日期選擇器在HTML裏面使用像以下代碼這樣:

<input datepicker ng-model="currentDate" select="updateMyText(date)" ></input>

也就是說我們想修改input輸入域,通過給她添加一個叫datepicker的屬性,來給它添加一些更多的功能(就像這樣:它的數據值綁定到一個model上,當一個日期被選擇的時候,輸入域能被提醒修改).那麽我們如何做到這一點哪?

我們將要復用現存的功能組件:jQueryUI的datepicker(日期選擇器),而不是我們從頭自己構建一個日期選擇器.我們只需要把它接入到AngularJS上並且理解它提供的鉤子(hooks):

angular.module(‘myApp.directives‘, [])
    .directive(‘datepicker‘, function() {
        return {
        // Enforce the angularJS default of restricting the directive to
        // attributes only
        restrict: ‘A‘,
        // Always use along with an ng-model
        require: ‘?ngModel‘,
        scope: {
            // This method needs to be defined and
            // passed in to the directive from the view controller
            select: ‘&‘ // Bind the select function we refer to the
                        // right scope
        },
        link: function(scope, element, attrs, ngModel) {
            if (!ngModel) return;

            var optionsObj = {};
            optionsObj.dateFormat = ‘mm/dd/yy‘;
            var updateModel = function(dateTxt) {
                scope.$apply(function () {
                    // Call the internal AngularJS helper to
                    // update the two-way binding
                    ngModel.$setViewValue(dateTxt);
                });
            };

            optionsObj.onSelect = function(dateTxt, picker) {
                updateModel(dateTxt);
                if (scope.select) {
                    scope.$apply(function() {
                        scope.select({date: dateTxt});
                    });
                }
            };

            ngModel.$render = function() {
                // Use the AngularJS internal ‘binding-specific‘ variable
                element.datepicker(‘setDate‘, ngModel.$viewValue || ‘‘);
            };
            element.datepicker(optionsObj);
        }
    };
});

上面代碼中的大多數都非常簡單直接,但是我們還是來看一下其中一些較重要的部分.

ng-model

我們可以得到一個ng-model屬性,這個屬性的值將會被傳入到指令的鏈接函數中.ng-model(這個屬性對於這個指令運行是必須的,因為指令定義中的require屬性定義--見上代碼)這個屬性幫助我們定義屬性和綁定到ng-model上的(js)對象在指令的上下文中的行為機制.這兒有兩點你需要註意一下:

ngModel.$setViewValue(dateTxt)

當Angular外部某些事件(在這個示例中就是jQueryUI日期選擇器組件中某日期被選定的事件)發生時,上面這條語句會被調用.這樣就可以通知AngularJS更新模型對象.這種語句一般是在某個DOM事件發生時被調用.

ngModel.$render

這是ng-model的另外一部分.這個可以協調Angular在模型對象發生變化時如何更新視圖.在我們的示例中,我們僅僅給jQueryUI日期選擇器傳遞了發生了改變的日期值.

綁定select函數

(結合代碼理解本小節內容-譯者註)取代使用屬性值然後用它計算成作用域對象(scope)的一個字符串屬性的做法(在這個案例中,嵌套在指令內部的函和對象不是可直接操作的),我們使用了函數方法綁定("&"作用域對象綁定-註意看上面scope對象定義部分代碼-譯者註).這就在scope作用域對象上建立了一個叫select的新方法.這個方法函數有一個參數,參數是一個對象.這個對象中的每個鍵必須匹配使用了該指令HTML元素中的一個確定參數.這個鍵的值將會作為傳遞給函數的參數.這個特性添加的優勢在於解耦:實現控制器時不需要知道DOM和指令的相關細節.這種回調函數僅僅根據指定參數執行他的動作,而且不需要知道綁定和刷新的的細節.

調用select函數

註意我們給datepicker傳遞了一個optionsObj參數,這個參數對象有一個onSelect函數屬性.jQueryUI組件(此處就是指datepicker)負責調用onSelect方法,這通常在AngularJS的執行上下文環境之外發生.當然,當像onSelect這樣的函數被調用的時候,AngularJS不會得到通知提示.讓AngularJS知道它需要對數據做一些操作是我們應用程序員的責任.我們如何來完成這個任務?通過使用scope.$apply.

現在我們可以很容易地在·scope.$apply·範圍之外調用$setViewValuescope.select方法,或者僅通過scope.$apply調用.但是前面那兩步(範圍之外地)的任何一步發生異常都會靜悄悄地被丟棄.但是如果異常是在scope.$apply函數內部發生,就會被AngularJS捕捉.

示例代碼的其它部分

為了完成這個示例,讓我看一下我們的控制器代碼,然後讓頁面正常地跑起來:

var app = angular.module(‘myApp‘, [‘myApp.directives‘]);
    app.controller(‘MainCtrl‘, function($scope) {
    $scope.myText = ‘Not Selected‘;
    $scope.currentDate = ‘‘;
    $scope.updateMyText = function(date) {
        $scope.myText = ‘Selected‘;
    };
});

非常簡單的代碼.我們聲明了一個控制器,設置了一些作用於對象($scope)變量,然後創建了一個方法(updateMyText),這個方法後來將會被用來綁定到datepicker的on-select事件上.下一步補上HTML代碼:

<!DOCTYPE html>
<html ng-app="myApp">
    <head lang="en">
        <meta charset="utf-8">
        <title>AngularJS Datepicker</title>
        <script
            src="http://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js">
        </script>
        <script src="http://code.jquery.com/ui/1.9.2/jquery-ui.js">
        </script>
        <script
            src="http://ajax.googleapis.com/ajax/libs/angularjs/1.0.3/
                 angular.min.js">
        </script>
        <link rel="stylesheet"
              href="http://code.jquery.com/ui/1.9.2/themes/base/jquery-ui.css">
        <script src="datepicker.js"></script>
        <script src="app.js"></script>
    </head>
    <body ng-controller="MainCtrl">
        <input id="dateField"
               datepicker
               ng-model="$parent.currentDate"
               select="updateMyText(date)">
        <br/>
        {{myText}} - {{currentDate}}
    </body>
</html>

註意HTML元素中的select屬性是如果被聲明的.在作用域對象(scope)的範圍內沒有"date"這個值.但是因為我們已經在指令綁定過程中裝配了我們的函數.AngularJS現在就知道這個函數將會有一個參數,參數名稱將會是"date".這個也就是當datepicker組件的onSelect事件綁定函數被調用時我們定義的的那個對象將會傳入這個參數.

對於ng-model,我們定義用$parent.currentDate取代了currentDate.為什麽?因為我們的指令創建了一個隔離的作用域以便於做select函數綁定這件事.這將使得currentDate不再被ng-model所即使我們設定了它.所以我們不得不顯式地告訴AngularJS:需要引用的currentDate變量不是在隔離作用域內,而是在父作用域內.

做到這一步,我們可以在瀏覽器內加載這個示例代碼,你將會看到一個文本框,當你點擊的時候,將會彈出一個jQueryUI日期選擇器.選定日期後,顯示文本"Not Selected"將會被更新為"Selected",選擇日期也刷新顯示,輸入框內的日期也會被更新.

"小組成員列表"應用:數據過濾與控制器之間的通信

在這個示例中,我們將同時處理很多事情,但是其中只有兩個新技術點:

1、怎樣使用數據過濾器--特別是已簡潔的方式使用--和重復指令(ng-repeat)一起用. 2、怎樣在沒有共同繼承關系的控制器之間通信.

這個示例應用本身非常簡單.其中只有數據,這些數據是各種體育運動隊的成員列表.其中包含的運動有籃球、足球(橄欖球式的不是英式足球那種)和曲棍球.對於這些運動隊的成員,我們有他們的姓名、所在城市、運動種類以及所在團隊是不是主力團隊.

我們想做的是顯示這個成員列表,在其左邊顯示過濾器,當過濾器數據發生變化的時候,成員列表數據做出相應的刷新.我們將要構建兩個控制器.一個用來保存列表成員數據,另外一個運行過濾機制.我們將要使用一個服務來為列表控制器和過濾控制器之中的過濾數據變化做通信.

想讓我們看看這個服務,它將用來驅動整個應用:

angular.module(‘myApp.services‘, []).
    factory(‘filterService‘, function() {
        return {
            activeFilters: {},
            searchText: ‘‘
        };
    });

哇哦,你也許會問:這就是全部?嗯,是的.我們此處所寫代碼基於這樣一個事實:AngularJS 服務是單例模式的(這個以小寫s打頭的單例(singleton)是作用域scope內的單例,而不是那種全局可見且可讀寫的那種.)當你聲明了一個過濾服務,我們就授權在整個應用範圍內只有一個過濾服務的實例對象.

接下來,我們裏完成使用過濾服務作為過濾控制器和列表控制器之間的通信渠道的其它代碼.兩個控制器都可以綁定到它(過濾服務)上,而且兩個都可以在它(過濾服務)更新時,讀取它的成員屬性.這兩個控制器實際上都簡單得要死,因為在其中除了簡單的賦值,基本沒做什麽別的.

var app = angular.module(‘myApp‘, [‘myApp.services‘]);
app.controller(‘ListCtrl‘, function($scope, filterService) {
    $scope.filterService = filterService;
    $scope.teamsList = [{
            id: 1, name: ‘Dallas Mavericks‘, sport: ‘Basketball‘,
            city: ‘Dallas‘, featured: true
        }, {
            id: 2, name: ‘Dallas Cowboys‘, sport: ‘Football‘,
            city: ‘Dallas‘, featured: false
        }, {
            id: 3, name: ‘New York Knicks‘, sport: ‘Basketball‘,
            city: ‘New York‘, featured: false
        }, {
            id: 4, name: ‘Brooklyn Nets‘, sport: ‘Basketball‘,
            city: ‘New York‘, featured: false
        }, {
            id: 5, name: ‘New York Jets‘, sport: ‘Football‘,
            city: ‘New York‘, featured: false
        }, {
            id: 6, name: ‘New York Giants‘, sport: ‘Football‘,
            city: ‘New York‘, featured: true
        }, {
            id: 7, name: ‘Los Angeles Lakers‘, sport: ‘Basketball‘,
            city: ‘Los Angeles‘, featured: true
        }, {
            id: 8, name: ‘Los Angeles Clippers‘, sport: ‘Basketball‘,
            city: ‘Los Angeles‘, featured: false
        }, {
            id: 9, name: ‘Dallas Stars‘, sport: ‘Hockey‘,
            city: ‘Dallas‘, featured: false
        }, {
            id: 10, name: ‘Boston Bruins‘, sport: ‘Hockey‘,
            city: ‘Boston‘, featured: true
        }
    ];
});
app.controller(‘FilterCtrl‘, function($scope, filterService) {
    $scope.filterService = filterService;
});

你也想知道:那部分代碼會很復雜?AngularJS確實使這個示例整體非常簡單,接下來我們所需要去做的就是把所有這一期在模版中整合在一起:

<!DOCTYPE html>
<html ng-app="myApp">
<head lang="en">
    <meta charset="utf-8">
    <title>Teams List App</title>
    <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js">
    </script>
    <script
        src="http://ajax.googleapis.com/ajax/libs/angularjs/1.0.3/angular.min.js">
    </script>
    <link rel="stylesheet"
        href="http://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/2.1.1/
        css/bootstrap.min.css">
    <script
        src="http://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/2.1.1/
        bootstrap.min.js">
    </script>
    <script src="services.js"></script>
    <script src="app.js"></script>
</head>
<body>
<div class="row-fluid">
    <div class="span3" ng-controller="FilterCtrl">
        <form class="form-horizontal">
            <div class="controls-row">
                <label for="searchTextBox" class="control-label">Search:</label>
                <div class="controls">
                    <input type="text"
                        id="searchTextBox"
                        ng-model="filterService.searchText">
                </div>
            </div>
            <div class="controls-row">
                <label for="sportComboBox" class="control-label">Sport:</label>
                <div class="controls">
                    <select id="sportComboBox"
                        ng-model="filterService.activeFilters.sport">
                        <option ng-repeat="sport in [‘Basketball‘, ‘Hockey‘, ‘Football‘]">
                            {{sport}}
                        </option>
                    </select>
                </div>
            </div>
            <div class="controls-row">
                <label for="cityComboBox" class="control-label">City:</label>
                <div class="controls">
                    <select id="cityComboBox"
                        ng-model="filterService.activeFilters.city">
                        <option ng-repeat="city in [‘Dallas‘, ‘Los Angeles‘,
                                                    ‘Boston‘, ‘New York‘]">
                            {{city}}
                        </option>
                    </select>
                </div>
            </div>
            <div class="controls-row">
                <label class="control-label">Featured:</label>
                <div class="controls">
                    <input type="checkbox"
                        ng-model="filterService.activeFilters.featured"
                        ng-false-value="" />
                </div>
            </div>
        </form>
    </div>
    <div class="offset1 span8" ng-controller="ListCtrl">
        <table>
            <thead>
            <tr>
                <th>Name</th>
                <th>Sport</th>
                <th>City</th>
                <th>Featured</th>
            </tr>
            </thead>
            <tbody id="teamListTable">
            <tr ng-repeat="team in teamsList | filter:filterService.activeFilters |
                           filter:filterService.searchText">
                <td>{{team.name}}</td>
                <td>{{team.sport}}</td>
                <td>{{team.city}}</td>
                <td>{{team.featured}}</td>
            </tr>
            </tbody>
        </table>
    </div>
</div>
</body>
</html>

在整個上面這個HTML模板代碼裏面,真正需要關註的只有四項.除此以外的舊的東西我們到目前為止可能都看了幾十遍了(甚至這些舊代碼點都曾經以這樣那樣的形式出現過).讓我們來挨個看一下那四個新代碼點:

搜索框

搜索框的值用ng-model指令綁定到了過濾服務的searchText域上(filterService.searchText),這個屬性域本身沒什麽值得註意的.但是在後面他將被用在過濾器上的方式使得現在這兒這步很關鍵.

組合框

這兒有兩個組合框,盡管我們只高亮了第一個.但是這兩個工作方式相同.他們都綁定到過濾服務(filterService)的激活過濾器域(activeFilters)的sports屬性或者city屬性(取決於具體組合框).這個基本設置了過濾服務(filtersService)的filter對象的sports屬性或者city屬性.

復選框

復選框綁定到了過濾服務(filterService)的激活過濾器域(activeFilters)的featured屬性上.這裏需要註意的是如果復選框被選定.我們想顯示那些featured=true的主力團隊.如果復選框沒有被選定,我們想顯示featured=truefeature=false的兩種團隊(也就是全部團隊).為了達到這個效果,我們用ng-false-value=""指令來告訴程序當復選框沒有選定的時候,featured這個過濾屬性將會被清掉.

叠代器

讓我們再一次看一下ng-repeat這條語句:

"team in teamsList | filter:filterService.activeFilters |
filter:filterService.searchText"

這條語句的第一部分和之前的一樣.後面的兩個新的過濾器則讓一切變得不一樣了.第一個過濾器告訴AngualrJS用filterService.activeFilters域過濾列表數據.使用過濾對象的每個屬性來過濾數據,確保叠代器裏的每個循環項的屬性值與過濾對象的對應屬性值相匹配.所以如果activeFilter[city]=Dallas,那麽叠代器裏面只有那些city=Dallas的被選擇出來顯示.如果有多個過濾器對象,那所有過濾器對象的屬性都得匹配.

第二個過濾器是個文本值過濾器.它基本上只過濾那些只有其屬性數據中出現filterService.searchText文本值即可.所以這個過濾的屬性值會包含所有數據項:cities、team names、sports和featured.

AngularJS中的文件上傳

另外一個我們即將要看的常用情景示例是在AngularJS應用中如何實現文件上傳功能.雖然目前支持這個功能可以通過HTML標準中的file類型的input輸入域來做,但是為了達到這個示例的目的,我們將會擴展一個現存的文件上傳解決方案.目前這方面的優秀實現之一是BlueImp‘s File Upload,它是用jquery和JqueryUI實現(或者BootStrap)的.它的API相當簡單,所以這樣使得我們的AngularJS指令也超級簡單.

讓我們從令定義的代碼開始:

angular.module(‘myApp.directives‘, [])
    .directive(‘fileupload‘, function() {
    return {
        restrict: ‘A‘,
        scope: {
            done: ‘&‘,
            progress: ‘&‘
        },
        link: function(scope, element, attrs) {
            var optionsObj = {
                dataType: ‘json‘
            };
            if (scope.done) {
                optionsObj.done = function() {
                    scope.$apply(function() {
                        scope.done({e: e, data: data});
                    });
                };
            }
            if (scope.progress) {
                optionsObj.progress = function(e, data) {
                    scope.$apply(function() {
                        scope.progress({e: e, data: data});
                    });
                }
            }
            // the above could easily be done in a loop, to cover
            // all API‘s that Fileupload provides
            element.fileupload(optionsObj);
        }
    };
});

這段代碼幫助我們以一個非常簡單的方式定義了我們這個指令,並且添加了onDoneonProgress兩個函數鉤子.我們使用函數綁定來使AngualrJS調用正確的方法而且使用正確的作用域.

這一切都是通過隔離的作用域定義來完成,它之中有兩個函數綁定:一個對應pregress,另外一個對應done.我們將在作用域對象scope上創建一個單參數函數.比如:scope.done以一個對象為參數.這個對象內有兩個屬性鍵:edata.這些都作為參數傳遞給原始定義的函數.這個函數我們將會在下一小節中看到.

下面來讓我們看一下我們的HTML代碼來看看我們如何使用函數綁定:

<!DOCTYPE html>
<html ng-app="myApp">
    <head lang="en">
        <meta charset="utf-8">
        <title>File Upload with AngularJS</title>
        <!-- Because we are loading jQuery before AngularJS,
             Angular will use the jQuery library instead of
             its own jQueryLite implementation -->
        <script
            src="http://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js">
        </script>
        <script
            src="http://raw.github.com/blueimp/jQuery-File-Upload/master/js/vendor/
            jquery.ui.widget.js">
        </script>
        <script
            src="http://raw.github.com/blueimp/jQuery-File-Upload/master/js/
            jquery.iframe-transport.js">
        </script>
        <script
            src="http://raw.github.com/blueimp/jQuery-File-Upload/master/js/
            jquery.fileupload.js">
        </script>
        <script
            src="//ajax.googleapis.com/ajax/libs/angularjs/1.0.3/angular.min.js">
        </script>
        <script src="app.js"></script>
    </head>
    <body ng-controller="MainCtrl">
        File Upload:
            <!-- We will define uploadFinished in MainCtrl and attach
                 it to the scope, so that it is available here -->
            <input id="testUpload"
                   type="file"
                   fileupload
                   name="files[]"
                   data-url="/server/uploadFile"
                   multiple
                   done="uploadFinished(e, data)">
    </body>
</html>

我們的input標簽僅僅添加了以下附加部分:

fileupload 這個使得input標簽成為一個文件上傳元素

data-url 這個屬性被FileUpload插件用來確定文件上傳的服務器端處理URL.在我們的示例中,我們假設在/server/uploadFileURL上有一個服務器端API在監聽處理上傳的文件數據.

multiple 這個multiple屬性告訴指令(以及fileupload組件)允許它一次性可以選擇多個文件.我們可以通過插件輕松實現此功能,而不需要多寫一行額外代碼,這又是內建插件的一個福利啊。

done 這是當插件文件上傳結束以後AngularJS要調用的函數.如果我們想做,我們也可以以類似的方式為progress事件也定義一個函數.這也指定了我們的指令定義中調用的那兩個參數函數.

那控制器看起來會是個什麽樣子那,正如你所期望的那樣,它的代碼是下面這樣:

var app = angular.module(‘myApp‘, [‘myApp.directives‘]);
    app.controller(‘MainCtrl‘, function($scope) {
    $scope.uploadFinished = function(e, data) {
        console.log(‘We just finished uploading this baby...‘);
    };
});

有了上面這些代碼,我們就有了一個簡單的、可運行且可復用的文件上傳指令.

使用Socket.IO

目前Web開發中一個常見需求就是構建實時Web應用,也就是服務器端數據一更新,前端瀏覽器的數據也立即實時刷新.之前使用的技巧如輪詢之類的被發現有缺陷,有時我們僅僅想建立一個連接前端的套接字(socket)用來通信.

Socket.IO是一個優秀的庫.它可以幫我們通過非常簡單的基於事件API構建實時Web應用.下面我們將要開發一個實時的、匿名的消息廣播系統(比如Twitter,不過不需要用戶名),這個系統將幫助用戶把自己的消息廣播給所有的Socket.IO用戶同時還可以看見系統的所有消息.這個系統中沒有數據會被持久化存儲,所以所有的消息只對那些在線活躍用戶是可見的,但是這系統用於說明Socket.IO如何被優雅地集成進AngularJS這件事已經綽綽有余.

說幹就幹,我們來把Socket.IO封裝到一個AngularJS服務裏.這樣做,我們就可以保證以下幾點:

  • Socket.IO的事件會在AngularJS的生命周期被激發和處理
  • 測試集成效果將變得很簡單

    var app = angular.module(‘myApp‘, []); // We define the socket service as a factory so that it // is instantiated only once, and thus acts as a singleton // for the scope of the application. app.factory(‘socket‘, function ($rootScope) { var socket = io.connect(‘http://localhost:8080‘); return { on: function (eventName, callback) { socket.on(eventName, function () { var args = arguments; $rootScope.$apply(function () { callback.apply(socket, args); }); }); }, emit: function (eventName, data, callback) { socket.emit(eventName, data, function () { var args = arguments; $rootScope.$apply(function () { if (callback) { callback.apply(socket, args); } }); }) } }; });

此處我們只封裝了我們關註的兩個函數,他們分別是Socket.IO API中的on事件和broadcast事件方法.API中還有很多事件方法,我們可以以類似的方式封裝他們.

下面我們來看一下簡單的index.html文件源碼,將展示一個帶發送按鈕的文本框和一個消息列表.在這個示例中,我們並不跟蹤誰發的消息以及什麽時候發的.

<!DOCTYPE html>
<html ng-app="myApp">
<head lang="en">
    <meta charset="utf-8">
    <title>Anonymous Broadcaster</title>
    <script src="/socket.io/socket.io.js">
    </script>
    <script
        src="http://ajax.googleapis.com/ajax/libs/angularjs/1.0.3/angular.min.js">
    </script>
    <script src="app.js"></script>
</head>
<body ng-controller="MainCtrl">
    <input type="text" ng-model="message">
    <button ng-click="broadcast()">Broadcast</button>
    <ul>
        <li ng-repeat="msg in messages">{{msg}}</li>
    </ul>
</body>
</html>

下面我們看一下MainCtrl控制器(這段代碼是app.js中的一部分),在這個控制器中我們將把上面這些整合起來:

function MainCtrl($scope, socket) {
    $scope.message = ‘‘;
    $scope.messages = [];
    // When we see a new msg event from the server
    socket.on(‘new:msg‘, function (message) {
        $scope.messages.push(message);
    });
    // Tell the server there is a new message
    $scope.broadcast = function() {
        socket.emit(‘broadcast:msg‘, {message: $scope.message});
        $scope.messages.push($scope.message);
        $scope.message = ‘‘;
    };
}

這個控制器本身非常簡單.它監聽套接字連接的事件,而且一旦用戶點擊廣播按鈕,就讓服務知道有新消息了.並且把新消息添加到消息列表把它直接顯示給當前用戶.

下面我們來完成最後一部分.這是NodeJS Server如何支撐前端應用的代碼,它相應地用Socket.IOAPI建立了服務器端.

var app = require(‘express‘)()
    , server = require(‘http‘).createServer(app)
    , io = require(‘socket.io‘).listen(server);
server.listen(8080);
app.get(‘/‘, function (req, res) {
    res.sendfile(__dirname + ‘/index.html‘);
});
app.get(‘/app.js‘, function(req, res) {
    res.sendfile(__dirname + ‘/app.js‘);
});
io.sockets.on(‘connection‘, function (socket) {
    socket.emit(‘new:msg‘, ‘Welcome to AnonBoard‘);
    socket.on(‘broadcast:msg‘, function(data) {
        // Tell all the other clients (except self) about the new message
        socket.broadcast.emit(‘new:msg‘, data.message);
    });
});

以後你可以輕松地擴展這段代碼以支持處理更多的消息和更復雜的結構,盡管如此,這段代碼已經打好了基礎,在其之上,你可以實現客戶端瀏覽器和服務器端之間的套接字連接.

整個示例應用非常簡單.它沒有做任何數據驗證(不管消息是否為空),但是它包含AngularJS默認提供的HTML代碼清理過濾功能.它沒有處理復雜的消息,但是它提供了一個將Socket.IO集成進AngularJS的完全可用的端到端實現.你可以馬上就基於它建立你自己的工作生產代碼.

一個簡單的分頁服務

大多數Web應用中一個常用的功能情景是顯示一個項目列表.通常,我們有著比一個單個頁面合理顯示量更大的數據量.這樣一個需求場景下,我們想以分頁的方式顯示這些數據,而且用戶還可以在不同的頁面之間穿梭.因為這一個在所有Web應用之中很常見的場景,所以有理由把這個功能抽取出來封裝成一個公共可復用的分頁服務.

我們的分頁服務(一個非常簡單的實現)將幫助該服務的用戶在給定的數據偏移量、單頁數據量、數據總量條件下取得分頁數據.它在內部將處理以下邏輯:某一頁要取那些數據,如何下一頁存在的情況下,下一頁是那頁等等必須功能邏輯.

這個服務可以進一步擴展來在服務類緩存數據項,但是這個就作為練習題留給廣大讀者了.我們的示例全部所需要的就是把當前頁數據項currentPageItems存儲在緩存裏.在它可用的情況下取出它,就相當於別的取數據函數fetch Function那一類的東西.

下面我們看一下這個服務的實現:

angular.module(‘services’, []).factory(‘Paginator‘, function() {
    // Despite being a factory, the user of the service gets a new
    // Paginator every time he calls the service. This is because
    // we return a function that provides an object when executed
    return function(fetchFunction, pageSize) {
        var paginator = {
            hasNextVar: false,
            next: function() {
                if (this.hasNextVar) {
                    this.currentOffset += pageSize;
                    this._load();
                }
            },
            _load: function() {
                var self = this;
                fetchFunction(this.currentOffset, pageSize + 1, function(items) {
                    self.currentPageItems = items.slice(0, pageSize);
                    self.hasNextVar = items.length === pageSize + 1;
                });
            },
            hasNext: function() {
                return this.hasNextVar;
            },
            previous: function() {
                if(this.hasPrevious()) {
                    this.currentOffset -= pageSize;
                    this._load();
                }
            },
            hasPrevious: function() {
                return this.currentOffset !== 0;
            },
            currentPageItems: [],
            currentOffset: 0
        };
        // Load the first page
        paginator._load();
        return paginator;
    };
});

分頁服務被調用的時候需要兩個參數:一個是fetch取數據的函數,還有一個就是每頁的大小.取數據的函數希望是如下這個函數簽名:

fetchFunction(offset, limit, callback);

一旦這個函數需要取得數來顯示一個頁面,它就會給以正確的數據偏移量、單頁數據大小兩個參數而被分頁服務調用.在這個函數的內部,它可以或者從一個大的數據數據中做切片或者想服務器端發出請求取回數據.一旦數據可用,取數(fetch)函數就需要調用那個作為參數的回調函數.

讓我們看一下啊這個函數的設計說明,將要澄清說明我們在有一個包含太多返回數據項的大數組的前提下如何使用這個函數。請註意:這是一個單元測試.由於其實現方式的原因,我們可以在不需要任何控制器和XHR異步請求的情況下測試這個服務.

describe(‘Paginator Service‘, function() {
    beforeEach(module(‘services‘));
    var paginator;
    var items = [1, 2, 3, 4, 5, 6];
    var fetchFn = function(offset, limit, callback) {
        callback(items.slice(offset, offset + limit));
    };
    beforeEach(inject(function(Paginator) {
        paginator = Paginator(fetchFn, 3);
    }));
    it(‘should show items on the first page‘, function() {
        expect(paginator.currentPageItems).toEqual([1, 2, 3]);
        expect(paginator.hasNext()).toBeTruthy();
        expect(paginator.hasPrevious()).toBeFalsy();
    });
    it(‘should go to the next page‘, function() {
        paginator.next();
        expect(paginator.currentPageItems).toEqual([4, 5, 6]);
        expect(paginator.hasNext()).toBeFalsy();
        expect(paginator.hasPrevious()).toBeTruthy();
    });
    it(‘should go to previous page‘, function() {
        paginator.next();
        expect(paginator.currentPageItems).toEqual([4, 5, 6]);
        paginator.previous();
        expect(paginator.currentPageItems).toEqual([1, 2, 3]);
    });
    it(‘should not go next from last page‘, function() {
        paginator.next();
        expect(paginator.currentPageItems).toEqual([4, 5, 6]);
        paginator.next();
        expect(paginator.currentPageItems).toEqual([4, 5, 6]);
    });
    it(‘should not go back from first page‘, function() {
        paginator.previous();
        expect(paginator.currentPageItems).toEqual([1, 2, 3]);
    });
});

分頁服務暴露其自身的currentPageItems當前分頁數據項這個變量,這樣它就可以在模板中被叠代器綁定(或者其它想顯示這些數據項的地方).hasNext()hsrPreviour兩個函數可已被用來確定是否顯示下一頁或者上一頁這兩個鏈接.而在click事件上,我們只需要分別調用next()或者previous()這兩個函數.

那麽在我們需要從服務器端取回每頁數據的條件下這個服務應該如何使用哪?這兒有這麽一個控制器:它每顯示一頁數據都需要從服務器端取回一次搜索結果數據.大概代碼如下:

var app = angular.module(‘myApp‘, [‘myApp.services‘]);
app.controller(‘MainCtrl‘, [‘$scope‘, ‘$http‘, ‘Paginator‘,
    function($scope, $http, Paginator) {
    $scope.query = ‘Testing‘;
    var fetchFunction = function(offset, limit, callback) {
        $http.get(‘/search‘,
            {params: {query: $scope.query, offset: offset, limit: limit}})
            .success(callback);
        };
    $scope.searchPaginator = Paginator(fetchFunction, 10);
}]);

使用這個分頁服務的HTML頁面代碼數據如下:

<!DOCTYPE html>
<html ng-app="myApp">
<head lang="en">
    <meta charset="utf-8">
    <title>Pagination Service</title>
    <script
        src="http://ajax.googleapis.com/ajax/libs/angularjs/1.0.3/angular.min.js">
    </script>
    <script src="pagination.js"></script>
    <script src="app.js"></script>
</head>
<body ng-controller="MainCtrl">
    <input type="text" ng-model="query">
    <ul>
        <li ng-repeat="item in searchPaginator.currentPageItems">
            {{item}}
        </li>
    </ul>
    <a href=""
        ng-click="searchPaginator.previous()"
        ng-show="searchPaginator.hasPrevious()">&lt;&lt; Prev</a>
    <a href=""
        ng-click="searchPaginator.next()"
        ng-show="searchPaginator.hasNext()">Next &gt;&gt;</a>
</body>
</html>

和服務器之間的協作與登錄

最後一個案例將要覆蓋眾多的場景,它們中的全部或者大多數都與$http資源有聯系.在我們的經驗中,$http服務是AngularJS核心服務之一.同時它可以被擴展來滿足Web應用的很多常見功能需求,包括:

  • 共享一個公共錯誤處理代碼點
  • 處理認證和登錄之後的重定向
  • 與不支持或者支持JSON通信的服務器協作.
  • 通過JSONP與外部服務(非同域的)之間的通信

所以在這個(輕度設計)的示例中,我們將會有一個成熟WebApp的骨架,它將會包括如下:

1.在butterbar指令顯示所有不可恢復的錯誤(不包括驗證失敗HTTP401響應),只有異常發生的時候,這個指令才會在屏幕上出現. 2.將會有一個ErrorService,它將會被用來在指令、視圖和控制器之間的通信工作. 3.在服務器端響應401驗證失敗時激發一個事件(事件loginRequired).它將會被覆蓋整個應用的根控制器所處理. 4.處理那些需要帶驗證頭信息的服務器請求,這些請求是特定於當前用戶的.

我們不會覆蓋整個應用的所有元素(比如路由、模板等等),而且大多數代碼是簡明易懂的.我們只高亮顯示那些與主題關系較密切的代碼(便於您把這些代碼復制粘帖到您的代碼庫中並以正確的方式開始編碼).這些代碼將會是完全功能性代碼.如果你想看定義服務或者工廠的代碼,請參閱第7章.如果你想看如何與服務器端協同合作,可以參考第5章.

首先讓我看回一下Error服務的代碼:

var servicesModule = angular.module(‘myApp.services‘, []);
servicesModule.factory(‘errorService‘, function() {
    return {
        errorMessage: null,
        setError: function(msg) {
            this.errorMessage = msg;
        },
        clear: function() {
            this.errorMessage = null;
        }
    };
});

我們的error message錯誤消息指令,它實際上與Error服務是獨立的,它指揮尋找一個彈出框的消息屬性,然後把它綁定到模板中.只有錯誤消息出現的情況下,彈出框才會顯示.

// USAGE: <div alert-bar alertMessage="myMessageVar"></div>
angular.module(‘myApp.directives‘, []).
directive(‘alertBar‘, [‘$parse‘, function($parse) {
    return {
        restrict: ‘A‘,
        template: ‘<div class="alert alert-error alert-bar"‘ +
            ‘ng-show="errorMessage">‘ +
            ‘<button type="button" class="close" ng-click="hideAlert()">‘ +
            ‘x</button>‘ +
            ‘{{errorMessage}}</div>‘,
        link: function(scope, elem, attrs) {
            var alertMessageAttr = attrs[‘alertmessage‘];
            scope.errorMessage = null;
            scope.$watch(alertMessageAttr, function(newVal) {
                scope.errorMessage = newVal;
            });
            scope.hideAlert = function() {
                scope.errorMessage = null;
                // Also clear the error message on the bound variable.
                // Do this so that if the same error happens again
                // the alert bar will be shown again next time.
                $parse(alertMessageAttr).assign(scope, null);
            };
        }
    };
}]);

我們添加進HTML的彈出框代碼將如下所示:

<div alert-bar alertmessage="errorService.errorMessage"></div>

我們需要保證在上面這段HTML被新增前,ErrorSerivce必須以"errorService"屬性名保存在作用域對象範圍之內.也就是說:如果RootController是負責擁有AlertBar指令的控制器,那麽代碼應如下:

app.controller(‘RootController‘,
               [‘$scope‘, ‘ErrorService‘, function($scope, ErrorService) {
    $scope.errorService = ErrorService;
});

它給我們一個像樣的框架來顯示或隱藏錯誤信息和提示框.現在讓我看看,如何利用攔截器來處理服務器端可能拋給我們的各種狀態碼:

servicesModule.config(function ($httpProvider) {
    $httpProvider.responseInterceptors.push(‘errorHttpInterceptor‘);
});
// register the interceptor as a service
// intercepts ALL angular ajax HTTP calls
servicesModule.factory(‘errorHttpInterceptor‘,
        function ($q, $location, ErrorService, $rootScope) {
    return function (promise) {
        return promise.then(function (response) {
            return response;
        }, function (response) {
            if (response.status === 401) {
                $rootScope.$broadcast(‘event:loginRequired‘);
            } else if (response.status >= 400 && response.status < 500) {
                ErrorService.setError(‘Server was unable to find‘ +
                    ‘ what you were looking for... Sorry!!‘);
            }
            return $q.reject(response);
        });
    };
});

對於某些地方一些控制器來說,所有需要做的就是註冊監聽loginRequired事件,然後重定向到登錄頁面(或者做相對更復雜的效果,比如顯示一個登錄模態對話框).

$scope.$on(‘event:loginRequired‘, function() {
    $location.path(‘/login‘);
});

剩下的就是處理需要認證授權的Web請求了.我們目前只說所有需要認證授權的Web請求都有一個"Authorization"報頭,這個報頭的的值對於每一個當前登錄用戶是特定的.因為這個報頭值每次登錄都會改變,所以我們不能用默認的transformRequests,因為它的數據改變是在config級.取而代之的是,我們將會封裝$http服務,從而構建我們自己的AuthService.

我們也會有一個認證服務,他負責存儲用戶的認證信息(在你需要的時候讀取它,通常是在登錄過程中發生).AuthHttp服務將會訪問這個認證服務並通過添加必要的報頭來給Web請求授權.

// This factory is only evaluated once, and authHttp is memorized. That is,
// future requests to authHttp service return the same instance of authHttp
servicesModule.factory(‘authHttp‘, function($http, Authentication) {
    var authHttp = {};
    // Append the right header to the request
    var extendHeaders = function(config) {
        config.headers = config.headers || {};
        config.headers[‘Authorization‘] = Authentication.getTokenType() +
            ‘ ‘ + Authentication.getAccessToken();
    };
    // Do this for each $http call
    angular.forEach([‘get‘, ‘delete‘, ‘head‘, ‘jsonp‘], function(name) {
        authHttp[name] = function(url, config) {
            config = config || {};
            extendHeaders(config);
            return $http[name](url, config);
        };
    });
    angular.forEach([‘post‘, ‘put‘], function(name) {
        authHttp[name] = function(url, data, config) {
            config = config || {};
            extendHeaders(config);
            return $http[name](url, data, config);
        };
    });
    return authHttp;
});

任何需要授權的請求其請求發起函數將會用authHttp.get()取代$http.get().只要Authentication服務的被設定是正確的信息,你的每次Web請求調用都會快捷如飛地通過認證.因為我們用一個服務來做授權這個事情,所以其信息對於整個Web應用來說都是可用的,也就不需要每次路由改變的時候都不得不去讀取驗證信息.

這已經覆蓋了我們在這個Web應用中需要的所有細節.你可以直接從這兒拷貝代碼到你自己的應用代碼中,讓它為你工作.祝你好運.

總結

當帶著我們到這本書末尾的時候,我們幾乎接近覆蓋了關於AngularJS的所有內容.寫這本書我們的目標就是給大家提供一個堅實的基礎,在這個基礎之上我們能開始我們的探索並且愉快地使用AngularJS做開發.我們覆蓋了所有的基礎知識(和一些高級話題),並且沿途提供了盡可能多的示例.

大功告成,一切都做完了嗎?不,我們還需要花大功夫去學習AngularJS外在功能之下的內在運行機制.比如我們的內容從沒有涉及過如何構建復雜且相互依賴的指令.還有那麽多的內容沒有提及,我們用三本甚至四本書來講可能都不夠.但是我們希望這本書能夠給你信心去處理碰到的更復雜的需求.

我們花了大量的時間來寫這本書,所以希望能夠在Internet上看到一些用AngularJS實現的令人驚艷的應用.

AngularJS之備忘與訣竅