使用 AngularJS 開發一個大規模的單頁應用(SPA)


下載源代碼
轉自:http://www.open-open.com/lib/view/open1410250605117.html
介紹

(SPA)這樣一個名字里面蘊含着什么呢? 如果你是經典的Seinfeld電視秀的粉絲,那么你一定知道Donna Chang這個名字。Jerry跟Donna見面,Donna其實不是華人,但是卻因在談論其對中國的固有印象比如在針灸上的興趣,以及偶然的一次單詞發音帶上了點兒中文口音,她將自己末尾的名字縮成了Chang Donna 在電話上同George的母親交談,(通過引用孔子)給她提了些建議。當George向自己的父母介紹Donna是,George的母親意識到Donna並不是華人,因此並沒有接受Donna的建議.

單頁面引用 (SPA), 被定義成一個目的在於提供一種接近桌面應用程序的流暢用戶體驗單web頁面應用程序,或者說網站. 在一個SPA中, 所有必需的代碼 – HTML, JavaScript, 以及 CSS – 都是在單頁面加載的時候獲取,或者相關的資源被動態的加載並按需添加到頁面中, 這常常是在響應用戶動作的時候發生的. 盡管現代的Web技術(比如那些在HTML5中引入的技術)提供了應用程序中各自獨立的邏輯頁面相互感知和導航的能力,頁面卻不會在過程中重新加載任何端點,或者將控制轉到另外一個頁面. 同單頁面應用程序的交互常常設計到同位於后台的web服務器的動態交互.

那么拿這項技術同 ASP.NET 的母版頁Master Pages相比呢? 誠然 ASP.NET 的母版頁讓你可以為自己應用程序里的頁面創建一個一直的布局。一個單獨的母版頁就可以定義好你想要在整個應用程序中的所有頁面(或者一組頁面)上應用的外觀和標准動作. 然后你就可以再來創建你想要展示的內容各自獨立頁面. 當用戶發起對內容頁面的請求時,它們會將來自母版頁的布局和來自內容頁面的內容混合到一起,產生輸出.

當你深入研究SPA和ASP.NET母版頁實現這兩者之間的不同時,你就開始會意識到它們之間相同的地方多於不同的地方——那就是SPA可以看做是一個簡單的裝着內容頁面的外殼頁面,就像是一個母版頁, 只是SPA中的外殼頁面不能像母版頁那樣根據每一個新的頁面請求來重新裝載和執行.

