原文地址:http://www.codeproject.com/Articles/808213/Developing-a-Large-Scale-Application-with-a-Single
渣譯,各位看官請勿噴
引言:
...
單頁面應用程序(SPA),被定義為在一個獨立的頁面上提供類似於桌面應用程序級用戶體驗為目標的網站。在SPA, 基本上所有的代碼 - 例如 HTML,JavaScript和CSS - 都是在響應用戶操作時動態加載的。頁面沒有在任何時候被重新刷新,也沒有跳轉到另一個頁面,但現代WEB技術(例如HTML5)可以提供類似單獨頁面一樣的導航。單頁面應用程序往往需要與后台Web服務器動態交互。
...
本文的目標是開發出能支撐大量用戶使用的豐富內容的企業級單頁面應用程序,其包括認證,授權,回話等功能。
概述 - AngularJS
本文的示例應用程序包含的創建和更新用戶帳戶,創建和更新客戶和產品功能。另外,該應用程序還允許你查詢、創建和更新銷售訂單。該示例應用程序將使用AngularJS建成。 AngularJS是一個開源Web應用框架, 其社區由谷歌維護的。
AngularJS使你創建單頁面應用程序只需要包含HTML,CSS和JavaScript等客戶端腳本。它的目標是增強Web應用程序的模型 - 視圖 - 控制器(MVC)的能力,使開發和測試更容易。
該庫讀取HTML中自定義標簽的屬性,通過這些自定義屬性,綁定輸入、輸出功能的JavaScript model變量。這些JavaScript變量可以被手動設置,也可以從JSON中獲取。
AngularJS入門-模板頁、模塊、路由
你需要做的第一件事情就是下載AngularJS框架到項目中。你可以在https://angularjs.org的AngularJS框架。本文的示例應用程序使用Web Express 2013 開發,所以這里通過NuGet安裝AngularJS包...
我創建了一個空的Visual Studio Web應用程序項目,並選擇了Microsoft Web API 2庫。此應用程序將使用Web API 2庫提供REST風格的接口服務。
現在,你需要做兩件事情要來構建一個AngularJS單頁面應用程序:建立模板頁面,並設置相關路由。首先,模板頁只需要一個引用AngularJS JavaScript庫並增加ng-view 指令告訴AngularJS哪些地方需要加載其他內容。
<!DOCTYPE html> <html lang="en"> <head> <title>AngularJS Shell Page example</title> </head> <body> <div> <ul> <li><a href="#Customers/AddNewCustomer">Add New Customer</a></li> <li><a href="#Customers/CustomerInquiry">Show Customers</a></li> </ul> </div> <!-- ng-view directive to tell AngularJS where to inject content pages --> <div ng-view></div> <script src="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 service加載不同的內容至模板頁中。 例如,如果用戶選擇“Add New Customer”鏈接,AngularJS會令其中ng-view指令存在的div標簽內插入添加客戶的內容。呈現的內容是部分HTML頁面。
同時app.js JavaScript也需要在模板被引用。 這個JS文件將會為應用程序創建一個AngularJS模塊。此外,對於路由配置也將在本文件中被定義。你可以把AngularJS模塊作為一個應用程序的不同部分。AngularJS應用程序沒有main方法。模塊需要聲明指定的應用程序如何配置。本文的示例應用程序將只能有一個AngularJS模塊,它包含了客戶、產品、訂單和用戶等應用程序的幾個不同部分。
現在,下面的app.js文件的主要目的是建立AngularJS路由。AngularJS $routeProvider服務使用when()方法來匹配URI。當匹配成功時,頁面的部分HTML內容被加載到模板頁,並關聯到對應的controller文件。controller文件很簡單,就是處理指定路由請求的JavaScript 文件。
//Define an angular module for our app var sampleApp = angular.module('sampleApp', []); //Define Routing for the application sampleApp.config(['$routeProvider', function($routeProvider) { $routeProvider. when('/Customers/AddNewCustomer', { templateUrl: 'Customers/AddNewCustomer.html', controller: 'AddNewCustomerController' }). when('/Customers/CustomerInquiry', { templateUrl: 'Customers/CustomerInquiry.html', controller: 'CustomerInquiryController' }). otherwise({ redirectTo: '/Customers/AddNewCustomer' }); }]);
AngularJS Controllers
AngularJS控制器是一個的JavaScript函數。控制器用於給你的視圖添加業務邏輯。視圖是HTML頁面。這些頁面將顯示出被雙向數據綁定的數據。控制器的負責將數據傳輸給視圖。
<div ng-controller="customerController"> <input ng-model="FirstName" type="text" style="width: 300px" /> <input ng-model="LastName" type="text" style="width: 300px" /> <div> <button class="btn btn-primary btn-large" ng-click="createCustomer()"/>Create</button>
對於上述AddCustomer模板,ng-controller指令將調用JavaScript函數中的customerController方法,並進行數據綁定。
function customerController($scope) { $scope.FirstName = "William"; $scope.LastName = "Gates"; $scope.createCustomer = function () { var customer = $scope.createCustomerObject(); customerService.createCustomer(customer, $scope.createCustomerCompleted, $scope.createCustomerError); } }
可擴展性性
當我開發了本文的示例程序,單頁面應用程序的前兩個可擴展性問題變得越來越明顯了。AngularJS需要在頁面上引用並下載所有相關JavaScript文件。一個大型的應用程序可能包含數百個JavaScript文件,這似乎並不理想。另一個問題是當我用AngularJS路由表,每一個路由規則我都要寫到路由表中,當路由表中有數百個規則時,這顯然不是一個好的辦法。
使用RequireJS動態加載javascript文件
對於此示例,我不想在模板頁上加載所有的JavaScript文件。大型應用程序通常需要數百個JavaScript文件。一般情況下,JavaScript文件是逐一使用script標簽加載的。此外,每個文件有可能依賴於其他文件。RequireJS這個JavaScript庫正可以動態加載JavaScript文件。
RequireJS是一個著名的JavaScript模塊和文件加載器,它支持在主流瀏覽器。使用RequireJS時,需要將JavaScript代碼分割成各個模塊,每個文件負責單一的功能。此外,我們還可以配置相應文件的依賴關系。
RequireJS提供了一個簡潔的方式來加載和管理你的Javascript應用程序。
AngularJS路由轉換
在AngularJS中,你需要為不同的規則配置不同的路由。 我決定來統一網頁和相關的JavaScript文件的命名約定,並允許應用程序解析路由的名稱,自動綁定將JS方法和頁面綁定。
例如,客戶維護內容頁被命名為CustomerMaintenance.Html,AngularJS控制器對應的JavaScript文件被命名為CustomerMaintenanceController.js。
讓我們開始修改示例應用程序。首先,每一個應用程序都需要某種形式的認證和授權機制來控制權限。此應用程序將使用ASP.NET Forms Authentication進行身份驗證。一旦通過驗證,用戶將能夠訪問應用程序的其余部分。一般網站都會有不同的母版頁,一個用 於顯示的登錄頁面,另一個母版頁通常包含一個主菜單欄、邊欄附和頭部等菜單選項,內容區域和頁腳區域。此示例應用程序通過多個模板頁面來實現。登錄成功后,用戶將被路由到一個新的模板頁面。
多模板頁
第一個模板頁是index.html。此頁面將顯示登錄和用戶注冊的相關內容。正如你所看到的,這里只引用了一個JavaScript文件。 Main.js將包含RequireJS的配置信息來動態加載模塊。我們將index.html的AngularJS控制器命名為indexController.js。如果用戶成功注 冊或登錄,該應用程序將跳轉到一個新的模板頁面applicationMasterPage.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異步加載腳本和JS的依賴管理。如先前所示,模板頁面將只引用main.js。這是RequireJS的配置文件。在下面的JavaScript文件有三個部分。
第一部分定義了所有需要的用於裝載的JavaScript文件和模塊的路徑。由於RequireJS只加載JavaScript文件,所以不需要添加.js的后綴。
第二部分定義了shim 部分。shim 構造了允許RequireJS加載非AMD的兼容腳本。
第三部分bootstraps等初始化腳本。
// main.js require.config({ baseUrl: "", // alias libraries paths paths: { 'application-configuration': 'scripts/application-configuration', 'angular': 'scripts/angular', 'angular-route': 'scripts/angular-route', 'angularAMD': 'scripts/angularAMD', 'ui-bootstrap' : 'scripts/ui-bootstrap-tpls-0.11.0', 'blockUI': 'scripts/angular-block-ui', 'ngload': 'scripts/ngload', 'mainService': 'services/mainServices', 'ajaxService': 'services/ajaxServices', 'alertsService': 'services/alertsServices', 'accountsService': 'services/accountsServices', 'customersService': 'services/customersServices', 'ordersService': 'services/ordersServices', 'productsService': 'services/productsServices', 'dataGridService': 'services/dataGridService', 'angular-sanitize': 'scripts/angular-sanitize', 'customersController': 'Views/Shared/CustomersController', 'productLookupModalController': 'Views/Shared/ProductLookupModalController' }, // Add angular modules that does not support AMD out of the box, put it in a shim shim: { 'angularAMD': ['angular'], 'angular-route': ['angular'], 'blockUI': ['angular'], 'angular-sanitize': ['angular'], 'ui-bootstrap': ['angular'] }, // kick start application deps: ['application-configuration'] });
Application-Configuration.js - Bootstrap等配置文件
AngularJS有兩個執行階段,配置階段和運行階段。Application-Configuration.js將執行AngularJS配置階段。我們將設置使用AngularJS routeProvider服務。
// application-configuration.js "use strict"; define(['angularAMD', 'angular-route', 'ui-bootstrap', 'angular-sanitize', 'blockUI', ], function (angularAMD) { var app = angular.module("mainModule", ['ngRoute', 'blockUI', 'ngSanitize', 'ui.bootstrap']); app.config(['$routeProvider', function ($routeProvider) { $routeProvider .when("/", angularAMD.route({ templateUrl: function (rp) { return 'Views/Main/default.html'; }, controllerUrl: "Views/Main/defaultController" })) .when("/:section/:tree", angularAMD.route({ templateUrl: function (rp) { return 'views/' + rp.section + '/' + rp.tree + '.html'; }, resolve: { load: ['$q', '$rootScope', '$location', function ($q, $rootScope, $location) { var path = $location.path(); var parsePath = path.split("/"); var parentPath = parsePath[1]; var controllerName = parsePath[2]; var loadController = "Views/" + parentPath + "/" + controllerName + "Controller"; var deferred = $q.defer(); require([loadController], function () { $rootScope.$apply(function () { deferred.resolve(); }); }); return deferred.promise; }] } })) .when("/:section/:tree/:id", angularAMD.route({ templateUrl: function (rp) { return 'views/' + rp.section + '/' + rp.tree + '.html'; }, resolve: { load: ['$q', '$rootScope', '$location', function ($q, $rootScope, $location) { var path = $location.path(); var parsePath = path.split("/"); var parentPath = parsePath[1]; var controllerName = parsePath[2]; var loadController = "Views/" + parentPath + "/" + controllerName + "Controller"; var deferred = $q.defer(); require([loadController], function () { $rootScope.$apply(function () { deferred.resolve(); }); }); return deferred.promise; }] } })) .otherwise({ redirectTo: '/' }) }]); // Bootstrap Angular when DOM is ready angularAMD.bootstrap(app); return app; });
定義RequireJS
打開application-configuration.js文件,我們會看到一個定義函數。定義函數是RequireJS的加載代碼模塊。requireJS定義模塊與傳統一個很大的不同是他可以保證其定義的變量處於某個范圍內,從而避免了全局命名污染。他能明確的羅列出其依賴,並且在那些依賴上找到處理辦法,而不是必須對那些依賴指定全局變量。
此應用程序依賴angularAMD, angular-route, ui-bootstrap, angular-sanitize和blockUI。
AngularAMD, UI-Bootstrap, Angular-Sanitize and BlockUI
Application-Configuration.js依賴angularAMD。angularAMD是有利於支持按需加載控制器和第三方模塊,如Angular-UI。
UI-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屬性:
'views/' + rp.section + '/' + rp.tree + '.html'.
引入 rp.setion和 rp.tree變量,可以很容易實現路徑匹配、路徑轉換。轉換完路徑后,唯一需要做的事是把擴展名 .html連接到字符串末尾。
加載控制器文件的過程有點復雜。 AngularJS路徑配置的控制器屬性只支持靜態的字符串。 它不支持含有變量的字符串,如下:
controller = "Views/" + parentPath + "/" + controllerName + "Controller";
經過一段時間的研究,我發現可以通過功能分解來設置控制器屬性。 結合使用AngularJS的location service和deferred promise特性,我最終實現動態加載js控制器文件時設置控制器屬性值。 js性能的一個提升意味着這次改造產生了最終的價值。
路由表里最終只有兩個主路徑,AngularJS需要對其進行匹配。第二個路徑
/:section/:tree/:id
是用來處理那些帶有參數的路徑的。現在,不管應用變得多大,路由表都將會保持的很小,而且只需要跟兩個路徑進行匹配,這樣就提高了路由匹配的效率。
最終,application-configuration.js使用angularAMD來引導AngularJS應用。