使用 MVC 6、Web API 2、ASPNET 5 和 Gulp 的 Angular JS 应用程序 – 第 1 部分






4.88/5 (51投票s)
使用 MVC 6 中的 Web API 构建 Angular JS 应用程序。
引言
在本文中,我将解释如何为 Angular JS 中的单页应用程序 (SPA) 设置 ASP.NET 5 项目。我已使用 MVC 6 Web API 显示一些静态数据,最终我将使用数据库通过 Web API 存储和显示数据。我还会展示如何使用 gulp、bower 和 npm 自动复制前端 JavaScript 文件。在后续模块中,我将描述如何合并和最小化前端 JavaScript 文件。
在我的演示中,我使用的是 Visual Studio 2015 RC,这是用于跨平台开发的下一代 Visual Studio 的最新预发布版本。我还会使用 Entity Framework 7 和 DNX (.NET 执行环境) 来迁移数据库并在不同的环境中运行网站。
背景
由于近年来前端工具和技术的进步,单页应用程序 (SPA) 现在已是众所周知的术语。其中,Angular JS 是构建 SPA 最受欢迎的选择。微软最近发布的 Visual Studio 2015 RC 是开发 SPA 的绝佳平台,可以在其中轻松管理和维护应用程序的前端工具。此外,MVC 6 中的 Web API 2 被用作后端服务,可以高效地支持从前端调用它。
我将全程使用 Visual Studio 2015 RC、MVC 6 Web API2、gulp、bower 和 npm 来进行前端和后端开发。
创建项目
首先,我打开了 Visual Studio 2015 RC,从开始页面中,我选择了新建项目,然后从可用模板中,我选择了 ASP.NET Web 应用程序。我将项目命名为 BooksAngularApp。

点击确定后,会出现模板选择向导,我将从可用的 ASP.NET 5 预览模板中选择网站。我选择此模板的原因是它为我们提供了一个有用的框架,而不是从空模板开始。
需要注意的是,网站默认带有 gulp 任务运行器,我将使用它来合并和最小化前端 JavaScript 和 CSS 文件。

点击确定后,我创建了以下项目,项目结构和预览页面如下所示

任务运行器
如果我们查看解决方案资源管理器中的项目结构,可以看到 bower 文件夹中预安装的包,例如 bootstrap、jquery、hammer 等。此外,我们还可以看到 npm 文件夹,其中已安装 gulp 和 rimraf 作为前端工具,用于管理前端脚本,特别是 JavaScript 和 CSS 文件。

现在让我们看一下 gulpfile.js,它将用于管理我们的前端代码或脚本。
var gulp = require("gulp"),
  rimraf = require("rimraf"),
  fs = require("fs");
eval("var project = " + fs.readFileSync("./project.json"));
var paths = {
  bower: "./bower_components/",
  lib: "./" + project.webroot + "/lib/"
};
gulp.task("clean", function (cb) {
  rimraf(paths.lib, cb);
});
gulp.task("copy", ["clean"], function () {
  var bower = {
    "bootstrap": "bootstrap/dist/**/*.{js,map,css,ttf,svg,woff,eot}",
    "bootstrap-touch-carousel": "bootstrap-touch-carousel/dist/**/*.{js,css}",
    "hammer.js": "hammer.js/hammer*.{js,map}",
    "jquery": "jquery/jquery*.{js,map}",
    "jquery-validation": "jquery-validation/jquery.validate.js",
    "jquery-validation-unobtrusive": 
            "jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"
  }
  for (var destinationDir in bower) {
    gulp.src(paths.bower + bower[destinationDir])
      .pipe(gulp.dest(paths.lib + destinationDir));
  }
});
我们可以看到脚本运行一些任务,在开发过程中文件发生任何更改时,可以复制和清理 wwwroot 文件夹中的 JavaScript 和 CSS 文件。
让我们看看这些是如何发生的。我从 视图->其他窗口->任务运行器 中打开了任务运行器。

我们可以看到 gulpfile.js 中定义的 clean 和 copy 任务。在我们运行它们中的任何一个之前,我们可以看到一个 lib 文件夹默认包含所有 JavaScript 和 CSS 文件的副本。
我运行了 clean 任务,它清除了 wwwroot 目录中的 lib 文件夹,这意味着 gulp 被用于运行 clean 任务以删除静态文件。

我运行了 copy 任务,lib 文件夹又回到了复制的文件。

