原文地址: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控制器是一个的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中,你需要为不同的规则配置不同的路由。 我决定来统一网页和相关的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'] });
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; });
打开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应用。