在 ASP.NET MVC 中集成 AngularJS(1)
介绍
当涉及到计算机软件的开发时,我想运用所有的最新技术。例如,前端使用最新的 JavaScript 技术,服务器端使用最新的基于 REST 的 Web API 服务。另外,还有最新的数据库技术、最新的设计模式和技术。
当选择最新的软件技术时,有几个因素在起作用,其中包括如何将这些技术整合起来。过去两年中,我最喜欢的一项技术就是设计单页面应用(SPA)的 AngularJS。作为一个微软stack开发者,我也是使用 ASP.NET MVC 平台实现 MVC 设计模式和并进行研究的粉丝,包括它的捆绑和压缩功能以及实现其对 RESTful 服务的 Web API 控制器。
为了兼得两者,本文介绍了在 ASP.NET MVC 中集成 AngularJS 的两全其美的方案。
由于本文篇幅较长,故会分为3篇,分别进行介绍。
概述
本文中示例的 Web 应用程序将有三个目标:
- 在前端页面中实现 AngularJS 和 JavaScript AngularJS 控制器
- 使用微软的 ASP.NET MVC 平台来建立、引导并捆绑一个应用
- 根据功能模型的需求,动态的加载 AngularJS 的控制器和服务
本文的示例应用程序将包含三个主要文件夹:关于联系和索引的主文件夹、允许你创建,更新和查询客户的客户文件夹、允许你创建,更新和查询产品的产品文件夹。
除了使用 AngularJS 和 ASP.NET MVC,这个应用程序也将实现使用微软的 ASP.NET Web API 服务来创建 RESTful 服务。微软的实体框架将用于生成并更新一个 SQL Server Express 数据库。
此应用程序也将用到一些使用 Ninject 的依赖注入。此外,也会运用流畅的界面和 lambda 表达式,来合并使用称为 FluentValidation的.NET 的小型验证库,用于构建驻留在应用业务层的验证业务规则。
AngularJS VS ASP.NET Razor 视图
几年来,我一直在使用完整的 Microsoft ASP.NET MVC 平台来开发 Web 应用程序。相比于使用传统的 ASP.NET Web 窗体的 postback 模型, ASP.NET MVC 平台使用的是 Razor 视图。 这带来的是:适当的业务逻辑、数据和表示逻辑之间关注点的分离。在使用它的约定优于配置和简洁的设计模式进行 MVC 开发之后,你将永远不会想回过头去做 Web 窗体的开发。
ASP.NET MVC 平台及其 Razor 视图引擎,不但比 Web 窗体简洁,还鼓励和允许你将 .NET 服务器端代码和样式混合。在 Razor 视图中的 HTML 混合的 .NET 代码看起来像套管代码。另外,在 ASP.NET MVC 模式下,一些业务逻辑是可以被最终写入在 MVC 的控制器中。在MVC控制器中,写入代码来控制表示层中的信息,这是很有诱惑力的。
AngularJS 提供了以下对微软 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 库并配有插件,它允许你自动化你的每一项工作。
如果你是一个微软开发者,你可以使用它们在 Visual Studio 中一键式发布你的 Web 应用,而不用学习使用任何第三发工具和库类。幸运的是,捆绑和压缩是 ASP.NET 4.5 ASP.NET 中的一项功能,可以很容易地将多个文件合并或捆绑到一个文件中。你可以创建 CSS,JavaScript 和其他包。较少的文件意味着更少的 HTTP 请求,这也可以提高第一个页面的加载性能。
使用 RequireJS 来实现 MVC 捆绑的动态加载
在开发 AngularJS 单页的应用程序时,其中有一件事情是不确定的。由于应用开始时会被引导和下载,所以在主页面索引时,AngularJS 会请求所有的 JavaScript 文件和控制器。对于可能包含数百个 JavaScript 文件的大规模应用,这可能不是很理想。因为我想使用 ASP.NET 的捆绑来加载所有的 AngularJS 控制器。一旦开始索引,一个 ASP.NET 捆绑中的巨大的挑战将会出现在服务器端。
为了实现示例程序动态地绑定 ASP.NET 文件包,我决定用 RequireJS JavaScript 库。RequireJS 是一个众所周知的 JavaScript 模块和文件加载器,最新版本的浏览器是支持 RequireJS 的。起初,这似乎是一个很简单的事情,但随着时间的推移,我完成了大量的代码的编写,却并没有解决使用服务器端 rendered bundle 与客户端 AngularJS 等技术的问题。
最终,在大量的研究和反复试验和失败后,我想出了少量代码却行之有效的解决方案。
本文的接下来部分将会展示,在 ASP.NET MVC 中集成 AngularJS 的过程。
创建 MVC 项目并安装 Angular NuGet 包
为了开始示例应用程序,我通过在 Visual Studio 2013 专业版中选择 ASP.NET Web 应用程序模板来创建一个 ASP.NET MVC 5 Web 应用程序。之后,我选择了 MVC 工程并在应用中会用到 MVC Web API 添加文件夹和引用。下一步是选择工具菜单中的“管理 NuGet 包的解决方案”,来下载并安装 NuGet AngularJS。
对于此示例应用程序,我安装了所有的以下的 NuGet 包:
- AngularJS - 安装整个 AngularJS 库
- AngularJS UI - AngularJS 框架的伙伴套件UI工具和脚本。
- AngularJS UI引导 - 包含一组原生 AngularJS 指令的引导标记和CSS
- AngularJS 块UI - AngularJS BlockUI 指令,块状化 HTTP 中的请求
- RequireJS - RequireJS 是一个 JavaScript 文件和模块加载
- Ninject – 提供了支持 MVC 和 MVC Web API 支持的依赖注入
- 实体框架 - 微软推荐的数据访问技术的新应用
- 流畅的验证 - 建立验证规则的 .NET 验证库。
- 优美字体- CSS 可立即定制的可升级的矢量图标
NuGet 是一个很好的包管理器。当你使用 NuGet 安装一个软件包,它会拷贝库文件到你的解决方案,并自动更新项目中的引用和配置文件。如果你删除一个包, NuGet 会让所有删除过程不会留下任何痕迹。
优美的URLS
对于此示例应用程序,我想在浏览器的地址栏中实现优美的网址。默认情况下,AngularJS 会将 URL 用#标签进行路由:
例如:
- http://localhost:16390/
- http://localhost:16390/#/contact
- http://localhost:16390/#/about
- http://localhost:16390/#/customers/CustomerInquiry
- http://localhost:16390/#/products/ProductInquiry
通过转向 html5Mode 和设置基本的 URL,可以很方便的清除 URLS 并去除 URL 中的#。在 HTML5 模式下,AngularJS 的$位置服务会和使用 HTML5 History API 的浏览器 URL 地址进行交互。HTML5 History API 是通过脚本来操作浏览器历史记录的标准方法,以这点为核心,是实现单页面应用的重点。
要打开 html5Mode,你需要在 Angular 的配置过程中,将 $locationProviderhtml5Mode 设置为 true,如下所示:
// CodeProjectRouting-production.js
angular.module("codeProject").config('$locationProvider', function ($locationProvider) {
$locationProvider.html5Mode(true);
}]);
当你使用 html5Mode 配置 $locationProvider 时,你需要使用 href 标记来指定应用的基本 URL。基本 URL 用于在整个应用程序中,解决所有相对 URL 的问题。你可以在应用程序中设置,如下所示的母版页的 header 部分的基本 URL:
<!-- _Layout.cshtml -->
<html>
<head>
<basehref="http://localhost:16390/"/>
</head>
对于示例应用程序,我以程序设置的方式将基本 URL 存储在 Web 配置文件中。这是一种最好的方式使得基本 URL 成为一种配置,这样能够让你根据环境、配置或者你开发的应用的站点的情况,来将基本 URL 设定为不同的值。此外,设置基本 URL 时,要确保基本 URL 以“/”为结尾,因为基本 URL 将是所有地址的前缀。
<!-- web.config.cs -->
<appsettings>
<addkey="BaseUrl"value="http://localhost:16390/"/>
</appsettings>
打开 html5Mode 并设置基本 URL 后,你需要以以下优美的 URL 作为结束:
- http://localhost:16390/
- http://localhost:16390/contact
- http://localhost:16390/about
- http://localhost:16390/customers/CustomerInquiry
- http://localhost:16390/products/ProductInquiry
目录结构与配置
按照惯例,一个 MVC 项目模板要求所有的 Razor 视图驻留在视图文件夹中; 所有的 JavaScript 文件驻留在脚本文件夹; 所有的内容文件驻留在内容文件夹中。对于此示例应用程序,我想将所有的 Angular 视图和相关的 Angular JavaScript 控制器放入相同的目录下。基于 Web 的应用程序会变得非常大,我不想相关功能以整个应用程序的目录结构存储在不同文件夹中。
在示例应用程序,会出现两个 Razor 视图被用到,Index.cshtml 和 _Layout.cshtml 母版页布局,这两个 Razor 视图将用于引导和配置应用程序。应用程序的其余部分将包括 AngularJS 视图和控制器。
对于示例应用程序,我在视图文件夹下创建了两个额外的文件夹,一个客户的子文件夹,一个产品的子文件夹。所有的客户的 Angular 视图和控件器将驻留在客户子文件夹中,所有的产品的 Angular 视图和控件器将驻留在产品子文件夹中 。
由于 Angular 视图是 HTML 文件,而 Angular 控制器是 JavaScript 文件,从 Views 文件夹到浏览器,ASP.NET MVC 必须被配置为允许 HTML 文件和 JavaScript文 件进行访问和传递。这是一个 ASP.NET MVC 默认的约定。幸运的是,你可以通过编辑视图文件下的 web.config 文件并添加一个 HTML 和 JavaScript 的处理器来更改此约定,这将会使这些文件类型能够被送达至浏览器进行解析。
<!-- web.config under the Views folder -->
<system.webserver>
<handlers>
<addname="JavaScriptHandler"path="*.js"verb="*"precondition="integratedMode"
type="System.Web.StaticFileHandler"/> <addname="HtmlScriptHandler"path="*.html"verb="*"precondition="integratedMode"
type="System.Web.StaticFileHandler"/>
</handlers>
</system.webserver>
应用程序版本自动刷新和工程构建
对于此示例应用程序,我想跟踪每一次编译的版本和内部版本号,在属性文件夹下使用 AssemblyInfo.cs 文件的信息测试并发布这个应用。每次应用程序运行的时候,我想获得最新版本的应用程序和使用的版本号,以实现最新的 HTML 文件和 JavaScript 文件生成时,帮助浏览器从缓存中,获取最新的文件来替换那些旧文件。
对于这种应用,我使用的 Visual Studio 2013 专业版,这让一切变得简单,我为 Visual Studio2013 专业版下载了一个自动版本的插件
https://visualstudiogallery.msdn.microsoft.com/dd8c5682-58a4-4c13-a0b4-9eadaba919fe
它会自动刷新 C# 和 VB.NET 项目的版本。将安装插件下载到名为自动版本设置的工具菜单中。该插件自带了配置工具,它允许你配置主要和次要版本号,以便每次编译时,自动的更新 AssemblyInfo.cs 文件。目前,这个插件只是在 Visual Studio 2013 专业版中支持,或者你也可以手动更新版本号或使用类似微软的 TFS 以持续构建和配置管理环境的方式,来管理你的版本号。
下面是一个使用更新的 AssemblyVersion 和 AssemlyFileVersion 号的示例,这个示例在版本编译之后会通过插件自动地进行更新。
// 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")]
使用 Angular 视图和控制器更换联系我们和关于 Razor 视图
要想使用 MVC 工程,首先要做的事情之一就是使用 AngularJS 视图和控制器来更换联系我们和关于 Razor 视图。这是一个很好的起点来测试你的配置是否能够使 AngularJS 正常建立并运行。随后如果不需要这些页面,你可以删除关于和联系我们的视图和控制器。
AngularJS 的这种创建控制器的方式是通过注入 $scope 实现的。示例应用程序的视图和控制器使用“controller as”语法。此语法并非使用控制器中的 $scope,而是简化你的控制器的语法。当你声明一个“controller as”语法的控制器时,你会得到该控制器的一个实例。
使用“controller as”语法,你的所有的连接到控制器(视图模式)的属性必须以你视图的别名作为前缀。在下面的视图代码片段,属性标题前面就加上了“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";
}
}]);
如前所述,在 MVC Razor 视图中使用 AngularJS 视图和控制器的优势之一,就是 Angular 提供了很好的机制来编写高质量的 JavaScript 模块、一种纯 HTML 视图和 JavaScript 控制器之间的完全分离的编码方式。你不再需要使用 AngularJS 双向数据绑定技术来解析浏览器的文件对象模型,这也就使得你能够编写单元测试的 JavaScript 代码。
作为一个注脚,您将在 aboutController 看到一个名为 register.controller 的方法。在本文的后面,你会看到注册方法是从哪儿来的和它用来做什么。
主页索引的 Razor 视图和 MVC 路由
ASP.NET MVC 中集成 AngularJS 的一件有趣的事情,就是应用程序实际上是如何启动和实现路由的。当你启动应用程序时,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
{
publicclass RouteConfig
{
publicstaticvoid 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 Home 主控制器,并执行主控制器中的索引方法。这样会以 MVC 默认工程模板的形式,将 Index.cshtml MVC Razor 视图传递到用户输出的主页面内容中。
这个应用程序的目标是使用 Angular 视图取代所有的 MVC 视图。但问题是,甚至在 AngularJS 被启动之前,主页的 Razor 视图索引就已经被执行和注入了 _Layout.cshtml 主页面中。
自从我决定,将主页面改为 AngularJS 视图,我就使用包含 AngularJS ng-view 标签的 div 标签删除了索引 Razor 视图的所有内容。
<!-- Index.cshtml -->
<divng-view></div>
该 AngularJS ngView 标签是一个指令,能以一种将当前路由的模板渲染成主页面布局的方式补充 $route service。我有两个选择,要么直接嵌入 NG-View 代码到母版页 _Layout.cshtml 或使用 Razor 视图将它注入到母版页。我决定简单地从索引 Razor 视图中注入标签。本质上,索引 Razor 视图在应用程序的引导过程中被简单的使用,并且在应用程序启动后不会被引用。
一旦应用程序被引导并开始启动,AngularJS 将会执行自己的路由系统并以路由表中配置来执行自己的默认路由。基于这一点,我创建了一个单独 AngularJS index.html 和主页的 IndexController.js 文件。
<!-- Index.html -->
<divng-controller="indexController as vm"ng-init="vm.initializeController()">
<h4class="page-header">{{vm.title}}</h4>
</div>
当视图加载时,索引 Angular 视图将会通过 ng-init 指令来执行索引控制器的初始化功能。
// 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 将会执行并尝试去查找一个用于客户路由的 MVC 控制器和视图。将会发生的是,你会获得一个叫做找不到该路由的视图或控制器的错误。
你当然会遇到这个错误,因为/View/Customers/CustomerInquiry的路由是个 Angular 路由,而不是 MVC 路由。MVC 并不知道这个路由。如果你还想直接运行这个页面,则需要解决这一问题,给 MVC 路由表增加另外的路由以便告诉 MVC 将所有的请求路由到 MVC 主控制器,并渲染Razor 视图、通过路由引导这个应用。
由于我有三个视图文件夹,主文件夹、客户文件夹和产品文件夹,我增加了一下的 MVC 路由配置类以便将所有的请求路由到主/索引路由中。当应用程序运行时点击 F5,同样也会进入 MVC 路由表。就 Angular 和单页面如何运行而言,当你点击 F5 时,基本上就是重启了 AngularJS 应用。
有了这些额外的路由,现在就可以直接执行 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
{
publicclass RouteConfig
{
publicstaticvoid 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 和动态加载控制器
当示例应用程序启动时,该应用程序将会预加载应用程序的核心控制器和服务。这包括 Home 目录中的所有控制器和应用程序的共享服务。
此应用程序的共享服务,将在所有模块中执行- 包括一个 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 服务器在配置阶段之后,动态地加载控制器。Angular 使用 $controllerProvider 服务来创建新的控制器。这种方法允许通过注册方法来实现控制器注册。
// aboutController.js
angular.module("codeProject").register.controller('aboutController',
['$routeParams', '$location', function ($routeParams, $location) {
"use strict";
var vm = this;
this.initializeController = function () {
vm.title = "About";
}
}]);
上述有关控制器被修改为执行 $controllerProvider 的寄存器方法。为了使这种注册方法有效,必须在配置阶段配置这种注册。下面的代码片段在应用程序启动之后,使用了 $controllerProvider 来使注册方法有效。在下面的例子中,提供了一种用于注册和动态加载两个控制器和服务的注册方法。如果你愿意,也可以包括 Angular 全部库和指令的注册功能。
// 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 MVC 中集成 AngularJS 的第一部分内容,后续内容会在本系列的后两篇文章中呈现,敬请期待!
文章来源:By Mark J. Caplin
原文链接:http://www.codeproject.com/Articles/1033076/Integrating-AngularJS-with-ASP-NET-MVC