也許“單頁面應用”是個不幸運的名字(像唐娜`程一樣),讓你相信這個技術不適合開發需要拓展到企業級,可能 包含上百頁面以及數千用戶的Web應用。

本文的目標是基於單頁面應用程序開發出擁有數百頁的內容,包括認證,授權,會話狀態等功能,可以支持上千個用戶的企業級應用。 

AngularJS - 概述 

本文的樣例包含的功能有創建/跟新用戶賬號,創建/更新客戶和產品。而且,它還允許用戶針對所有信息執行查詢,創建和跟新銷售訂單。為了實現這些功能,該樣例將會基於AngularJS來開發。 AngularJS 是一個由Google和AngularJS社區的開發人員維護的開源的Web應用框架。

 AngularJS僅需HTML,CSS和JavaScript就可在客戶端創建單頁面應用。它的目標是是開發和測試更容易,增強MVC Web應用的性能。

這個庫讀取HTML中包含的其他定制的標簽屬性;然后服從這個定制的屬性的指令,把頁面的I/O結合到有標准JavaScript變量生成的模塊中。這些JavaScript標准變量的值可以手動設置,或者從靜態或動態的JSON數據源中獲取。

使用 AngularJS 開發一個大規模的單頁應用(SPA)

AngularJS使用入門 - 外殼頁面,模塊和路由

你首先要做的一件事情就是講AngularJS框架下載到你的項目中,你可以從 https://angularjs.org 獲得框架. 本文的示例程序是使用MS Visual Studio Web Express 2013 Edition開發的,因此我是使用如下的命令從一個Nuget包安裝AngularJS的:

Install-Package AngularJS -Version 1.2.21

在Nuget包管理控制台上. 為了保持簡單和靈活性,我創建了一個空的 Visual Studio web 應用程序項目,並將Microsoft Web API 2庫選進了核心引用. 這個應用程序將使用Web API 2 庫來實現 RESTful API 的服務器端請求.

現在當你要使用AngularJS創建一個SPA應用程序是,首先要做的兩件事情就是設置一個外殼頁面,以及用於獲取內容頁面的路由表. 開始的時候,外殼頁面只需要一個隊AngularJS JavaScript庫的引用,還有一個ng-view,來告訴AngularJS內容頁面需要在外殼頁面的那個地方被渲染.

<!DOCTYPE html>
<html lang="en">
<head>
<title>AngularJS Shell Page example</title>
</head>
<body> 
<div>
<ul>
<li><a href="#Customers/AddNewCustomer">Add New Customer</a></li>
<li><a href="#Customers/CustomerInquiry">Show Customers</a></li>
</ul>
</div>
<!-- ng-view directive to tell AngularJS where to inject content pages -->
<div ng-view></div>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.0.7/angular.min.js"></script>
<script src="app.js"></script>
</body>
</html>

在上面的外殼頁面示例中,幾個鏈接唄映射到了AngularJS的路由。div標簽上的ng-view指令是一個能將選定路由的被渲染內容頁面包含到外殼頁面來補充AngularJS的$route服務的指令. 每次當目前的路由變化時,包含的視圖也會根據$route服務的配置隨之改變. 比如,當用戶選擇了 "Add New Customer" 鏈接,AngularJS 就會在ng-view所在的div里面渲染用於添加一個新顧客的內容 . 被渲染的內容是一個HTML片段.

接下來的app.js文件同樣也被外殼頁面引用了。這個文件里的JavaScript將會為應用程序創建AngularJS模塊。此外,應用程序所有的路由配置也會在這個文件中定義。你可以把一個AngularJS模塊想象成封裝你應用程序不同部分的容器。大多數的應用程序都會有一個主方法,用來初始化應用程序的不同部分,並將它們聯系起來。AngularJS應用程序卻沒有一個主方法,而是讓模塊聲明性的指定應用程序如何啟動和配置. 本文的示例程序將只會有一個AngularJS模塊,雖然應用程序中存在幾個明顯不同的部分(顧客,產品,訂單和用戶).

現在,app.js的主要目的就是如下所示,用來設置AngularJS的路由。AngularJS的$routeProvider服務會接受  when() 方法,它將為一個Uri匹配一個模式. 當發現一次匹配時,獨立頁面的HTML內容會跟隨相關內容的控制器文件一同被加載到外殼頁面中. 控制器文件就簡單的只是一個JavaScript文件,它將獲得帶有某個特定路由請求內容的引用.

//Define an angular module for our app
var sampleApp = angular.module(&apos;sampleApp&apos;, []);
//Define Routing for the application
sampleApp.config([&apos;$routeProvider&apos;,
    function($routeProvider) {
        $routeProvider.
            when(&apos;/Customers/AddNewCustomer&apos;, {
                templateUrl: &apos;Customers/AddNewCustomer.html&apos;,
                controller: &apos;AddNewCustomerController&apos;
            }).
            when(&apos;/Customers/CustomerInquiry&apos;, {
                templateUrl: &apos;Customers/CustomerInquiry.html&apos;,
                controller: &apos;CustomerInquiryController&apos;
            }).
            otherwise({
                redirectTo: &apos;/Customers/AddNewCustomer&apos;
            });
}]);

AngularJS 的控制器

AngularJS 控制器無非就是一個原生的JavaScript函數,只是被綁定到了一個特定的范圍而已。控制器用來將邏輯添加到你的視圖。視圖就是HTML頁面。這些頁面只是做簡單的數據展示工作,我們會使用雙向數據綁定來將數據綁定到這些HTML頁面上. 將模型(也就是數據)同數據粘合起來基本山就是控制器的職責了.

<div ng-controller="customerController">
<input ng-model="FirstName" type="text" style="width: 300px" />
<input ng-model="LastName" type="text" style="width: 300px" />       
<div>
<button class="btn btn-primary btn-large" ng-click="createCustomer()"/>Create</button>

 

對於上面的AddCustomer模板,ng-controller指令將會引用JavaScript函數customerController,這個控制會執行所有的數據綁定以及針對該視圖的JavaScript函數.

function customerController($scope) 
{
    $scope.FirstName = "William";
    $scope.LastName = "Gates"; 

    $scope.createCustomer = function () {          
        var customer = $scope.createCustomerObject();
        customerService.createCustomer(customer, 
                        $scope.createCustomerCompleted, 
                        $scope.createCustomerError);
    }
}

開箱即用 - 可擴展性問題

當我為本文開發這個實力程序時,首當其沖的兩個擴展性問題在應用單頁面應用程序時變得明顯起來。其實一個開箱即用,AngularJS需要應用程序的外殼頁面中所有的JavaScript文件和控制器在啟動中伴隨應用程序的啟動被引入和下載. 對於一個大型的應用程序而言,可能會有上百個JavaScript文件,這樣情況看上去就會不怎么理想。我遇到的另外一個問題就是AngularJS的路由表。我找到的所有示例都有針對所有內容的所有路由的硬編碼。而我想要的確不是一個在路由表里包含上百項路由記錄的方案.

使用RequireJS動態加載js文件

在示例中,加載頁面元素前我不想加載js文件。 當然,頁面中可能會有很多頁面元素和js文件。大型的網頁應用通常如此。 一般是通過在頁面中添加script標簽來加載它。 另外,每一個js文件可能依賴其它js文件。 在訪問網頁時,為了動態加載js文件,我發明了RequireJS,它是一個js類庫。RequrieJS 是一個優秀的js模板和文件加載器,最新的版本已兼容各主流瀏覽器。在RequireJS中,js代碼被分割為多個模塊,每個模塊實現一個功能。 另外,加載js文件的時候,需要配置它依賴的文件。

RequireJS提供一種簡潔的方式來加載和管理js代碼中依賴的文件。你可以在 http://requirejs.org下載RequireJS,如果你使用Visual Studio開發,你可以用Nuget命令下載:

Install-Package RequireJS

AngularJS的約定優先的路由方式

AngularJS提供一個開箱即用的路由配置,在里面你可以根據路由路徑來配置返回不同的頁面。我希望使用一個約定優先的技術,而不是用硬編碼的方式來配置所有路由。首先我決定命名約定來給我所有的頁面和關聯的JavaScript文件進行命名,這將使得應用程序能夠解析路由的名稱,並動態的決定內容頁面需要加載哪個JavaScript文件。

例如,用戶管理頁面被命名為CustomerMaintenance.Html,它所對應的AngularJS的JavaScript控制器文件被命名為CustomerMaintenanceController.js。使用一個約定優先的方式,使得路由表不受硬編碼路由規則的影響。

瀏覽示例應用程序

讓我從瀏覽示例應用程序開始。首先,每一個大型的應用都需要某種類型的認證和授權機制,來控制對應用的訪問。本應用將會使用一個登錄頁面,它包含一個ASP.NET表單認證來達到認證和授權的目的。 一旦認證成功,用戶就會擁有訪問其余功能的權限。由於是大型應用,它們通常都有幾個分離的主頁面,一個展示登錄頁面,另一個展示應用的其它部分,通常包括一個頂部主菜單欄,一個側部附加按鈕選項,一個功能頁面區和一個注腳區。該示例應用是通過擁有多個單頁面外殼頁面來實現這一點的。成功登錄后,用戶將被導向到一個新的外殼頁面。

多個外殼頁面(Shell Pages)

第一個外殼頁面是index.html。這個頁面將會容納登錄頁面和用戶注冊頁面。正如你能看到的,這里只引用了一個JavaScript文件。Main.js 將會包含RequireJS的設定和配置信息,用來在每個頁面需要它們的時候動態的加載模塊、JavaScript文件和其它依賴。由於使用了約定優先的路由技術,index.html 頁面將會受到名為indexController.js的AngularJS控制器的控制。當用戶成功的注冊或者登錄之后,應用程序將會導向到一個新的外殼頁面,名為applicationMasterPage.html,它和index.html類似,不過多了一個位於一側的菜單選項導航欄。在這個外殼頁面中,有一處ng-view指令。如前所述,這個指令將會告訴AngularJS在外殼頁面的什么地方展示實際的內容頁面。

<!-- index.html -->

<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title> </title>
    <script data-main="main.js" src="Scripts/require.js"> </script>
    <link href="Content/angular-block-ui.css" rel="stylesheet" />
    <link href="Content/bootstrap.css" rel="stylesheet" />
    <link href="Content/Application.css" rel="stylesheet" />
    <link href="Content/SortableGrid.css" rel="stylesheet" />
</head>
<body ng-controller="indexController" ng-init="initializeController()" >
    <div class="navbar navbar-inverse navbar-fixed-top">
        <div class="container">
            <div class="navbar-collapse collapse" id="MainMenu">
                <ul class="nav navbar-nav" ng-repeat="menuItem in MenuItems">
                    <li> <a href="{{menuItem.Route}}">{{menuItem.Description}} </a> </li>
                </ul>
            </div>
        </div>
    </div>
    <!-- ng-view directive to tell AngularJS where to put the content pages-->
    <div style="margin: 75px 50px 50px 50px" ng-view> </div>      
</body>
</html>

Main.js - RequireJS 的設定和配置文件

本應用將使用RequireJS來進行異步腳本加載和JavaScript依賴管理。如前所示,外殼頁面只應用了一個JavaScript文件,就是main.js,它位於該應用的跟目錄下。它是RequireJS的配置文件。下面的JavaScript文件有三個部分。

第1部分定義了加載該應用所需要的普通JavaScript文件和模塊的所有路徑。由於RequireJS只加載JavaScript文件,所以實際的JavaScript文件名不需要加".js"后綴。

下面代碼中,第2段定義了一個shim塊。 shim塊實現讓RequireJS加載不兼容AMD的腳本。異步模塊加載機制(AMD)是一個JavaScript API,它描述了模塊的定義、依賴關系、引用關系以及加載機制。 展示網頁內容時,通過異步加載js模塊的方法來縮短網頁的響應時間,是非常有效的。 為了加載多個js文件,在開發的時候可以使用AMD把js文件封裝到不同的文件中。 然后可以把所有的js源碼連接起來封裝到一個小的文件中,用於產品發布。

第3段中,通過引用application-configuration.js來引導和啟動應用程序配置文件。 

 

// main.js
 
require.config({
    baseUrl: "",
    // 配置類庫路徑別名
    paths: {
        &apos;application-configuration&apos;: &apos;scripts/application-configuration&apos;,       
        &apos;angular&apos;: &apos;scripts/angular&apos;,
        &apos;angular-route&apos;: &apos;scripts/angular-route&apos;,
        &apos;angularAMD&apos;: &apos;scripts/angularAMD&apos;,
         &apos;ui-bootstrap&apos; : &apos;scripts/ui-bootstrap-tpls-0.11.0&apos;,
        &apos;blockUI&apos;: &apos;scripts/angular-block-ui&apos;,
        &apos;ngload&apos;: &apos;scripts/ngload&apos;,       
        &apos;mainService&apos;: &apos;services/mainServices&apos;,
        &apos;ajaxService&apos;: &apos;services/ajaxServices&apos;,
        &apos;alertsService&apos;: &apos;services/alertsServices&apos;,
        &apos;accountsService&apos;: &apos;services/accountsServices&apos;,
        &apos;customersService&apos;: &apos;services/customersServices&apos;,
        &apos;ordersService&apos;: &apos;services/ordersServices&apos;,
        &apos;productsService&apos;: &apos;services/productsServices&apos;,
        &apos;dataGridService&apos;: &apos;services/dataGridService&apos;,         
        &apos;angular-sanitize&apos;: &apos;scripts/angular-sanitize&apos;,
        &apos;customersController&apos;: &apos;Views/Shared/CustomersController&apos;,
        &apos;productLookupModalController&apos;: &apos;Views/Shared/ProductLookupModalController&apos;
    },
    // 配置不支持AMD的js文件
    shim: {
        &apos;angularAMD&apos;: [&apos;angular&apos;],
        &apos;angular-route&apos;: [&apos;angular&apos;],
        &apos;blockUI&apos;: [&apos;angular&apos;],
        &apos;angular-sanitize&apos;: [&apos;angular&apos;],
        &apos;ui-bootstrap&apos;: [&apos;angular&apos;]
          
    },
    // 啟動應用程序
    deps: [&apos;application-configuration&apos;]
});

 

Application-Configuration.js - 引導程序和配置文件

AngularJS 有兩個執行階段,配置階段和運行階段。Application-Configuration.js 會由RequireJS來執行,它會屏蔽掉AngularJS的配置階段。初始的配置將會使用AngularJS的routeProvider 服務來設定應用程序的路由。在后面瀏覽示例應用的時候,還會在應用的引導過程中,添加另外的配置函數到配置階段中去。

// application-configuration.js

"use strict";
define([&apos;angularAMD&apos;, &apos;angular-route&apos;, &apos;ui-bootstrap&apos;, &apos;angular-sanitize&apos;, &apos;blockUI&apos;, ], 
function (angularAMD) {
    
    var app = angular.module("mainModule", 
        [&apos;ngRoute&apos;, &apos;blockUI&apos;, &apos;ngSanitize&apos;, &apos;ui.bootstrap&apos;]);
          
    app.config([&apos;$routeProvider&apos;, function ($routeProvider) {
   
    $routeProvider

    .when("/", angularAMD.route({
                         
        templateUrl: function (rp) {  return &apos;Views/Main/default.html&apos;;  },               
                controllerUrl: "Views/Main/defaultController"            
    }))

    .when("/:section/:tree", angularAMD.route({

        templateUrl: function (rp) { 
                     return &apos;views/&apos; + rp.section + &apos;/&apos; + rp.tree + &apos;.html&apos;; },

        resolve: {
        load: [&apos;$q&apos;, &apos;$rootScope&apos;, &apos;$location&apos;, 
            function ($q, $rootScope, $location) {
                
                 var path = $location.path();
                 var parsePath = path.split("/");
                 var parentPath = parsePath[1];
                 var controllerName = parsePath[2];
                 var loadController = "Views/" + parentPath + "/" + 
                                       controllerName + "Controller";

                 var deferred = $q.defer();
                 require([loadController], function () {
                        $rootScope.$apply(function () {
                        deferred.resolve();
                 });
            });
            return deferred.promise;
            }]
        }
    }))
    
    .when("/:section/:tree/:id", angularAMD.route({

        templateUrl: function (rp) { 
                     return &apos;views/&apos; + rp.section + &apos;/&apos; + rp.tree + &apos;.html&apos;; },

        resolve: {
        load: [&apos;$q&apos;, &apos;$rootScope&apos;, &apos;$location&apos;, 
            function ($q, $rootScope, $location) {
                var path = $location.path();
                var parsePath = path.split("/");
                var parentPath = parsePath[1];
                var controllerName = parsePath[2];
                var loadController = "Views/" + parentPath + "/" + 
                                      controllerName + "Controller";
                                             
                var deferred = $q.defer();
                require([loadController], function () {
                    $rootScope.$apply(function () {
                        deferred.resolve();
                        });
            });
            return deferred.promise;
            }]
            }
        }))
        .otherwise({ redirectTo: &apos;/&apos; }) 
    }]);                
  
    // Bootstrap Angular when DOM is ready
    angularAMD.bootstrap(app);
  
    return app;
});

RequireJS的Define語句

查看 application-configuration.js 文件,你很快會看到這個define語句. Define語句是一個將會加載一個代碼模塊的RequireJS語句. 模塊不同於傳統的腳本文件,它是能夠避免污染全局命名空間的界定良好的對象。它可以明確列出它的依賴項,並獲取這些依賴項上的句柄,無需引用全局對象,而是將依賴作為定義了此模塊的函數的參數進行接收.

RequireJS 中的模塊式模塊模式的一種擴展,其優點是不用全局地區引用其它的模塊. RequireJS用於模塊的語法允許它們盡快加載,即使加載順序是亂的,其后也能計算出正確的依賴順序, 而由於沒有創建全局變量,在一個頁面上加載一個模塊的不同版本就有了可能。本應用程序在應用程序范圍內依賴於angularAMD, angular-route, ui-bootstrap, angular-sanitize 和 blockUI 這些庫.

AngularAMD, UI-Bootstrap, Angular-Sanitize 和 BlockUI

Application-Configuration.js 引用了 angularAMD 作為依賴項. 我在互聯網上沖浪時在 http://marcoslin.github.io/angularAMD/#/home 發現了angularAMD. angularAMD 改進了RequireJS在AngularJS應用程序中的使用,支持控制器和第三方模塊的按需加載,比如本應用程序所使用的Angular-UI.
       
UI-Bootstrap 是一個包含了一整套基於Bootstrap的標記和CSS的本地AngularJS指令的資源庫. 本應用程序使用了許多來自Angular-UI和Twitter Bootstrap CSS的空間和樣式.  

angular-sanitize 庫需要被用來允許HTML被注入到視圖模板中。默認情況下,出於安全的考量,AngularJS是阻止HTML標記的注入的。

最后,應用程序使用了AngularJS的blockUI配置庫來讓你在發起AJAX請求過程中可以阻塞用戶界面.

動態路由表

application-configuration.js 的最大目的是為內容頁面和與之關聯的JavaScript控制器設置路由、渲染和加載規則。探索如何使用約定而不是硬編碼的方式來創建動態路由表,猶如一次探險。在這次探險過程中我發現了Per Ploug's的博客http://scriptogr.am/pploug/post/convention-based-routing-in-angularjs。在他的博客中他提到了路由的下面這些元素,這些元素可以從AngularJS的的路由提供器中獲得:
            
/:secion/:tree/:action/:id

這條文檔中幾乎沒有提到的功能,為我們開啟了一扇門,告訴我們需要什么才能完成基於約定的動態路由。

在示例中,大部分網頁文件在Views文件夾下。 Veiws文件夾中,一個模塊對應一個子文件夾,如Accounts, Customers, Orders, Products等。 修改用戶頁面的根路徑是 /Views/Customers/CustomerMaintenance, 查詢訂單頁面的根路徑是/Views/Orders/OrderInquiry.為了方便控制器動態加載文件,我把這些頁面的控制器代碼文件也放到Views文件夾下。

修改用戶頁面的控制器文件路徑是 /Views/Customers/CustomerMaintenanceController.js,這樣可以簡化開發。把公共的代碼放到工程的同一個文件夾下,可以讓你快速定位需要查看的代碼。 在MVC框架里,控制器文件通常被單獨放在一個文件夾下,當工程變得比較龐大時,這些文件會難以維護。

渲染HTML模板很容易。 只需設置一下templateUrl屬性:

'views/' + rp.section + '/' + rp.tree + '.html'.

 

引入 rp.setion和 rp.tree變量,可以很容易實現路徑匹配、路徑轉換。轉換完路徑后,唯一需要做的事是把擴展名 .html連接到字符串末尾。

加載控制器文件的過程有點復雜。 AngularJS路徑配置的控制器屬性只支持靜態的字符串。 它不支持含有變量的字符串,如下:

 

controller = "Views/" + parentPath + "/" + controllerName + "Controller";

 

AngularJS 還需要更多的創新。

 

經過一段時間的研究,我發現可以通過功能分解來設置控制器屬性。 結合使用AngularJS的location service和deferred promise特性,我最終實現動態加載js控制器文件時設置控制器屬性值。 js性能的一個提升意味着這次改造產生了最終的價值。

路由表里最終只有兩個主路徑,AngularJS需要對其進行匹配。第二個路徑

/:section/:tree/:id

是用來處理那些帶有參數的路徑的。現在,不管應用變得多大,路由表都將會保持的很小,而且只需要跟兩個路徑進行匹配,這樣就提高了路由匹配的效率。

最終,application-configuration.js使用angularAMD來引導AngularJS應用。

客戶管理頁面 - 創建和編輯客戶信息

 使用 AngularJS 開發一個大規模的單頁應用(SPA)

單頁應用中的頁面與asp.net頁面類似。 相同之處,兩者都是html的一個子集。 對於asp.net,當瀏覽器開始渲染頁面元素時,html、js、數據被傳入控制層代碼,然后,瀏覽器進行計算、展示。在單頁應用中,RequireJS使用ng-view指令把頁面內容注入到一個div標簽中。

頁面初始化時,瀏覽器通常只渲染html代碼。 若在單頁應用中使用RequireJS,js會被動態加載。 當頁面加載完,瀏覽器以ajax異步調用的方式從服務器讀取數據。

構建於ASP.NET母版頁之上的SPA應用程序及其內容內面,你將可以馬上收獲的性能之一,就死SPA的內容將會被緩存到客戶端,而每一個頁面都會從服務器端獲取到. 使用你拿手的瀏覽器開發工具,就可以看到內容已經被緩存了。最終你所有的頁面都會被緩存,而最后你只是通過AJAX請求通過網絡獲取服務器段數據而已. 所有這些都促成了高效的響應時間已經增強的用戶體驗.

<!-- CustomerMaintenance.html -->

<div ng-controller="customerMaintenanceController" ng-init="initializeController()">

<h3> Customer Maintenance  </h3>
     <table class="table" style="width:100%">
<tr>
<td class="input-label" align="right"> <label class="required">Customer Code: </label> </td>
<td class="input-box">
<div ng-bind="CustomerCode" ng-show="DisplayMode"> </div>
<div ng-show="EditMode"> 
     <input ng-model="CustomerCode" type="text" style="width: 300px" 
     ng-class="{&apos;validation-error&apos;: CustomerCodeInputError}" /> 
</div>
</td>
</tr>
<tr>
<td class="input-label" align="right"> <label class="required">Company Name: </label> </td>
<td class="input-box">
<div ng-bind="CompanyName" ng-show="DisplayMode"> </div>
<div ng-show="EditMode"> 
     <input ng-model="CompanyName" type="text" style="width: 300px" 
            ng-class="{&apos;validation-error&apos;: CompanyNameInputError}" /> 
</div>
</td>
</tr>
<tr>
<td class="input-label" align="right"> <label>Address: </label> </td>
<td class="input-box">
<div ng-bind="Address" ng-show="DisplayMode"> </div>
<div ng-show="EditMode"> 
     <input ng-model="Address" type="text" style="width: 300px" /> 
</div>
</td>
</tr>
<tr>
<td class="input-label" align="right"> <label>City: </label> </td>
<td class="input-box">
<div ng-bind="City" ng-show="DisplayMode"> </div>
<div ng-show="EditMode"> 
     <input ng-model="City" type="text" style="width: 300px" /> 
</div>
</td>
</tr>
<tr>
<td class="input-label" align="right"> <label>Region: </label> </td>
<td class="input-box">
<div ng-bind="Region" ng-show="DisplayMode"> </div>
<div ng-show="EditMode"> 
     <input ng-model="Region" type="text" style="width: 300px" /> 
</div>
</td>
</tr>
<tr>
<td class="input-label" align="right"> <label>Postal Code: </label> </td>
<td class="input-box">
<div ng-bind="PostalCode" ng-show="DisplayMode"> </div>
<div ng-show="EditMode"> 
     <input ng-model="PostalCode" type="text" style="width: 300px" /> 
</div>
</td>
</tr>
<tr>
<td class="input-label" align="right"> <label>Country: </label> </td>
<td class="input-box">
<div ng-bind="CountryCode" ng-show="DisplayMode"> </div>
<div ng-show="EditMode"> 
     <input ng-model="CountryCode" type="text" style="width: 300px" /> 
</div>
</td>
</tr>
<tr>
<td class="input-label" align="right"> <label>Phone Number: </label> </td>
<td class="input-box">
<div ng-bind="PhoneNumber" ng-show="DisplayMode"> </div>
<div ng-show="EditMode"> 
     <input ng-model="PhoneNumber" type="text" style="width: 300px" /> 
</div>
</td>
</tr>
<tr>
<td class="input-label-bottom" align="right"> <label>Web Site URL: </label> </td>
<td class="input-box-bottom">
<div ng-bind="WebSiteURL" ng-show="DisplayMode"> </div>
<div ng-show="EditMode"> 
     <input ng-model="WebSiteURL" type="text" style="width: 300px" /> 
</div>
</td>
</tr>
</table>

<span ng-show="ShowCreateButton"> 
<button class="btn btn-primary btn-large" ng-click="createCustomer()">Create </button> </span>
<span ng-show="ShowEditButton"> 
<button class="btn btn-primary btn-large" ng-click="editCustomer()">Edit </button> </span>
<span ng-show="ShowUpdateButton"> 
<button class="btn btn-primary btn-large" ng-click="updateCustomer()">Update </button> </span>
<span ng-show="ShowCancelButton"> 
<button class="btn btn-primary btn-large" ng-click="cancelChanges()">Cancel </button> </span>
<div style="padding-top:20px">

<alert ng-repeat="alert in alerts" type="{{alert.type}}" close="closeAlert($index)"> 
<div ng-bind-html="MessageBox"> </div> </alert>

</div>
</div>

 

數據綁定及關注點的分離(SoC)

查看上面用於示例程序的顧客維護頁面的HTML內容,你能夠看到其實你可以創建出一個看起來很清晰,也容易閱讀的HTML。內容里面也沒有引用任何JavaScript.

借助於data-binding指令,AngularJS提供了內容視圖及內容控制器之間清晰的關注點分離. 對於輸入控制,雙向數據綁定通過ng-bind這個指令以及顧客維護控制器的$scope屬性得到了實現. AngularJS中的數據綁定功能同其它的JavaScript庫,諸如KnockoutJS,功能相似, 對於文檔對象模型的轉換需求已經成為過去式——這是好事,因為許多的JavaScript問題都源於DOM的轉換.

ng-show 指令是的顯示隱藏的HTML內容變得容器. 對於顧客維護頁面來說,這將會讓頁面只用設置一個JavaScript的AngularJS $scope變量,就可以同時支持編輯模式和只讀模式. ng-click  指令將會執行在按下按鈕時執行的控制器函數.

顧客維護控制器

示例中的每一個控制器都會被封裝到一個RequireJS定義語句中,幫助AngularJS對控制器進行注冊. 此外,定義語句將告知RequireJS顧客維護控制器正常運行所依賴的其它庫和服務. 在本例中,控制器依賴於 application-configuration,customersService 以及 alertsServices 這些功能. 這些JavaScript依賴將會通過RequireJS被動態加載進來.

AngularJS 使用了依賴注入, 因此控制器所需的所有東西都會通過參數被注入到其中. 如果你希望使用一種單元測試工具,比如Jasmine,來在你的JavaScript控制器上進行單元測試的話,這就會很有用.

$scope 對象提供了視圖和控制器之間的雙向數據綁定. 控制器里面再也不需要對於HTML內容的直接引用了. 控制器通過執行initializeContent函數啟動,這個函數是借助內容頁面中的ng-init指令被初始化的 .

顧客維護頁面將引用  $routeParams  服務來決定是否傳入了顧客的編號. 如果是,控制器就將在customerService上執行一個getCustomer函數,該函數會向服務器發起一次AJAX調用,隨后返回的JSON格式的顧客數據將會被填充到$scope屬性中,繼而會更新HTML模板 .

當用戶點擊創建按鈕時,控制層會調用 createCustormer 函數。 然后,createCustormer 函數會創建一個customer類型的js對象,控制層將js對象傳遞給服務器,實現將數據保存到數據庫中。 示例中使用了微軟的WEB API、實體框架,服務器端使用了 SQL Server 數據庫,從技術上講,可以用AngularJS 與任意類型的數據庫進行交互。

// customerMaintenanceController.js

"use strict";
define([&apos;application-configuration&apos;, &apos;customersService&apos;, &apos;alertsService&apos;], function (app) 
{
    app.register.controller(&apos;customerMaintenanceController&apos;, 
    [&apos;$scope&apos;, &apos;$rootScope&apos;, &apos;$routeParams&apos;, &apos;customersService&apos;, &apos;alertsService&apos;,

    function ($scope, $rootScope, $routeParams, customerService, alertsService) 
    {
        $scope.initializeController = function () {
    
            var customerID = ($routeParams.id || "");
           
            $rootScope.alerts = [];
            $scope.CustomerID = customerID;
    
            if (customerID == "") {
                $scope.CustomerCode = "";
                $scope.CompanyName = "";
                $scope.Address = "";
                $scope.City = "";
                $scope.Region = "";
                $scope.PostalCode = "";
                $scope.CountryCode = "";
                $scope.PhoneNumber = ""
                $scope.WebSiteURL = "";
             
                $scope.EditMode = true;
                $scope.DisplayMode = false;
                $scope.ShowCreateButton = true;
                $scope.ShowEditButton = false;
                $scope.ShowCancelButton = false;
                $scope.ShowUpdateButton = false;
              
            }
            else
            {
                var getCustomer = new Object();
                getCustomer.CustomerID = customerID;
                customerService.getCustomer(getCustomer, 
                                $scope.getCustomerCompleted, 
                                $scope.getCustomerError);
            }
          
        }

        $scope.getCustomerCompleted = function (response) {

            $scope.EditMode = false;
            $scope.DisplayMode = true;
            $scope.ShowCreateButton = false;
            $scope.ShowEditButton = true;
            $scope.ShowCancelButton = false;
            $scope.ShowUpdateButton = false;

            $scope.CustomerCode = response.Customer.CustomerCode;
            $scope.CompanyName = response.Customer.CompanyName;
            $scope.Address = response.Customer.Address;
            $scope.City = response.Customer.City;
            $scope.Region = response.Customer.Region;
            $scope.PostalCode = response.Customer.PostalCode;
            $scope.CountryCode = response.Customer.Country;
            $scope.PhoneNumber = response.Customer.PhoneNumber;
            $scope.WebSiteURL = response.Customer.WebSiteUrl;          
        }

        $scope.getCustomerError = function (response) {
            alertsService.RenderErrorMessage(response.ReturnMessage);
        }
        
  
        $scope.createCustomer = function () {          
            var customer = $scope.createCustomerObject();
            customerService.createCustomer(customer, 
                                            $scope.createCustomerCompleted, 
                                            $scope.createCustomerError);
        }
      
        $scope.createCustomerCompleted = function (response, status) {

            $scope.EditMode = false;
            $scope.DisplayMode = true;
            $scope.ShowCreateButton = false;
            $scope.ShowEditButton = true;
            $scope.ShowCancelButton = false;
            $scope.CustomerID = response.Customer.CustomerID;

            alertsService.RenderSuccessMessage(response.ReturnMessage);

            $scope.setOriginalValues();
        }

        $scope.createCustomerError = function (response) {
            alertsService.RenderErrorMessage(response.ReturnMessage);
            $scope.clearValidationErrors();
            alertsService.SetValidationErrors($scope, response.ValidationErrors);
        }
    
        $scope.createCustomerObject = function () {

            var customer = new Object();

            customer.CustomerCode = $scope.CustomerCode;
            customer.CompanyName = $scope.CompanyName;
            customer.Address = $scope.Address;
            customer.City = $scope.City;
            customer.Region = $scope.Region;
            customer.PostalCode = $scope.PostalCode;
            customer.Country = $scope.CountryCode;
            customer.PhoneNumber = $scope.PhoneNumber;
            customer.WebSiteUrl = $scope.WebSiteURL;

            return customer;
        }

        $scope.clearValidationErrors = function () {
            $scope.CustomerCodeInputError = false;
            $scope.CompanyNameInputError = false;          
        }
      
    }]);
});

 

Controller As 語法

示例中,顯示層和控制層使用 $scope 技術實現 web應用和數據庫的雙向綁定。在上面的控制層代碼中,你可以看到很多地方都使用了 $scope 對象。 在 AngularJS 中,這是實現數據綁定比較常見的方式。 AngularJS 控制層代碼近期進行了細微的、影響比較大的優化。

最新的趨勢是使用 Controller as ControllerName 這樣的語法,而不是直接將$scope注入到你的控制器中。例如,顧客維護控制器可以像如下視圖中這樣被引用:

<div ng-controller="customerController as customer">
<input ng-model="customer.FirstName" type="text" style="width: 300px" />
<input ng-model="customer.LastName" type="text" style="width: 300px" />       
<div>
<button class="btn btn-primary btn-large" ng-click="createCustomer()"/>Create</button>
</div>

 

填充數據綁定屬性的控制器語法就可以像下面這樣:                        

this.FirstName = "";
this.LastName = "";

 

使用 "this" 對象來引用控制器的scope看上去比直接將$scope注入到控制器中更加清晰。這里需要重申,$scope是“經典”技術,而“controller as"則是AngularJS里更加新晉的東西. 它們倆都能能工作得很好,不管是選擇哪一種技術,都要記用着方便為出發點. 現有的實例更多使用的是$scope,而”controller as“則正在慢慢紅火起來. 其中一個會比另外一個好么?這我們就得等待並觀察AngularJS隨時間發生的演變了.

自定義服務 - AngularJS 服務

AngularJS 服務是可替換的對象,這些對象使用依賴注入連接在一起。 在程序里,你可以使用服務來組織和共享你的代碼。 AngularJS 服務是延遲初始化的 – 只有當應用程序組件依賴它時,AngularJS 才會初始化一個服務。

AngularJS 服務是單例類型 – 依賴服務的每個組件都會引用AngularJS 服務工廠類產生的一個實例。 雖然AngularJS 提供一些常用的服務(如$http),但是對於大多數應用來說,你可能想要創建自己的服務。

顧客維護控制器依賴於 CustomerService. 這個顧客服務組件被應用程序用於組織所有訪問和向應用程序服務器傳遞顧客相關數據所需要的Web API路由. 為了保持示例應用程序所有控制器中路由的清晰, 我為每一個部分(包括顧客、訂單、產品)都創建了服務層. AngularJS 服務能幫助你組織好你的JavaScript,以獲得更好的重用性和可維護性.

顧客服務引用了由控制器設置的回調函數. 這個回調函數會在服務器調用完成時執行. 如你所能看見的,顧客服務沒有執行向服務器發起HTTP調用的實際工作。在定義語句中,則會有對將會被動態加載進來的ajaxService的依賴.

// customerService.js

define([&apos;application-configuration&apos;, &apos;ajaxService&apos;], function (app) {

    app.register.service(&apos;customersService&apos;, [&apos;ajaxService&apos;, function (ajaxService) {

        this.importCustomers = function (successFunction, errorFunction) {
            ajaxService.AjaxGet("/api/customers/ImportCustomers", 
                successFunction, errorFunction);
        };

        this.getCustomers = function (customer, successFunction, errorFunction) {          
            ajaxService.AjaxGetWithData(customer, "/api/customers/GetCustomers", 
                successFunction, errorFunction);
        };

        this.createCustomer = function (customer, successFunction, errorFunction) {
            ajaxService.AjaxPost(customer, "/api/customers/CreateCustomer", 
                successFunction, errorFunction);
        };

        this.updateCustomer = function (customer, successFunction, errorFunction) {
            ajaxService.AjaxPost(customer, "/api/customers/UpdateCustomer", 
                successFunction, errorFunction);
        };
     
        this.getCustomer = function (customerID, successFunction, errorFunction) {
            ajaxService.AjaxGetWithData(customerID, "/api/customers/GetCustomer", 
                successFunction, errorFunction);
        };

    }]);

});

AJAX 服務

為本應用程序所創建的AJAX服務將會被所有的HTTP請求重用。AJAX 服務使用了AngularJS 的 $http 服務 , 該服務會實際執行面向服務器的 HTTP GET 和 POST 調用. 服務器調用的則是 RESTful 服務,返回的是簡單的 JSON 對象.
       
AJAX 服務還使用了blockUI在HTTP請求進行時使用UI來阻塞用戶的交互. 此外你還可以應用安全功能來檢查用戶是否已經被認證. 此應用程序使用了Forms Authentication,它會在每一個請求時附帶向服務器發送一個認證的token. 我已經添加了一行代碼,通過檢查來自服務器的響應消息中一個普通的IsAuthenicated 屬性,來看看用戶是否仍然是通過認證的.

如果session已經超時,則對IsAuthenicated的檢查會將用戶路由到登陸頁面. 讓一個AJAX服務成為管理你所有的AJAX調用的中心,可以使得對整個應用程序的AJAX調用功能的實現和修改變得容易起來.

// ajaxService.js

define([&apos;application-configuration&apos;], function (app) 
{
    app.register.service(&apos;ajaxService&apos;, [&apos;$http&apos;, &apos;blockUI&apos;, function ($http, blockUI) {
        this.AjaxPost = function (data, route, successFunction, errorFunction) {
            blockUI.start();
            setTimeout(function () {
                $http.post(route, data).success(function 
                          (response, status, headers, config) 
                {
                    blockUI.stop();
                    successFunction(response, status);
                }).error(function (response) {
                    blockUI.stop();
                    if (response.IsAuthenicated == false) 
                    { 
                        window.location = "/index.html"; 
                    }
                    errorFunction(response);
                });
            }, 1000);
        }
     
        this.AjaxGet = function (route, successFunction, errorFunction) {
            blockUI.start();
            setTimeout(function () {
                $http({ method: &apos;GET&apos;, url: route }).success(
                function (response, status, headers, config) {
                    blockUI.stop();
                    successFunction(response, status);
                }).error(function (response) {
                    blockUI.stop();
                    if (response.IsAuthenicated == false) 
                    { 
                        window.location = "/index.html"; 
                    }
                    errorFunction(response);
                });
            }, 1000);
        }

        this.AjaxGetWithData = function (data, route, successFunction, errorFunction) {
            blockUI.start();
            setTimeout(function () {
                $http({ method: &apos;GET&apos;, url: route, params: data }).success(
                function (response, status, headers, config) {
                    blockUI.stop();
                    successFunction(response, status);
                }).error(function (response) {
                    blockUI.stop();
                    if (response.IsAuthenicated == false) 
                    { 
                        window.location = "/index.html"; 
                    }
                    errorFunction(response);
                });
            }, 1000);
        }
    
    }]);
});

 

用於AJAX服務的額外配置

在application-configuration.js文件中,加入了用於AJAX服務器請求的額外配置. 為了配置AngularJS 跟隨每次請求傳遞Forms Authentication的 cookie 信息, $httpProvider 會需要一個用於讓 withCredentials 屬性被設置為true的值.

在http連接中,AngularJS 不默認返回一個XMLHttpRequest對象,但是你可以在$httpProvider服務里配置。 當瀏覽器請求中含有一些阻塞UI展示的配置項時,你可以使用blockUI組件,實現在前台展示自定義的消息。

// application-configuration.js

app.config(function ($httpProvider) {
    $httpProvider.defaults.headers.common[&apos;X-Requested-With&apos;] = &apos;XMLHttpRequest&apos;;
    $httpProvider.defaults.withCredentials = true;
});
app.config(function (blockUIConfigProvider) {
    // 修改默認的提示信息
    blockUIConfigProvider.message("executing...");
    // 修改UI不可見時默認的延遲時間為100ms
    blockUIConfigProvider.delay(1);
    // 禁用自動阻塞頁面展示配置項
    blockUIConfigProvider.autoBlock(false);
});

 

在每個頁面請求中進行身份驗證

在示例中,indexController控制前台頁面的展示。 基於這一點,加載配置項時,我在application-configuration.js中定義indexController。這樣,在應用程序運行之前,indexController和AngularJS一起被加載、注冊。 大型的網頁應用中,對於每個頁面的請求,通常優先進行身份驗證、授權。 為了解決這個問題,indexController包含一個函數,實現在每個頁面請求前,對用戶身份進行驗證。

AngularJS 可以配置、監聽客戶端頁面上用戶觸發的事件。 其中一個事件是$routeChangeStart。 每次請求路由定位時,都會觸發這個事件。 為了使監聽器工作,你只需使用$scope.$on指令配置下這個事件。

由於indexController 控制頁面的跳轉,因此可以在indexController 里配置$routeChangeStart 事件。在下面的示例中,為了判斷用戶是否被授權,瀏覽器在頁面請求前優先執行了一個http get請求。 如果返回的isAuthenicated值為false,瀏覽器會跳轉到登陸頁面。 另外,你可以進行額外的安全性檢查來判斷用戶是否有權限訪問請求的頁面。

// indexController.js

var indexController = function ($scope, $rootScope, $http, $location, blockUI) {
             
    $scope.$on(&apos;$routeChangeStart&apos;, function (scope, next, current) {                          
        $scope.authenicateUser($location.path(),
                $scope.authenicateUserComplete, $scope.authenicateUserError);                 
    });
      
    $scope.authenicateUser = function (route, successFunction, errorFunction) {
        var authenication = new Object();
        authenication.route = route;
        $scope.AjaxGet(authenication, "/api/main/AuthenicateUser", 
                successFunction, errorFunction);
    };
           
    $scope.authenicateUserComplete = function (response) {         
        if (response.IsAuthenicated==false)    
        {           
            window.location = "/index.html";
        }
    }
         
};

 

AngularJS $rootScope

在AngularJS里面,每個應用程序都有一個單獨的root scope. 所有其他scope都是root scope的衍生物. Scope隔離了模型和視圖. 你可以將屬性設置在$rootScope之下,這些屬性在外殼頁面(shell page)的生存周期內一直保留其屬性值. 只要用戶刷新了瀏覽器,$rootScope的值就會消失,必須要重新設置.
       
當示例應用程序初始化加載的時候,它使用$rootScope保存從服務器返回的菜單選項.在用戶登錄后,拓展后的菜單選項列表將會從服務器返回,它允許用戶訪問應用程序的其它部分.$rootScope是一個很好的用來保存菜單選項等會話級別信息的地方.

$rootScope.MenuItems = response.MenuItems;

 

在外殼頁面(shell page), 菜單項是數據綁定到無序列表的,在每個頁面請求時保持設定的狀態.

<div class="navbar-collapse collapse" id="MainMenu">
<ul class="nav navbar-nav" ng-repeat="menuItem in MenuItems">
    <li> <a href="{{menuItem.Route}}">{{menuItem.Description}} </a> </li>
</ul>
</div>

AngularUI

下面的示例中使用了AngularUI的各種UI組件。AngularUI 是AngularJS 框架的一個輔助套件。示例中使用的主要組件大部分來在AngularUI 的一個子集UI Bootstrap。UI Bootstrap是從Twitter Bootstrap派生出來的,它使用AngularJS編碼實現。 UI Bootstrap庫包含了一套使用Bootstrap標識和樣式的AngularJS 指令。 這使得它不依賴jQuery.js和Bootstrap.js。

Alert (ui.bootstrap.alert)

AngularJS Alert 是由Bootstrap alert 派生過來的。 使用ng-repeat指令,可以實現使用動態模型里的數據彈窗提示。

 

<div style="padding-top:20px">
<alert ng-repeat="alert in alerts" type="{{alert.type}}" close="closeAlert($index)">
        <div ng-bind-html="MessageBox"></div>
</alert>
</div>

 

Alert指令支持展示紅色的錯誤信息,綠色的提示信息和黃色的警告信息。 在示例的修改用戶信息頁面,當用戶沒有輸入必填字段用戶名時,頁面會彈出一個錯誤提示。 我擴展了alert的功能:當發生錯誤時,alert可以高亮顯示待輸入的文本框。

使用 AngularJS 開發一個大規模的單頁應用(SPA)

為了更深入的拓展警告指令, 這個示例應用程序包含了一個自定義的指令服務(custom alerts service).它可以在整個應用程序中使用,以渲染警告信息.信息的內容設置在$rootScope里面,它來自於服務器的業務層的驗證過程,並在AJAX請求完成后渲染到客戶端.

// alertsService.js

define([&apos;application-configuration&apos;], function (app) 
{
    app.register.service(&apos;alertsService&apos;, [&apos;$rootScope&apos;, function ($rootScope) {

        $rootScope.alerts = [];
        $rootScope.MessageBox = "";

        this.SetValidationErrors = function (scope, validationErrors) {
            for (var prop in validationErrors) {
                var property = prop + "InputError";
                scope[property] = true;
            }       
        }

        this.RenderErrorMessage = function (message) {
            var messageBox = formatMessage(message);
            $rootScope.alerts = [];
            $rootScope.MessageBox = messageBox;
            $rootScope.alerts.push({ &apos;type&apos;: &apos;danger&apos;, &apos;msg&apos;: &apos;&apos; });
        };

        this.RenderSuccessMessage = function (message) {
            var messageBox = formatMessage(message);
            $rootScope.alerts = [];
            $rootScope.MessageBox = messageBox;
            $rootScope.alerts.push({ &apos;type&apos;: &apos;success&apos;, &apos;msg&apos;: &apos;&apos; });
        };

        this.RenderWarningMessage = function (message) {
            var messageBox = formatMessage(message);
            $rootScope.alerts = [];
            $rootScope.MessageBox = messageBox;
            $rootScope.alerts.push({ &apos;type&apos;: &apos;warning&apos;, &apos;msg&apos;: &apos;&apos; });
        };

        this.RenderInformationalMessage = function (message) {
            var messageBox = formatMessage(message);
            $rootScope.alerts = [];
            $rootScope.MessageBox = messageBox;
            $rootScope.alerts.push({ &apos;type&apos;: &apos;info&apos;, &apos;msg&apos;: &apos;&apos; });
        };

        this.closeAlert = function (index) {
            $rootScope.alerts.splice(index, 1);
        };

        function formatMessage(message) {
            var messageBox = "";
            if (angular.isArray(message) == true) {
                for (var i = 0; i < message.length; i++) {
                    messageBox = messageBox + message[i];
                }
            }
            else {
                messageBox = message;
            }
            return messageBox;
        }
    }]);
});

 

當創建一個客戶記錄出錯時,下面的代碼被執行,同時驗證警告服務的調用過程.

$scope.createCustomerError = function (response) {
    alertsService.RenderErrorMessage(response.ReturnMessage);
    $scope.clearValidationErrors();
    alertsService.SetValidationErrors($scope, response.ValidationErrors);
}

Datepicker控件 (ui.bootstrap.datepicker)

UI Bootstrap Datepicker控件 是一種清潔、靈活和完全可定制的日期選擇器。用戶可以瀏覽數月乃至數年。

使用 AngularJS 開發一個大規模的單頁應用(SPA)

把Datepicker輸入框(input box)標簽里,只需把Datepicker相關的參數添加到輸入框,然后添加一個按鈕,用戶可以通過單擊日歷圖標顯示Datepicker。

<tr>
<td class="input-label" align="right"><label class="required">Required Ship Date:</label></td>
<td class="input-box" style="height:50px">
<div ng-bind="RequiredDate" ng-show="DisplayMode"></div>
<div ng-show="EditMode">
<div class="row">
<div class="col-md-6">
<p class="input-group">

<input ng-class="{&apos;validation-error&apos;: RequiredDateInputError}" type="text" style="width:100px"
               datepicker-popup="MM/dd/yyyy"
               ng-model="RequiredDate"
               is-open="opened"
               datepicker-options="dateOptions"
               date-disabled="disabled(date, mode)"
               ng-required="true"
               close-text="Close" />

<button type="button" ng-click="open($event)"><i style="height:10px"
              class="glyphicon glyphicon-calendar"></i></button>

</p>
</div>
</div>
</div>
</td>
</tr>

Modal (ui.bootstrap.modal

UI Bootstrap的Modal是一種服務,它可以快速的創建擁有Angular屬性的模態對話框.創建定制化的modal是很簡單,只需創建部分視圖,增加一個控制器,然后在使用服務的時候引用它們.

使用 AngularJS 開發一個大規模的單頁應用(SPA)

    
下面的JavaScript代碼為Product Inquiry Modal打開了一個HTML模板,並創建了一個modal實例.當一個產品項目被選中的時候,產品id通過modal實例的結果方法返回.這個modal實例從服務器獲取產品信息.產品信息返回到調用頁面后,modal消失.

$scope.openModal = function () {

    var modalInstance = $modal.open({
        templateUrl: &apos;productLookupModal.html&apos;,
        controller: ModalInstanceCtrl,
        windowClass: &apos;app-modal-window&apos;
    });

    modalInstance.result.then(function (productID) {

        var getProduct = new Object();
        getProduct.ProductID = productID;
        productService.getProduct(getProduct, 
                                  $scope.getProductCompleted, 
                                  $scope.getProductError);

    }, function () {
        // function executed on modal dismissal
    });
};

var ModalInstanceCtrl = function ($scope, $modalInstance) {

    $scope.ProductCode = "";
    $scope.ProductDescription = "";

    $scope.productSelected = function (productID) {
        $modalInstance.close(productID);
    };

    $scope.cancel = function () {
        $modalInstance.dismiss(&apos;cancel&apos;);
    };
};

Typeahead (ui.bootstrap.typeahead)

Typeahead是AngularJS Bootstrap v2版本的typeahead插件.這個指令可以快速創建一個漂亮的基於任意文本框的typeahead控件.Product Inquiry Modal窗口使用了Typeahead指令

<input type="text" ng-model="Description"
          typeahead="product for products in getProducts($viewValue)">

 

在上面例子中的typeahead指令,將把輸入框中的輸入信息作為參數並執行getProducts函數.然后getProducts函數會調用Products Service來執行一個AJAX請求.這個請求將返回一個基於用戶輸入信息的產品數據的頁面,並設置產品查詢數據列表.

$scope.getProducts = function () {
    var productInquiry = $scope.createProductInquiryObject();
    productService.getProducts(productInquiry,
                    $scope.productInquiryCompleted, $scope.productInquiryError);
}

Pagination (ui.bootstrap.pagination)

Pagination是一個輕量級的分頁指令,它集中於提供數據列表分頁,顯示分頁欄以及正確啟用和禁用按鈕.

<pagination boundary-links="true" total-items="TotalProducts" 
                items-per-page="PageSize" ng-change="pageChanged()" 
                ng-model="CurrentPageNumber" class="pagination-lg" 
                previous-text="Prev" next-text="Next" first-text="First" 
                last-text="Last"></pagination>

 

這個應用程序的所有的數據列表都使用了UI Bootstrap分頁控件.實際上,有了HTML模板和數據綁定功能,實現多用途的數據列表是很容易的.這個數據列表包含類似於這個應用程序的分頁和排序功能.

下面的產品查詢數據列表的HTML模板,詳細描述了如何使用視圖來排序以及划分分頁.在控制器的視圖模型中的數據是和表格綁定,其中表格的行是通過AngularJS的ng-repeat指令動態渲染的.這個指令也用於為每個列頭創建動態的列頭標簽.用戶可以通過點擊列頭來排序.HTML模板和數據綁定功能提供了強大的和簡潔的動態生成功能.使用一段時間的HTML模板后,你將不願再回到使用ASP.NET Server Control的一團糟的狀況了.

<!-- productLookupModal.html -->

<table class="table table-striped table-hover" style="width: 100%;">
<thead>
<tr>
<th colspan="2" style="width: 50%">
<span ng-bind="TotalProducts"></span> Products
                        </th>
<th colspan="5" style="text-align: right; width: 50%">
                            Page <span ng-bind="CurrentPageNumber"></span> of 
<span ng-bind="TotalPages"></span>
</th>
</tr>
<tr>
<th ng:repeat="tableHeader in tableHeaders" ng:class="setSortIndicator(tableHeader.label)" 
    ng:click="changeSorting(tableHeader.label)">{{tableHeader.label}}</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="product in products">

    <td style="width: 25%; height: 25px"><a ng-click="ok(product.ProductID)" 
                                        style=" cursor pointer; 
                                        text-decoration underline; 
                                        color black">{{product.ProductCode}}</a></td>

    <td style="width: 50%; white-space: nowrap"><div ng-bind="product.Description"></div></td>
    <td style="width: 25%; text-align:left; white-space: nowrap">
               <div>{{product.UnitPrice | currency}}</diV></td>

</tr>

</tbody>
</table>
<pagination boundary-links="true" total-items="TotalProducts" items-per-page="PageSize" 
            ng-change="pageChanged()" ng-model="CurrentPageNumber" class="pagination-lg" 
            previous-text="Prev" next-text="Next" first-text="First" last-text="Last">
</pagination>

最后,包裝一下產品查詢列表,下面的產品查詢模態控制器包含了一個自定義數據列表服務引用.它用來在示例應用程序中,為所有的數據列表實現排序功能.這是又一個使用AngularJS Services和Factories的例子.它把代碼封裝成小的可重復使用的,簡潔的,易讀的和易於維護的模塊.

// productLookupModalController.js

"use strict";
define([&apos;application-configuration&apos;, &apos;productsService&apos;, &apos;alertsService&apos;, &apos;dataGridService&apos;], 
    function (app) {
    app.register.controller(&apos;productLookupModalController&apos;, [&apos;$scope&apos;, &apos;$rootScope&apos;, 
    &apos;productsService&apos;, &apos;alertsService&apos;, &apos;dataGridService&apos;,

        function ($scope, $rootScope, productService, alertsService, dataGridService) {

            $scope.initializeController = function () {
                         
                $rootScope.alerts = [];

                dataGridService.initializeTableHeaders();
                dataGridService.addHeader("Product Code", "ProductCode");
                dataGridService.addHeader("Product Description", "Description");              
                dataGridService.addHeader("Unit Price", "UnitPrice");

                $scope.tableHeaders = dataGridService.setTableHeaders();
                $scope.defaultSort = dataGridService.setDefaultSort("Description");

                $scope.changeSorting = function (column) {
                    dataGridService.changeSorting(column, 
                                    $scope.defaultSort, $scope.tableHeaders);

                    $scope.defaultSort = dataGridService.getSort();
                    $scope.SortDirection = dataGridService.getSortDirection();
                    $scope.SortExpression = dataGridService.getSortExpression();
                    $scope.CurrentPageNumber = 1;
                    $scope.getProducts();
                };

                $scope.setSortIndicator = function (column) {
                    return dataGridService.setSortIndicator(column, $scope.defaultSort);
                };

                $scope.ProductCode = "";
                $scope.Description = "";
                $scope.PageSize = 5;
                $scope.SortDirection = "ASC";
                $scope.SortExpression = "Description";
                $scope.CurrentPageNumber = 1;
                $rootScope.closeAlert = dataGridService.closeAlert;
                $scope.products = [];
                $scope.getProducts();
            }

            $scope.productInquiryCompleted = function (response, status) {
                alertsService.RenderSuccessMessage(response.ReturnMessage);
                $scope.products = response.Products;
                $scope.TotalProducts = response.TotalRows;
                $scope.TotalPages = response.TotalPages;
            }

            $scope.searchProducts = function () {
                $scope.CurrentPageNumber = 1;
                $scope.getProducts();
            }

            $scope.pageChanged = function () {
                $scope.getProducts();
            }

            $scope.getProducts = function () {
                var productInquiry = $scope.createProductInquiryObject();
                productService.getProducts(productInquiry,
                               $scope.productInquiryCompleted, $scope.productInquiryError);
            }

            $scope.getProductsTypeAheadProductCode = function (productCode) {
                $scope.ProductCode = productCode;               
                var productInquiry = $scope.createProductInquiryObject();
                productService.getProductsWithNoBlock(productInquiry, 
                               $scope.productInquiryCompleted, $scope.productInquiryError);
            }

            $scope.getProductsTypeAheadDescription = function (description) {
                $scope.Description = description;
                var productInquiry = $scope.createProductInquiryObject();
                productService.getProductsWithNoBlock(productInquiry, 
                               $scope.productInquiryCompleted, $scope.productInquiryError);
            }

            $scope.productInquiryError = function (response, status) {
                alertsService.RenderErrorMessage(response.Error);
            }

            $scope.resetSearchFields = function () {
                $scope.ProductCode = "";
                $scope.Description = "";
                $scope.getProducts();
            }

            $scope.createProductInquiryObject = function () {

                var productInquiry = new Object();

                productInquiry.ProductCode = $scope.ProductCode;
                productInquiry.Description = $scope.Description;
                productInquiry.CurrentPageNumber = $scope.CurrentPageNumber;
                productInquiry.SortExpression = $scope.SortExpression;
                productInquiry.SortDirection = $scope.SortDirection;
                productInquiry.PageSize = $scope.PageSize;

                return productInquiry;

            }
            $scope.setHeaderAlignment = function (label) {
                if (label == "Unit Price")
                    return { &apos;textAlign&apos;: &apos;right&apos; }
                else
                    return { &apos;textAlign&apos;: &apos;left&apos; }
            }
        }]);
});

 

結論

我敢說jQuery過時了嗎?當然,jQuery仍然很流行並廣泛使用.但是,過去的一些年見證了結構化設計模式的框架和庫,如MVC和MVVM(Model-View-ViewModel)的崛起.這些框架和庫包括Backbone.js, Ember.js和AngularJS等.

AngularJS是一個MVC/MVVM framework.它由google創建,以開發具有良好體系結構的和可維護的web應用程序.AngularJS定義了大量的概念來合理的組織web應用程序.應用程序由相互依賴的模塊來定義.它通過新的屬性或者標簽和表達式,關聯指令到頁面來增強HTML,以定義功能強大的模板.它也將應用程序的行為封裝到控制器,這些控制器通過依賴注入的方式實例化.這有利於結構化,而且非常容易測試JavaScript代碼.是的,這里有你開發大型應用程序前端代碼所需的所有東西.AngularJS可能是自jQuery之后,下一個JavaScript大事件.
JavaScript世界開始變得很有意思.我還沒有提到MEAN Stack(AngularJS,Express,NodeJS,MongoDB)的繁榮.它實現了JavaScript從前端到后端的整個平台.很值得期待,所有這些在將來會去向何處.

創建示例應用程序所用到的技術

AngularJS
RequireJS
Visual Studio Express 2013 for Web
Microsoft .NET 4.5.1
Microsoft .NET C#
Microsoft Web API 2
Microsoft Entity Framework 6.0
SQL Server Express


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM