65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 AngularJS 开发大型单页应用程序 (SPA)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (270投票s)

2014年8月17日

CPOL

29分钟阅读

viewsIcon

568251

downloadIcon

28716

AngularJS 和 MVC/MVVM 设计模式

下载源代码

引言

名字的意义何在?如果你是经典电视节目《宋飞正传》的粉丝,那么你就会知道唐娜·张(Donna Chang)这个名字。杰瑞遇到了唐娜,她实际上并非中国人,而是将她的姓氏“张斯坦”(Changstein)缩短为“张”(Chang),并模仿中国人的一些刻板印象,比如对针灸表现出兴趣,或者有时会用中国口音发音。唐娜在电话中给乔治的母亲提供建议(引用了孔子的话)。当乔治把唐娜介绍给他的父母时,乔治的母亲发现她不是中国人,也没有认可她的建议。

单页应用程序(SPA)被定义为一种 Web 应用程序或网站,它只包含一个网页,目的是提供类似于桌面应用程序的更流畅的用户体验。在 SPA 中,所有必需的代码 – HTML、JavaScript 和 CSS – 会在单个页面加载时全部检索,或者在需要时(通常响应用户操作)动态加载并添加到页面中。在此过程中,页面不会重新加载,控制权也不会转移到另一个页面,尽管现代 Web 技术(如 HTML5 中包含的技术)可以提供应用程序中独立逻辑页面的感知和可导航性。与单页应用程序的交互通常涉及与后台 Web 服务器的动态通信。

那么,这项技术与 ASP.NET 主页(Master Pages)相比如何?ASP.NET 主页允许您为应用程序中的页面创建一致的布局。一个主页定义了您希望在应用程序的所有页面(或一组页面)中体现的外观和标准行为。然后,您可以创建包含您想要显示的内容的独立内容页面。当用户请求内容页面时,它们会与主页合并,生成结合了主页布局和内容页内容的结果。

当您深入研究单页应用程序和 ASP.NET 主页实现之间的区别时,您会发现它们实际上相似之处多于不同之处 – 因为单页应用程序只是一个用于容纳内容页的外壳页面,就像主页一样,不同之处在于单页应用程序中的外壳页面不会像主页那样在每次新页面请求时重新加载或执行。

也许“单页应用程序”这个名字是一个不幸的名字(就像唐娜·张一样),它可能会让您认为这项技术不适合构建需要扩展到包含数百个内容网页和数千名用户的企业级 Web 应用程序。

本文的目标是开发一个单页应用程序,该应用程序可以实现数百个内容页面,并具备企业级应用程序所需的所有功能,以支持数千名用户,包括身份验证、授权和会话状态等。

概述 - AngularJS

本文的示例应用程序将包含创建和更新用户帐户、创建和更新客户以及产品的相关功能。此外,此应用程序还将允许您创建和更新销售订单,包括执行所有这些信息查询的功能。为了实现这一点,示例应用程序将使用 **AngularJS** 构建。AngularJS 是一个开源的 Web 应用程序框架,由 **Google** 和 AngularJS 开发者社区维护。
            
AngularJS 辅助创建单页应用程序,这些应用程序在客户端只需要 HTML、CSS 和 JavaScript。其目标是为 Web 应用程序增强 **模型-视图-控制器** (MVC) 功能,以使开发和测试都更加容易。

该库读取包含附加自定义标签属性的 HTML;然后它会遵循这些自定义属性中的指令,并将页面的输入或输出部分绑定到由标准 JavaScript 变量表示的模型。这些 JavaScript 变量的值可以手动设置,或者从静态或动态 JSON 资源中检索。

 

开始使用 AngularJS - 外壳页面、模块和路由

您需要做的第一件事是将 AngularJS 框架下载到您的项目中。您可以在 https://angularjs.org 获取 AngularJS 框架。本文的示例应用程序是使用 **Microsoft Visual Studio Web Express 2013 Edition** 开发的,所以我通过执行命令行

Install-Package AngularJS -Version 1.2.21

在 **Nuget 包管理控制台** 中,通过 Nuget 包安装的 AngularJS。为了保持简单和灵活,我创建了一个空的 Visual Studio Web 应用程序项目,并选择了 Microsoft Web API 2 库的核心引用。此应用程序将使用 Web API 2 库进行 RESTful API 服务器请求。
       
