原文地址:http://www.codeproject.com/Articles/808213/Developing-a-Large-Scale-Application-with-a-Singlehtml
客戶管理頁面-新增、修改客戶web
單頁應用中的頁面與asp.net頁面相似,二者都是html頁面。 對於asp.net,瀏覽器加載html、js、數據,而後,瀏覽器進展現。而單頁應用,頁面內容經過ng-view 指令被注入到一個div標籤中。ajax
頁面初始化時,瀏覽器一般只渲染html代碼。 若在單頁應用中使用RequireJS,js會被動態加載。 當頁面加載完,瀏覽器以ajax異步調用的方式從服務器讀取數據。shell
使用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)api
查看上面用於示例程序的顧客維護頁面的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; } }]); });
示例中,顯示層和控制層使用 $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服務將會被全部的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); } }]); });
在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裏面,每一個應用程序都有一個單獨的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>