AngularJS 与 ASP.NET MVC 集成






4.95/5 (97投票s)
单页应用程序和模型视图控制器设计模式
引言
每次我去我最喜欢的纽约熟食店,我都很难点餐。我太爱熟食店的食物了,以至于菜单上的所有东西看起来都很棒。我想要熏牛肉。我想要咸牛肉。我想要切片的烤火鸡。我想要土豆煎饼。我想要土豆沙拉。我想要奶油馅饼。我想要,我想要,我想要。我全都要。
在开发新的计算机软件方面,我也想要一切。我想要前端最新的 JavaScript 技术。我想要后端最新的 RESTful Web API 服务技术。我想要最新的数据库技术,以及最新的设计模式和技术。
想要鱼与熊掌兼得,本文将介绍一个使用 AngularJS 与 ASP.NET MVC 集成的场景,以获得两全其美。
概述
本文的示例 Web 应用程序将有三个目标
- 在前端实现 AngularJS,用于视图和 JavaScript AngularJS 控制器。
- 使用 Microsoft 的 ASP.NET MVC 平台来交付、引导和打包应用程序。
- 按功能模块按需加载 AngularJS 控制器和服务。
本文是我上一篇文章《AngularJS - 使用 AngularJS 开发大型单页应用程序 (SPA)》的后续。
https://codeproject.org.cn/Articles/808213/Developing-a-Large-Scale-Application-with-a-Single
本文的示例应用程序将包含三个主要文件夹:“主页”文件夹包含“关于”、“联系我们”和“索引”视图;“客户”文件夹允许您创建、更新和查询客户;以及“产品”文件夹用于创建、更新和查询产品信息。
除了使用 AngularJS 和 ASP.NET MVC 之外,该应用程序还将实现 Microsoft 的 ASP.NET Web API 服务来创建 RESTful 服务。Microsoft 的 Entity Framework 将用于生成和更新 SQL Server Express 数据库。
该应用程序还将使用一些依赖注入,使用Ninject。此外,还将集成一个用于 .NET 的小型验证库,名为FluentValidation,该库使用流畅的接口和 lambda 表达式来构建将驻留在应用程序业务层中的验证业务规则。
AngularJS 与 ASP.NET Razor 视图
多年来,我一直使用整个 Microsoft ASP.NET MVC 平台来开发 Web 应用程序。与传统的 ASP.NET Web Forms 回发模型相比,ASP.NET MVC 平台使用Razor 视图;它在业务逻辑、数据和表示逻辑之间提供了正确的关注点分离 (SoC)。在使用 MVC 及其约定优于配置和干净的设计模式进行开发后,您将永远不想回到 Web Forms 开发。
然而,ASP.NET MVC 平台及其 Razor 视图引擎,虽然比 Web Forms 更干净,但仍然鼓励并允许您将 .NET 服务器端代码与表示混合。在 Razor 视图中混合 .NET 代码和 HTML 可能会很快变得像意大利面条代码。此外,在 ASP.NET MVC 模型中,一些业务逻辑可能会最终写在 MVC 控制器中。在 MVC 控制器中编写代码来控制表示层中的信息是很诱人的。
AngularJS 相对于 Microsoft ASP.NET MVC Razor 视图提供了以下增强功能
- AngularJS 视图是纯 HTML
- AngularJS 视图在客户端缓存,响应速度更快,而不是在每次请求时在服务器端生成
- AngularJS 为编写高质量的客户端 JavaScript 代码提供了一个完整的框架
- AngularJS 在 JavaScript 控制器和 HTML 视图之间提供了完整的关注点分离
ASP.NET MVC 打包和最小化
打包和最小化是您可用于提高 Web 应用程序请求加载时间的两种技术。打包和最小化通过减少到服务器的请求数量和减小请求资源(如 CSS 和 JavaScript)的大小来提高加载时间。最小化还可以通过混淆代码中的逻辑来增加他人破解您的 JavaScript 代码的难度。
当涉及到打包技术和 AngularJS 框架时,您通常会看到打包和最小化过程使用Grunt或Gulp等框架进行自动化。Grunt 和 Gulp 等技术是流行的 Web 库,拥有庞大的生态系统,提供有用的插件,可以自动化您可能遇到的几乎所有任务。
然而,如果您是 Microsoft 开发者,您习惯于只需按一下按钮即可从 Visual Studio 发布 Web 应用程序,而无需学习任何第三方工具或库。幸运的是,打包和最小化是 ASP.NET 自 ASP.NET 4.5 以来的一项功能,可以轻松地将多个文件合并或打包到一个文件中。您可以创建 CSS、JavaScript 和其他包。文件越少,HTTP 请求就越少,这可以提高首次页面加载性能。
使用RequireJS动态加载MVC包
在开发 AngularJS 单页应用程序时,一个问题是,开箱即用地,AngularJS 要求在引导应用程序启动时,应用程序的所有 JavaScript 文件和控制器都必须在主布局页中引用和下载。对于可能包含数百个 JavaScript 文件的超大型应用程序,这可能不是理想的。由于我想使用 ASP.NET 打包来加载我所有的 AngularJS 控制器,因此出现了一个巨大的挑战。ASP.NET 包是在服务器端渲染的,一旦引导,AngularJS 中的所有内容都在客户端发生。
为了动态加载这个示例应用程序的 ASP.NET 包,我决定使用RequireJS JavaScript 库。RequireJS 是一个知名的 JavaScript 模块和文件加载器,支持流行浏览器的最新版本。起初,这似乎很简单,但随着时间的推移,我最终编写了大量代码,但未能解决将服务器端渲染的包与 AngularJS 等客户端技术结合使用,并在应用程序引导并运行时按需加载这些包的问题。
最终,经过大量的研究、试错和失败,我找到了一个解决方案,代码量大大减少,并且效果很好。本文的其余部分将介绍集成 AngularJS 与 ASP.NET MVC 的过程。
创建 MVC 项目并安装 Angular NuGet 包
为了开始使用示例应用程序,我通过在 Visual Studio 2013 Professional 中选择 ASP.NET Web 应用程序模板创建了一个 ASP.NET MVC 5 Web 应用程序。之后,我选择了 MVC 项目并选中了 Web API 复选框,以添加该应用程序将使用的 MVC Web API 的文件夹和引用。下一步是通过在“工具”菜单中选择“管理解决方案的 NuGet 程序包”来从 NuGet 下载和安装 AngularJS。
对于这个示例应用程序,我安装了以下所有 NuGet 包
- AngularJS - 安装整个 AngularJS 库
- Angular UI - AngularJS 框架的配套 UI 小部件和脚本套件。
- Angular UI Bootstrap - 包含一组基于 Bootstrap 标记和 CSS 的原生 AngularJS 指令
- Angular Block UI - AngularJS BlockUI 指令,在 HTTP 请求期间阻止 UI
- RequireJS - RequireJS 是一个 JavaScript 文件和模块加载器
- Ninject - 为 MVC 和 MVC Web API 提供依赖注入支持
- Entity Framework - Microsoft 推荐的新应用程序数据访问技术
- Fluent Validation - 用于构建验证规则的 .NET 验证库。
- Font Awesome - 提供可缩放矢量图标,可通过 CSS 即时自定义
NuGet 是一个出色的包管理器。当您使用 NuGet 安装包时,它会将库文件复制到您的解决方案,并自动更新您的项目引用和配置文件)。如果您删除一个包,NuGet 会撤销它所做的所有更改,以免留下任何杂乱。
美观 URL
对于这个示例应用程序,我希望在浏览器地址栏中实现美观 URL。默认情况下,AngularJS 会将 URL 路由为带有哈希标签
例如
- https://:16390/
- https://:16390/#/contact
- https://:16390/#/about
- https://:16390/#/customers/CustomerInquiry
- https://:16390/#/products/ProductInquiry
通过启用html5Mode并设置基本 URL,可以轻松获得干净的 URL 并删除 URL 中的哈希标签。在 HTML5 模式下,AngularJS 的$location 服务通过 HTML5 History API 与浏览器 URL 地址进行交互。HTML5 History API是一种通过脚本操作浏览器历史记录的标准方法。其核心是,这是单页应用程序的中央辅助功能。
要启用 html5Mode,您需要在 Angular 的配置阶段将 $locationProvider 的 html5Mode 设置为 true,如下所示
// CodeProjectRouting-production.js
angular.module("codeProject").config('$locationProvider', function ($locationProvider) {
$locationProvider.html5Mode(true);
}]);
当您配置 $locationProvider 以使用 html5Mode 时,您需要通过base href 标签指定应用程序的基本 URL。基本 URL 用于解析应用程序中的所有相对 URL。您可以在应用程序主布局页的标题部分设置基本 URL,如下所示
<!-- _Layout.cshtml -->
<html>
<head>
<base href="https://:16390/" />
</head>
对于示例应用程序,我将基本 URL 存储在 web.config 文件中的应用程序设置中。将基本 URL 设置为配置设置是一种最佳实践,这样您就可以根据要部署应用程序的环境和配置或站点设置不同的基本 URL 值。另外,在设置基本 URL 时,请确保基本 URL 以“/”结尾,因为基本 URL 会前置到您所有的路由。
<!-- web.config.cs -->
<appsettings>
<add key="BaseUrl" value="https://:16390/" />
</appsettings>
启用 html5Mode 并设置基本 URL 后,您将获得以下美观 URL 路由
- https://:16390/
- https://:16390/contact
- https://:16390/about
- https://:16390/customers/CustomerInquiry
- https://:16390/products/ProductInquiry
目录结构和配置
按照约定,MVC 项目模板要求您所有的 Razor 视图都位于Views 文件夹中;您所有的 JavaScript 文件都位于Scripts 文件夹中;您所有的内容文件都位于Content 文件夹中。对于这个示例应用程序,我想将所有 Angular 视图与其关联的 Angular JavaScript 控制器放在同一个目录下。基于 Web 的应用程序可能会变得非常庞大,我不想将相关功能分散到应用程序目录结构的各个文件夹中。
在示例应用程序中,将只使用两个 Razor 视图:Index.cshtml和_Layout.cshtml主布局页。这两个 Razor 视图将用于引导和配置应用程序。应用程序的其余部分将由 AngularJS 视图和控制器组成。
对于示例应用程序,我在 Views 文件夹下创建了两个额外的文件夹:一个用于客户的子文件夹,一个用于产品的子文件夹。所有客户的 Angular 视图和控制器将驻留在客户子文件夹中,所有产品的 Angular 视图和控制器将驻留在产品子文件夹中。
由于 Angular 视图是 HTML 文件,Angular 控制器是 JavaScript 文件,因此必须配置 ASP.NET MVC 以允许从 Views 文件夹访问 HTML 文件和 JavaScript 文件并将其传递给浏览器。这是 ASP.NET MVC 的默认约定。幸运的是,您可以通过编辑 Views 文件夹下的web.config文件并为 HTML 文件和 JavaScript 文件添加处理程序来更改此约定,这将允许这些文件类型被提供给浏览器。
<!-- web.config under the Views folder -->
<system.webserver>
<handlers>
<add name="JavaScriptHandler" path="*.js" verb="*" precondition="integratedMode"
type="System.Web.StaticFileHandler" />
<add name="HtmlScriptHandler" path="*.html" verb="*" precondition="integratedMode"
type="System.Web.StaticFileHandler" />
</handlers>
</system.webserver>
应用程序版本递增和项目构建
对于这个示例应用程序,我想跟踪版本和构建号,每次我编译、测试和发布应用程序时,都可以使用 Properties 文件夹下AssemblyInfo.cs文件中的信息。每次应用程序运行时,我都想获取应用程序的最新版本,并使用版本号来帮助处理诸如在 HTML 文件和 JavaScript 文件末尾附加版本号之类的事情,这将告诉浏览器获取这些文件的新版本,而不是从其浏览器缓存中运行旧版本的文件,当这些文件发生更改时。
对于这个应用程序,我使用的是Visual Studio Professional 2013,为了方便起见,我从
https://visualstudiogallery.msdn.microsoft.com/dd8c5682-58a4-4c13-a0b4-9eadaba919fe
下载了一个Visual Studio Professional 2013 的自动版本插件,它会自动递增 C# 和 VB.NET 项目的程序集版本。下载将插件安装到名为自动版本设置的“工具”菜单中。该插件附带一个配置工具,允许您配置主要和次要构建号,这些号将在每次编译时自动更新您的 AssemblyInfo.cs 文件。目前该插件仅在 Visual Studio Professional 2013 版本中受支持。或者,您可以手动更新版本号,或使用 Microsoft 的 TFS 等工具在一个完全集成的持续构建和配置管理环境中管理您的构建号。
下面是一个 AssemblyInfo.cs 的示例,其中包含一个在构建完成后由插件自动更新的AssemblyVersion 和 AssemblyFileVersion号。
// AssemblyInfo.cs
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
[assembly: AssemblyTitle("CodeProject.Portal")]
[assembly: AssemblyProduct("CodeProject.Portal")]
[assembly: AssemblyCopyright("Copyright © 2015")]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("1d9cf973-f876-4adb-82cc-ac4bdf5fc3bd")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Revision and Build Numbers
// by using the '*' as shown below:
[assembly: AssemblyVersion("2015.9.12.403")]
[assembly: AssemblyFileVersion("2015.9.12.403")]
用 Angular 视图和控制器替换关于、联系我们、Razor 视图
您对 MVC 项目进行的第一个操作是用 AngularJS 视图和控制器替换“联系我们”和“关于”Razor 视图。这是测试您的配置的良好起点,以确保 AngularJS 已正确设置并正常工作。之后,如果您不需要这些页面,您可以决定删除“关于”和“联系我们”视图和控制器。
在 AngularJS 中创建控制器的经典方式是通过注入$scope。示例应用程序中的视图和控制器使用“controller as”语法。此语法取代了控制器中使用 $scope,从而简化了控制器的语法。当您使用“controller as”语法声明控制器时,您会获得该控制器的单个实例。
使用“controller as”语法时,您附加到控制器作用域(视图模型)的所有属性都必须在视图中以别名开头。在下面的视图片段中,属性title以“vm”别名开头。
<!-- aboutController.js -->
<div ng-controller="aboutController as vm" ng-init="vm.initializeController()">
<h4 class="page-header">{{vm.title}}</h4>
</div>
使用“controller as”语法时,当调用控制器构造函数时,会创建一个名为“this”的对象,该对象就是控制器实例。您不必使用 Angular 提供的 $scope 变量,只需声明一个名为vm的变量(代表视图模型)并将“this”(控制器函数的实例)赋值给它。然后,所有变量都可以赋值给 vm 对象而不是 $scope。将变量赋值给控制器函数的实例后,我们就可以使用别名在视图中访问这些变量。
此外,示例应用程序中的所有控制器都使用“use strict”JavaScript 命令在严格模式下运行。严格模式有助于编写“安全”的 JavaScript。严格模式将以前接受的“坏语法”变成真正的错误。例如,在普通 JavaScript 中,错写变量名会创建一个新的全局变量。在严格模式下,这将抛出错误,从而不可能意外创建全局变量。
// aboutController.js
angular.module("codeProject").register.controller('aboutController',
['$routeParams', '$location', function ($routeParams, $location) {
{
"use strict";
var vm = this;
this.initializeController = function () {
vm.title = "About Us";
}
}]);
如前所述,AngularJS 视图和控制器相对于 MVC Razor 视图的一个优点是,Angular 提供了一个出色的机制,可以编写高质量的 JavaScript 模块和代码,并在纯 HTML 视图和 JavaScript 控制器之间实现完全分离。您不再需要使用 AngularJS 的双向数据绑定技术来解析浏览器的文档对象模型 (DOM),从而允许您编写单元可测试的 JavaScript 代码。
作为脚注,您将在 aboutController 中看到一个名为register.controller.的方法。稍后在本文中,您将看到 register 方法的来源及其用途。
主页索引 Razor 视图和 MVC 路由
在集成 AngularJS 与 ASP.NET MVC 时,了解应用程序实际启动和路由方式的一件有趣的事情是。当您启动应用程序时,ASP.NET MVC 会介入并查看其默认设置的路由表,如下所示
// RouteConfig.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
namespace CodeProject.Portal
{
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
}
}
上面开箱即用的 MVC 路由表将在应用程序启动时将应用程序路由到 MVC 主控制器,并执行主控制器内的索引方法,该方法反过来将向用户呈现Index.cshtml MVC Razor 视图,输出将呈现 MVC 默认项目模板的标准主页内容。
此应用程序的目标是用 Angular 视图替换所有 MVC 视图。问题是,主页的索引 Razor 视图在 AngularJS 启动之前就会被执行并注入到主布局页_Layout.cshtml中。
由于我决定让主页来自 Angular 视图,我只需删除索引 Razor 视图中的所有内容,留下一个包含 AngularJS ng-view标记的 div 标记。
<!-- Index.cshtml -->
<div ng-view></div>
AngularJS ngView 标记是补充$route 服务的指令,它将当前路由的渲染模板包含到主布局中。我有两个选择:要么将 ng-view 标记直接嵌入到 _Layout.cshtml 主布局页中,要么通过索引 Razor 视图将其注入到主布局页中。我决定直接从索引 Razor 视图注入标记。基本上,索引 Razor 视图仅在应用程序引导期间使用,并且在应用程序启动后将不再被引用。
一旦应用程序被引导并启动,AngularJS 就会介入并执行自己的路由系统,并执行其自身路由表中配置的默认路由。此时,我为 AngularJS 主页创建了一个单独的 Index.html 和一个 IndexController.js 文件,AngularJS 在启动时会渲染这些文件。
<!-- Index.html -->
<div ng-controller="indexController as vm" ng-init="vm.initializeController()">
<h4 class="page-header">{{vm.title}}</h4>
</div>
Index Angular 视图通过ng-init指令在视图加载时执行 indexController 的initializeController函数。
// indexController.js
angular.module("codeProject").register.controller('indexController',
['$routeParams', '$location', function ($routeParams, $location) {
"use strict";
var vm = this;
this.initializeController = function () {
vm.title = "Home Page";
}
}]);
RouteConfig.cs
当您开发 AngularJS 应用程序时,您可能会遇到的第一件事是,您可能正在开发一个名为 CustomerInquiry 的页面,该页面位于路由
/Views/Customers/ CustomerInquiry
当您在该视图的 HTML 页面中并点击 Visual Studio 中的运行按钮直接执行该页面时,MVC 将执行并尝试查找 Customers 路由的 MVC 控制器和视图。结果是,您将收到一个 MVC 路由错误,提示它找不到该路由的视图或控制器。
当然,您会收到此错误,因为 /View/Customers/CustomerInquiry 路由是 Angular 路由而不是 MVC 路由。MVC 对此路由一无所知。但您仍然想直接运行此页面。为了缓解此问题,需要在 MVC 路由表中添加额外的路由,以告知 MVC 将所有有效请求路由到 MVC 主控制器并从该路由引导应用程序。
由于我有三个视图文件夹:主页、客户和产品,我在 MVC RouteConfig 类中添加了以下内容,以将所有请求路由到 Home/Index 路由。运行时按F5也会经过此 MVC 路由表。基本上,在浏览器中按 F5 会重启 AngularJS 应用程序,就 Angular 和单页应用程序的运行方式而言。
有了这些额外的路由,您现在就可以直接执行 AngularJS 路由了。或者,您可能有一个单一的通配符路由在 MVC 路由表中处理您的路由,但我更喜欢在路由表中保持明确,并让 MVC 拒绝任何无效的路由。
要记住的基本一点是,MVC 路由发生在 AngularJS 启动之前,一旦引导完成,AngularJS 就会接管之后的所有路由请求。
// RouteConfig.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
namespace CodeProject.Portal
{
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "HomeCatchAllRoute",
url: "Home/{*.}",
defaults: new { controller = "Home", action = "Index",
id = UrlParameter.Optional }
);
routes.MapRoute(
name: "CustomersCatchAllRoute",
url: "Customers/{*.}",
defaults: new { controller = "Home", action = "Index",
id = UrlParameter.Optional }
);
routes.MapRoute(
name: "ProductsCatchAllRoute",
url: "Products/{*.}",
defaults: new { controller = "Home", action = "Index",
id = UrlParameter.Optional }
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index",
id = UrlParameter.Optional }
);
}
}
}
$controllerProvider 和动态加载控制器
当示例应用程序启动时,应用程序会预加载应用程序的核心控制器和服务。这包括主页目录中的所有控制器和应用程序的共享服务。
此应用程序的共享服务是在所有模块中执行的服务——包括 Ajax 服务和警报服务。如前所述,该应用程序有三个功能模块:一个用于基本“关于”、“联系我们”和“主页”的模块;一个客户模块;以及一个产品模块。
由于该应用程序可能随时间推移而增长,因此我不想在应用程序的配置和引导阶段预先加载所有功能模块。应用程序启动后,我只希望在用户请求客户和产品模块时加载这些模块的控制器。
默认情况下,AngularJS 被设计为预加载所有控制器。典型的控制器可能如下所示
// aboutController.js
angular.module("codeProject").controller('aboutController',
['$routeParams', '$location', function ($routeParams, $location) {
"use strict";
var vm = this;
this.initializeController = function () {
vm.title = "About";
}
}]);
如果您尝试在配置阶段之后动态加载上述控制器,您将收到一个 Angular 错误。您需要做的是使用 $controllerProvider 服务在配置阶段之后动态加载控制器。$controllerProvider 服务由 Angular 用于创建新控制器。此提供程序允许通过 register 方法进行控制器注册。
// aboutController.js
angular.module("codeProject").register.controller('aboutController',
['$routeParams', '$location', function ($routeParams, $location) {
"use strict";
var vm = this;
this.initializeController = function () {
vm.title = "About";
}
}]);
上面的 about 控制器已修改为执行 $controllerProvider 的register方法。要启用 register 方法,必须在配置阶段配置 register 方法。下面的代码片段使用 $controllerProvider 使 register 方法在应用程序启动后可用。在下面的示例中,提供了 register 方法用于注册和动态加载控制器和服务。如果您愿意,您也可以包含一个用于 Angular factories 和 directives 的 register 函数。
// CodeProjectBootStrap.js
(function () {
var app = angular.module('codeProject', ['ngRoute', 'ui.bootstrap', 'ngSanitize', 'blockUI']);
app.config(['$controllerProvider', '$provide', function ($controllerProvider, $provide) {
app.register =
{
controller: $controllerProvider.register,
service: $provide.service
};
}]);
})();
ASP.NET 打包和最小化
CSS 和 JavaScript 的打包和最小化是 ASP.NET MVC 最受欢迎和最强大的功能之一。打包和最小化减少了 HTTP 请求数量和有效负载大小,从而提高了 ASP.NET MVC 网站的性能。有多种方法可以减少和合并 CSS 和 JavaScript 的大小。
打包可以轻松地将多个文件合并或打包到一个文件中。您可以创建 CSS、JavaScript 和其他包。最小化执行各种代码优化,例如删除不必要的空格和注释,以及将变量名缩短为一个字符。由于打包和最小化减小了 JavaScript 和 CSS 文件的大小,因此通过 HTTP 传输的字节数显著减少。
配置包时,您需要考虑打包策略以及如何组织包。下面的BundleConfig类是内置 ASP.NET 打包功能的配置文件。在 BundleConfig 类中,我决定按功能模块组织我的文件。我为项目中的每个文件夹设置了一个单独的包,包括 Scripts、Content、Angular 核心文件、共享 JavaScript 文件以及 Home 目录、Customers 目录和 Products 目录的单独包。
我为 Customers 和 Products 目录创建了单独的包,目的是当用户请求这些应用程序区域的资源时,应用程序将动态加载这些包。由于 AngularJS 是一个纯客户端框架,动态加载 ASP.NET 包(一种服务器端技术)并将这两种技术结合在一起成为了开发示例应用程序的最大挑战,该挑战还要求支持发布和调试模式。
// BundleConfig.cs
using System.Web;
using System.Web.Optimization;
public class BundleConfig
{
// For more information on bundling, visit http://go.microsft.com/fwlink/?LinkId=301862
public static void RegisterBundles(BundleCollection bundles)
{
bundles.Add(new ScriptBundle("~/bundles/jquery").Include(
"~/Scripts/jquery-{version}.js"));
bundles.Add(new ScriptBundle("~/bundles/bootstrap").Include(
"~/Scripts/bootstrap.js",
"~/Scripts/respond.js"
));
bundles.Add(new StyleBundle("~/Content/css").Include(
"~/Content/bootstrap.css",
"~/Content/site.css",
"~/Content/SortableGrid.css",
"~/Content/angular-block-ui.min.css",
"~/Content/font-awesome.min.css"
));
bundles.Add(new ScriptBundle("~/bundles/angular").Include(
"~/Scripts/angular.min.js",
"~/Scripts/angular-route.min.js",
"~/Scripts/angular-sanitize.min.js",
"~/Scripts/angular-ui.min.js",
"~/Scripts/angular-ui/ui-bootstrap.min.js",
"~/Scripts/angular-ui/ui-bootstrap-tpls.min.js",
"~/Scripts/angular-ui.min.js",
"~/Scripts/angular-block-ui.js"
));
bundles.Add(new ScriptBundle("~/bundles/shared").Include(
"~/Views/Shared/CodeProjectBootstrap.js",
"~/Views/Shared/AjaxService.js",
"~/Views/Shared/AlertService.js",
"~/Views/Shared/DataGridService.js",
"~/Views/Shared/MasterController.js"
));
bundles.Add(new ScriptBundle("~/bundles/routing-debug").Include(
"~/Views/Shared/CodeProjectRouting-debug.js"
));
bundles.Add(new ScriptBundle("~/bundles/routing-production").Include(
"~/Views/Shared/CodeProjectRouting-production.js"
));
bundles.Add(new ScriptBundle("~/bundles/home").Include(
"~/Views/Home/IndexController.js",
"~/Views/Home/AboutController.js",
"~/Views/Home/ContactController.js",
"~/Views/Home/InitializeDataController.js"
));
bundles.Add(new ScriptBundle("~/bundles/customers").Include(
"~/Views/Customers/CustomerMaintenanceController.js",
"~/Views/Customers/CustomerInquiryController.js"
));
bundles.Add(new ScriptBundle("~/bundles/products").Include(
"~/Views/Products/ProductMaintenanceController.js",
"~/Views/Products/ProductInquiryController.js"
));
}
}
通过 ASP.NET 打包进行缓存破坏
使用 ASP.NET 打包的一个优点是其“缓存破坏”辅助方法,这些方法通过自动检测您是否更改了缓存的 CSS 或 JavaScript 来实现对打包文件的轻松缓存和缓存破坏。下面的示例代码片段在一个 MVC Razor 视图(通常在 _Layout.cshtml 主布局页中)中执行。Scripts.Render方法将包渲染到客户端,当在非调试模式下执行时,它会生成包的虚拟路径,并在包末尾附加版本号。当您更改包的内容并重新发布应用程序时,将向包附加一个新的版本号,这有助于破坏客户端的浏览器缓存并强制重新下载您的包。
// _Layout.cshtml
@Scripts.Render("~/bundles/customers")
@Scripts.Render("~/bundles/products")
Scripts.Render 函数是一个很好的功能,但在本示例应用程序中,我想使用 AngularJS 在客户端动态加载 customers 和 products 包,因此我无法使用 Render 函数来渲染我的某些包。挑战就从这里开始了。问题是:如何使用 AngularJS 从客户端 JavaScript 渲染服务器端 ASP.NET 包?
_Layout.cshtml - 服务器端启动代码
使用 ASP.NET MVC 引导您的 AngularJS 应用程序的一个好处是,您可以在加载和执行 AngularJS 代码之前,在 _Layout.cshtml 主布局页中执行一些服务器端代码。这是帮助解决通过客户端代码渲染服务器端包的困境的第一步。当然,您可以在客户端代码中简单地嵌入脚本标签,但我需要一种方法来渲染包并引用和维护用于缓存破坏而附加到包的自动版本号。
首先,我在 _Layout.cshtml 主布局页的顶部创建了一些服务器端代码。我做的第一件事是从 AssemblyInfo 类中获取应用程序的版本号,并从应用程序设置中检索基本 URL。这两个值将在之后由Razor 视图引擎在 HTML 中解析。
下面的代码片段的核心内容是生成了我想要稍后按需动态加载的包列表。我不想在应用程序启动时预加载所有包。我需要的重要信息是每个包的虚拟路径和长版本号。幸运的是,打包功能带有用于访问包信息的类和方法。
下面的关键代码行引用了BundleTable。此代码行执行ResolveBundleUrl方法,该方法返回每个引用包的虚拟路径和版本号。基本上,代码生成一个包列表,并将该列表转换为 JSON 集合。稍后,JSON 集合将被添加到 AngularJS。拥有 JSON 集合中的包信息是允许应用程序从客户端 AngularJS 加载服务器端包的初步桥梁。
// _Layout.cshtml
@using CodeProject.Portal.Models
@{
string version = typeof(CodeProject.Portal.MvcApplication).Assembly.GetName().Version.ToString();
string baseUrl = System.Configuration.ConfigurationManager.AppSettings["BaseUrl"].ToString();
List<CustomBundle> bundles = new List<CustomBundle>();
CodeProject.Portal.Models.CustomBundle customBundle;
List<string> codeProjectBundles = new List<string>();
codeProjectBundles.Add("home");
codeProjectBundles.Add("customers");
codeProjectBundles.Add("products");
foreach (string controller in codeProjectBundles)
{
customBundle = new CodeProject.Portal.Models.CustomBundle();
customBundle.BundleName = controller;
customBundle.Path = BundleTable.Bundles.ResolveBundleUrl("~/bundles/" + controller);
customBundle.IsLoaded = false;
bundles.Add(customBundle);
}
BundleInformation bundleInformation = new BundleInformation();
bundleInformation.Bundles = bundles;
string bundleInformationJSON = Newtonsoft.Json.JsonConvert.SerializeObject(
bundleInformation, Newtonsoft.Json.Formatting.None);
}
ASP.NET Bundle 类有很多功能。例如,如果您想遍历包内的所有文件,您可以执行EnumerateFiles方法并返回特定包内每个文件的虚拟路径。
foreach (var file in bundle.EnumerateFiles(new BundleContext(
new HttpContextWrapper(HttpContext.Current), BundleTable.Bundles, "~/bundles/shared")))
{
string filePath = file.IncludedVirtualPath.ToString();
}
_Layout.cshtml - Header
在 HTML 文档的 Header 部分,有一个对RequireJS的引用。此应用程序将使用 RequireJS 通过客户端 AngularJS 代码动态加载包。RequireJS 是一个用于加载 JavaScript 模块的异步模块定义 (AMD) API。RequireJS 有许多功能,但对于本应用程序的目的,仅在后续应用程序中使用 RequireJS 的require函数就足够了。
此外,在 header 部分正在执行Scripts.Render和Styles.Render方法。在调试模式下运行应用程序或当 EnableOptimizations 设置为 false 时,Render 方法会为包中的每个项目生成多个脚本标签。在发布模式且优化已启用时,Render 方法会生成一个指向版本戳 URL 的单个脚本标签,该 URL 代表整个包。
这导致了另一个挑战:应用程序需要支持在发布模式下生成包脚本标签以及在调试模式下生成包中单个文件的脚本标签的能力。这很重要,因为您希望在调试模式下能够在 JavaScript 代码中设置断点,这在发布模式下使用优化后的 JavaScript 代码包是无法实现的。
最后,在 header 部分,基本 URL 被设置为使用 Razor 语法创建的服务器端 baseUrl 变量。
<!-- _Layout.cshtml -->
!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />
<title>AngularJS MVC Code Project</titlev>
<script src="~/Scripts/require.js"></script>
@Scripts.Render("~/bundles/jquery")
@Scripts.Render("~/bundles/bootstrap")
@Scripts.Render("~/bundles/modernizr")
@Scripts.Render("~/bundles/angular")
@Styles.Render("~/Content/css")
<base href="#baseUrl" />
</head>
调试模式与发布模式
当 EnableOptimizations 设置为 false 或在调试模式下运行时,@Scripts.Render 方法会为包中的每个项目生成多个脚本标签。如果您希望设置断点并调试 JavaScript 文件,这是必需的。另一个选择是使用RenderFormat方法在调试模式下渲染自定义脚本标签。
下面的代码片段包含在_layout.cshtml主布局页中,当应用程序在调试模式下运行时使用 RenderFormat,它会在脚本标签中附加应用程序版本号,用于包中的所有 JavaScript 文件。这比标准渲染的脚本标签格式有一个小小的增强,即它不包含附加的版本号。
有时,当您从 Visual Studio 启动应用程序时,您可能会遇到浏览器缓存问题,同时还会花费时间猜测您是否正在运行最新版本的 JavaScript 文件。在浏览器中按 F5 可能会解决此问题。为了完全避免此问题,应用程序版本号会附加到脚本标签。通过自动版本插件,版本号会在每次构建时自动递增。通过这种技术,我节省了很多时间,同时也知道每次编译和运行时都使用了最新版本的 JavaScript 文件。
// _Layout.cshtml
@if (HttpContext.Current.IsDebuggingEnabled)
{
@Scripts.RenderFormat("<script type=\"text/javascript\" src=\"{0}?ver =" + @version + " \">
</script>", "~/bundles/shared")
@Scripts.RenderFormat("<script type=\"text/javascript\" src=\"{0}?ver =" + @version + " \">
</script>","~/bundles/routing-debug")
}
else
{
@Scripts.Render("~/bundles/shared")
@Scripts.Render("~/bundles/routing-production")
}
在服务器端 Razor 数据和 AngularJS 之间传递数据
现在我已经创建了一个服务器端包数据集合,下一个挑战是将服务器端数据注入并创建客户端 AngularJS 代码与服务器端数据之间的桥梁。在 _Layout.cshtml 主布局页中,我创建了一个匿名 JavaScript 函数,该函数创建一个 AngularJS provider。最初,我计划在 _Layout.cshtml 文件中创建一个普通的 AngularJS 服务或工厂,该服务可以使用 Razor 语法注入数据,并包含服务器端数据。
不幸的是,AngularJS 服务和工厂在AngularJS 配置阶段完成后才能使用,因此我无法在主布局页中创建服务而不会收到 AngularJS 错误。为了克服此限制,需要创建一个AngularJS provider。Provider 函数是构造函数,其实例负责“提供”服务的工厂。Providers 允许您在 Angular 的配置阶段创建和配置服务。
服务提供商名称以其提供的服务名称开头,后跟“Provider”一词。在下面的代码片段中,代码正在创建一个'applicationConfiguration' provider,它以applicationConfigurationProvider的名称引用。该 provider 在构造函数中配置,该构造函数设置程序集版本号和此应用程序将需要按需动态加载的包列表。MVC Razor 代码在构造函数中注入服务器端数据。
// _Layout.cshtml
(function () {
var codeProjectApplication = angular.module('codeProject');
codeProjectApplication.provider('applicationConfiguration', function () {
var _version;
var _bundles;
return {
setVersion: function (version) {
_version = version;
},
setBundles: function (bundles) {
_bundles = bundles;
},
getVersion: function () {
return _version;
},
getBundles: function () {
return _bundles;
},
$get: function () {
return {
version: _version,
bundles: _bundles
}
}
}
});
codeProjectApplication.config(function (applicationConfigurationProvider) {
applicationConfigurationProvider.setVersion('@version');
applicationConfigurationProvider.setBundles('@Html.Raw(bundleInformationJSON)');
});
})();
生产路由和动态加载的 MVC 包
到目前为止,您可能已经看到很多实现每个内容页硬编码路由的 AngularJS 示例。示例应用程序的路由使用基于约定的方法,该方法允许路由表不被硬编码的路由所束缚。使用基于约定的方法;所有内容页和关联的 JavaScript 文件都遵循命名约定,允许应用程序解析路由并动态确定每个内容页所需的 JavaScript 文件。
示例应用程序的以下路由表只需要解析三个路由
- 一个用于根路径'/'
- 一个用于标准路由路径,例如'/:section/:tree'
- 一个包含路由参数的路由'/:section/:tree/:id'
由于我决定从 ASP.NET 包加载 JavaScript 文件,因此下面的路由配置代码还需要包含一些引用先前创建的 applicationConfigurationProvider 的代码,该 provider 包含包信息。包信息被解析为 JSON 集合。包的 JSON 集合将用于返回包的虚拟路径。此外,JSON 集合还将用于跟踪已加载的包。一旦加载了包,就不需要确定是否需要再次下载该包。
路由代码中有几件事情在发生。首先,每当用户选择加载某个功能模块中的页面时,也会下载该模块所有 JavaScript 文件的包。例如,当用户选择客户模块中的一个内容页面时,下面的代码会检查该模块的包是否已加载,方法是检查JSON _bundles 集合的isLoaded属性,如果 isLoaded为 false,则加载该包并将该包的 isLoaded 属性设置为 true。
当确定需要下载模块的包时,使用延迟的 promise和RequireJS来加载这两个东西。延迟的 promise 帮助您异步运行函数,并在处理完成后返回 promise。在这种情况下,一旦包完全加载,就会返回 promise。
现在,本文的最后一块难题是确定一种从客户端代码加载包的方法。在我之前的 CodeProject.com 文章(先前提到)中使用了 RequireJS 来动态加载 JavaScript 文件,我尝试将 RequireJS 用于包加载。使用 RequireJS 的“require”函数,我将包的虚拟路径传递到 require 函数中。结果是,require 函数会加载您传递给它的任何路径,并且它完美地加载了我的包。这是达到目的的手段。
当我开始使用 RequireJS 加载包时,我已经完全实现了 RequireJS 及其所有配置和设置的辅助功能。结果是,我可以删除所有这些,只加载 RequireJS 库并使用其 require 函数。事实上,我甚至不必在动态加载的控制器前面加上 RequireJS define 语句。经过大量的试错,我达到了本文的圣杯。我现在可以通过客户端代码加载服务器端包。
// CodeProjectRouting-production.js
angular.module("codeProject").config(
['$routeProvider', '$locationProvider', 'applicationConfigurationProvider',
function ($routeProvider, $locationProvider, applicationConfigurationProvider) {
var baseSiteUrlPath = $("base").first().attr("href");
var _bundles = JSON.parse(applicationConfigurationProvider.getBundles());
this.getApplicationVersion = function () {
var applicationVersion = applicationConfigurationProvider.getVersion();
return applicationVersion;
}
this.getBundle = function (bundleName) {
for (var i = 0; i < _bundles.Bundles.length; i++) {
if (bundleName.toLowerCase() == _bundles.Bundles[i].BundleName) {
return _bundles.Bundles[i].Path;
}
}
}
this.isLoaded = function (bundleName) {
for (var i = 0; i < _bundles.Bundles.length; i++) {
if (bundleName.toLowerCase() == _bundles.Bundles[i].BundleName) {
return _bundles.Bundles[i].IsLoaded;
}
}
}
this.setIsLoaded = function (bundleName) {
for (var i = 0; i < _bundles.length; i++) {
if (bundleName.toLowerCase() == _bundles.Bundles[i].BundleName) {
_bundles.Bundles[i].IsLoaded = true;
break;
}
}
}
$routeProvider.when('/:section/:tree',
{
templateUrl: function (rp) { return baseSiteUrlPath + 'views/' +
rp.section + '/' + rp.tree + '.html?v=' + this.getApplicationVersion(); },
resolve: {
load: ['$q', '$rootScope', '$location', function ($q, $rootScope, $location) {
var path = $location.path().split("/");
var parentPath = path[1];
var bundle = this.getBundle(parentPath);
var isBundleLoaded = this.isLoaded(parentPath);
if (isBundleLoaded == false) {
this.setIsLoaded(parentPath);
var deferred = $q.defer();
require([bundle], function () {
$rootScope.$apply(function () {
deferred.resolve();
});
});
return deferred.promise;
}
}]
}
});
$routeProvider.when('/:section/:tree/:id',
{
templateUrl: function (rp) { return baseSiteUrlPath + 'views/' +
rp.section + '/' + rp.tree + '.html?v=' + this.getApplicationVersion(); },
resolve: {
load: ['$q', '$rootScope', '$location', function ($q, $rootScope, $location) {
var path = $location.path().split("/");
var parentPath = path[1];
var bundle = this.getBundle(parentPath);
var isBundleLoaded = this.isLoaded(parentPath);
if (isBundleLoaded == false) {
this.setIsLoaded(parentPath);
var deferred = $q.defer();
require([bundle], function () {
$rootScope.$apply(function () {
deferred.resolve();
});
});
return deferred.promise;
}
}]
}
});
$routeProvider.when('/',
{
templateUrl: function (rp) { return baseSiteUrlPath +
'views/Home/Index.html?v=' + this.getApplicationVersion(); },
resolve: {
load: ['$q', '$rootScope', '$location', function ($q, $rootScope, $location) {
var bundle = this.getBundle("home");
var isBundleLoaded = this.isLoaded("home");
if (isBundleLoaded == false) {
this.setIsLoaded("home");
var deferred = $q.defer();
require([bundle], function () {
$rootScope.$apply(function () {
deferred.resolve();
});
});
return deferred.promise;
}
}]
}
});
$locationProvider.html5Mode(true);
}
]);
调试路由表 - HTML 缓存破坏
就在我以为我完成了示例应用程序的时候,我意识到我必须提供两个版本的路由表,一个用于在调试模式下运行应用程序,另一个用于在发布模式下运行应用程序。在调试模式下,JavaScript 文件单独下载,不进行最小化。如果您想调试 JavaScript 控制器并设置断点,这是必需的。事实上,生产版本的路由代码带来了一些挑战,因为我无法调试代码,因为生产路由代码使用 JavaScript 包,并且在 Visual Studio 中无法像正常情况那样单步调试包。我必须放一些 console.log 命令和一些 JavaScript alert 语句来开发和测试生产路由表。
两个版本的路由代码都包含一项支持:对 HTML 文件进行缓存破坏。就像包和 JavaScript 一样,您还需要提供一个版本号,该版本号会附加到 HTML AngularJS 视图。在调试和生产路由代码中,都会从 applicationConfigurationProvider 中拉取程序集版本号并附加到 HTML 路径以进行缓存破坏。
// CodeProjectRouting-debug.js angular.module("codeProject").config( ['$routeProvider', '$locationProvider', 'applicationConfigurationProvider', function ($routeProvider, $locationProvider, applicationConfigurationProvider) { this.getApplicationVersion = function () { var applicationVersion = applicationConfigurationProvider.getVersion(); return applicationVersion; } var baseSiteUrlPath = $("base").first().attr("href"); $routeProvider.when('/:section/:tree', { templateUrl: function (rp) { return baseSiteUrlPath + 'views/' + rp.section + '/' + rp.tree + '.html?v=' + this.getApplicationVersion(); }, resolve: { load: ['$q', '$rootScope', '$location', function ($q, $rootScope, $location) { var path = $location.path().split("/"); var directory = path[1]; var controllerName = path[2]; var controllerToLoad = "Views/" + directory + "/" + controllerName + "Controller.js?v=" + this.getApplicationVersion(); var deferred = $q.defer(); require([controllerToLoad], function () { $rootScope.$apply(function () { deferred.resolve(); }); }); return deferred.promise; }] } }); $routeProvider.when('/:section/:tree/:id', { templateUrl: function (rp) { return baseSiteUrlPath + 'views/' + rp.section + '/' + rp.tree + '.html?v=' + this.getApplicationVersion(); }, resolve: { load: ['$q', '$rootScope', '$location', function ($q, $rootScope, $location) { var path = $location.path().split("/"); var directory = path[1]; var controllerName = path[2]; var controllerToLoad = "Views/" + directory + "/" + controllerName + "Controller.js?v=" + this.getApplicationVersion(); var deferred = $q.defer(); require([controllerToLoad], function () { $rootScope.$apply(function () { deferred.resolve(); }); }); return deferred.promise; }] } }); $routeProvider.when('/', { templateUrl: function (rp) { return baseSiteUrlPath + 'views/Home/Index.html?v=' + this.getApplicationVersion(); }, resolve: { load: ['$q', '$rootScope', '$location', function ($q, $rootScope, $location) { var controllerToLoad = "Views/Home/IndexController.js?v=" + this.getApplicationVersion(); var deferred = $q.defer(); require([controllerToLoad], function () { $rootScope.$apply(function () { deferred.resolve(); }); }); return deferred.promise; }] } }); $locationProvider.html5Mode(true); }]);
测试浏览器缓存
在开发 Web 应用程序时,您需要做的一件事是测试所有浏览器缓存和缓存破坏功能。您需要确保您的应用程序内容已正确下载和缓存,并且在后续页面请求后内容是从缓存中获取的。
您还需要对内容进行一些更改,重新构建应用程序,并确保更改会破坏缓存并重新下载您内容的新版本。
要测试所有这些,我通过在 web.config 中将调试模式设置为 false并在Chrome中运行应用程序,然后按F12调出 Chrome 的网络选项卡来运行应用程序的发布版本。在这里,您可以查看下载内容所需的时间,并查看内容是从服务器还是从浏览器缓存获取的。您甚至可以看到您的包被下载,其版本号由 ASP.NET 打包功能呈现。
最终,当您浏览应用程序中的所有页面时,您会注意到所有内容都来自浏览器缓存。这就是单页应用程序架构的美妙之处。您的所有内容最终都会被缓存,从而获得出色的响应时间,唯一需要访问 Web 服务器的是返回 JSON 格式的数据,从 RESTful Web API 调用以显示在视图中。
其他值得关注的点
示例应用程序中其他值得关注的点包括几个在服务器端执行的 .NET 库。对于验证数据输入,该应用程序在业务层中使用 FluentValidation 库。
FluentValidation 是一个用于 .NET 的小型验证库,它使用流畅的接口和 lambda 表达式来构建验证规则。
当尝试在示例应用程序中创建客户时,客户代码和公司名称是必填字段。示例应用程序使用 FluentValidation 库在业务层管理验证。通过将填充的客户对象传递到CreateCustomer方法,可以根据使用 FluentValidation 表达式设置的业务规则来验证对象上的属性。如果业务对象验证失败,业务层可以从验证库返回一组错误,并将错误集合发送回客户端,客户端可以使用该集合在浏览器中渲染错误消息。
/// <summary>
/// Create Customer
/// </summary>
/// <param name="customer"></param>
/// <param name="transaction"></param>
/// <returns></returns>
public Customer CreateCustomer(Customer customer, out TransactionalInformation transaction)
{
transaction = new TransactionalInformation();
try
{
CustomerBusinessRules customerBusinessRules = new CustomerBusinessRules();
ValidationResult results = customerBusinessRules.Validate(customer);
bool validationSucceeded = results.IsValid;
IList<ValidationFailure> failures = results.Errors;
if (validationSucceeded == false)
{
transaction = ValidationErrors.PopulateValidationErrors(failures);
return customer;
}
_customerDataService.CreateSession();
_customerDataService.BeginTransaction();
_customerDataService.CreateCustomer(customer);
_customerDataService.CommitTransaction(true);
transaction.ReturnStatus = true;
transaction.ReturnMessage.Add("Customer successfully created.");
}
catch (Exception ex)
{
string errorMessage = ex.Message;
transaction.ReturnMessage.Add(errorMessage);
transaction.ReturnStatus = false;
}
finally
{
_customerDataService.CloseSession();
}
return customer;
}
下面是一个客户业务规则类,它定义了客户对象的业务规则。使用 FluentValidation 库,您可以定义一组 lambda 表达式,并创建业务规则以及与每个验证相关的错误消息。FluentValidation 库附带一套完整的不同类型的 lambda 表达式,用于验证业务对象或实体上的属性。
// CustomerBusinessRules.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using FluentValidation;
using CodeProject.Business.Entities;
using System.Configuration;
using CodeProject.Interfaces;
namespace CodeProject.Business
{
public class CustomerBusinessRules : AbstractValidator<Customer>
{
public CustomerBusinessRules()
{
RuleFor(c => c.CompanyName).NotEmpty().WithMessage("Company Name is required.");
RuleFor(c => c.CustomerCode).NotEmpty().WithMessage("Customer Code is required.");
}
}
}
示例应用程序中还有一点值得关注,那就是 Ninject 库的依赖注入实现。当从NuGet安装 Ninject 时,会为您创建一个配置文件NinjectWebCommon.cs。在这里,您可以告诉 Ninject 库在应用程序的某些区域被执行时要创建哪些对象,例如在 Web API 服务中。在下面的RegisterServices中,我正在告诉 Ninject 将客户数据服务和产品数据服务与其实现的相应接口相关联。这告诉 Ninject 从哪里加载 DLL 引用,这些引用可以是实现匹配接口的任何 DLL。
// NinjectWebCommon.cs
namespace CodeProject.Portal.App_Start
{
using System;
using System.Web;
using Microsoft.Web.Infrastructure.DynamicModuleHelper;
using Ninject;
using Ninject.Web.Common;
public static class NinjectWebCommon
{
private static readonly Bootstrapper bootstrapper = new Bootstrapper();
/// <summary>
/// Starts the application
/// </summary>
public static void Start()
{
DynamicModuleUtility.RegisterModule(typeof(OnePerRequestHttpModule));
DynamicModuleUtility.RegisterModule(typeof(NinjectHttpModule));
bootstrapper.Initialize(CreateKernel);
}
/// <summary>
/// Stops the application.
/// </summary>
public static void Stop()
{
bootstrapper.ShutDown();
}
/// <summary>
/// Creates the kernel that will manage your application.
/// </summary>
/// <returns>The created kernel.</returns>
private static IKernel CreateKernel()
{
var kernel = new StandardKernel();
try
{
kernel.Bind<Func<IKernel>>().ToMethod(ctx => () => new Bootstrapper().Kernel);
kernel.Bind<IHttpModule>().To<HttpApplicationInitializationHttpModule>();
RegisterServices(kernel);
System.Web.Http.GlobalConfiguration.Configuration.DependencyResolver =
new Ninject.Web.WebApi.NinjectDependencyResolver(kernel);
return kernel;
}
catch
{
kernel.Dispose();
throw;
}
}
/// <summary>
/// Load your modules or register your services here!
/// </summary>
/// <param name="kernel">The kernel.</param>
private static void RegisterServices(IKernel kernel)
{
kernel.Bind<CodeProject.Interfaces.ICustomerDataService>().
To<CodeProject.Data.EntityFramework.CustomerDataService>();
kernel.Bind<CodeProject.Interfaces.IProductDataService>().
To<CodeProject.Data.EntityFramework.ProductDataService>();
}
}
}
通过使用 Ninject 数据注释[Inject],您可以告诉 Ninject 库何时何地实例化您的对象。在下面的 Web API 服务中,Customer Data Service 由 Ninject 创建。由于 Customer Business Service 依赖于 Customer Data Service 来访问数据,因此 Customer Data Service 被注入到 Customer Business Service 的构造函数中。所有这些都是通过为 Customer Data Service 创建一个接口,然后简单地实现 Customer Data Service 中的接口来完成的。依赖注入非常适合创建松散耦合的应用程序层,这允许您隔离地模拟和测试应用程序代码。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;
using CodeProject.Portal.Models;
using CodeProject.Business.Entities;
using CodeProject.Business;
using CodeProject.Interfaces;
using Ninject;
namespace CodeProject.Portal.WebApiControllers
{
[RoutePrefix("api/CustomerService")]
public class CustomerServiceController : ApiController
{
[Inject]
public ICustomerDataService _customerDataService { get; set; }
/// <summary>
/// Create Customer
/// </summary>
/// <param name="request"></param>
/// <param name="customerViewModel"></param>
/// <returns></returns>
[Route("CreateCustomer")]
[HttpPost]
public HttpResponseMessage CreateCustomer(HttpRequestMessage request,
[FromBody] CustomerViewModel customerViewModel)
{
TransactionalInformation transaction;
Customer customer = new Customer();
customer.CompanyName = customerViewModel.CompanyName;
customer.ContactName = customerViewModel.ContactName;
customer.ContactTitle = customerViewModel.ContactTitle;
customer.CustomerCode = customerViewModel.CustomerCode;
customer.Address = customerViewModel.Address;
customer.City = customerViewModel.City;
customer.Region = customerViewModel.Region;
customer.PostalCode = customerViewModel.PostalCode;
customer.Country = customerViewModel.Country;
customer.PhoneNumber = customerViewModel.PhoneNumber;
customer.MobileNumber = customerViewModel.MobileNumber;
CustomerBusinessService customerBusinessService =
new CustomerBusinessService(_customerDataService);
customerBusinessService.CreateCustomer(customer, out transaction);
if (transaction.ReturnStatus == false)
{
customerViewModel.ReturnStatus = false;
customerViewModel.ReturnMessage = transaction.ReturnMessage;
customerViewModel.ValidationErrors = transaction.ValidationErrors;
var responseError = Request.CreateResponse<CustomerViewModel>
(HttpStatusCode.BadRequest, customerViewModel);
return responseError;
}
customerViewModel.CustomerID = customer.CustomerID;
customerViewModel.ReturnStatus = true;
customerViewModel.ReturnMessage = transaction.ReturnMessage;
var response = Request.CreateResponse<CustomerViewModel>
(HttpStatusCode.OK, customerViewModel);
return response;
}
结论
集成 AngularJS 与 ASP.NET MVC 和 ASP.NET 打包似乎起初是一项简单的任务,但最终变成了一个挑战。每一次试错迭代,这个挑战都变成了一种痴迷,然后变成了一种执念。我只是想让这一切协同工作,我不会停止尝试。
您可以争论使用 ASP.NET 打包来打包和最小化 JavaScript 和 CSS 并将其与 AngularJS 集成的优点,与使用 Grunt 和 Gulp 领域中的流行最小化工具相比,但如果您是一位 Microsoft 开发者,他只是喜欢按一下按钮即可从 Visual Studio 发布应用程序,而无需学习任何额外的技术或工具,那么您很可能想使用 ASP.NET 打包功能。我发现这项功能最终正是我想要的;只是花了很多时间才弄清楚如何将其与 AngularJS 集成。
如今有很多技术值得书写。我未来的一些文章可能会包括 AngularJS 2 和 MEAN 堆栈的其余部分,包括 Node.JS、Express 和 MongoDB。
还有用于移动应用程序开发的 Apache Cordova,它已包含在 Visual Studio 2015 的最新版本中。Ionic 框架,一个与 Apache Cordova 配合使用的先进 HTML5 混合移动应用程序框架,看起来也很有前景。据说 Ionic 可以轻松地使用 HTML5 和 AngularJS 构建出色且交互式的移动应用程序。敬请关注!