使用 AngularJS 开发一个大规模的单页应用(SPA)

jopen 9年前

下载源代码

介绍

(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和推ter 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.setionrp.tree变量,可以很容易实现路径匹配、路径转换。转换完路径后,唯一需要做的事是把扩展名 .html连接到字符串末尾。

加载控制器文件的过程有点复杂。 AngularJS路径配置的控制器属性只支持静态的字符串。 它不支持含有变量的字符串,如下:

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

AngularJS 还需要更多的创新。

经过一段时间的研究,我发现可以通过功能分解来设置控制器属性。 结合使用AngularJS的location servicedeferred 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是从推ter 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