安装 Angular 包
对于我们的 SPA,我将使用三个 Angular 包:angular、angular-route 和 angular-resource。目前,默认的 bower.json 文件如下所示
>
{
  "name": "ASP.NET",
  "private": true,
  "dependencies": {
    "bootstrap": "3.0.0",
    "jquery": "1.10.2",
    "jquery-validation": "1.11.1",
    "jquery-validation-unobtrusive": "3.2.2",
    "hammer.js": "2.0.4",
    "bootstrap-touch-carousel": "0.8.0"
  }
}
我已添加以下三个 angular 包
{
  "name": "ASP.NET",
  "private": true,
  "dependencies": {
    "bootstrap": "3.0.0",
    "jquery": "1.10.2",
    "jquery-validation": "1.11.1",
    "jquery-validation-unobtrusive": "3.2.2",
    "hammer.js": "2.0.4",
    "bootstrap-touch-carousel": "0.8.0",
    "angular": "*",
    "angular-route": "*",
    "angular-resource": "*"
  }
}
添加三行 angular 后,我尚未保存 bower.json 文件,并且在解决方案资源管理器的 Bower 文件夹中,没有 angular 模块。

保存文件后,几秒钟后我们可以在 bower 文件夹的依赖项列表中看到 angular 组件。

您可能会看到 bower 文件夹如下所示,其中未安装 angular 模块

如果它们未安装,请右键单击 Bower 文件夹并点击恢复包,它将恢复添加到 bower.json 文件中的所有未安装包。