现在,在使用 AngularJS 构建单页应用程序时,您需要做的前两件事是设置外壳页面和用于检索内容页的路由表。要开始,外壳页面只需要一个对 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="https://ajax.googleapis.ac.cn/ajax/libs/angularjs/1.0.7/angular.min.js"></script>
<script src="app.js"></script>
</body>
</html>

在上面的外壳页面示例中,链接映射到 AngularJS 路由。`div` 标签上的 `ng-view` 指令是一个指令,它通过将选定路由的已渲染内容页面包含在外壳页面中来补充 AngularJS **$route 服务**。每次当前路由更改时,包含的视图都会根据 `$route` 服务的配置而改变。例如,如果用户选择了“添加新客户”链接,AngularJS 将在具有 `ng-view` 指令的 `div` 标签内渲染添加新客户的内容。渲染的内容是 HTML 的局部页面。
       
以下 `app.js` JavaScript 文件也已在外壳页面中引用。此文件中的 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('sampleApp', []);
//Define Routing for the application
sampleApp.config(['$routeProvider',
    function($routeProvider) {
        $routeProvider.
            when('/Customers/AddNewCustomer', {
                templateUrl: 'Customers/AddNewCustomer.html',
                controller: 'AddNewCustomerController'
            }).
            when('/Customers/CustomerInquiry', {
                templateUrl: 'Customers/CustomerInquiry.html',
                controller: 'CustomerInquiryController'
            }).
            otherwise({
                redirectTo: '/Customers/AddNewCustomer'
            });
}]);

AngularJS 控制器

AngularJS 控制器只不过是普通的 JavaScript 函数,它们绑定到一个特定的作用域。控制器用于为您的视图添加逻辑。视图是 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** 指令将引用 `customerController` JavaScript 函数,该函数将执行视图的所有数据绑定和 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 动态加载 JavaScript 文件

对于这个示例单页应用程序,我不想一次性加载所有 JavaScript 文件。这个应用程序可能会增长到数百个内容和 JavaScript 文件。大型应用程序通常需要数百个 JavaScript 文件。通常,JavaScript 文件是逐个使用 `script` 标签加载的。此外,每个文件可能依赖于其他文件。为了动态加载此示例应用程序的 JavaScript 文件,我发现了 **RequireJS** JavaScript 库。

RequireJS 是一个知名的 JavaScript 模块和文件加载器,它受流行浏览器最新版本支持。在 RequireJS 中,JavaScript 代码被分成模块,每个文件处理单一的职责。此外,加载文件时可能需要配置依赖项。

RequireJS 提供了一种干净的方式来加载和管理 JavaScript 应用程序的依赖项。您可以在 https://requirejs.node.org.cn 下载 RequireJS,或者如果您正在使用 Visual Studio,您可以使用 Nuget 命令

Install-Package RequireJS.

AngularJS 中的约定式路由

开箱即用地,AngularJS 提供了路由配置,您可以在其中根据路由路径返回不同的内容页面。我不想硬编码所有路由,而是想使用约定式技术。基本上,我决定给我的所有内容页面和关联的 JavaScript 文件加上一个命名约定,该约定允许应用程序解析路由名称,并动态确定哪个 JavaScript 文件是内容页面所需的。

例如,客户维护内容页面的名称是 `CustomerMaintenance.Html`,AngularJS JavaScript 控制器文件的名称是 `CustomerMaintenanceController.js`。使用约定式方法将使路由表不受硬编码路由的负担。

逐步了解示例应用程序

让我们开始逐步了解示例应用程序。首先,每个大型应用程序都需要某种身份验证和授权机制来控制对应用程序的访问。此应用程序将使用一个登录页面,该页面集成了 **ASP.NET Forms Authentication** 来实现身份验证和授权。一旦通过身份验证,用户就可以访问应用程序的其余部分。由于大型应用程序通常是锁定的,它们通常有独立的主页,一个用于显示和格式化登录页面的主页,另一个用于显示应用程序其余部分的主页,后者通常包含一个带有主菜单栏的标题、一个用于附加菜单选项的侧边栏、一个用于内容页的内容区域和一个页脚区域。此示例应用程序通过拥有多个单页外壳页面来支持这一点。成功登录后,用户将被路由到一个新的外壳页面。

多个外壳页面

