[译]用AngularJS构建大型ASP.NET单页应用(二)
原文地址:http://www.codeproject.com/Articles/808213/Developing-a-Large-Scale-Application-with-a-Single
客户管理页面-新增、修改客户
单页应用中的页面与asp.net页面类似,两者都是html页面。 对于asp.net,浏览器加载html、js、数据,然后,浏览器进展示。而单页应用,页面内容通过ng-view 指令被注入到一个div标签中。
页面初始化时,浏览器通常只渲染html代码。 若在单页应用中使用RequireJS,js会被动态加载。 当页面加载完,浏览器以ajax异步调用的方式从服务器读取数据。
使用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="{'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>
数据绑定及Separation of Concerns (SoC)
查看上面用于示例程序的顾客维护页面的HTML内容,你能够看到这是一个看起来很清晰,很容易阅读的HTML。内容里面也没有引用任何JavaScript。
借助于data-binding指令,AngularJS提供了内容视图及内容控制器之间清晰的关注点分离. 对于输入控制,双向数据绑定通过ng-bind这个指令以及客户管理控制器的$scope属性得到了实现. AngularJS中的数据绑定功能同其它的JavaScript库,如KnockoutJS,功能相似, 对于文档对象模型的转换需求已经成为过去式。
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、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; } }]); });
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(['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服务将会被所有的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(['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的值.
在http连接中,AngularJS 不默认返回一个XMLHttpRequest对象,但是你可以在$httpProvider服务里配置。 当浏览器请求中含有一些阻塞UI展示的配置项时,你可以使用blockUI组件,实现在前台展示自定义的消息。
// 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一起被加载、注册。 大型的网页应用中,对于每个页面的请求,通常优先进行身份验证、授权。 为了解决这个问题,indexController包含一个函数,实现在每个页面请求前,对用户身份进行验证。
AngularJS 可以配置、监听客户端页面上用户触发的事件。 其中一个事件是$routeChangeStart。 每次请求路由定位时,都会触发这个事件。 为了使监听器工作,你只需使用$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里面,每个应用程序都有一个单独的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>