为 Angular 脚本创建 Gulp 任务
现在我已经安装了 angular 包,我们需要更新任务运行器,以便将 angular 文件与其他 bower 组件一起复制到 lib 文件夹。
我已更新 copy 任务,如下所示
gulp.task("copy", ["clean"], function () {
  var bower = {
    "bootstrap": "bootstrap/dist/**/*.{js,map,css,ttf,svg,woff,eot}",
    "bootstrap-touch-carousel": "bootstrap-touch-carousel/dist/**/*.{js,css}",
    "hammer.js": "hammer.js/hammer*.{js,map}",
    "jquery": "jquery/jquery*.{js,map}",
    "jquery-validation": "jquery-validation/jquery.validate.js",
    "jquery-validation-unobtrusive": 
            "jquery-validation-unobtrusive/jquery.validate.unobtrusive.js",
    "angular": "angular/angular*.{js,map}",
    "angular-route": "angular-route/angular-route*.{js,map}",
    "angular-resource": "angular-resource/angular-resource*.{js,map}"
  }
现在我再次运行 copy 任务,可以看到 angular 文件已复制到 wwwroot 的 lib 文件夹中。

我已配置 copy 和 copyapp 任务在应用程序的每次构建后运行,以确保更改被复制。

现在我们已准备好开始使用 SPA BooksAngularApp。
创建数据模型和 Web API
对于 SPA,我将使用一个虚构的书店,我们可以在其中查看书籍列表及其详细信息。在此模块中,我将使用一些静态数据作为书籍项目。为此,我需要一个数据模型,用于访问和显示书籍。
我在项目 Models 文件夹中添加了一个名为 Book 的新类,模型如下所示
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.ComponentModel.DataAnnotations;
namespace BooksAngularApp.Models
{
   /// <summary>
   /// The entity class with Book properties
   /// </summary>
    public class Book
    {
        [Key]
        public int Id { get; set; }
        public string Title { get; set; }
        public string Author { get; set; }
        public string Type { get; set; }
        public DateTime DatePublished { get; set; }
        public decimal Price { get; set; }
    }
}
之后,我创建了一个名为 BooksController 的 MVC 控制器类,它将作为我们数据访问的 API 控制器。

默认情况下,Controller 类如下所示

我已将该类更改为 API 控制器,通过添加...
 [Route("api/[controller]")]
...在类定义之前,这样通过 Route 属性,它将充当 API 控制器。
我已添加 HttpGet 方法以显示静态书籍列表,如下所示
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc;
using BooksAngularApp.Models;
namespace BooksAngularApp.Controllers
{
    [Route("api/[controller]")]
    public class BooksController : Controller
    {
        [HttpGet]
        public IEnumerable<Book> Get()
        {
            return new List<Book>
                {
                    new Book {Id=1, Title="Wonders of the Sky", 
                    Author="Martin James", DatePublished=Convert.ToDateTime("1/1/2013"), 
                    Type="Science",Price=23.23m },
                    new Book {Id=2, Title="Secrets of the Mind ", 
                    Author="Allan Sue ", DatePublished=Convert.ToDateTime("2/1/2011"), 
                    Type="Psychology", Price=12.50m },
                    new Book {Id=3, Title="We are Alive", 
                    Author="Dick Smith", DatePublished=Convert.ToDateTime("2/11/2010"), 
                    Type="Science Fiction", Price=21.25m } ,
                    new Book {Id=4, Title="Last day of the world", 
                    Author="Martin James", DatePublished=Convert.ToDateTime("1/1/2013"), 
                    Type="History", Price=10.40m },
                };
        }
    }
}

为了查看我创建的 API 控制器是否正常工作,让我们从浏览器运行 API

我们可以看到数据以 JSON 格式从 API 控制器显示,这意味着我们已准备好使用 API 来显示数据。
编写 Angular 代码
我在应用程序根目录中创建了一个 app 文件夹,我将在此处放置所有与 Angular 相关的模块,例如控制器、服务等。
我添加了 app.js 作为第一个模块,如下所示
(function () {
    'use strict';
    angular.module('booksApp', ['booksServices']);
})();
booksApp Angular 模块将依赖于名为 booksServices 的服务,我将立即定义它。
我在 app 文件夹中创建了一个名为 services 的新文件夹,并添加了 booksServices.js,如下所示
(function () {
    'use strict';
    var booksServices = angular.module('booksServices', ['ngResource']);
    booksServices.factory('Books', ['$resource',
      function ($resource) {
          return $resource('/api/books/', {}, {
              query: { method: 'GET', params: {}, isArray: true }
          });
      }]);
})();
该服务将注入 ngResource 作为依赖项,它最终将调用 API 控制器以从 API 获取数据。
现在我们需要另一个 Angular 模块,我们将其命名为 booksController,我已在 app 文件夹中的 controllers 文件夹中创建,命名为 booksController.js。
(function () {
    'use strict';
    angular
    .module('booksApp')
    .controller('booksController',booksController)
    booksController.$inject = ['$scope', 'Books'];
    function booksController($scope, Books)
    {
        $scope.books = Books.query();
    }
})();
在这里我们可以看到,控制器注入了 scope 对象,该对象返回 API 的查询结果。
为应用程序脚本编写任务
我们需要编写任务,将文件复制到 wwwroot 文件夹,就像我们之前对其他 JavaScript 文件所做的那样。
var paths = {
  bower: "./bower_components/",
  lib: "./" + project.webroot + "/lib/",
  app: "./" + project.webroot + "/app/",
  srcapp:  "./app/",
};
gulp.task("cleanappp", function (cb) {
    rimraf(paths.app, cb);
});
gulp.task("copyapp",["cleanappp"], function () {
    var app = {
        "controllers": "controllers/booksController.js",
        "services": "services/booksServices*.js",
        "/": "app.js"
    }
    for (var destinationDir in app) {
        gulp.src(paths.srcapp + app[destinationDir])
          .pipe(gulp.dest(paths.app + destinationDir));
    }
});
我已在 paths 变量中添加了 app 和 srcapp,以定义应用程序脚本的源和目标。我添加了一个名为 cleanapp 的新任务,它将用于清理 wwwroot 中的 app 文件夹。最后,我添加了另一个名为 copyapp 的任务,它注入了 cleanapp 任务,这意味着在复制应用程序脚本之前,它将首先清理文件夹,然后复制所有脚本文件。
<!DOCTYPE html>
<html ng-app="booksApp">
<head>
    <meta charset="utf-8" />
    <title>List of Books</title>
    <link href="lib/bootstrap/css/bootstrap.css" rel="stylesheet" />
    <link href="lib/bootstrap/css/bootstrap-theme.css" rel="stylesheet" />
    <script src="lib/jquery/jquery.min.js"></script>
    <script src="lib/angular/angular.js"></script>
    <script src="lib/bootstrap/js/bootstrap.js"></script>
    <script src="lib/angular-resource/angular-resource.js"></script>
    <script src="app/app.js"></script>
    <script src="app/controllers/booksController.js"></script>
    <script src="app/services/booksServices.js"></script>
</head>
<body ng-cloak>
    <div ng-controller="booksController">
        <table class="table table-bordered table-striped">
            <thead>
                <tr>
                    <th>Title</th>
                    <th>Author</th>
                    <th>Type</th>
                    <th>Date Published</th>
                    <th>Price</th>
                </tr>
            </thead>
            <tbody>
                <tr ng-repeat="book in books">
                    <td>{{book.Title}}</td>
                    <td>{{book.Author}}</td>
                    <td>{{book.Type}}</td>
                    <td>{{book.DatePublished|date:"dd/MM/yyyy"}}</td>
                    <td>${{book.Price}}</td>
                </tr>
            </tbody>
        </table>
    </div>
</body>
</html>


在这里,网站同时运行 IIS Express 和 WebListener。

历史
- 2015 年 5 月 16 日:首次提交
- 2015 年 5 月 24 日:向文章添加了 ZIP 解决方案
源代码
本文的源代码可在 GitHub 上获取:https://github.com/mostafaasad/ASPNET5/tree/master/BooksAngular-Part1/BooksAngularApp
下一篇
在下一个模块中,我将探讨数据的搜索和过滤,以及使用 Entity Framework 7 和 DNX 进行数据库迁移。