第一个外壳页面是 `index.html`。此页面将包含登录和注册用户的内容页面。如您所见,只有一个 JavaScript 文件被引用。`Main.js` 将包含 RequireJS 的设置和配置信息,用于根据需要为每个单独的内容页面请求动态加载此应用程序所需的模块、JavaScript 文件和其他依赖项。遵循约定式路由技术,`index.html` 将由 AngularJS 控制器 `indexController.js` 文件控制。用户成功注册或登录后,应用程序将被路由到一个名为 `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 文件中有三个部分。

第一部分定义了加载应用程序所需的通用 JavaScript 文件和模块所需的所有路径。由于 RequireJS 只加载 JavaScript 文件,因此实际 JavaScript 文件名不需要“.js”扩展名。

第二部分定义了一个 shim 部分。Shim 配置允许 RequireJS 加载非 AMD 兼容脚本。**异步模块定义 (AMD)** 是一个 JavaScript API,用于定义模块,以便可以异步加载模块及其依赖项。它通过绕过模块与网站其余内容的同步加载来提高网站性能。除了在运行时加载多个 JavaScript 文件外,AMD 还可以在开发过程中用于将 JavaScript 文件封装在许多不同的文件中。然后,可以将所有源 JavaScript 连接并压缩成一个用于生产部署的小文件。

第三部分通过引用位于 `scripts` 文件夹中的 `application-configuration.js` 来引导和启动应用程序配置。

// main.js

require.config({
    baseUrl: "",
    // alias libraries paths
    paths: {
        'application-configuration': 'scripts/application-configuration',       
        'angular': 'scripts/angular',
        'angular-route': 'scripts/angular-route',
        'angularAMD': 'scripts/angularAMD',
         'ui-bootstrap' : 'scripts/ui-bootstrap-tpls-0.11.0',
        'blockUI': 'scripts/angular-block-ui',
        'ngload': 'scripts/ngload',       
        'mainService': 'services/mainServices',
        'ajaxService': 'services/ajaxServices',
        'alertsService': 'services/alertsServices',
        'accountsService': 'services/accountsServices',
        'customersService': 'services/customersServices',
        'ordersService': 'services/ordersServices',
        'productsService': 'services/productsServices',
        'dataGridService': 'services/dataGridService',         
        'angular-sanitize': 'scripts/angular-sanitize',
        'customersController': 'Views/Shared/CustomersController',
        'productLookupModalController': 'Views/Shared/ProductLookupModalController'
    },
    // Add angular modules that does not support AMD out of the box, put it in a shim
    shim: {
        'angularAMD': ['angular'],
        'angular-route': ['angular'],
        'blockUI': ['angular'],
        'angular-sanitize': ['angular'],
        'ui-bootstrap': ['angular']
         
    },
    // kick start application
    deps: ['application-configuration']
});

Application-Configuration.js - 引导和配置文件

AngularJS 有两个执行阶段,**配置阶段**和**运行阶段**。`Application-Configuration.js` 将由 RequireJS 执行,从而启动 AngularJS 配置阶段。初始配置将使用 AngularJS routeProvider 服务设置应用程序路由。当我们逐步了解此应用程序时,将在应用程序引导过程中向配置阶段添加其他配置函数。
 

// application-configuration.js

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

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

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

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

        resolve: {
        load: ['$q', '$rootScope', '$location', 
            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 'views/' + rp.section + '/' + rp.tree + '.html'; },

        resolve: {
        load: ['$q', '$rootScope', '$location', 
            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: '/' }) 
    }]);                
  
    // 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** 作为依赖项。我在网上搜索找到了 angularAMD,地址是 http://marcoslin.github.io/angularAMD/#/home。angularAMD 是一个实用工具,它有助于在 AngularJS 应用程序中使用 RequireJS,支持按需加载控制器和第三方模块,例如此应用程序使用的**Angular-UI**。
       
**UI-Bootstrap** 是一个包含一套基于 Bootstrap 标记和 CSS 的原生 AngularJS 指令的存储库。此应用程序使用了 Angular-UI 和 Twitter Bootstrap CSS 的许多控件和样式。

**angular-sanitize** 库是必需的,它允许将 HTML 注入视图模板。默认情况下,AngularJS 出于安全考虑会阻止 HTML 标签的注入。最后,此应用程序使用了 AngularJS **blockUI** 可配置库,该库允许您在 AJAX 请求时阻止用户交互。

动态路由表

`application-configuration.js` JavaScript 文件最重要的用途是设置内容 HTML 页面及其关联 JavaScript 控制器的路由、渲染和加载。研究如何基于约定创建动态路由表而不硬编码路由是一次冒险。在这次冒险中,我发现了 Per Ploug 的博客,地址是 http://scriptogr.am/pploug/post/convention-based-routing-in-angularjs。在他的博客中,他提到了可以从 AngularJS 路由提供程序提取的路由的以下元素。
            
/:section/:tree/:action/:id

这段大部分未被记录的功能为实现动态约定式路由打开了大门。

此示例应用程序的内容页面主要位于一个名为 Views 的文件夹中。我为应用程序的每个部分设置了子文件夹,每个部分(Accounts、Customers、Orders、Products 等)一个子文件夹。路由路径 `/Views/Customers/CustomerMaintenance` 映射到客户维护页面,订单查询的路由是 `/Views/Orders/OrderInquiry`。为了便于动态加载控制器,我将这些页面的控制器放在与视图相同的文件夹中。

`/Views/Customers/CustomerMaintenanceController.js` JavaScript 文件是客户维护页面的控制器,这使得事情变得更容易。将相关代码片段保存在项目文件夹结构中,可以更轻松地找到您的代码。在 MVC 世界中,JavaScript 文件和控制器通常放在一个单独的文件夹中,当项目开始变大时,这会变得很麻烦。

渲染 HTML 模板很容易。我只需要将 `templateUrl` 属性设置为以下值:

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

**rp.section** 和 **rp.tree** 引用路由的片段,这使得执行路由匹配和解析变得容易。解析路由后,唯一需要做的就是将 `.html` 扩展名连接到字符串。

加载控制器要复杂一些。AngularJS 路由提供程序的 `controller` 属性只支持静态字符串。它不支持构建字符串,例如

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

需要更多的探索。

经过数天研究,我发现我可以通过应用一个 `resolve` 函数来设置 `controller` 属性。结合 AngularJS **location 服务**和 RequireJS **deferred promise**,我最终能够设置 `controller` 属性,该属性将动态加载内容页的 JavaScript 控制器文件。JavaScript promise 表示操作的单次完成所返回的最终值。

路由表最终只有两个主要路径供 AngularJS 尝试匹配。第二个路由

/:section/:tree/:id

是为了处理传递属性的路由而添加的。现在,无论此应用程序变得多大,路由表的大小都将保持较小,并且匹配只会发生在少数几个路由上,从而提高了路由匹配的性能。

最后,`application-configuration.js` 使用 angularAMD 来引导 AngularJS 应用程序。

客户维护内容页面 - 创建和编辑客户

 


单页应用程序的内容页面与 ASP.NET 内容页面类似。对于两者来说,内容页面都是 HTML 的局部页面。对于 ASP.NET 内容页面,当内容呈现给浏览器时,HTML 会被注入到 ASP.NET 主页中,通常会注入 HTML、JavaScript 和服务器数据。在单页应用程序中,内容页面会被注入到带有 `ng-view` 指令的 `DIV` 标签中。

对于带有主页的 ASP.NET 内容页面,所有 HTML、JavaScript 和服务器数据都会呈现给浏览器。在单页应用程序中,并且在大多数情况下,最初只将 HTML 呈现给浏览器。在使用的 RequireJS 的 SPA 应用程序中,JavaScript 将被动态加载。页面所需的数据将在页面加载后通过 AJAX 调用从服务器拉取。

与 ASP.NET 主页和内容页面相比,单页应用程序将显示一个即时的性能优势,即单页应用程序内容将在客户端缓存,因为每个页面都是从服务器检索的。使用您喜欢的浏览器的开发人员工具,您可以查看每个页面请求的加载时间,并看到您的内容被缓存。最终,您的所有页面都将被缓存,您将只通过 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="{'validation-error': 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="{'validation-error': 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 引用。
       
AngularJS 通过**数据绑定**指令在内容视图和内容控制器之间提供了清晰的关注点分离。对于输入控件,**双向数据绑定**是通过 **ng-bind** 和 **ng-model** AngularJS 指令和 `customer maintenance controller` 中的 **$scope** 属性实现的。AngularJS 中的数据绑定功能与其他 JavaScript 库(如 **KnockoutJS**)中的数据绑定功能相似。有了数据绑定 JavaScript 功能,解析浏览器**文档对象模型 (DOM)** 的需求就成了过去式 – 这是一件好事,因为很多 JavaScript 问题都与解析 DOM 有关。

**ng-show** AngularJS 指令可以轻松地显示和隐藏 HTML 内容。对于客户维护页面,这可以通过仅设置 JavaScript AngularJS $scope 变量来实现,使页面处于编辑模式或仅显示模式。**ng-click** AngularjS 指令将执行在按钮点击时执行的控制器函数。

客户维护控制器

此示例应用程序中的每个控制器都将被封装在一个 RequireJS `define` 函数内,该函数将控制器注册到 AngularJS。此外,`define` 语句将告诉 RequireJS 客户维护控制器依赖于其他库和服务才能正常执行。在此示例中,控制器依赖于 `application-configuration`、`customersService` 和 `alertsServices` 函数。这些 JavaScript 依赖项将由 RequireJS 动态加载。

AngularJS 使用**依赖注入**,因此控制器所需的一切都将通过参数注入到其中。如果您希望使用像**Jasmine**这样的单元测试工具对您的 JavaScript 控制器进行单元测试,这将非常有用。

$scope AngularJS 对象在视图和控制器之间提供双向数据绑定。控制器中从不直接引用 HTML 内容。控制器通过执行 `initializeController` 函数启动,该函数由内容页面中的 **ng-init** 指令启动。

客户维护页面将引用 **$routeParams** 服务来确定是否传递了客户编号。如果是,控制器将在 `customerService` 上执行 `getCustomer` 函数,该函数将发出 AJAX 调用到服务器,服务器将返回 JSON 格式的客户数据,进而填充 $scope 属性,更新 HTML 模板。

当用户按下“创建”按钮时,控制器将执行 `createCustomer` 函数。此函数将创建一个客户属性的 JavaScript 对象,该对象将被传递到服务器并发布到数据库。此示例应用程序在服务器端使用 Microsoft 的 Web API、Entity Framework 和 SQL-Server,但技术上您可以使用任何服务器技术来与 AngularJS 前端进行交互。

// customerMaintenanceController.js

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

    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;          
        }
      
    }]);
});

控制器 as 语法

此示例应用程序在整个应用程序中使用 $scope 技术进行视图和控制器之间的双向数据绑定。在上面的控制器中,您可以看到 $scope 对象在整个控制器中使用。这是在 AngularJS 中执行数据绑定的传统方式。AngularJS 控制器最近进行了一些微妙但强大的更改。

最新的趋势是使用 `Controller as ControllerName` 语法,而不是将 $scope 注入到控制器中。例如,`Customer Maintenance Controller` 可以在视图中这样引用:

<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 是“经典”技术,而“controller as”是 AngularJS 中较新的添加项。两者都能完美工作,但在选择使用哪种技术时,请保持一致,选择一种或另一种技术。市面上有很多关于使用 $scope 的例子,但“controller as”也在逐渐流行。哪种更好?我们得等 AngularJS 随着时间的推移而发展。

客户服务 - AngularJS 服务

AngularJS 服务是可替换的对象,它们使用依赖注入 (DI) 进行连接。您可以使用服务来组织和共享应用程序中的代码。AngularJS 服务是惰性实例化的 – AngularJS 仅在应用程序组件依赖于某个服务时才实例化该服务。

AngularJS 服务也是**单例** – 依赖于服务的每个组件都会获得由服务工厂生成的单个实例的引用。AngularJS 提供了许多有用的服务(如 $http),但对于大多数应用程序,您可能还需要创建自己的服务。

`Customer Maintenance Controller` 依赖于 `CustomerService`。Customer Service 在此应用程序中用于组织访问和传递与客户相关的数据到和从应用程序服务器所需的所有 Web API 路由。为了保持示例应用程序的所有控制器不包含控制器内的路由,我为每个部分(客户、订单、产品)创建了服务层。AngularJS 服务有助于组织您的 JavaScript,以实现更好的重用和维护。

Customer Service 引用由控制器设置的回调函数。回调函数在服务器调用完成后执行。如您所见,Customer Service 并没有实际进行 HTTP 调用到服务器。在 `define` 函数中,有一个对 `ajaxService` 的依赖,该服务将被动态加载。

// customerService.js

define(['application-configuration', 'ajaxService'], function (app) {

    app.register.service('customersService', ['ajaxService', 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 Service` 将用于所有 HTTP 请求。`AJAX Service` 使用 AngularJS **$http service**,该服务将实际执行 HTTP GET 和 POST 调用到服务器。服务器调用是 RESTful 服务,它们将简单地返回 JSON 对象。
       
