下载源代码
介绍
(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使用入门 - 外壳页面,模块和路由
你首先要做的一件事情就是讲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内容页面需要在外壳页面的那个地方被渲染.
04 | <title>AngularJS Shell Page example</title> |
09 | <li><a href= "#Customers/AddNewCustomer" >Add New Customer</a></li> |
10 | <li><a href= "#Customers/CustomerInquiry" >Show Customers</a></li> |
13 | <!-- ng-view directive to tell AngularJS where to inject content pages --> |
15 | <script src= "http://ajax.googleapis.com/ajax/libs/angularjs/1.0.7/angular.min.js" ></script> |
16 | <script src= "app.js" ></script> |
在上面的外壳页面示例中,几个链接呗映射到了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文件,它将获得带有某个特定路由请求内容的引用.
02 | var sampleApp = angular.module('sampleApp', []); |
04 | sampleApp.config(['$routeProvider', |
05 | function($routeProvider) { |
07 | when('/Customers/AddNewCustomer', { |
08 | templateUrl: 'Customers/AddNewCustomer.html', |
09 | controller: 'AddNewCustomerController' |
11 | when('/Customers/CustomerInquiry', { |
12 | templateUrl: 'Customers/CustomerInquiry.html', |
13 | controller: 'CustomerInquiryController' |
16 | redirectTo: '/Customers/AddNewCustomer' |
AngularJS 的控制器
AngularJS 控制器无非就是一个原生的JavaScript函数,只是被绑定到了一个特定的范围而已。控制器用来将逻辑添加到你的视图。视图就是HTML页面。这些页面只是做简单的数据展示工作,我们会使用双向数据绑定来将数据绑定到这些HTML页面上. 将模型(也就是数据)同数据粘合起来基本山就是控制器的职责了.
1 | <div ng-controller= "customerController" > |
2 | <input ng-model= "FirstName" type= "text" style= "width: 300px" /> |
3 | <input ng-model= "LastName" type= "text" style= "width: 300px" /> |
5 | <button class = "btn btn-primary btn-large" ng-click= "createCustomer()" />Create</button> |
对于上面的AddCustomer模板,ng-controller指令将会引用JavaScript函数customerController,这个控制会执行所有的数据绑定以及针对该视图的JavaScript函数.
01 | function customerController($scope) |
03 | $scope.FirstName = "William" ; |
04 | $scope.LastName = "Gates" ; |
06 | $scope.createCustomer = function () { |
07 | var customer = $scope.createCustomerObject(); |
08 | customerService.createCustomer(customer, |
09 | $scope.createCustomerCompleted, |
10 | $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命令下载:
1 | 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在外壳页面的什么地方展示实际的内容页面。
04 | <html xmlns= "http://www.w3.org/1999/xhtml" > |
07 | <script data-main= "main.js" src= "Scripts/require.js" > </script> |
08 | <link href= "Content/angular-block-ui.css" rel= "stylesheet" /> |
09 | <link href= "Content/bootstrap.css" rel= "stylesheet" /> |
10 | <link href= "Content/Application.css" rel= "stylesheet" /> |
11 | <link href= "Content/SortableGrid.css" rel= "stylesheet" /> |
13 | <body ng-controller= "indexController" ng-init= "initializeController()" > |
14 | <div class = "navbar navbar-inverse navbar-fixed-top" > |
15 | <div class = "container" > |
16 | <div class = "navbar-collapse collapse" id= "MainMenu" > |
17 | <ul class = "nav navbar-nav" ng-repeat= "menuItem in MenuItems" > |
18 | <li> <a href= "{{menuItem.Route}}" >{{menuItem.Description}} </a> </li> |
23 | <!-- ng-view directive to tell AngularJS where to put the content pages--> |
24 | <div style= "margin: 75px 50px 50px 50px" ng-view> </div> |
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来引导和启动应用程序配置文件。
07 | 'application-configuration': 'scripts/application-configuration', |
08 | 'angular': 'scripts/angular', |
09 | 'angular-route': 'scripts/angular-route', |
10 | 'angularAMD': 'scripts/angularAMD', |
11 | 'ui-bootstrap' : 'scripts/ui-bootstrap-tpls-0.11.0', |
12 | 'blockUI': 'scripts/angular-block-ui', |
13 | 'ngload': 'scripts/ngload', |
14 | 'mainService': 'services/mainServices', |
15 | 'ajaxService': 'services/ajaxServices', |
16 | 'alertsService': 'services/alertsServices', |
17 | 'accountsService': 'services/accountsServices', |
18 | 'customersService': 'services/customersServices', |
19 | 'ordersService': 'services/ordersServices', |
20 | 'productsService': 'services/productsServices', |
21 | 'dataGridService': 'services/dataGridService', |
22 | 'angular-sanitize': 'scripts/angular-sanitize', |
23 | 'customersController': 'Views/Shared/CustomersController', |
24 | 'productLookupModalController': 'Views/Shared/ProductLookupModalController' |
28 | 'angularAMD': ['angular'], |
29 | 'angular-route': ['angular'], |
30 | 'blockUI': ['angular'], |
31 | 'angular-sanitize': ['angular'], |
32 | 'ui-bootstrap': ['angular'] |
36 | deps: ['application-configuration'] |
Application-Configuration.js - 引导程序和配置文件
AngularJS 有两个执行阶段,配置阶段和运行阶段。Application-Configuration.js 会由RequireJS来执行,它会屏蔽掉AngularJS的配置阶段。初始的配置将会使用AngularJS的routeProvider 服务来设定应用程序的路由。在后面浏览示例应用的时候,还会在应用的引导过程中,添加另外的配置函数到配置阶段中去。
04 | define(['angularAMD', 'angular-route', 'ui-bootstrap', 'angular-sanitize', 'blockUI', ], |
05 | function (angularAMD) { |
07 | var app = angular.module( "mainModule" , |
08 | ['ngRoute', 'blockUI', 'ngSanitize', 'ui.bootstrap']); |
10 | app.config(['$routeProvider', function ($routeProvider) { |
14 | .when( "/" , angularAMD.route({ |
16 | templateUrl: function (rp) { return 'Views/Main/ default .html'; }, |
17 | controllerUrl: "Views/Main/defaultController" |
20 | .when( "/:section/:tree" , angularAMD.route({ |
22 | templateUrl: function (rp) { |
23 | return 'views/' + rp.section + '/' + rp.tree + '.html'; }, |
26 | load: ['$q', '$rootScope', '$location', |
27 | function ($q, $rootScope, $location) { |
29 | var path = $location.path(); |
30 | var parsePath = path.split( "/" ); |
31 | var parentPath = parsePath[ 1 ]; |
32 | var controllerName = parsePath[ 2 ]; |
33 | var loadController = "Views/" + parentPath + "/" + |
34 | controllerName + "Controller" ; |
36 | var deferred = $q.defer(); |
37 | require([loadController], function () { |
38 | $rootScope.$apply(function () { |
42 | return deferred.promise; |
47 | .when( "/:section/:tree/:id" , angularAMD.route({ |
49 | templateUrl: function (rp) { |
50 | return 'views/' + rp.section + '/' + rp.tree + '.html'; }, |
53 | load: ['$q', '$rootScope', '$location', |
54 | function ($q, $rootScope, $location) { |
55 | var path = $location.path(); |
56 | var parsePath = path.split( "/" ); |
57 | var parentPath = parsePath[ 1 ]; |
58 | var controllerName = parsePath[ 2 ]; |
59 | var loadController = "Views/" + parentPath + "/" + |
60 | controllerName + "Controller" ; |
62 | var deferred = $q.defer(); |
63 | require([loadController], function () { |
64 | $rootScope.$apply(function () { |
68 | return deferred.promise; |
72 | .otherwise({ redirectTo: '/' }) |
76 | angularAMD.bootstrap(app); |
RequireJS的Define语句
查看 application-configuration.js 文件,你很快会看到这个define语句. Define语句是一个将会加载一个代码模块的RequireJS语句. 模块不同于传统的脚本文件,它是能够避免污染全局命名空间的界定良好的对象。它可以明确列出它的依赖项,并获取这些依赖项上的句柄,无需引用全局对象,而是将依赖作为定义了此模块的函数的参数进行接收.
RequireJS 中的模块式模块模式的一种扩展,其优点是不用全局地区引用其它的模块. RequireJS用于模块的语法允许它们尽快加载,即使加载顺序是乱的,其后也能计算出正确的依赖顺序, 而由于没有创建全局变量,在一个页面上加载一个模块的不同版本就有了可能。本应用程序在应用程序范围内依赖于angularAMD, angular-route, ui-bootstrap, angular-sanitize 和 blockUI 这些库.
AngularAMD, UI-Bootstrap, Angular-Sanitize 和 BlockUI
Application-Configuration.js 引用了 angularAMD 作为依赖项. 我在互联网上冲浪时在 http://marcoslin.github.io/angularAMD/#/home 发现了angularAMD. angularAMD 改进了RequireJS在AngularJS应用程序中的使用,支持控制器和第三方模块的按需加载,比如本应用程序所使用的Angular-UI.
UI-Bootstrap 是一个包含了一整套基于Bootstrap的标记和CSS的本地AngularJS指令的资源库. 本应用程序使用了许多来自Angular-UI和Twitter Bootstrap CSS的空间和样式.
angular-sanitize 库需要被用来允许HTML被注入到视图模板中。默认情况下,出于安全的考量,AngularJS是阻止HTML标记的注入的。
最后,应用程序使用了AngularJS的blockUI配置库来让你在发起AJAX请求过程中可以阻塞用户界面.
动态路由表
application-configuration.js 的最大目的是为内容页面和与之关联的JavaScript控制器设置路由、渲染和加载规则。探索如何使用约定而不是硬编码的方式来创建动态路由表,犹如一次探险。在这次探险过程中我发现了Per Ploug's的博客http://scriptogr.am/pploug/post/convention-based-routing-in-angularjs。在他的博客中他提到了路由的下面这些元素,这些元素可以从AngularJS的的路由提供器中获得:
/:secion/:tree/:action/:id
这条文档中几乎没有提到的功能,为我们开启了一扇门,告诉我们需要什么才能完成基于约定的动态路由。
在示例中,大部分网页文件在Views文件夹下。 Veiws文件夹中,一个模块对应一个子文件夹,如Accounts, Customers, Orders, Products等。 修改用户页面的根路径是 /Views/Customers/CustomerMaintenance, 查询订单页面的根路径是/Views/Orders/OrderInquiry.为了方便控制器动态加载文件,我把这些页面的控制器代码文件也放到Views文件夹下。
修改用户页面的控制器文件路径是 /Views/Customers/CustomerMaintenanceController.js,这样可以简化开发。把公共的代码放到工程的同一个文件夹下,可以让你快速定位需要查看的代码。 在MVC框架里,控制器文件通常被单独放在一个文件夹下,当工程变得比较庞大时,这些文件会难以维护。
渲染HTML模板很容易。 只需设置一下templateUrl属性:
1 | 'views/' + rp.section + '/' + rp.tree + '.html' . |
引入 rp.setion和 rp.tree变量,可以很容易实现路径匹配、路径转换。转换完路径后,唯一需要做的事是把扩展名 .html连接到字符串末尾。
加载控制器文件的过程有点复杂。 AngularJS路径配置的控制器属性只支持静态的字符串。 它不支持含有变量的字符串,如下:
1 | controller = "Views/" + parentPath + "/" + controllerName + "Controller" ; |
AngularJS 还需要更多的创新。
经过一段时间的研究,我发现可以通过功能分解来设置控制器属性。 结合使用AngularJS的location service和deferred promise特性,我最终实现动态加载js控制器文件时设置控制器属性值。 js性能的一个提升意味着这次改造产生了最终的价值。
路由表里最终只有两个主路径,AngularJS需要对其进行匹配。第二个路径
/:section/:tree/:id
是用来处理那些带有参数的路径的。现在,不管应用变得多大,路由表都将会保持的很小,而且只需要跟两个路径进行匹配,这样就提高了路由匹配的效率。
最终,application-configuration.js使用angularAMD来引导AngularJS应用。
客户管理页面 - 创建和编辑客户信息
单页应用中的页面与asp.net页面类似。 相同之处,两者都是html的一个子集。 对于asp.net,当浏览器开始渲染页面元素时,html、js、数据被传入控制层代码,然后,浏览器进行计算、展示。在单页应用中,RequireJS使用ng-view指令把页面内容注入到一个div标签中。
页面初始化时,浏览器通常只渲染html代码。 若在单页应用中使用RequireJS,js会被动态加载。 当页面加载完,浏览器以ajax异步调用的方式从服务器读取数据。
构建于ASP.NET母版页之上的SPA应用程序及其内容内面,你将可以马上收获的性能之一,就死SPA的内容将会被缓存到客户端,而每一个页面都会从服务器端获取到. 使用你拿手的浏览器开发工具,就可以看到内容已经被缓存了。最终你所有的页面都会被缓存,而最后你只是通过AJAX请求通过网络获取服务器段数据而已. 所有这些都促成了高效的响应时间已经增强的用户体验.
001 | <!-- CustomerMaintenance.html --> |
003 | <div ng-controller= "customerMaintenanceController" ng-init= "initializeController()" > |
005 | <h3> Customer Maintenance </h3> |
006 | <table class = "table" style= "width:100%" > |
008 | <td class = "input-label" align= "right" > <label class = "required" >Customer Code: </label> </td> |
009 | <td class = "input-box" > |
010 | <div ng-bind= "CustomerCode" ng-show= "DisplayMode" > </div> |
011 | <div ng-show= "EditMode" > |
012 | <input ng-model= "CustomerCode" type= "text" style= "width: 300px" |
013 | ng- class = "{'validation-error': CustomerCodeInputError}" /> |
018 | <td class = "input-label" align= "right" > <label class = "required" >Company Name: </label> </td> |
019 | <td class = "input-box" > |
020 | <div ng-bind= "CompanyName" ng-show= "DisplayMode" > </div> |
021 | <div ng-show= "EditMode" > |
022 | <input ng-model= "CompanyName" type= "text" style= "width: 300px" |
023 | ng- class = "{'validation-error': CompanyNameInputError}" /> |
028 | <td class = "input-label" align= "right" > <label>Address: </label> </td> |
029 | <td class = "input-box" > |
030 | <div ng-bind= "Address" ng-show= "DisplayMode" > </div> |
031 | <div ng-show= "EditMode" > |
032 | <input ng-model= "Address" type= "text" style= "width: 300px" /> |
037 | <td class = "input-label" align= "right" > <label>City: </label> </td> |
038 | <td class = "input-box" > |
039 | <div ng-bind= "City" ng-show= "DisplayMode" > </div> |
040 | <div ng-show= "EditMode" > |
041 | <input ng-model= "City" type= "text" style= "width: 300px" /> |
046 | <td class = "input-label" align= "right" > <label>Region: </label> </td> |
047 | <td class = "input-box" > |
048 | <div ng-bind= "Region" ng-show= "DisplayMode" > </div> |
049 | <div ng-show= "EditMode" > |
050 | <input ng-model= "Region" type= "text" style= "width: 300px" /> |
055 | <td class = "input-label" align= "right" > <label>Postal Code: </label> </td> |
056 | <td class = "input-box" > |
057 | <div ng-bind= "PostalCode" ng-show= "DisplayMode" > </div> |
058 | <div ng-show= "EditMode" > |
059 | <input ng-model= "PostalCode" type= "text" style= "width: 300px" /> |
064 | <td class = "input-label" align= "right" > <label>Country: </label> </td> |
065 | <td class = "input-box" > |
066 | <div ng-bind= "CountryCode" ng-show= "DisplayMode" > </div> |
067 | <div ng-show= "EditMode" > |
068 | <input ng-model= "CountryCode" type= "text" style= "width: 300px" /> |
073 | <td class = "input-label" align= "right" > <label>Phone Number: </label> </td> |
074 | <td class = "input-box" > |
075 | <div ng-bind= "PhoneNumber" ng-show= "DisplayMode" > </div> |
076 | <div ng-show= "EditMode" > |
077 | <input ng-model= "PhoneNumber" type= "text" style= "width: 300px" /> |
082 | <td class = "input-label-bottom" align= "right" > <label>Web Site URL: </label> </td> |
083 | <td class = "input-box-bottom" > |
084 | <div ng-bind= "WebSiteURL" ng-show= "DisplayMode" > </div> |
085 | <div ng-show= "EditMode" > |
086 | <input ng-model= "WebSiteURL" type= "text" style= "width: 300px" /> |
092 | <span ng-show= "ShowCreateButton" > |
093 | <button class = "btn btn-primary btn-large" ng-click= "createCustomer()" >Create </button> </span> |
094 | <span ng-show= "ShowEditButton" > |
095 | <button class = "btn btn-primary btn-large" ng-click= "editCustomer()" >Edit </button> </span> |
096 | <span ng-show= "ShowUpdateButton" > |
097 | <button class = "btn btn-primary btn-large" ng-click= "updateCustomer()" >Update </button> </span> |
098 | <span ng-show= "ShowCancelButton" > |
099 | <button class = "btn btn-primary btn-large" ng-click= "cancelChanges()" >Cancel </button> </span> |
100 | <div style= "padding-top:20px" > |
102 | <alert ng-repeat= "alert in alerts" type= "{{alert.type}}" close= "closeAlert($index)" > |
103 | <div ng-bind-html= "MessageBox" > </div> </alert> |
数据绑定及关注点的分离(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 与任意类型的数据库进行交互。
004 | define(['application-configuration', 'customersService', 'alertsService'], function (app) |
006 | app.register.controller('customerMaintenanceController', |
007 | ['$scope', '$rootScope', '$routeParams', 'customersService', 'alertsService', |
009 | function ($scope, $rootScope, $routeParams, customerService, alertsService) |
011 | $scope.initializeController = function () { |
013 | var customerID = ($routeParams.id || "" ); |
015 | $rootScope.alerts = []; |
016 | $scope.CustomerID = customerID; |
018 | if (customerID == "" ) { |
019 | $scope.CustomerCode = "" ; |
020 | $scope.CompanyName = "" ; |
024 | $scope.PostalCode = "" ; |
025 | $scope.CountryCode = "" ; |
026 | $scope.PhoneNumber = "" |
027 | $scope.WebSiteURL = "" ; |
029 | $scope.EditMode = true ; |
030 | $scope.DisplayMode = false ; |
031 | $scope.ShowCreateButton = true ; |
032 | $scope.ShowEditButton = false ; |
033 | $scope.ShowCancelButton = false ; |
034 | $scope.ShowUpdateButton = false ; |
039 | var getCustomer = new Object(); |
040 | getCustomer.CustomerID = customerID; |
041 | customerService.getCustomer(getCustomer, |
042 | $scope.getCustomerCompleted, |
043 | $scope.getCustomerError); |
048 | $scope.getCustomerCompleted = function (response) { |
050 | $scope.EditMode = false ; |
051 | $scope.DisplayMode = true ; |
052 | $scope.ShowCreateButton = false ; |
053 | $scope.ShowEditButton = true ; |
054 | $scope.ShowCancelButton = false ; |
055 | $scope.ShowUpdateButton = false ; |
057 | $scope.CustomerCode = response.Customer.CustomerCode; |
058 | $scope.CompanyName = response.Customer.CompanyName; |
059 | $scope.Address = response.Customer.Address; |
060 | $scope.City = response.Customer.City; |
061 | $scope.Region = response.Customer.Region; |
062 | $scope.PostalCode = response.Customer.PostalCode; |
063 | $scope.CountryCode = response.Customer.Country; |
064 | $scope.PhoneNumber = response.Customer.PhoneNumber; |
065 | $scope.WebSiteURL = response.Customer.WebSiteUrl; |
068 | $scope.getCustomerError = function (response) { |
069 | alertsService.RenderErrorMessage(response.ReturnMessage); |
073 | $scope.createCustomer = function () { |
074 | var customer = $scope.createCustomerObject(); |
075 | customerService.createCustomer(customer, |
076 | $scope.createCustomerCompleted, |
077 | $scope.createCustomerError); |
080 | $scope.createCustomerCompleted = function (response, status) { |
082 | $scope.EditMode = false ; |
083 | $scope.DisplayMode = true ; |
084 | $scope.ShowCreateButton = false ; |
085 | $scope.ShowEditButton = true ; |
086 | $scope.ShowCancelButton = false ; |
087 | $scope.CustomerID = response.Customer.CustomerID; |
089 | alertsService.RenderSuccessMessage(response.ReturnMessage); |
091 | $scope.setOriginalValues(); |
094 | $scope.createCustomerError = function (response) { |
095 | alertsService.RenderErrorMessage(response.ReturnMessage); |
096 | $scope.clearValidationErrors(); |
097 | alertsService.SetValidationErrors($scope, response.ValidationErrors); |
100 | $scope.createCustomerObject = function () { |
102 | var customer = new Object(); |
104 | customer.CustomerCode = $scope.CustomerCode; |
105 | customer.CompanyName = $scope.CompanyName; |
106 | customer.Address = $scope.Address; |
107 | customer.City = $scope.City; |
108 | customer.Region = $scope.Region; |
109 | customer.PostalCode = $scope.PostalCode; |
110 | customer.Country = $scope.CountryCode; |
111 | customer.PhoneNumber = $scope.PhoneNumber; |
112 | customer.WebSiteUrl = $scope.WebSiteURL; |
117 | $scope.clearValidationErrors = function () { |
118 | $scope.CustomerCodeInputError = false ; |
119 | $scope.CompanyNameInputError = false ; |
Controller As 语法
示例中,显示层和控制层使用 $scope 技术实现 web应用和数据库的双向绑定。在上面的控制层代码中,你可以看到很多地方都使用了 $scope 对象。 在 AngularJS 中,这是实现数据绑定比较常见的方式。 AngularJS 控制层代码近期进行了细微的、影响比较大的优化。
最新的趋势是使用 Controller as ControllerName 这样的语法,而不是直接将$scope注入到你的控制器中。例如,顾客维护控制器可以像如下视图中这样被引用:
1 | <div ng-controller= "customerController as customer" > |
2 | <input ng-model= "customer.FirstName" type= "text" style= "width: 300px" /> |
3 | <input ng-model= "customer.LastName" type= "text" style= "width: 300px" /> |
5 | <button class = "btn btn-primary btn-large" ng-click= "createCustomer()" />Create</button> |
填充数据绑定属性的控制器语法就可以像下面这样:
使用 "this" 对象来引用控制器的scope看上去比直接将$scope注入到控制器中更加清晰。这里需要重申,$scope是“经典”技术,而“controller as"则是AngularJS里更加新晋的东西. 它们俩都能能工作得很好,不管是选择哪一种技术,都要记用着方便为出发点. 现有的实例更多使用的是$scope,而”controller as“则正在慢慢红火起来. 其中一个会比另外一个好么?这我们就得等待并观察AngularJS随时间发生的演变了.
自定义服务 - AngularJS 服务
AngularJS 服务是可替换的对象,这些对象使用依赖注入连接在一起。 在程序里,你可以使用服务来组织和共享你的代码。 AngularJS 服务是延迟初始化的 – 只有当应用程序组件依赖它时,AngularJS 才会初始化一个服务。
AngularJS 服务是单例类型 – 依赖服务的每个组件都会引用AngularJS 服务工厂类产生的一个实例。 虽然AngularJS 提供一些常用的服务(如$http),但是对于大多数应用来说,你可能想要创建自己的服务。
顾客维护控制器依赖于 CustomerService. 这个顾客服务组件被应用程序用于组织所有访问和向应用程序服务器传递顾客相关数据所需要的Web API路由. 为了保持示例应用程序所有控制器中路由的清晰, 我为每一个部分(包括顾客、订单、产品)都创建了服务层. AngularJS 服务能帮助你组织好你的JavaScript,以获得更好的重用性和可维护性.
To continue: http://blog.csdn.net/fxismonk/article/details/51321494