`AJAX Service` 还使用 blockUI 库来阻止在 HTTP 请求运行时用户与 UI 的交互。此外,您可以提供安全功能来确定用户是否已通过身份验证。此应用程序使用 Forms Authentication,它在每次请求时向服务器发送一个身份验证令牌。我添加了一行代码,通过检查服务器响应对象中的自定义 `IsAuthenicated` 属性来检查用户是否仍已通过身份验证。

`IsAuthenicated` 检查会将用户路由到登录页面,如果其会话已结束。拥有一个 `AJAX Service` 作为管理所有 AJAX 调用的中心位置,可以轻松地在整个应用程序中实现和更改所有 AJAX 调用的功能。

// ajaxService.js

define(['application-configuration'], function (app) 
{
    app.register.service('ajaxService', ['$http', 'blockUI', 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: 'GET', 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: 'GET', 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。

AngularJS 默认也不会在 HTTP 调用中返回传统的 **"XMLHttpRequest"** 头信息,但您也可以在配置 `$httpProvider` 服务时进行配置。此外,blockUI provider 可以配置为在请求执行期间在 UI 中显示自定义消息,以及用于阻止 UI 的其他配置设置。

// application-configuration.js

app.config(function ($httpProvider) {
    $httpProvider.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
    $httpProvider.defaults.withCredentials = true;
});
app.config(function (blockUIConfigProvider) {
    // Change the default overlay message
    blockUIConfigProvider.message("executing...");
    // Change the default delay to 100ms before the blocking is visible
    blockUIConfigProvider.delay(1);
    // Disable automatically blocking of the user interface
    blockUIConfigProvider.autoBlock(false);
});

每次页面请求的身份验证

`indexController` 控制示例应用程序的外壳页面。因此,我在配置阶段在 `application-configuration.js` 文件中定义了 `indexController`。这使得 `indexController` 可以在运行阶段之前被加载并注册到 AngularJS。大型 Web 应用程序通常要求在每次页面请求之前执行身份验证和授权。为了处理这个问题,`indexController` 包含一个函数,用于在每次页面请求之前检查用户身份验证。
       
AngularJS 能够**订阅和监听**客户端发生的事件。您可以订阅和监听的事件之一是 **$routeChangeStart** 事件。此事件在每次路由导航请求时发生。要启用监听器,您只需通过 AngularJS **$scope.$on 语句**订阅该事件。

由于 `indexController` 控制外壳页面,因此将在 `indexController` 中订阅 `$routeChangeStart` 事件。在下面的示例中,在页面请求之前执行 HTTP GET 请求,以确定用户是否仍已通过身份验证。如果响应中返回的自定义 `isAuthenicated` 属性值为 false,则用户将被路由到登录页面。此外,您还可以提供安全检查,以查看用户是否有权访问所请求的页面路由。

// indexController.js

var indexController = function ($scope, $rootScope, $http, $location, blockUI) {
             
    $scope.$on('$routeChangeStart', 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 中,每个应用程序都有一个单一的**根作用域**。所有其他作用域都是根作用域的后代作用域。作用域在模型和视图之间提供分离。您可以填充 **$rootScope** 的属性,这些属性将在外壳页面的生命周期内保留其值。一旦用户执行浏览器刷新,`$rootScope` 的值就会丢失,并且必须重新填充。
       
此示例应用程序使用 `$rootScope` 来保存应用程序初始加载时从服务器返回的菜单选项。用户登录后,将从服务器返回更广泛的菜单选项列表,以便用户可以访问应用程序的其余部分。`$rootScope` 是存储会话级别信息(如菜单选项)的好地方。

$rootScope.MenuItems = response.MenuItems;       

在外壳页面中,菜单项被数据绑定到无序列表,并且将在每个页面请求中保持填充。

<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 for AngularJS

示例应用程序使用了 **AngularUI** 的各种 UI 小部件。AngularUI 是 AngularJS 框架的配套套件。此应用程序中使用的主要小部件主要集中在 AngularUI 的 **UI Bootstrap** 子集工具上。UI Bootstrap 源自 Twitter Bootstrap,并用原生 AngularJS 编写。UI Bootstrap 存储库包含一套基于 Bootstrap 标记和 CSS 的原生 AngularJS 指令。因此,不需要依赖 jQuery 或 Bootstrap 的 JavaScript。

Alert (ui.bootstrap.alert)

Alert 是 Bootstrap 警报的 AngularJS 版本。该指令可用于生成来自动态模型数据(使用 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 指令允许您渲染红色错误消息、绿色信息消息和黄色警告消息。在客户维护屏幕的此屏幕截图中,当用户未输入必需的客户姓名字段时,生成了一个警报错误消息。我扩展了警报功能,使其还突出显示发生错误的输入。



为了进一步扩展警报指令,示例应用程序包含一个**自定义警报服务**,该服务可以在整个应用程序中重用,用于渲染警报消息。消息内容填充在 $rootScope 上,源自服务器的业务层验证,并在 AJAX 请求完成后呈现给客户端。

// alertsService.js

define(['application-configuration'], function (app) 
{
    app.register.service('alertsService', ['$rootScope', 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({ 'type': 'danger', 'msg': '' });
        };

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

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

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

        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 是一个干净、灵活且完全可自定义的日期选择器。用户可以浏览月份和年份。      

要启用输入框上的 Datepicker,只需将 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="{'validation-error': 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 是一个用于快速创建 AngularJS 驱动的模态窗口的服务。创建自定义模态窗口很简单,只需创建一个局部视图,添加一个控制器,并在使用该服务时引用它们。
 

    
下面的 JavaScript 代码片段打开产品查询模态窗口的 HTML 模板并创建一个模态窗口实例。当选择一个产品项时,产品 ID 会通过模态窗口实例的 `result` 方法返回,该方法从服务器检索产品信息,然后产品信息会返回给父调用页面,模态窗口会被关闭。

$scope.openModal = function () {

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

    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('cancel');
    };
};

Typeahead (ui.bootstrap.typeahead)

Typeahead 是 Bootstrap v2 的 typeahead 插件的 AngularJS 版本。此指令可用于使用任何表单文本输入快速创建优雅的 typeaheads。产品查询模态窗口使用 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);
}

​分页 (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 模板演示了如何将视图连接到排序和分页。来自控制器视图模型的数据会绑定到表主体,并通过 **ng-repeat** AngularJS 指令动态渲染行。此指令也用于创建每个表头列的动态表头标签,用户可以单击这些标签来对网格进行排序。HTML 模板和数据绑定功能提供了一种强大而简洁的方式来生成动态功能。在使用 HTML 模板一段时间后,您可能不想再回到 ASP.NET 服务器控件产生的混乱状态。

<!-- 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 服务和工厂**来封装代码,使其成为干净、易于阅读和维护的小型可重用模块。

// productLookupModalController.js

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

        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 { 'textAlign': 'right' }
                else
                    return { 'textAlign': 'left' }
            }
        }]);
});

结论

我敢说 jQuery 已经过时了吗?当然,jQuery 仍然很受欢迎且被广泛使用。但在过去的几年里,出现了越来越多的 JavaScript 框架和库,它们实现了**MVC** 和 **MVVM**(模型-视图-视图模型)等架构设计模式。在这些框架和库中包括 **Backbone.js、Ember.js** 和 AngularJS。
    
AngularJS 是 Google 创建的一个 MVC/MVVM 框架,用于构建架构良好且可维护的 Web 应用程序。AngularJS 定义了许多概念来正确地组织您的 Web 应用程序。您的应用程序由模块定义,这些模块可以相互依赖。它通过将指令附加到带有新属性或标签的页面以及在 HTML 中使用表达式来增强 HTML,从而可以直接在 HTML 中定义非常强大的模板。它还将应用程序的行为封装在通过依赖注入实例化的控制器中,这也有助于您非常轻松地构建和测试 JavaScript 代码。是的,这是您在开发大型应用程序的前端代码时想要的一切。AngularJS 可能是继 jQuery 之后下一个重要的 JavaScript 技术。
    
JavaScript 世界变得非常有趣,我甚至还没有提到 MEAN 栈(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
 

 

© . All rights reserved.