Angular--NET-开发学习手册-全-

Angular .NET 开发学习手册(全)

原文:zh.annas-archive.org/md5/1D7CD4769EDA3E96BB350F0A5265564A

译者:飞龙

协议:CC BY-NC-SA 4.0

第一章:使用 Angular 入门

如果您正在阅读本书,那是因为您是.NET 开发人员,想了解如何将 Angular 与.NET Framework 技术一起使用,如 ASP.NET Model View Controller(MVC)和 Web API,以及诸如 Web Forms 和 Web Services 之类的传统技术。它使开发人员能够开发由 Angular 驱动的更丰富和动态的.NET Web 应用程序。Angular 是一个帮助创建动态 Web 应用程序的开源 JavaScript 框架。

在本章中,我们将涵盖以下主题:

  • 介绍 Angular

  • Angular 架构

  • 使用 Angular 构建一个 Hello World 应用程序

介绍 Angular


在向您介绍 Angular 之前,让我们讨论一下 AngularJS 的历史。一切都始于改进客户端 Web 开发过程。作为改进的一部分,微软引入了 XML HTTP 请求对象以从服务器检索数据。随着引入了像 jQuery 和 Prototype 这样先进的 JavaScript 库,开发人员开始使用 Ajax 从服务器异步请求数据。这些库被广泛用于操作 DOM 并绑定数据到 UI,直到 90 年代末。

Ajax 是异步 JavaScript 和 XML 的缩写。Ajax 可以使 Web 应用程序在不干扰页面显示和行为的情况下异步发送数据到服务器或从服务器检索数据。Ajax 允许 Web 应用程序动态更改内容,而无需重新加载整个页面,通过将数据交换层与表现层解耦来实现。

2010 年底,引入了两个 JavaScript MVC 框架:Backbone 和 Knockout。Backbone 提供了完整的模型-视图-控制器(MVC)体验,而 Knockout 主要侧重于使用 MVVM 模式进行绑定。随着这些框架的发布,人们开始相信客户端 MVC 框架的威力。

AngularJS 的诞生

来自 Google 的开发人员认为市场上存在的客户端 MVC 框架中有一个主要的缺失部分,即可测试性。他感觉有更好的方法来实现客户端 MVC,这让他开始了构建 Angular 的旅程。

Google 支持了 Angular 项目,看到了它的潜力,并且使其开源供世界免费使用。Angular 在市场中的所有 MVC 框架之间引起了很大的关注,因为它得到了 Google 的支持,并且具有诸如可测试性和指令等特性。如今,Angular 团队已经从单个开发人员发展到了大量开发人员,并且已经成为在小型、中型或大型 Web 应用程序中添加客户端 MVC 功能的首选。

为什么选择 AngularJS?

让我们讨论为什么使用 AngularJS 以及通过使用 AngularJS 我们的应用程序可以获得什么好处或增值:

  • AngularJS 提供双向绑定:许多客户端 MVC 框架只提供单向绑定。这意味着其他 MVC 框架只会使用来自服务器的模型来更新 HTML,当用户在页面上更改模型时,框架不会根据所做的更改更新模型。开发人员需要编写代码来根据用户操作更新模型。然而,AngularJS 方便了双向绑定,并通过根据用户在其上的操作更新模型使开发人员的生活更轻松。

  • AngularJS 利用声明性视图:这意味着功能将以 HTML 中的声明性指令的形式进行通信,以渲染模型并与 DOM 交互,根据模型的改变改变页面状态。这大大减少了用于此目的的代码量,将其减少了约 50%至 75%,并简化了开发人员的工作。

  • AngularJS 支持指令概念:这就像为 Web 应用程序编写一个特定领域的语言。指令将扩展 HTML 的功能,并根据应用程序的变化动态渲染它们,而不仅仅是显示 HTML 页面。

  • AngularJS 非常易于测试:如前所述,Angular 开发的主要目标之一是引入可测试的客户端 MVC 框架。AngularJS 非常易于测试,事实上,Angular 团队已经推出了两个框架:Karma 和 Protractor,用于编写端到端单元测试,以确保代码的稳定性,并确保自信地重构代码。

Angular 2

AngularJS 是一个很好的框架。然而,它已经有六年的历史了,在这六年里,Web 世界发生了很多变化。为了适应 AngularJS 中所有这些现代发展,它将不得不在现有的实现中进行许多改变,这使得 Angular 团队从头开始编写 AngularJS。

在 2014 年 10 月举行的 ngEurope 大会上,宣布了 Angular 2 作为构建复杂 Web 应用的 Angular 1 的重大更新。ngCommunity 有点不满,因为他们在学习和实施 Angular 1 上投入了很多时间,而现在他们又不得不重新学习和实施 Angular。然而,谷歌在从 Angular 1 升级到 2 的迁移和升级过程中投入了大量精力,引入了 ngUpgrade 和 ngForward。一旦开发人员开始学习并使用 Angular 2 构建产品,他们就意识到了更清洁、更快速和更容易的 Angular 2 的威力。

Angular 2 是从零开始重写的。它帮助我们编写干净的、可测试的代码,可以在任何设备和平台上运行。Angular 2 消除了 Angular 1 中的许多概念。Angular 2 遵循了 ECMAScript 2015 的标准化。随着最近的 Web 标准化,影子 DOM 取代了传递和 ECMAScript 6 模块取代了 Angular 模块。Angular 2 比 Angular 1.x 快五倍。

Angular 2 的优势

以下是 Angular 2 的特性和优势:

  • 它支持跨平台应用程序开发,比如高性能应用程序,如使用 Ionic Framework、NativeScript、React Native 创建本机应用程序,并通过使用 Angular 方法访问本机操作系统 API 创建桌面安装应用程序。

  • Angular 2 继承了 Angular 1 的所有优点。它用组件取代了控制器和指令。

  • Angular 2 是用 TypeScript 编写的,也让开发人员能够使用 TypeScript 编写 Angular 2 应用程序。

  • Angular 2 比 Angular 1 明显快得多。新的组件路由器只会加载渲染所请求的视图所需的代码。模板语法使开发人员能够快速创建具有强大模板语法的视图。

  • Angular 2 使我们能够使用阴影 DOM(Document Object Model)。阴影 DOM 封装了 CSS、模板和组件。这样就可以与主文档的 DOM 分离。

  • 这是更简单的认知模型。Angular 2 中删除了许多指令,这意味着 Angular 2 的部件更少,移动部件也更少,因此使用 Angular 2 比使用 Angular 1 更容易构建更大的应用程序。

Angular 2 中的开发流程

Angular 2 有两个开发过程,即以下内容:

  • 使用转译器

  • 没有转译器

什么是 ECMAScript 6?

ES6 是脚本语言规范的最新版本。它是世界范围内用于客户端脚本的 JavaScript 语言。ECMAScript 6 是 JavaScript 语言的一个伟大更新,这些特性正在 JavaScript 引擎中的实现过程中。

什么是转译器?

转译器基本上将任何特定语言转换为 JavaScript。一个很好的例子就是 Typescript 转译器,它将 Typescript 代码转换为 JavaScript。

什么是 TypeScript?

TypeScript 是由微软开发的开源编程语言。它是 JavaScript 的超集,它使程序员能够用 JavaScript 编写面向对象的程序。 TypeScript 还用于开发转译器,将 TypeScript 转换为 JavaScript。它旨在开发更大型的应用程序。 TypeScript 是根据 ECMAScript 标准的提案开发的。 TypeScript 具有类、模块和箭头函数语法等功能,这些功能是 ECMAScript 6 标准中提出的。

JavaScript 的开发流程

在讨论使用转译器的开发过程之前,让我们看看特定于 JavaScript 构建 Web 应用程序的开发过程。我们将在ECMAScript 5中编写我们的代码并部署服务器上。 ECMAScript 5 是今天每个浏览器都理解的脚本。当浏览器发出请求时,服务器将提供脚本,浏览器将在客户端运行它。下面的图表显示了 JavaScript 的典型开发流程::

JavaScript 的开发流程

带有构建时转译器的开发

我们不仅可以使用当前版本的 JavaScript(ECMAScript 5)编写脚本,还可以使用 Typecript 编写 ECMAScript 6+ 的脚本并将其 转译ECMAScript 5。然后,将转译后的脚本 部署服务器,然后 浏览器请求 将提供要在客户端执行的 转译后的脚本 ,即 ECMAScript 5。这样做的好处是我们可以使用最新版本的 JavaScript 或 ECMAScript 的新功能。

使用构建时转译器的开发过程

使用运行时转译器进行开发

还有一种开发选项称为运行时转译器。在这种情况下,我们首先使用 Typecript 或 CoffeeScript 在 ECMAScript 6+ 中编写脚本,然后 部署服务器。当 请求 到达 服务器 时,它简单地提供在 浏览器 中不经转译的 ECMAScript 6+ 代码。然后,浏览器使用运行时转译器将脚本转译为 ECMAScript 5 在客户端执行。这种类型的选项对于生产应用程序不好,因为它会给浏览器增加额外的负载。

使用运行时转译器的开发过程

转译器选项

在 Angular 2 中,我们有两种选项 - 使用转译器或不使用转译器。以下是一些可用的转译器类型:

  • Traceur:这是谷歌公司最受欢迎的转译器,可以在构建时模式和运行时模式下使用。

  • Babel:这个转译器适用于最新版本的 ECMAScript。

  • TypeScript:这是 Angular 中最受欢迎和首选的转译器之一。Angular 团队与 TypeScript 团队合作,他们一起合作构建了 Angular 2。

Angular 3 发生了什么?

在发布 Angular 2 后,团队决定采用语义版本控制。语义版本控制遵循三数版本控制,表示主要、次要和补丁。补丁版本是版本中的最后一个数字,通常用于修复 bug。次要版本是版本中的中间数字,处理新功能或增强的发布。最后,主要版本是版本中的第一个数字,用于具有重大更改的发布。

Angular 团队从 Angular 2 使用的 TypeScript 1.8 切换到了 TypeScript 2.2。这带来了一些重大变化,很明显需要增加主要版本号。此外,当前路由模块的版本是 3.3.0,与其他仍在 2.3.0 版本的 Angular 模块不一致。因此,为了使所有模块版本保持同步并遵循语义版本控制,Angular 团队决定在下一个主要发布中使用 Angular 而不是 Angular 3。

Angular 中的新功能是什么?

以下是 Angular 中的新功能:

  • Angular 需要的脚本语言是 TyepScript 2.1+。

  • 预编译模式使得 Angular 在构建过程中编译模板并生成 JavaScript 代码。这有助于我们在构建时识别模板中的错误,而不是在运行时。

  • Angular 动画有着自己的包,这意味着你不需要为那些不需要动画的项目提供动画包。

  • 模板标签现在已经被弃用,因为它会与 Web 组件中使用的模板 HTML 标签引起混淆。所以,ng-template 被引入作为 Angular 中的模板。

除此之外,还有新功能在代码级别上被引入。

为何对于.NET 开发者来说 Angular 是个好选择?

在.NET Web 应用程序中使用 JavaScript 编写客户端代码的复杂性不断增加,比如数据绑定、服务器调用和验证。.NET 开发人员在使用 JavaScript 编写客户端验证时遇到了困难。所以,他们发现并开始使用 jQuery 插件来进行验证,并大多仅仅用来根据用户动作改变视图。在后来阶段,.NET 开发人员得到了能确保代码结构并提供良好功能以简化客户端代码的 JavaScript 库的照顾。然后,他们最终使用了一些市场上的客户端 MVC 框架。然而,他们只是用 MVC 框架来与服务器通信并更新视图。

后来,SPA单页应用)的趋势在 Web 开发场景中出现。这种类型的应用将会用一个初始页面提供服务,可能是在布局视图或主视图中。然后,其他视图将在请求时加载到主视图上。这种情景通过实现客户端路由来实现,这样客户端将从服务器请求视图的一小部分而不是整个视图。这些步骤的实现增加了客户端开发的复杂性。

AngularJS 为.NET 开发者带来了福音,使他们能够减少处理应用程序的客户端开发所需的工作,比如 SPA 等。数据绑定是 Angular 中最酷的功能,它使开发人员能够集中精力处理应用程序的其他部分,而不是编写大量代码来处理数据绑定、遍历、操作和监听 DOM。Angular 中的模板只是简单的纯 HTML 字符串,将被浏览器解析为 DOM;Angular 编译器遍历 DOM 以进行数据绑定和渲染指令。Angular 使我们能够创建自定义 HTML 标签并扩展 DOM 中现有元素的行为。通过内建的依赖注入支持,Angular 通过提供它们的实例来解析依赖参数。

用 Angular 构建一个 Hello World 应用


在我们开始构建我们的第一个 Angular 应用之前,让我们设置开发环境来开始使用 Angular 应用。

设置开发环境

在编写任何代码之前要做的第一件事是设置本地开发环境。我们需要一个编辑器来编写代码,一个本地服务器来运行应用程序,包管理工具来管理外部库,编译器来编译代码等等。

安装 Visual Studio Code

Visual Studio Code 是用于编写 Angular 应用程序的最佳编辑器之一。因此,我们首先安装 Visual Studio Code。前往code.visualstudio.com/,然后点击Download Code for Windows。Visual Studio Code 支持 Windows、Linux 和 OS X 等平台。因此,根据您的需求也可以在其他平台上下载它。

Visual Studio Code 的首页

Visual Studio Code 是一款开源的跨平台编辑器,支持 Windows、Linux 和 OS X。它是一个功能强大的文本编辑器,包括诸如导航、可自定义绑定的键盘支持、语法高亮、括号匹配、自动缩进和片段等功能,支持许多编程语言。它具有内置的 IntelliSense 代码补全、更丰富的语义代码理解和导航、代码重构支持。它提供了简化的、集成的调试体验,支持 Node.js 调试。它是 Visual Studio 的一个轻量级版本。它不包含任何内置的开发服务器,如 IIS Express。但是,在开发过程中,测试 Web 应用程序在本地 Web 服务器中非常重要。市场上有几种可用的方法来设置本地 Web 服务器。

但是,我选择了 lite-server,因为它是一个轻量级的仅用于开发的 Node 服务器,用于提供静态内容,检测更改,刷新浏览器,并提供许多自定义选项。Lite-server 作为 Node.js 的 NPM 包可用。首先,我们将在下一节看如何安装 Node.js。

安装 Node.js

Node.js 用于开发服务器端 Web 应用程序。它是一个开源的跨平台运行时环境。Node.js 中的内置库允许应用程序充当独立的 Web 服务器。Node.js 可用于需要轻量级实时响应的场景,例如通讯应用程序和基于 Web 的游戏。

Node.js 可用于多种平台,如 Windows、Linux、Mac OS X、Sun OS 和 ARM。您还可以下载 Node.js 的源代码,并根据您的需求进行定制。

要安装 Node.js,请前往nodejs.org/en/,并下载适用于 Windows 的成熟可靠的 LTS(长期支持)版本。

Node.js 的首页

Node.js 带有 NPM,一个用于获取和管理 JavaScript 库的软件包管理器。要验证 Node.js 和 NPM 的安装是否成功,请按照以下步骤进行检查:

  1. 打开 Windows 命令提示符,输入node -v命令并运行。您将得到我们安装的 Node.js 的版本。

  2. 现在,检查 NPM 是否与 Node.js 一起安装。运行NPM -v命令,您将得到已安装的 NPM 的版本号。

使用命令验证 Node.js 和 NPM 安装的命令提示符

现在,我们拥有了写我们的第一个 Angular 应用程序所需的一切。让我们开始吧。

创建一个 Angular 应用程序

我假设您已经安装了 Node.js、NPM 和 Visual Studio Code,并准备好用它们进行开发。现在,让我们按照以下步骤通过克隆 git 存储库创建一个 Angular 应用程序:

  1. 打开 Node.Js 命令提示符并执行以下命令:
      git clone https://github.com/angular/quickstart my-
      angular

这个命令将克隆 Angular 快速起步存储库,并为你创建一个名为my-angular的 Angular 应用程序,其中包含所需的所有样板代码。

  1. 使用 Visual Studio Code 打开克隆的my-angular应用程序:

my-angular 应用程序的文件夹结构

文件夹结构和样板代码按照angular.io/docs/ts/latest/guide/style-guide.html上的官方样式指南进行组织。src文件夹中包含与应用程序逻辑相关的代码文件,e2e文件夹中包含与端到端测试相关的文件。现在不要担心应用程序中的其他文件。现在让我们只关注package.json

  1. 点击package.json文件;它将包含有关元数据和项目依赖项配置的详细信息。以下是package.json文件的内容:
      {
      "name":"angular-quickstart",
      "version":"1.0.0",
      "description":"QuickStart package.json from the 
      documentation,             
      supplemented with testing support",
      "scripts":{
      "build":"tsc -p src/",
      "build:watch":"tsc -p src/ -w",
      "build:e2e":"tsc -p e2e/",
      "serve":"lite-server -c=bs-config.json",
      "serve:e2e":"lite-server -c=bs-config.e2e.json",
      "prestart":"npm run build",
      "start":"concurrently \"npm run build:watch\" \"npm 
      run serve\"",
      "pree2e":"npm run build:e2e",
      "e2e":"concurrently \"npm run serve:e2e\" \"npm run 
      protractor\"             
      --kill-others --success first",
      "preprotractor":"webdriver-manager update",
      "protractor":"protractor protractor.config.js",
      "pretest":"npm run build",
      "test":"concurrently \"npm run build:watch\" \"karma 
      start             
      karma.conf.js\"",
      "pretest:once":"npm run build",
      "test:once":"karma start karma.conf.js --single-
      run",
      "lint":"tslint ./src/**/*.ts -t verbose"
      },
      "keywords":[
      ],
      "author":"",
      "license":"MIT",
      "dependencies":{
      "@angular/common":"~4.0.0",
      "@angular/compiler":"~4.0.0",
      "@angular/core":"~4.0.0",
      "@angular/forms":"~4.0.0",
      "@angular/http":"~4.0.0",
      "@angular/platform-browser":"~4.0.0",
      "@angular/platform-browser-dynamic":"~4.0.0",
      "@angular/router":"~4.0.0",
      "angular-in-memory-web-api":"~0.3.0",
      "systemjs":"0.19.40",
      "core-js":"².4.1",
      "rxjs":"5.0.1",
      "zone.js":"⁰.8.4"
      },
      "devDependencies":{
      "concurrently":"³.2.0",
      "lite-server":"².2.2",
      "typescript":"~2.1.0",
      "canonical-path":"0.0.2",
      "tslint":"³.15.1",
      "lodash":"⁴.16.4",
      "jasmine-core":"~2.4.1",
      "karma":"¹.3.0",
      "karma-chrome-launcher":"².0.0",
      "karma-cli":"¹.0.1",
      "karma-jasmine":"¹.0.2",
      "karma-jasmine-html-reporter":"⁰.2.2",
      "protractor":"~4.0.14",
      "rimraf":"².5.4",
      "@types/node":"⁶.0.46",
      "@types/jasmine":"2.5.36"
      },
      "repository":{
      }
      }
  1. 现在,我们需要在命令窗口中运行 NPM install 命令,通过导航到应用程序文件夹来安装package.json中指定的必需依赖项:

执行 NPM 命令来安装 package.json 中指定的依赖项

  1. 现在,您将会在node_modules文件夹下添加所有的依赖项,如下图所示:

node_modules文件夹下的依赖项

  1. 现在,让我们运行这个应用程序。要运行它,在命令窗口中执行以下命令:
 npm start
  1. 打开任何浏览器,并导航到http://localhost:3000/;您将会在应用程序中看到以下页面。运行这个命令会构建应用程序,启动 lite-server,并在上面托管应用程序。

在 VS Code 中激活调试窗口

现在让我们详细看一下index.html的内容。以下是index.html的内容:

<!DOCTYPE html>
<html>
<head>
<title>Hello Angular </title>
<base href="/">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="styles.css">
<!-- Polyfill(s) for older browsers -->
<script src="img/shim.min.js"></script>
<script src="img/zone.js"></script>
<script src="img/system.src.js"></script>
<script src="img/systemjs.config.js"></script>
<script>
System.import('main.js').catch(function(err){ console.error(err); });
</script>
</head>
<body>
<my-app>My first Angular app for Packt Publishing...</my-app>
</body>
</html>

到目前为止,我们已经看到了如何通过克隆 GitHub 上的官方 QuickStart 存储库来创建 Angular 应用程序。我们将在接下来的章节详细介绍创建 Angular 应用程序的步骤。请注意,脚本是使用 System.js 加载的。System.js 是在运行时加载模块的模块加载器。

Angular 的架构


在我们跳转到 Angular 上的 Hello World 应用程序之前,请让我快速介绍一下 Angular 的架构。Angular 的架构由八个核心构建块组成:模块,组件,模板,元数据,数据绑定,服务,指令和依赖注入。

Angular 的架构

一个 Angular 应用程序通常是从使用 Angular 标签或标记设计模板开始。然后,我们编写组件来处理模板。应用程序特定的逻辑将添加到服务中。最后,起始组件或根组件将传递给 Angular 启动器。

当我们运行应用程序时,Angular 负责向浏览器呈现模板,并根据组件和指令中提供的逻辑处理模板中元素的用户交互。

让我们看看 Angular 的每个模块的目标:

  • 任何 Angular 应用程序都将由一组组件组成。

  • 服务将被注入组件中。

  • 模板负责以 HTML 形式呈现组件。

  • 组件包含支持视图或模板的应用程序逻辑。

  • Angular 本身是一组模块。在 Angular 1 中,使用ng-app指令引导主模块或应用程序模块。我们可以包含我们的应用程序模块或主模块依赖的其他模块列表;它们将在angular.module('myApp', [])中定义为空数组。Angular 使用 ES6 模块,模块中定义的函数或变量应显式导出以供其他模块消费。通过使用 import 关键字,导出的函数或变量可在其他模块中使用,后跟函数名,然后跟随模块名。例如,import {http} from @angular/http

  • 每个 Angular 库实际上是许多相关的私有模块的外观。

  • 指令提供指令以呈现模板。

我们将在接下来的章节中详细介绍 Angular 架构的每个构建块。

总结


很简单,不是吗?我们刚刚向您介绍了 Angular 框架。

我们从 AngularJS 的历史开始。然后,我们讨论了 AngularJS 的优点和 AngularJS 的诞生。我们讨论了 Angular 的新功能,并对 Angular 的架构进行了简要介绍。

我们还看到了编写 Angular 应用程序所需的开发环境设置。

最后,我们演示了如何使用 Visual Studio Code 和 Node.js 创建你的第一个 Angular 应用程序。

这一章节我们有了一个很好的开端,在学习了一些基础知识。然而,这只是开始。在下一章中,我们将讨论 Angular 架构的一些核心构建模块,比如模块、组件、模板和指令。让我们开始吧!

第二章:Angular 构建模块 - 第一部分

本章将详细介绍 Angular 架构的核心构建模块。

在本章中,我们将涵盖以下主题:

  • 模块

  • 组件

  • 装饰器和元数据

  • 模板

  • 绑定

  • 指令

  • 依赖注入

模块(NgModules)


模块是实现不同功能的单个实现单元。通过多个模块的集合来实现复杂的应用程序。实现模块模式有助于避免变量和方法的全局冲突。JavaScript 通过实现模块模式将私有方法和公共方法封装在单个对象中。模块模式在 JavaScript 中使用闭包来实现封装。JavaScript 不支持访问修饰符;然而,使用函数作用域可以实现相同的效果。所有的 Angular 应用都是模块化的。我们通过创建许多模块来开发 Angular 应用。我们开发模块来封装独立且具有单一职责的功能。一个模块导出该模块中可用的类。Angular 模块称为NgModules。在任何 Angular 应用程序中都会至少存在一个 Angular 模块:根模块,它被表示为AppModuleAppModule是一个被@NgModule装饰的类。

AppModule class:
import { NgModule }      from '@angular/core'; 
import { BrowserModule } from '@angular/platform-browser'; 
@NgModule({ 
  imports:      [ BrowserModule ], 
  providers:    [ Logger ], 
  declarations: [ AppComponent ], 
  exports:      [ AppComponent ], 
  bootstrap:    [ AppComponent ] 
}) 
export class AppModule { } 

在上述代码中,从@angular/core导入的NgModule被装饰为AppModule类。请注意,NgModule具有一些重要属性,如 imports、exports、providers、declarations 和 bootstrap。

元数据声明应该分配给视图类,如组件、指令和管道,这些类属于该模块。元数据的 exports 将被分配给在组件模板中可用的组件、指令或管道。元数据的 imports 应该分配给组件模板中使用的导出类。元数据 provider 将分配给在整个应用程序中使用或访问的服务。它创建分配的服务的实例,并将其添加到服务的全局集合中,以便这些服务可以在整个 Angular 应用程序中被消耗。元数据 bootstrap 分配给负责渲染应用程序主视图的根组件。

Angular 模块

一个示例AppComponent类如下所示。该export语句公开了组件,并且AppComponent类可被应用程序中的其他模块访问:

export class AppComponent { } 

类是包含对象方法和变量定义的模板。对象是类的一个实例,因此它可以保存变量的真实值,并且方法可以针对实际值执行操作。注意,当前版本的 JavaScript 不支持类。它是一种无类语言。在 JavaScript 中,一切都是对象,并且函数被用来模拟类。ECMAScript 6 通过在 JavaScript 中引入类来引入对 JavaScript 基于原型的继承的一种语法糖。

在这里,我们利用了 TypeScript 作为 JavaScript 的超集的能力。语句中的 export 关键字表示我们正在向应用程序的其他模块导出或公开一个AppComponent类。

假设我们已经把这个组件保存在一个名为app.component.ts的文件中。为了访问或引用被公开的AppComponent类,我们需要在我们将要访问的文件中导入它。下面的语句完成了这个操作:

import {AppComponent} from './app.component';

在这里,语句中的 import 关键字表示我们正在导入一个被公开的类:AppComponent。from 关键字表示或指向导入组件所在的文件或模块。例如,在我们的情况下,它是app.component.ts。一个模块名是组件的文件名去掉扩展名;所以,在这里,模块名是app.component。我们用相对文件路径(./)开头的模块文件名,并表示相同的文件夹。

模块也可以包含其他模块的集合,这样的模块被称为库模块。Angular 本身有许多库模块。一些库模块是核心,公用,路由等。我们从@angular/core库模块中导入Component,这是我们大多数情况下使用的主要模块:

import {Component} from '@angular/core'; 

所有的 Angular 库模块都将在 from 子句中以没有相对文件路径的方式提及。

组件


AngularJS 具有控制器,作用域和指令来处理视图,绑定数据,并通过更新数据来响应事件。在 Angular 中,组件取代了 AngularJS 的控制器,作用域和指令。

Angular 引入了支持面向对象组件模型的组件,以编写更干净的代码。一个组件是一个简单的类,它保持管理相关模板或视图的逻辑。一个简单的组件类如下所示:

Class FirstComponent { 
} 

在组件类中,我们将属性和方法暴露给模板或视图。组件属性可以为模板或视图提供数据,并允许用户修改属性值。根据用户在视图上的操作,可以调用组件方法。

Angular 组件 FirstComponent

正如你所看到的,上述代码创建了一个名为**FirstComponent**的简单 JavaScript 类。也许你想知道一个 JavaScript 普通类如何被视为组件,模板如何与这个类被连接起来。为了实现这一点,Angular 利用了 TypeScript 语法来按照 2015 年的 ES6 规范对**FirstComponent**类进行注释,将其声明为组件并将模板与选择器的标识符进行连接。下面的代码展示了带有注释的组件类,声明类为组件并用模板将其与标记标识符连接起来:

import { Component } from '@angular/core';
@Component({
  selector: 'first-component',
  template: `<h1>{{getGreetingPhrase()}} {{name}}</h1>`,
})
export class FirstComponent {
  name: string;
  constructor() {
  this.name = 'Rajesh Gunasundaram';
}
getGreetingPhrase() {
  return 'Hello Author,';
}
}
getGreetingPhrase() function to fetch and display the phrase to greet, and it will also access the name property to display the name. The @Component() preceding the FirstComponent class is the annotation that denotes this class is a Component, and the markup identifier first component for this component is assigned to the metadata of @Component named selector.

也许你会惊讶地发现我们没有使用$scope来暴露FirstComponent的属性和方法。在这里,我们的组件实例化并可在模板或视图中使用。因此,我们可以访问该实例的任何属性;同时,我们可以根据用户在视图或模板中的操作或输入调用实例中的方法。组件实例提供了有关该实例的封装数据,类似于 AngularJS 中的隔离作用域。

当根组件的模板具有另一个组件选择器的特殊标记时,Angular 中的组件可以继承,并且这也使子组件能够访问其父级和同级组件。

应用程序的组件层次结构

组件的生命周期

Angular 管理组件的生命周期。Angular 负责创建和渲染组件及其子组件,并在从 DOM 中删除之前销毁它们。Angular 跟踪组件属性值的变化。以下是 Angular 组件的生命周期事件按调用顺序给出:

  • OnChanges: 当绑定值发生变化时会触发此事件。此方法将有权访问旧值和新值。

  • OnInit: 这个事件在由于绑定值的更改而执行OnChanges事件之后触发。

  • DoCheck: 这个事件会在检测到每次变化时被触发,开发人员可以编写自定义逻辑来检查属性的变化。

  • AfterContentInit: 当指令的内容完全初始化后将触发此事件。

  • AfterContentChecked: 这个事件将在指令内容被检查后触发。

  • AfterViewInit: 当组件模板完全初始化后将触发此事件。

  • AfterViewChecked: 这个事件将在组件模板被检查后触发。

  • OnDestroy: 这个事件将在销毁指令或组件之前触发。

您可以实现所有这些事件,也可以只实现组件所需的特定事件。

装饰器和元数据


如您在上一节中所看到的,我们为组件定义了 JavaScript 普通类,并对其进行了一些信息注释,以通知 Angular 框架该类是一个组件。

我们利用了 Typescript 语法,并使用装饰符功能将类附加元数据。为了使一个类成为组件,我们添加@Component装饰符,如下所示:

@Component({...})
export class FirstComponent {...}
FirstComponent class has been decorated as a component.

现在,让我们使用装饰符语法为FirstComponent类附加元数据:

@Component({ 
   selector: 'first-component', 
   templateUrl: 'app/first.component.html' 
}) 
export class FirstComponent {...} 

在这里,我们已经添加了诸如选择器和templateUrl之类的元数据。组件中配置的选择器元数据告诉 Angular 在遇到<first-controller>标记时创建该组件的实例:

<first-controller></first-controller> 

templateUrl提供了组件渲染的模板文件的 URL。当您运行应用程序时,<first-controller>标记将被templateUrl中引用的模板内容替换。此元数据实际上是@Component装饰符的一个参数,而装饰符是一个函数。

通过装饰符添加元数据,我们实际上告诉 Angular 如何处理定义的类。组件、模板和元数据一起构成一个视图。

模板


当对组件进行注解时,您可能已经注意到我们为视图或模板添加了内联标记。我们还可以添加一个模板 URL,将视图或模板标记隔离在一个单独的 HTML 文件中,而不是将其作为内联视图或模板。

模板由 HTML 标记组成,向 Angular 提供有关呈现组件的信息。以下代码行中给出了一个简单的模板内容。它呈现了书名和出版商:

<div> 
  The Name of the book is {{bookName}} and is published by {{pubName}}. 
</div> 

内联模板

内联模板在需要呈现非常简单内容(例如一行)时使用。在这种情况下,内联视图或模板将直接在注释中定义:

@Component({ 
  selector: 'first-component', 
  template: "<div>{{getGreetingPhrase()}} {{name}}</div>" 
}) 

隔离模板

隔离模板主要用于模板包含更多内容的情况。在这种情况下,内容将被移到一个单独的文件中,并将 HTML 文件的 URL 分配给templateUrl,如下所示:

@Component({ 
  selector: 'first-component', 
  templateUrl: FirstPage.html' 
}) 

本地模板变量

Angular 允许创建模板作用域变量,在模板中移动数据:

<div *ngFor="let todo of todos"> 
  <todo-item [todo]="todo"></todo-item> 
</div> 

在前面的模板标记中,我们使用 let 关键字声明了一个本地变量 todo。然后,我们遍历 todos 集合变量;每个 todo 项目都被分配给 todo,并且可以在<todo-item>中使用。

也可以使用本地模板变量来保存 DOM 元素。以下代码显示了作者将保存输入元素本身,并且可以使用 author.value 访问元素的值:

<!-- author refers to input element and passes its `value`to the event handler --> 
<input #author placeholder="Author Name"> 
<button (click)="updateAuthor(author.value)">Update</button> 

绑定


绑定技术将使您能够将数据绑定到模板,并允许用户与绑定的数据进行交互。Angular 绑定框架负责将数据呈现到视图,并根据用户在视图上的操作进行更新。

以下截图让您快速了解了 Angular 中各种绑定技术。我们将逐个详细介绍每种绑定技术:

各种绑定语法

单向绑定

诸如插值属性属性样式等绑定类型支持从数据源(从组件公开)到视图或模板的单向数据流。让数据从组件属性或方法流向模板的模板标记在下表中给出(单向绑定):

模板代码 描述
{{表达式}} 这显示了从数据源构建的表达式
[目标] = "表达式" 这将数据源的表达式分配给目标属性
bind-target = "表达式" 这将数据源的表达式分配给绑定目标属性

让数据从模板流向组件属性或方法的模板标记在下表中给出(单向绑定):

模板代码 描述
(目标) = "语句" 这将数据源的表达式分配给目标属性
on-target = "语句" 这将数据源的表达式分配给绑定目标属性

内插绑定

内插是 Angular 的主要特性之一。您可以将任何属性值或表达式插值到任何 HTML 元素的内容中,例如divli。您可以通过双大括号{{和}}来实现此目的,如下行代码所示:

<div>Hello, {{authorName}}!</div>

在这里,我们将authorName插值到div标签的内容中。这是一种单向绑定,其中数据从组件属性或方法流向模板。

属性绑定

属性绑定用于将组件属性绑定到 HTML 元素属性:

<div [hidden]="hidePubName>Packt Publishing</div> 
hidePubName component property to the div property hidden. This is also a one-way binding where the data flows from a component property to a template.

事件绑定

HTML 元素具有各种 DOM 事件,当事件触发时将触发。例如,单击按钮时将触发点击事件。我们挂钩事件监听器以便在事件触发时得到通知:

<button (click)="doSomething()">Do Something</button>

前面的 Angular 代码片段将事件名称放在括号中,需要挂接事件监听器,以便在触发单击事件时调用它。

双向绑定

Angular 已经从其框架中移除了一个核心功能,这是 AngularJS 诞生的一个主要原因,即双向绑定。因此,默认情况下不支持双向绑定。现在,让我们看看如何在 Angular 中实现双向绑定。

Angular 结合属性和事件绑定,使我们能够实现双向绑定,如下面的代码所示:

<input [(ngModel)]="authorName">
ngModel is wrapped with parentheses and then with square brackets. The parentheses indicate that the component property is tied up with the ngChange event, and the square brackets indicate that the component property is assigned to a value property of the input element. So, when the value of the input element changes, it fires up the change event that eventually updates authorName with the new value from the event object. ngModel in the markup is the built-in directive in Angular that unifies property and event binding.

可以帮助数据双向流动的模板标记,从模板到组件,从组件到模板,如下表所示(双向绑定):

模板代码 描述
[(目标)] = "表达式" 这将数据源的表达式分配给目标属性
bindon-target = "表达式" 这将数据源的表达式分配给绑定目标属性

指令


我们详细介绍了 Angular 组件及其装饰方式。@Component 本身是一个带有在元数据中配置的模板的指令。因此,一个没有模板的指令是一个组件,而 @directive 在 Typescript 中用于附加元数据。

结构指令

结构指令处理通过添加新元素、删除现有元素和用新元素替换现有元素来修改 DOM 中的元素。下面的标记显示了两个结构指令:*ngFor*ngIf

<div *ngFor="#todo of todos"></div> 
<todo-item *ngIf="selectedTodo"></todo-item> 

*ngFor 遍历 todos 集合中的每个项目,并为每个项目添加一个 div 标签。而 *ngIf 仅在 selectedTodo 可用时呈现 <todo-item>

属性指令

属性指令将像属性一样添加到现有的 HTML 元素中,并且可以修改或扩展 HTML 元素的行为。例如,如果将 ngModel 指令添加到输入元素中,它将通过更新其 value 属性和响应更改事件来扩展它:

<input [(ngModel)]="author.name">

除了使用现有的指令,我们还可以编写自己的指令,比如 ngSwitchngStylesngClass

依赖注入


依赖注入是一种处理依赖关系并解决它们的设计模式。依赖项的实例将传递给依赖项,以便使用它。如果客户端模块或类依赖于一个服务,它需要在使用之前创建该服务的一个实例。我们可以使用依赖注入模式注入或传递服务的实例给客户端,而不是客户端模块构建服务。

应用依赖注入使我们能够创建一个不知道要构建的服务和实际消费的服务的客户端。客户端只会知道服务的接口,因为它需要知道如何使用服务。

为什么依赖注入?

假设我们正在创建一个 Mobile 类,并且它依赖于 camerainternet 连接。

Mobile 类的代码片段

Camera and Internet are created in the constructor of the Mobile class. These are the features of Mobile. Instead of requesting for the feature, the Mobile class created the feature by itself. This means that the Mobile class is bound to a certain version of features, such as a 2 MP camera and 2G Internet. Later, if we want to upgrade the camera to 20 MP and Internet to 3G or 4G, we need to rewrite the code of the Mobile class.

Mobile 类依赖于 CameraInternet,这增加了测试的难度。我们只能用 2G 互联网和 2 MP 相机来测试 Mobile,因为我们无法控制依赖,因为 Mobile 类通过自身负责依赖的实例。

现在,让我们修改构造函数,接收 CameraInternet 的实例作为参数,如下面的代码行所示:

constructor(public camera: Camera, public internet: Internet) { } 

现在,Mobile 类将不再创建 CameraInternet 的实例。它只消耗从构造函数参数中收到的 CameraInternet 的实例。这意味着我们将依赖项移到了构造函数中。客户端可以通过向构造函数传递 CameraInternet 的实例来创建一个 Mobile 类,如下面的代码片段所示:

// Simple mobile with 2MP camera and 2G internet. 
var mobile = new Mobile(new Camera2MP(), new Internet2G()); 

您可以看到CameraInternet的定义已经与Mobile类解耦。只要客户端传递的CameraInternet类型符合CameraInternet的接口,我们就可以传递任何类型的具有不同百万像素的摄像头和不同带宽的互联网,比如 2G,3G 和 4G。

// an advanced mobile with 20MP camera and 4G internet. 
var mobile = new Mobile(new Camera20MP(), new Internet4G()); 

Mobile类中没有改变,以适应 20 MP 摄像头和 4G 互联网的依赖性。Mobile类更容易通过各种组合的CameraInternet进行测试,因为我们对依赖性有完全的控制。我们还可以在测试中使用模拟技术,并将CameraInternet的模拟传递给构造函数,以便所有必要的操作都将针对CameraInternet的模拟进行。

注入器的作用

我们刚刚了解了什么是依赖注入,以及它如何从外部客户端接收依赖性而不是自己创建它们。然而,客户端需要更新其代码,以传递 20 MP 摄像头和 4G 互联网依赖的实例。任何想要使用Mobile类的客户端都必须创建CameraInternet的实例,因为Mobile类依赖于它们。我们从Mobile类中消除了创建依赖实例的责任,并将其移动到将使用Mobile类的客户端。

现在,成为可怜的客户端的问题,要创建CameraInternet的实例。因此,为了减少客户端创建依赖实例的额外工作,我们需要注入器来负责为客户端组装所需的CameraInternet的实例。依赖注入框架有一个叫做注入器的东西,我们在其中注册我们的类,比如Mobile。然后我们可以请求注入器为我们创建Mobile的实例。注入器将负责解析依赖关系并创建mobile,如下面的代码行所示:

var mobile = injector.get(Mobile); 

在 Angular 中处理依赖注入

Angular 有自己的依赖注入框架,并且我们将通过一个示例看到它如何处理依赖注入。

首先,我们将在app/todos/todo.ts下创建一个Todo类,该类具有诸如iddescriptionisCompleted等属性,如下截图所示:

Todo 类的代码片段

然后,创建一个TodoListComponent组件,并添加一个属性来保存从注入的TodoService检索到的待办事项集合。当依赖注入框架实例化TodoListComponent时,服务将被注入到构造函数中。您将在第三章Angular 构建块-第二部分中了解更多关于服务的内容。

TodoListComponent 类的代码片段

代码是使用 Typescript 编写的,当它将代码编译为 JavaScript 时,会包含有关类元数据的信息,因为类被装饰为 @component。这个类元数据包含了关联todoService参数和TodoService类的信息。这使得 Angular 注入器在创建新的 TodoListComponent 时能够注入 TodoService 的实例。

在我们的代码中,我们不需要显式调用注入器来注入服务。相反,Angular 的自动依赖注入会处理它。当 Angular 遇到通过 HTML 标记或通过路由导航到组件时遇到<todo-list>选择器时,注入器会在实例化组件的同时被隐式调用。

现在,我们将创建 TodosComponent,在 @Component 指令的 providers 参数中注册 TodoServiceTodoService 的实例在TodosComponent中和它的所有子项中都可以被注入使用。

import { Component } from '@angular/core';
import { TodoListComponent } from './todo-list.component';
@Component({
  selector: 'my-todos',
  template: '<h2>Todolist</h2><todo-list></todo-list>',
  providers: [TodoService],
  directives: [TodoListComponent]
})
export class TodosComponent { }

现在,让我们创建返回待办事项集合的 TodoService 服务。

TodoService 的代码片段

在生产环境的 TodoList 应用程序中,TodoService 中的 getTodos 方法将进行一个 HTTP 请求来获取待办事项列表。在基本情况下,我们从mock-todos中返回待办事项的集合。

最后,我们需要创建 mock-todos,其中包含待办事项的集合,如下面的屏幕截图所示:

mock-todos 的代码片段

该文件用作内存中的集合,以保存待办事项,并且可以在导入该文件的组件中使用。这种方法适用于开发阶段,但在生产阶段需要从远程服务器获取待办事项。

在 VS Code 中按下F5运行应用程序,您将得到 Angular TodoList 应用程序的输出,如下面的屏幕截图所示:

在浏览器中运行的 TodoList 应用程序

总结


哇!到现在为止,您一定已经学到了很多关于 Angular 架构的核心构建块。我们从 e 开始,讨论了它如何封装了独立且具有单一职责的功能。然后,您学习了组件的概念,以及它们如何取代了 AngularJS 中的控制器、作用域和指令。您还了解了装饰器和元数据,它们利用了 Typescript 语法将普通的 JavaScript 类转换为 Angular 组件。然后,我们讨论了模板以及内联模板和独立模板之间的区别。您还学习了如何在模板中实现各种绑定技术。稍后,我们通过指令讨论了指令以及指令与组件的区别。最后,您学习了一个最受欢迎的设计模式之一,依赖注入,以及它如何被 Angular 处理。

在下一章中,我们将讨论 Angular 架构中剩下的部分。

第三章:Angular 构建模块-第二部分

本章将详细介绍 Angular 架构中尚未涵盖的核心构建模块。 在本章中,我们将涵盖以下主题:

  • 表单

  • 管道

  • 路由

  • 服务

  • 观察者

表单


每个应用程序都有一个数据输入点,它使最终用户能够输入数据。表单旨在向服务器和页面插入或更新输入数据。在提交以进行进一步操作之前,应验证输入数据。应用了两种类型的验证方法:客户端验证和服务器端验证:

  • 服务器端验证:服务器端验证将由服务器处理。 收到的信息将由服务器处理和验证。 如果提交表单时存在任何错误,则需要使用适当的信息更新 UI。 如果信息无效或不足,则将适当的响应发送回客户端。 这种验证方法更加安全,因为即使浏览器中关闭了 JavaScript,它也可以工作,并且恶意用户无法绕过服务器端验证。 但是,这种方法的缺点是只有在将表单提交到服务器后才会验证表单。 因此,用户必须等到完全提交表单到服务器,才能知道所提供的所有数据是否有效。

  • 客户端验证:虽然服务器端验证更加安全,但它不会提供更好的用户体验。 使用脚本语言,如 JavaScript,实现客户端验证,并在客户端上进行验证。 用户输入的数据可以在用户输入时验证。 这会通过在屏幕上提供验证错误的即时响应,提供更丰富的体验。 用户无需等待整个表单提交,即可知道输入的数据是否有效。

Angular 具有 FormBuilder、Control 和 Validators 等类来处理表单。 它使您能够使用 Control 和 Validators 轻松设置验证规则。

表单工具

Angular 有各种工具可实现应用程序中的表单。 以下是这些工具及其各自的目的:

  • 控件:这些通过封装表单的输入提供对象

  • 验证器:这些有助于验证表单中的输入数据

  • 观察者:这些有助于跟踪表单中的更改并通知用户任何验证错误

Angular 形式的类型

Angular 提供了两种处理表单的方法:模板驱动表单和模型驱动表单。

模板驱动表单

AngularJS 使用ng-model指令处理表单,并利用了使开发人员生活更轻松的双向绑定功能。 Angular 使开发人员能够使用ngModel构建模板驱动表单,这类似于 AngularJS 中的ng-model

以下是模板驱动表单的实现:

  1. 让我们在 Visual Studio CodeVS Code)中创建一个名为 First Template Form 的应用程序。

  2. package.json 中添加所需的包和依赖详情,并使用 npm install 命令进行安装。

      {
      "name":"first-template-form",
      "version":"1.0.0",
      "private":true,
      "description":"First template form",
      "scripts":{
      "test:once":"karma start karma.conf.js --single-
       run",
      "build":"tsc -p src/",
      "serve":"lite-server -c=bs-config.json",
      "prestart":"npm run build",
      "start":"concurrently \"npm run build:watch\" \"npm  
       run serve\"",
      "pretest":"npm run build",
      "test":"concurrently \"npm run build:watch\" \"karma 
       start 
      karma.conf.js\"",
      "pretest:once":"npm run build",
      "build:watch":"tsc -p src/ -w",
      "build:upgrade":"tsc",
      "serve:upgrade":"http-server",
      "build:aot":"ngc -p tsconfig-aot.json && rollup -c  
       rollup-
      config.js",
      "serve:aot":"lite-server -c bs-config.aot.json",
      "build:babel":"babel src -d src --extensions 
      \".es6\" --source-
      maps",
      "copy-dist-files":"node ./copy-dist-files.js",
      "i18n":"ng-xi18n",
      "lint":"tslint ./src/**/*.ts -t verbose"
      },
      "keywords":[
      ],
      "author":"",
      "license":"MIT",
      "dependencies":{
      "@angular/common":"~4.0.0",
      "@angular/compiler":"~4.0.0",
      "@angular/compiler-cli":"~4.0.0",
      "@angular/core":"~4.0.0",
      "@angular/forms":"~4.0.0",
      "@angular/http":"~4.0.0",
      "@angular/platform-browser":"~4.0.0",
      "@angular/platform-browser-dynamic":"~4.0.0",
      "@angular/platform-server":"~4.0.0",
      "@angular/router":"~4.0.0",
      "@angular/tsc-wrapped":"~4.0.0",
      "@angular/upgrade":"~4.0.0",
      "angular-in-memory-web-api":"~0.3.1",
      "core-js":"².4.1",
      "rxjs":"5.0.1",
      "systemjs":"0.19.39",
      "zone.js":"⁰.8.4"
      },
      "devDependencies":{
      "@types/angular":"¹.5.16",
      "@types/angular-animate":"¹.5.5",
      "@types/angular-cookies":"¹.4.2",
      "@types/angular-mocks":"¹.5.5",
      "@types/angular-resource":"¹.5.6",
      "@types/angular-route":"¹.3.2",
      "@types/angular-sanitize":"¹.3.3",
      "@types/jasmine":"2.5.36",
      "@types/node":"⁶.0.45",
      "babel-cli":"⁶.16.0",
      "babel-preset-angular2":"⁰.0.2",
      "babel-preset-es2015":"⁶.16.0",
      "canonical-path":"0.0.2",
      "concurrently":"³.0.0",
      "http-server":"⁰.9.0",
      "jasmine":"~2.4.1",
      "jasmine-core":"~2.4.1",
      "karma":"¹.3.0",
      "karma-chrome-launcher":"².0.0",
      "karma-cli":"¹.0.1",
      "karma-jasmine":"¹.0.2",
      "karma-jasmine-html-reporter":"⁰.2.2",
      "karma-phantomjs-launcher":"¹.0.2",
      "lite-server":"².2.2",
      "lodash":"⁴.16.2",
      "phantomjs-prebuilt":"².1.7",
      "protractor":"~4.0.14",
      "rollup":"⁰.41.6",
      "rollup-plugin-commonjs":"⁸.0.2",
      "rollup-plugin-node-resolve":"2.0.0",
      "rollup-plugin-uglify":"¹.0.1",
      "source-map-explorer":"¹.3.2",
      "tslint":"³.15.1",
      "typescript":"~2.2.0"
      },
      "repository":{
      }
      }
  1. 创建一个书籍类,并添加以下代码片段:
      export class Book {
      constructor(
      public id: number,
      public name: string,
      public author: string,
      public publication?: string
      ) { }
      }
  1. 创建 AppComponent,并添加以下代码:
      import { Component } from '@angular/core';
      @Component({
      selector: 'first-template-form',
      template: '<book-form></book-form>'
      })
      export class AppComponent { }

这里展示的 AppComponent 是应用程序的根组件,将托管 BookFormComponentAppComponent 被装饰为第一个模板表单选择器,模板中包含带有<book-form/>特殊标签的内联 HTML。这个标签在运行时将被更新为实际模板。

  1. 现在,让我们使用以下代码片段向 book-form.component.ts 中添加代码:
      import { Component } from '@angular/core';
      import { Book } from './book';
      @Component({selector: 'book-form',
      templateUrl: './book-form.component.html'
      })
      export class BookFormComponent {
      model = new Book(1, 'book name','author 
      name','publication name 
      is optional');
      onSubmit() {
      // code to post the data
      }
      newBook() {
      this.model = new Book(0,'','','');
      }
      }

在这里,注意到我们从 book.ts 中导入了 Book。Book 是该表单的数据模型。BookFormComponent 被装饰为 @Component 指令,该指令从 @angular/core 中引入。选择器值设置为 book-form,templateUrl 被分配为模板 HTML 文件。在 BookFormCompoent 中,我们用虚拟数据初始化了 Book 模型。我们有两个方法--onSubmit()newBook()--一个用于向 API 提交数据,另一个用于清空表单。

  1. 现在,让我们向以下 HTML 内容中添加 book-form.component.html 模板文件:
      <div class="container">
      <h1>New Book Form</h1>
      <form (ngSubmit)="onSubmit()" #bookForm="ngForm">
      <div class="form-group">
      <label for="name">Name</label>
      <input type="text" class="form-control" id="name"
      required
      [(ngModel)]="model.name" name="name"
      #name="ngModel">
      <div [hidden]="name.valid || name.pristine"
      class="alert alert-danger">
      Name is required
      </div>
      </div>
      <div class="form-group">
      <label for="author">Author</label>
      <input type="text" class="form-control" id="author"
      required
      [(ngModel)]="model.author" name="author"
      #author="ngModel">
      <div [hidden]="author.valid || author.pristine"
      class="alert alert-danger">
      Author is required
      </div>
      </div>
      <div class="form-group">
      <label for="publication">Publication</label>
      <input type="text" class="form-control" 
      id="publication"
      [(ngModel)]="model.publication" name="publication"
      #publication="ngModel">
      </div>
      <button type="submit" class="btn btn-success"       
      [disabled]="!bookForm.form.valid">Submit</button>
      &nbsp;&nbsp;
      <button type="button" class="btn btn-default"        
      (click)="newBook()">Clear</button>
      </form>
      </div>
      <style>
      .no-style .ng-valid {
      border-left: 1px solid #CCC
      }
      .no-style .ng-invalid {
      border-left: 1px solid #CCC
      }
      </style>

这是一个简单的模板表单,包含三个输入控件用于输入书名、作者和出版商名称,一个提交按钮用于提交详情,以及一个清除按钮用于清空表单。Angular 隐式地将 ngForm 指令应用于模板中的表单。我们将 ngForm 指令分配给了 #bookForm 本地变量。

使用 #bookForm 本地变量,我们可以跟踪表单的错误,并检查它们是有效还是无效、被触碰还是未触碰以及原始还是脏。在这里,只有当 ngForm 的 valid 属性返回 true 时,提交按钮才会被启用,因为它被分配到按钮的 disabled 属性。

BookFormComponent 中的 onSubmit 函数被分配给了表单的 ngSubmit 事件。因此,当单击提交按钮时,它将调用 BookFormComponent 中的 onSubmit 函数。

请注意,所有输入控件都包含 ngModel 事件-属性属性,并且将其分配给它们各自的模型属性,比如 model.namemodel.authormodel.publication。通过这种方式,我们可以实现双向绑定,这样当在对应的输入控件中输入值时,BookFormComponent 中的模型属性将被更新为它们各自的值:

  1. 我们已经放置了所需的模板和组件。现在,我们需要创建一个 AppModule 来引导我们应用程序的根组件 AppComponent。创建一个名为 app.module.ts 的文件,并添加以下代码片段:
      import { NgModule } from '@angular/core';
      import { BrowserModule } from '@angular/platform-
      browser';
      import { FormsModule } from '@angular/forms';
      import { AppComponent } from './app.component';
      import { BookFormComponent } from './book-
      form.component';
      @NgModule({
        imports: [
        BrowserModule,
        FormsModule
        ],
        declarations: [
        AppComponent,
        BookFormComponent
        ],
        bootstrap: [ AppComponent ]
      })
      export class AppModule { }

正如我们在第二章Angular 构建块-第一部分中讨论的,任何 Angular 应用程序都将有一个根模块,该模块将使用NgModule指令进行装饰,并包含导入、声明和引导等元数据详细信息。

在上述代码中,请注意我们将AppComponent类分配为引导元数据,以通知 AngularAppComponent是应用程序的根组件。

  1. 现在我们已经准备好了所有所需的模板和类,我们需要引导模块。让我们创建一个名为main.ts的文件,其中包含以下代码片段,用于引导模块:
      import { platformBrowserDynamic } from 
      '@angular/platform-
      browser-dynamic';
      import { AppModule } from './app/app.module';
      platformBrowserDynamic().bootstrapModule(AppModule)
  1. 最后,添加以下内容的 index.html 文件:
      <!DOCTYPE html>
      <html>
      <head>
      <title>Book Form</title>
      <base href="/">
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, 
      initial-
      scale=1">
      <link rel="stylesheet"
      href="https://unpkg.com/bootstrap@3.3.7/
      dist/css/bootstra p.min.cs
      s">
      <link rel="stylesheet" href="styles.css">
      <link rel="stylesheet" href="forms.css">
      <!-- Polyfills -->
      <script src="node_modules/core-
      js/client/shim.min.js"></script>
      <script src="img/zone.js">
      </script>
      <script 
      src="img/system.src.js">
      </script>
      <script src="img/systemjs.config.js"></script>
      <script>
      System.import('main.js').catch(function(err){   
      console.error(err); 
      });
      </script>
      </head>
      <body>
      <first-template-form>Loading...</first-template-
      form>
      </body>
      </html>

注意在正文中添加了<first-template-form/>特殊标记。该标记将在运行时使用实际模板进行更新。另外,请注意,在运行时使用System.js模块加载器加载必需的库。systemjs.config.js文件应包含有关映射 npm 包和我们应用程序起始点的指令。在这里,我们的应用程序在main.ts中引导,这将在构建应用程序后被转译为main.jssystemjs.config.js的内容如下所示:

/**
* System configuration for Angular samples
* Adjust as necessary for your application needs.
*/
(function (global) {
System.config({
paths: {
  // paths serve as alias
  'npm:': 'node_modules/'
},
// map tells the System loader where to look for things
map: {// our app is within the app folder
'app': 'app',
// angular bundles
'@angular/animations': 'npm:@angular/animations/bundles/animations.umd.js',
'@angular/animations/browser': 'npm:@angular/animations/bundles/animations-browser.umd.js',
'@angular/core': 'npm:@angular/core/bundles/core.umd.js',
'@angular/common': 'npm:@angular/common/bundles/common.umd.js',
'@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js',
'@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js',
'@angular/platform-browser/animations': 'npm:@angular/platform-browser/bundles/platform-browser-animations.umd.js',
'@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js',
'@angular/http': 'npm:@angular/http/bundles/http.umd.js',
'@angular/router': 'npm:@angular/router/bundles/router.umd.js',
'@angular/router/upgrade': 'npm:@angular/router/bundles/router-upgrade.umd.js',
'@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js',
'@angular/upgrade': 'npm:@angular/upgrade/bundles/upgrade.umd.js',
'@angular/upgrade/static': 'npm:@angular/upgrade/bundles/upgrade-static.umd.js',
// other libraries
'rxjs': 'npm:rxjs',
'angular-in-memory-web-api': 'npm:angular-in-memory-web-api/bundles/in-memory-web-api.umd.js'
},
// packages tells the System loader how to load when no filename and/or no extension
packages: {
app: {
  main: './main.js',
  defaultExtension: 'js',
meta: {
'./*.js': {
  loader: 'systemjs-angular-loader.js'
}
}
},
rxjs: {
  defaultExtension: 'js'
}
}
});
})(this);
  1. 现在,我们已经准备好了所有所需的内容。通过按下F5来运行应用程序,索引页面将以由BookFormComponent提供模板的方式呈现,如下所示:

FirstTemplateForm应用程序的输出

  1. 现在移除分配给输入控件的虚拟文本,并注意表单验证已触发,显示验证错误消息,保持Submit按钮处于禁用状态:

检查控制台日志以进行表单提交

在这个模板驱动表单中,您可能已经注意到我们已经将required属性应用于输入控件。类似于这样,我们还可以应用最小长度和最大长度验证。然而,这样应用验证会将验证逻辑紧密耦合到模板中,并且我们只能通过编写基于浏览器的端到端测试来测试这些验证。

模型驱动表单

Angular 提供了FormGroupFormControl属性来实现模型驱动表单。

模型驱动表单的基本对象

FormControlFormGroup是模型驱动表单中的两个基本对象。FormControl是 Angular 表单中的输入字段,它封装了输入字段的值,其状态(有效性),是否已更改(脏),或是否有任何错误。

当我们构建一个表单时,我们需要创建控件并附加元数据到这些控件。我们必须通过添加formControlName属性将 Control 类附加到 DOM 输入元素,如下所示:

<input type="text" formControlName="name" />

FormGroup可以由 FormBuilder 进行实例化。我们还可以用默认值在组件中手动构建FormGroup,如下所示:

this.bookForm = new FormGroup({
  name: new FormControl('book name', Validators.required),
  author: new FormControl('author name', Validators.required),
  publication: new FormControl('publication name is optional')
});

让我们在Visual Studio CodeVS Code)中创建一个名为ModelDrivenForm的应用程序。以下是模型驱动表单的实现:

  1. 添加所需的包和依赖项详细信息,并使用npm install命令来安装它们:
      {
      "name":"model-driven-form",
      "version":"1.0.0",
      "private":true,
      "description":"Model driven form",
      "scripts":{
      "test:once":"karma start karma.conf.js --single-
       run",
      "build":"tsc -p src/",
      "serve":"lite-server -c=bs-config.json",
      "prestart":"npm run build",
      "start":"concurrently \"npm run build:watch\" \"npm 
      run serve\"",
      "pretest":"npm run build",
      "test":"concurrently \"npm run build:watch\" \"karma 
       start 
      karma.conf.js\"",
      "pretest:once":"npm run build",
      "build:watch":"tsc -p src/ -w",
      "build:upgrade":"tsc",
      "serve:upgrade":"http-server",
      "build:aot":"ngc -p tsconfig-aot.json && rollup -c 
       rollup-
      config.js",
      "serve:aot":"lite-server -c bs-config.aot.json",
      "build:babel":"babel src -d src --extensions 
      \".es6\" --source-
      maps",
      "copy-dist-files":"node ./copy-dist-files.js",
      "i18n":"ng-xi18n",
      "lint":"tslint ./src/**/*.ts -t verbose"
      },
      "keywords":[
      ],
      "author":"",
      "license":"MIT",
      "dependencies":{
      "@angular/common":"~4.0.0",
      "@angular/compiler":"~4.0.0",
      "@angular/compiler-cli":"~4.0.0",
      "@angular/core":"~4.0.0",
      "@angular/forms":"~4.0.0","@angular/http":"~4.0.0",
      "@angular/platform-browser":"~4.0.0",
      "@angular/platform-browser-dynamic":"~4.0.0",
      "@angular/platform-server":"~4.0.0",
      "@angular/router":"~4.0.0",
      "@angular/tsc-wrapped":"~4.0.0",
      "@angular/upgrade":"~4.0.0
      ",
      "angular-in-memory-web-api":"~0.3.1",
      "core-js":"².4.1",
      "rxjs":"5.0.1",
      "systemjs":"0.19.39",
      "zone.js":"⁰.8.4"
      },
      "devDependencies":{
      "@types/angular":"¹.5.16",
      "@types/angular-animate":"¹.5.5",
      "@types/angular-cookies":"¹.4.2",
      "@types/angular-mocks":"¹.5.5",
      "@types/angular-resource":"¹.5.6",
      "@types/angular-route":"¹.3.2",
      "@types/angular-sanitize":"¹.3.3",
      "@types/jasmine":"2.5.36",
      "@types/node":"⁶.0.45",
      "babel-cli":"⁶.16.0",
      "babel-preset-angular2":"⁰.0.2",
      "babel-preset-es2015":"⁶.16.0",
      "canonical-path":"0.0.2",
      "concurrently":"³.0.0",
      "http-server":"⁰.9.0",
      "jasmine":"~2.4.1",
      "jasmine-core":"~2.4.1",
      "karma":"¹.3.0",
      "karma-chrome-launcher":"².0.0",
      "karma-cli":"¹.0.1",
      "karma-jasmine":"¹.0.2",
      "karma-jasmine-html-reporter":"⁰.2.2",
      "karma-phantomjs-launcher":"¹.0.2",
      "lite-server":"².2.2",
      "lodash":"⁴.16.2",
      "phantomjs-prebuilt":"².1.7",
      "protractor":"~4.0.14",
      "rollup":"⁰.41.6",
      "rollup-plugin-commonjs":"⁸.0.2",
      "rollup-plugin-node-resolve":"2.0.0",
      "rollup-plugin-uglify":"¹.0.1",
      "source-map-explorer":"¹.3.2",
      "tslint":"³.15.1",
      "typescript":"~2.2.0"
      },
      "repository":{
      }
      }
  1. 创建一个Book类,并添加以下代码片段:
      export class Book {
      constructor(
      public id: number,
      public name: string,
      public author: string,
      public publication?: string
      ) { }
      }
  1. 创建AppComponent并添加以下代码:
      import { Component } from '@angular/core';
      @Component({
      selector: 'first-model-form',
      template: '<book-form></book-form>'
      })
      export class AppComponent { }

此前展示的AppComponent是应用程序的根组件,将托管BookFormComponentAppComponent带有第一个模型表单选择器和模板,其中包含带有特殊标签<book-form/>的内联 HTML。这个标签将在运行时更新为实际模板。

  1. 现在,让我们添加book-form.component.ts,使用以下代码片段:
      import { Component, OnInit } from '@angular/core';
      import { FormControl, FormGroup, Validators } from 
      '@angular/forms';
      import { Book } from './book';
      @Component({
      selector: 'book-form',
      templateUrl: './book-form.component.html'
      })
      export class BookFormComponent implements OnInit {
      bookForm: FormGroup;
      public submitted: boolean;
      constructor() { }
      ngOnInit() {
      this.bookForm = new FormGroup({
      name: new FormControl('book name', 
      Validators.required),
      author: new FormControl('author name', 
      Validators.required),
      publication: new FormControl('publication name is 
      optional')
      });
      }
      onSubmit(model: Book, isValid: boolean) {
      this.submitted = true;
      console.log(model, isValid);
      // code to post the data
      }
      }

在这里,注意我们从@angular/forms中导入了FormControlFormGroupValidators。这些是实现模型驱动表单的基本类。我们还从@angular/core中导入了ComponentOnInit,用于组件类的实现,然后我们从book.ts中导入了 Book。Book 是该表单的数据模型。

BookFormComponent带有从@angular/core导入的@Component指令。选择器值设置为book-formtemplateUrl分配了模板 HTML 文件。

BookFormCompoent中,我们通过实例化FormGroup并将其分配给属性,如名称、作者和出版物,来初始化表单模型。我们有onSubmit()方法来将提交的数据提交到 API。

  1. 现在,让我们添加book-form.component.html模板文件,并添加以下 HTML 内容:
      <div class="container">
      <h1>New Book Form</h1>
      <form [formGroup]="bookForm" novalidate       
      (ngSubmit)="onSubmit(bookForm.value, 
       bookForm.valid)">
      <div class="form-group">
      <label for="name">Name</label>
      <input type="text" class="form-control" 
       formControlName="name">
      <small [hidden]="bookForm.controls.name.valid ||       
      (bookForm.controls.name.pristine && !submitted)" 
      class="text-
      danger">
      Name is required.
      </small>
      </div>
      <div class="form-group">
      <label for="author">Author</label>
      <input type="text" class="form-control" 
      formControlName="author">
      <small [hidden]="bookForm.controls.author.valid ||       
      (bookForm.controls.author.pristine && !submitted)" 
      class="text-
      danger">
      Author is required.
      </small>
      </div>
      <div class="form-group">
      <label for="publication">Publication</label>
      <input type="text" class="form-control" 
      formControlName="publication">
      </div>
      <button type="submit" class="btn btn-
      success">Submit</button>
      </form>
      </div>
      <style>
      .no-style .ng-valid {
      border-left: 1px solid #CCC
      }
      .no-style .ng-invalid {
      border-left: 1px solid #CCC
      }
      </style>

与模板驱动表单类似,这是一个简单的模型驱动表单,其中包含三个输入控件用于输入图书、作者和出版商名称,以及一个提交按钮来提交详细信息。在表单标签中,我们添加了formGroup指令来分配给表单,并将其分配给了bookForm。每个输入控件都有一个特殊的属性formControlName,分别分配有各自的formControl,比如名称、作者和出版物。

BookFormComponent中的onSubmit函数分配给了表单的ngSubmit事件。因此,当单击提交按钮时,它将调用BookFormComponent中的onSubmit函数,传递bookForm的值和有效属性。

注意,所有的输入控件都没有任何事件兼属性属性,就像模板驱动表单中一样。在这里,我们可以通过将模型值从bookForm.value属性传递到onSubmit函数,并从组件中访问模型来实现双向绑定。

我们已经准备好所需的模板和组件。现在我们需要创建一个AppModule来引导我们应用程序的根组件AppComponent。创建一个名为app.module.ts的文件,并添加以下代码片段:

      import { NgModule } from '@angular/core';
      import { BrowserModule } from '@angular/platform-
      browser';
      import { FormsModule, ReactiveFormsModule } from 
      '@angular/forms';
      import { AppComponent } from './app.component';
      import { BookFormComponent } from './book-
      form.component';
      @NgModule({
      imports: [
      BrowserModule,
      ReactiveFormsModule
      ],
      declarations: [
      AppComponent,
      BookFormComponent
      ],
      bootstrap: [ AppComponent ]
      })
      export class AppModule { }

在上述代码中,请注意,我们已将 AppComponent 类分配为引导元数据,以通知 Angular AppComponent 是应用程序的根组件。还要注意,我们已从 @angular/forms 导入了 FormsModuleReactiveFormsModule

  1. 现在,我们已经准备好所有所需的模板和类,我们需要引导模块。让我们创建一个名为 main.ts 的文件,其中包含如下代码段来引导模块:
      import { platformBrowserDynamic } from 
      '@angular/platform-
      browser-dynamic';
      import { AppModule } from './app/app.module';
      platformBrowserDynamic().bootstrapModule(AppModule)
  1. 最后,使用以下内容添加 index.html 文件:
      <!DOCTYPE html>
      <html>
      <head>
      <title>Hero Form</title>
      <base href="/">
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, 
      initial-
      scale=1">
      <link rel="stylesheet"
      href="https://unpkg.com/bootstrap@3.3.7
      /dist/css/bootstra   p.min.css">
      <link rel="stylesheet" href="styles.css">
      <link rel="stylesheet" href="forms.css">
      <!-- Polyfills -->
      <script src="node_modules/core-   
      js/client/shim.min.js"></script>
      <script src="img/zone.js">
      </script>
      <script 
      src="img/system.src.js">
      </script>
      <script src="img/systemjs.config.js"></script>
      <script>
      System.import('main.js').catch(function(err){ 
      console.error(err); 
      });
      </script>
      </head>
      <body>
      <first-model-form>Loading...</first-model-form>
      </body>
      </html>

请注意,<first-model-form/> 特殊标记被添加到正文中。此标记将在运行时更新为实际模板。还要注意,使用 System.js 模块加载器在运行时加载所需的库。systemjs.config.js 文件应该包含有关如何映射 npm 包和我们应用程序的起始点的指令。在这里,我们的应用程序在 main.ts 中启动,在构建应用程序后,它将被转译为 main.jssystemjs.config.js 的内容如下:

/**
* System configuration for Angular samples
* Adjust as necessary for your application needs.
*/
(function (global) {
System.config({
paths: {
// paths serve as alias
'npm:': 'node_modules/'
},
// map tells the System loader where to look for things
map: {
// our app is within the app folder
'app': 'app',
// angular bundles
'@angular/animations': 'npm:@angular/animations/bundles/animations.umd.js',
'@angular/animations/browser': 'npm:@angular/animations/bundles/animations-browser.umd.js',
'@angular/core': 'npm:@angular/core/bundles/core.umd.js',
'@angular/common': 'npm:@angular/common/bundles/common.umd.js',
'@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js',
'@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js',
'@angular/platform-browser/animations': 'npm:@angular/platform-browser/bundles/platform-browser-animations.umd.js',
'@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js',
'@angular/http': 'npm:@angular/http/bundles/http.umd.js',
'@angular/router': 'npm:@angular/router/bundles/router.umd.js',
'@angular/router/upgrade': 'npm:@angular/router/bundles/router-upgrade.umd.js',
'@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js',
'@angular/upgrade': 'npm:@angular/upgrade/bundles/upgrade.umd.js',
'@angular/upgrade/static': 'npm:@angular/upgrade/bundles/upgrade-static.umd.js',
// other libraries
'rxjs': 'npm:rxjs',
'angular-in-memory-web-api': 'npm:angular-in-memory-web-api/bundles/in-memory-web-api.umd.js'
},
// packages tells the System loader how to load when no filename and/or no extension
packages: {
app: {
main: './main.js',
defaultExtension: 'js',
meta: {
'./*.js': {
loader: 'systemjs-angular-loader.js'
}
},
rxjs: {
defaultExtension: 'js'
}
}
});
})(this);
  1. 现在,我们已经拥有了所需的一切。按下 F5 运行应用程序,索引页面将以BookFormComponent为模板进行渲染,如下所示:

模型驱动表单的输出

在 Chrome 浏览器的开发者工具中保持控制台窗口打开的情况下,单击 提交 按钮。请注意,日志记录模型对象是表单有效值为 false,因为作者属性缺少值。

现在,让我们在作者属性中输入一些值,并在 Chrome 浏览器的开发者工具中保持控制台窗口打开的情况下,单击提交 按钮。请注意,模型对象与填充了值的所有必需属性的表单有效值都被记录如下:

检查模型驱动表单提交

当我们使用 FormGroup 在组件中配置验证时,我们将验证逻辑从模板松散耦合移动到了组件中。所以,我们可以使用任何测试框架编写测试方法来通过断言组件来验证验证逻辑。参考第八章,测试 Angular 应用 来了解如何测试 Angular 应用。

管道


在 Angular 中,管道是 AngularJS 1.x 中过滤器的替代品。管道是过滤器的改良版本,可以转换常见数据。大多数应用程序都会从服务器获取数据,并在在前端显示数据之前对其进行转换。在这种情况下,管道在渲染模板时非常有用。Angular 为此提供了这些强大的管道 API。管道将数据作为输入,并根据需要输出转换后的数据。

常用的管道

以下是 @angular/core 中提供的内置管道,并将看到一些带有示例的管道:

  • AsyncPipe

  • CurrencyPipe

  • DatePipe

  • DecimalPipe

  • I18nPluralPipe

  • I18nSelectPipe

  • JsonPipe

  • LowerCasePipe

  • PercentPipe

  • SlicePipe

  • TitleCasePipe

  • UpperCasePipe

带参数的管道

我们可以通过冒号(:)符号向管道传递参数,如下所示:

<p>Price of the book is {{ price | currency:'USD' }} </p>

通过(:)分隔的方式可以将多个输入传递给管道,如下所示:

<li *ngFor="let book of books | slice:1:3">{{i}}</li>

管道链

在某些情况下,可能需要使用多个管道。例如,考虑一种情况,需要以大写形式和长日期格式显示数据。以下代码以大写形式和长日期格式显示书籍的出版日期:

Publishing Date: {{ pubDate | date | uppercase}}

货币管道

货币管道将数字格式化为所需的货币格式。这是货币管道的语法:

expression | currency[:currencyCode[:symbolDisplay[:digitInfo]]] 

expression是管道的输入数据;currency是管道的关键词,它接受三个参数,分别为currencyCode,取值为 USD、INR、GBP 和 EUR,symbolDisplay,接受 true 或 false 来显示/隐藏货币符号,以及digitInfo,用于货币的小数格式。以下模板演示了如何使用货币管道:

实现货币管道的模板

对于各种货币格式,模板的输出如下:

使用货币管道的输出

日期管道

日期管道将输入数据转换为日期管道支持的各种日期格式。日期管道的语法如下:

expression | date[:format] 

假设组件中的dateData被赋予了Date.now()。在模板中实现日期管道的方式如下截图所示:

实现日期管道的模板

应用各种日期格式后的模板输出如下:

使用日期管道的输出

日期管道支持各种格式,如medium(yMMMdjms)、short(yMdjm)、mediumDate(yMMMd)、shortDate(yMd)、fullDate(yMMMMEEEEd)、longDate(yMMMMd)、mediumTime(jms)和shortTime(jm)。

大写和小写管道

大写和小写管道将输入数据分别转换为大写和小写。以下模板同时显示作者姓名的大写和小写形式:

实现大写和小写管道的模板

此模板的输出如下:

实现大写和小写管道的输出

JSON 管道

JSON 管道类似于在 JavaScript 中应用 JSON.Stringify 对持有 JSON 值的对象进行操作。模板中使用 JSON 管道的用法如下截图所示:

实现 JSON 管道的模板

在模板中使用 JSON 管道的输出如下所示:

使用 JSON 管道的输出

AppComponent


AppComponent是一个应用程序的组件,其配置为根组件,并且处理app.component.htmlÂ模板的渲染。在前面的章节中,我们看到了实现各种管道及其各自输出的模板代码。以下代码片段显示了模板的组件:

import { Component } from '@angular/core'; 

@Component({ 
  selector: 'pipe-page', 
  templateUrl: 'app/app.component.html' 
}) 
export class AppComponent { 
    numberData : number; 
    currencyData : number; 
    dateData : number; 
    authorName : string; 
    object: Object = {autherName: 'Rajesh Gunasundaram',   
    pubName: 'Packt Publishing'} 
    constructor() { 
        this.numberData = 123.456789; 
        this.currencyData = 50; 
        this.dateData = Date.now(); 
        this.authorName = 'rAjEsH gUnAsUnDaRaM'; 
    } 
} 

管道,这是 Angular 提供的非常强大且易于使用的 API,能够在显示在屏幕上之前格式化数据,这极大地简化了我们的流程。

路由器


AngularJS 使用ngRoute模块来运行具有基本功能的简单路由器。它通过将路径映射到使用$routeProvider服务配置的路由来使 URL 与组件和视图进行深度链接。AngularJS 1.x 需要安装ngRoute模块才能在应用中实现路由。

Angular 引入了一个组件路由器,用于深度链接 URL 请求并导航到模板或视图。如果有任何参数,它会将其传递给标注为该路线的相应组件。

组件路由的核心概念

Angular 使用一个组件路由器作为视图系统。它还适用于 AngularJS 1.x。它支持拦截路由并为加载的组件提供特定路由值,自动深度链接,嵌套和同级路由。让我们来看一下组件路由器的一些核心功能。

设置组件路由器

组件路由器不是核心 Angular 框架的一部分。它作为单独库@angular/router的一部分出现在 Angular NPM 包中。我们需要将@angular/router添加到packages.json中的依赖项部分。然后,在app.routing.ts中,我们需要从@angular/router中导入RoutesRouterModule。路由器包括诸如RouterOutletRouterLinkRouterLinkActive这样的指令,一个RouterModule服务和Routes的配置。

<base> tag with the href attribute that is to be added to the head tag in the index file, considering that the app folder is the root of the application. This is required when you run your application in HTML5 mode. It helps resolve all the relative URLs in the application:
<base href="/"> 

配置路由

app.module.ts:
import { RouterModule } from '@angular/router';
RouterModule.forRoot([
{
  path: 'about',
  component: AboutComponent
},
{
  path: 'contact',
  component: ContactComponent
}
])

在这里,我们配置了两个路由,帮助用户在单击时转到aboutcontact视图。路由基本上是路由定义的集合。所定义的路径值标识出匹配路径的 URL 时要实例化的组件。然后,实例化的组件将负责渲染视图。

现在,我们需要将配置的路由添加到AppModule中,从@angular/router中导入RouterModule,并将其添加到@NgModule的 imports 部分中,如下所示:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouterModule } from '@angular/router';
import { AppComponent } from './app.component';
import { AboutComponent } from './heroes.component';
@NgModule({
  imports: [
  BrowserModule,
  FormsModule,
  RouterModule.forRoot([
{
  path: 'about',
  component: AboutComponent
}
])
],
declarations: [
AppComponent,
AboutComponent
],
bootstrap: [ AppComponent ]
})
export class AppModule { }

在这里,forRoot()方法提供了路由器服务提供程序和指令来执行导航。

路由出口和路由链接

当用户将'/about'添加到应用程序 URL 的末尾后,将其传递到浏览器地址栏中时,路由将使用'about'匹配该请求,并启动AboutComponent来处理about视图的渲染。我们需要以某种方式告知路由器在哪里显示此about视图。可以通过指定<router-outlet/>来实现这一点,这类似于 AngularJS 1.x 中的<ng-view/>标记,用于加载与路由相应路径相关的模板。

路由链接可通过单击锚标记中指定的链接来导航到路由 URL。以下是一个示例路由链接标记:

<a [routerLink]="['/about']">About</a>

服务


我们创建的应用程序处理大量的数据。大多数数据将从服务中检索,并且将在应用程序的各个部分重用。让我们创建一个可以使用http检索数据的服务。服务应该与组件松散耦合,因为组件的主要重点应该是支持视图。因此,可以使用依赖注入将服务注入到组件中。这种方法将使我们能够模拟服务以进行单元测试组件。

TodoService is shown here. TodoService has a property named todos of the type array that can hold a collection of Todo items and is hardcoded with the Todo items in the constructor:
import {Injectable} from '@angular/core'; 
import { Todo } from './todo'; 

@Injectable()  
export class TodoService { 
    todos: Array<Todo>; 
    constructor() { 
        this.todos = [ 
    {"title": "First Todo", "completed":  false}, 
    {"title": "Second Todo", "completed": false}, 
    {"title": "Third Todo", "completed": false} 
            ] 
    } 

    getTodos() { 
        return this.todos; 
    } 
} 

请注意,用@Injectable装饰的服务是为了让 Angular 知道这个服务是可注入的。

我们可以将可注入的TodoService注入到AppComponent的构造函数中,如下所示:

import { Component } from '@angular/core'; 
import { Todo } from './Todo'; 
import { TodoService } from './TodoService'; 
@Component({ 
  selector: 'my-service', 
  templateUrl: 'app/app.component.html' 
}) 
export class AppComponent { 
    todos: Array<Todo>; 
    constructor(todoService: TodoService) { 
        this.todos = todoService.getTodos(); 
    } 
} 

在引导过程中,我们还需要传递TodoService,这样 Angular 将创建服务的实例,并在其被注入的任何地方保持可用。因此,让我们在main.ts文件中如所示地向bootstrap函数传递TodoService

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { TodoService } from './TodoService';
@NgModule({
imports: [
BrowserModule,
],
declarations: [
AppComponent,
],
providers: [ TodoService ],
bootstrap: [ AppComponent ]
})
export class AppModule { }

注意,可注入服务用方括号括起来。这是一种应用依赖注入的方法。有关 Angular 依赖注入更多信息,请参考第二章, Angular Building Blocks - Part 1。Angular 已经改进了依赖注入,可以创建TodoService的实例并将其注入到组件中。

app.component.html模板中,我们遍历AppComponenttodos属性的每个项目并列出它们:

<h2>My Todo List</h2> 
<ul> 
    <li *ngFor="let todo of todos"> 
        {{ todo.title }} - {{ todo.completed }} 
    </li> 
</ul> 

此模板的内容将在index.html文件的<my-service>特殊标签下呈现:

 <body> 
        <my-service>Loading...</my-service> 
 </body> 

运行时,应用程序将呈现如下的todo项目清单:

我的待办事项应用程序的输出

可观察对象


在 AngularJS 中,我们使用服务以异步方式使用 $http 中的 promise 获取数据。在 Angular 中,我们有了 Http 服务取代了 $http,它返回一个可观察对象而不是 promise,因为它应用了类似模式。 Angular 利用了从 ReactiveX 库采用的 Observable 类。 ReactiveX 是一个用于应用观察者、迭代器模式和函数式编程完成异步编程的 API。你可以在 reactivex.io/ 找到有关反应式编程的更多信息。

Observer 模式将在依赖对象更改时通知依赖者。迭代器模式将方便地访问集合,无需了解集合中元素的结构。在 ReactiveX 中结合这些模式使观察者能够订阅可观察的集合对象。观察者不需要等到可观察的集合对象可用时才能做出反应,而是在获得可观察对象更改通知时做出反应。

Angular 使用名为 RxJS 的 JavaScript 实现,它是一组库而不是一个特定的 API。它在 HTTP 服务和事件系统中使用 Observables。promise 总是返回一个值。

http.get() 方法将返回 Observables,并且客户端可以订阅以获取从服务返回的数据。 Observables 可以处理多个值。因此,我们还可以调用多个 http.get() 方法,并将它们包装在 Observables 提供的 forkJoin 方法下。

我们还可以控制服务调用并通过 Observable 延迟调用,通过应用一个规则,只有在上次对服务的调用是 500 毫秒前才调用服务。

Observables 是可取消的。所以,我们也可以通过取消订阅来取消之前的请求,并发起新的请求。我们随时可以取消任何之前未完成的调用。

让我们修改 TodoService 以使用 Observable,并将硬编码的 JSON 值替换为对 todos.json 文件的 http.get() 调用。更新后的 TodoService 如下所示:

import {Injectable} from '@angular/core';
import {Http} from '@angular/http';
import 'rxjs/add/operator/toPromise';
@Injectable()
export class TodoService {
constructor(private http: Http) {
this.http = http;
}
getTodos() {
  return this.http.get('/app/todos.json')
  .toPromise()
  .then(response => response.json().data)
  .catch(this.handleError);
}
}

请注意,我们从 @angular/http 中导入了 HTTP 模块、rsjs/Rx 中的响应,以及基于 ReactiveX 的 Observable 模块。getTodos 方法通过调用 todos.json 查询并返回一组待办事项来更新。

AppComponentTodoServiceapp.module.ts 文件中进行了引导,如下所示:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpModule } from '@angular/http';
import { AppComponent } from './app.component';
import { TodoComponent } from './todo.component';
import { TodoService } from './hero.service';
@NgModule({
  imports: [
  BrowserModule,
  HttpModule,
  AppRoutingModule
  ],
  declarations: [
  AppComponent,
  TodoComponent
  ],
  providers: [ TodoService ],
  bootstrap: [ AppComponent ]
})
export class AppModule { }

'@angular/platform-browser-dynamic' 中导入 {bootstrap};模板被更新以渲染待办事项列表,如下所示:

import {HTTP_PROVIDERS} from '@angular/http'; 
import 'rxjs/add/operator/map'; 
import {AppComponent} from './app.component'; 
import {TodoService} from './TodoService';
bootstrap(AppComponent, [HTTP_PROVIDERS, TodoService]); 

运行应用将呈现从 TodoService 中返回的从 Observables 订阅的数据:

从 Observables 订阅的渲染数据的 index.html 输出

总结


哇呜!你已经学习完了 Angular 架构的其余构建模块。我们从表单开始介绍本章,并讨论了 Angular 中可用的表单类型以及如何实现它们。然后,您了解了管道,这是 AngularJS 1.x 中筛选器的替代方案。接下来,我们讨论了路由器,并学习了如何在 Angular 中配置路由器到组件是多么容易。最后,您学会了如何在 Angular 中创建服务以及如何使用 HTTP 模块访问外部服务。您还了解了使用 Observables 的优势以及在服务调用中如何实现它。

在下一章中,我们将讨论 TypeScript 的基础知识。

第四章:使用 TypeScript 与 Angular

本章讨论了 TypeScript 的基本原理以及使用 TypeScript 编写 Angular 应用程序的好处。

在这一章中,我们将涵盖以下主题:

  • 什么是 TypeScript?

  • 基本类型

  • 接口

  • 模块

  • 函数

  • 泛型

  • 装饰器

  • TypeScript 和 Angular

什么是 TypeScript?


TypeScript 是由微软开发和维护的开源编程语言。它是 JavaScript 的超集,并且方便编写面向对象的编程。它应该与 JavaScript 一起编译,以在任何浏览器上运行。

TypeScript 提供了最好的工具和高级的自动完成、导航和重构功能。它用于开发客户端和服务器端的 JavaScript 应用程序。借助类、模块和接口,我们可以使用 TypeScript 构建强大的组件。

它相对于 JavaScript 提供的主要优势在于,它能够在编译时进行类型检查以避免错误。由于类型转换问题,可以避免意外的运行时错误。此外,它提供了写面向对象编程的语法糖。

基本类型


在编程语言中,我们处理各种小单位的数据,比如numberssting和布尔值。TypeScript 支持这些类型的数据,与 JavaScript 一样,支持枚举和结构类型。

布尔值

布尔数据类型可以保存truefalse。声明和初始化这种数据类型非常简单,如下所示:

let isSaved: boolean = false; 

在这里,isSaved变量被声明为boolean类型,并赋值为false。如果开发人员错误地将一个字符串值赋给isSaved变量,TypeScript 会显示错误并突出显示该语句。

数字

数字数据类型保存浮点值。与 JavaScript 类似,TypeScript 将所有数字视为浮点值。声明和初始化数字数据类型变量可以使用以下方法:

let price: number = 101; 

在这里,price变量被声明为number类型,并赋值为 101。Number 类型可以包含十进制、二进制、十六进制和八进制等不同的值,如下所示:

let decimal: number = 6; 
let hex: number = 0xf00d; 
let binary: number = 0b1010; 
let octal: number = 0o744; 

字符串

字符串数据类型可以保存一系列字符。声明和初始化string变量非常简单,如下所示:

let authorName: string = "Rajesh Gunasundaram"; 

在这里,我们声明了一个名为authorName的变量,类型为string,并赋值为"Rajesh Gunasundaram"。TypeScript 支持使用双引号(")或单引号(')括起来的string值。

数组

数组数据类型用于保存特定类型的值的集合。在 TypeScript 中,我们可以以两种方式定义array,如下所示:

var even:number[] = [2, 4, 6, 8, 10]; 

这个语句声明了一个number类型的数组变量,使用number数据类型后的方括号([]),并赋值为从 2 到 10 的一系列偶数。定义数组的第二种方法是这样的:

let even:Array<number> = [2, 4, 6, 8, 10]; 

这个语句使用了泛型的数组类型,它使用了Array关键字后面跟着包裹number数据类型的尖括号(<>)。

枚举

枚举数据类型将具有一组命名的值。我们使用枚举器为识别某些值的常量提供友好名称:

enum Day {Mon, Tue, Wed, Thu, Fri, Sat, Sun}; 
let firstDay: Day = Day.Mon; 

在这里,我们有enum类型Day变量,它包含代表一周中每一天的值的系列。第二个语句展示了如何访问一天中的特定enum值并将其赋值给另一个变量。

任意

Any数据类型是一个可以容纳任何值的动态数据类型。如果将 string 类型的变量赋给整数类型的变量,TypeScript 会抛出编译时错误。如果您不确定变量将持有什么值,并且希望在赋值时免除编译器对类型的检查,您可以使用Any数据类型:

let mixedList:any[] = [1, "I am string", false]; 
mixedList [2] = "no you are not"; 

在这里,我们使用了任意类型的数组,因此它可以容纳任何类型,比如numberstringboolean

任何

Void 实际上是什么都没有。它可用作函数的返回类型,声明这个函数不会返回任何值:

function alertMessage(): void { 
    alert("This function does not return any value"); 
} 

接口


接口是定义类行为的抽象类型。它为可以在客户端之间交换的对象提供类型定义。这使得客户端只能交换符合接口类型定义的对象;否则,我们会得到编译时错误。

在 TypeScript 中,接口定义了您代码内部和项目外部的对象的约束。让我们看一个示例,介绍如何在 TypeScript 中使用:

function addCustomer(customerObj: {name: string}) { 
  console.log(customerObj.name); 
} 
let customer = {id: 101, name: "Rajesh Gunasundaram"}; 
addCustomer(customer); 

类型检查器验证了addCustomer方法调用并检查了其参数。addCustomer期望一个具有string类型的name属性的对象。然而,调用addCustomer的客户端传递了一个具有idname两个参数的对象。

然而,编译器会忽略对id属性的检查,因为它不在addCustomer方法的参数类型中。对于编译器来说,重要的是所需的属性是否存在。

让我们演示将interface作为参数类型重写方法:

interface Customer { 
  name: string; 
} 
function addCustomer(customerObj: Customer) { 
  console.log(customerObj.name); 
}  
let customer = {id: 101, name: "Rajesh Gunasundaram"}; 
addCustomer(customer); 
Customer interface. It only looks for the name property of the string type in the parameter and then allows it if present.

可选属性

在某些情况下,我们可能只想为最小的参数传递值。在这种情况下,我们可以将接口中的属性定义为可选属性,如下所示:

interface Customer { 
  id: number; 
  name: string; 
  bonus?: number; 
}  
function addCustomer(customer: Customer) {  
  if (customer.bonus) { 
    console.log(customer.bonus); 
  } 
}  
addCustomer({id: 101, name: "Rajesh Gunasundaram"}); 

在这里,bonus属性通过在name属性末尾添加问号(?)来定义为可选属性。

函数类型接口

我们刚刚看到了如何在接口中定义属性。类似地,我们也可以在接口中定义函数类型。我们可以通过给出带有返回类型的函数签名来在接口中定义函数类型。请注意,在下面的代码片段中,我们没有添加函数名:

interface AddCustomerFunc { 
  (firstName: string, lastName: string); void 
} 

现在,AddCustomerFunc准备好了。让我们定义一个函数类型变量,AddCustomerFunc,并将具有相同签名的函数赋值给它,如下所示:

let addCustomer: AddCustomerFunc; 
addCustomer = function(firstName: string, lastName: string) { 
  console.log('Full Name: ' + firstName + ' ' + lastName); 
} 

函数签名中的参数名可以变化,但数据类型不能变化。例如,我们可以修改字符串类型的fnln函数参数,如下所示:

addCustomer = function(fn: string, ln: string) {
console.log('Full Name: ' + fn + ' ' + ln);
} 

因此,如果我们改变此处参数的数据类型或函数的返回类型,编译器将抛出关于参数不匹配或返回类型不匹配AddCustomerFunc接口的错误。

数组类型接口

我们还可以为数组类型定义一个接口。我们可以指定index数组的数据类型和数组项的数据类型,如下所示:

interface CutomerNameArray { 
  [index: number]: string; 
}  
let customerNameList: CutomerNameArray; 
customerNameList = ["Rajesh", "Gunasundaram"]; 

TypeScript 支持numberstring两种类型的index。此数组类型接口还强制数组的返回类型与声明匹配。

类类型接口

类型接口定义了类的约定。实现接口的类应满足接口的要求:

interface CustomerInterface { 
    id: number; 
    firstName: string; 
    lastName: string; 
    addCustomer(firstName: string, lastName: string); 
    getCustomer(id: number): Customer; 
}  
class Customer implements CustomerInterface { 
    id: number; 
    firstName: string; 
    lastName: string; 
    constructor() { } 
    addCustomer(firstName: string, lastName: string): void { 
        // code to add customer 
    } 
    getCustomer(id: number): Customer { 
        // code to return customer where the id match with id parameter 
    } 
}  

类类型接口只处理类的公共成员。因此,不可能向接口添加私有成员。

扩展接口

接口可以进行扩展;扩展一个接口使其共享另一个接口的属性,如下所示:

interface Manager { 
    hasPower: boolean; 
}
interface Employee extends Manager { 
    name: string; 
} 
let employee = <Employee>{}; 
employee.name = "Rajesh Gunasundaram"; 
employee.hasPower = true; 

这里,Employee接口扩展了Manager接口,并将hasPowerEmployee接口共享。

混合类型接口

当我们希望将对象既作为函数又作为对象使用时,就会使用混合类型接口。如果对象实现了混合类型接口,我们可以像调用函数一样调用对象,或者我们可以将其作为对象使用并访问其属性。这种类型的接口使您能够将一个接口用作对象和函数,如下所示:

interface Customer { 
    (name: string): string; 
    name: string; 
    deleteCustomer(id: number): void; 
} 
let c: Customer; 
c('Rajesh Gunasundaram'); 
c.name = 'Rajesh Gunasundaram'; 
c.deleteCustomer(101); 

Classes


类是一个可扩展的模板,用于创建具有成员变量以保存对象状态和处理对象行为的成员函数的对象。

当前版本的 JavaScript 仅支持基于函数和基于原型的继承来构建可重用组件。JavaScript 的下一个版本 ECMAScript 6 支持面向对象编程,通过添加原型化类定义和继承的语法糖。然而,TypeScript 使开发人员能够使用面向对象编程技术编写代码,并将代码编译为与所有浏览器和平台兼容的 JavaScript:

class Customer { 
    name: string; 
    constructor(name: string) { 
        this.name = name; 
    } 
    logCustomer() { 
        console.log('customer name is ' + this.name); 
    } 
}  
let customer = new Customer("Rajesh Gunasundaram"); 

Customer类有三个成员:name属性、构造函数和logCustomer方法。Customer类外的最后一条语句使用new关键字创建customer类的一个实例。

Inheritance


继承是指继承另一个类或对象的一些行为的概念。它有助于实现代码的可重用性,并建立类或对象之间的关系层次结构。此外,继承可以帮助您对相似的类进行强制转换。

ES5 标准的 JavaScript 不支持类,因此在 JavaScript 中无法进行类继承。但是,我们可以通过原型继承来实现类继承。让我们看看 ES5 中的继承示例。

首先,创建一个名为Animal的函数,如下所示。在这里,我们创建一个名为Animal的函数,其包含两个方法:sleepeat

var Animal = function() { 
    this.sleep = function() { 
       console.log('sleeping'); 
    } 
    this.eat = function() { 
       console.log('eating'); 
    } 
} 

现在,让我们使用原型扩展这个Animal函数,如下所示:

Animal.prototype.bark = function() { 
    console.log('barking'); 
} 

现在,我们可以创建Animal的实例并调用扩展函数 bark,如下所示:

var a = new Animal(); 
a.bark(); 

我们可以使用Object.Create方法来克隆父级原型并创建一个子对象。然后,我们可以通过添加方法来扩展子对象。让我们创建一个名为Dog的对象,并从Animal继承它:

var Dog = function() { 
    this.bark = new function() { 
       console.log('barking'); 
    } 
} 

现在,让我们克隆Animal的原型,并继承Dog函数中的所有行为。然后,我们可以使用Dog实例调用Animal方法,如下所示:

Dog.prototype = Object.create(animal.prototype); 
var d = new Dog(); 
d.sleep(); 
d.eat(); 

TypeScript 中的继承

我们刚刚看到了如何使用原型在 JavaScript 中实现继承。现在,我们将看到如何在 TypeScript 中实现继承。

在 TypeScript 中,类接口可以扩展,而且我们也可以通过继承另一个类来扩展一个类,如下所示:

class SimpleCalculator { 
    z: number; 
    constructor() { } 
    addition(x: number, y: number) { 
        z = x + y; 
    } 
    subtraction(x: number, y: number) { 
        z = x - y; 
    } 
}  
class ComplexCalculator extends SimpleCalculator { 
    constructor() { super(); } 
    multiplication(x: number, y: number) { 
        z = x * y; 
    } 
    division(x: number, y: number) { 
        z = x / y; 
    } 
} 
var calculator = new ComplexCalculator(); 
calculator.addition(10, 20); 
calculator.Substraction(20, 10); 
calculator.multiplication(10, 20); 
calculator.division(20, 10); 

在这里,我们能够通过扩展SimpleCalculator的实例来访问SimpleCalculator的方法,因为ComplexCalculator扩展了SimpleCalculator

私有/公共修饰符

在 TypeScript 中,类中的所有成员默认都是public的。我们必须明确添加private关键字来控制成员的可见性:

class SimpleCalculator { 
    private x: number; 
    private y: number; 
    z: number; 
    constructor(x: number, y: number) { 
       this.x = x; 
       this.y = y; 
    } 
    addition() { 
        z = x + y; 
    } 
    subtraction() { 
        z = x - y; 
    } 
} 
class ComplexCalculator { 
    z: number; 
    constructor(private x: number, private y: number) { } 
    multiplication() { 
        z = this.x * this.y; 
    } 
    division() { 
        z = this.x / this.y; 
    } 
} 

请注意,在SimpleCalculator类中,我们将xy定义为private属性,这将不会在类外可见。在ComplexCalculator中,我们使用参数属性定义了xy。这些参数属性将使我们能够在一个语句中创建和初始化成员。在这里,xy在构造函数中创建并初始化,而不需要在其中写任何其他语句。同时,xy是私有的,以便将它们隐藏起来以避免被外部类或模块访问。

访问器

我们还可以对属性实现getterssetters,以控制客户端对它们的访问。我们可以在设置属性变量的值之前或获取属性变量值之前拦截一些过程:

var updateCustomerNameAllowed = true; 
Class Customer { 
    Private _name: string; 
    get name: string { 
          return this._name; 
    } 
    set name(newName: string) { 
          if (updateCustomerNameAllowed == true) { 
                this._name = newName; 
          } 
          else { 
                alert("Error: Updating Customer name not allowed!"); 
          } 
    } 
} 

这里,name属性的setter确保顾客的name可以更新。否则,它将显示一个不可能的警报消息。

静态属性

这些类型的属性不是特定于实例的,并且通过类名而不是使用this关键字来访问:

class Customer { 
     static bonusPercentage = 20; 
     constructor(public salary: number) {  } 
      calculateBonus() { 
          return this.salary * Customer.bonusPercentage/100; 
     } 
} 
var customer = new Customer(10000); 
var bonus = customer.calculateBonus(); 

在这里,我们声明了一个static变量bonusPercentage,在calculateBonus方法中使用Customer类名访问它。bonusPercentage属性不是特定于实例的。

模块


JavaScript 是一种强大而动态的语言。由于根据 ES5 和更早的标准在 JavaScript 中进行动态编程的自由,我们有责任结构化和组织代码。这将使代码的可维护性更容易,并且还可以使我们轻松地定位特定功能的代码。我们可以通过应用模块化模式来组织代码。代码可以分为各种模块,并且相关代码可以放在每个模块中。

TypeScript 使得按照 ECMAScript 6 规范实现模块化编程变得更容易。模块使您能够控制变量的范围,代码的重用性和封装性。TypeScript 支持两种类型的模块:内部和外部。

命名空间

我们可以使用 namespace 关键字在 TypeScript 中创建命名空间,如下所示。在命名空间下定义的所有类都将在该特定命名空间下使用,并且不会附加到全局范围下:

namespace Inventory { 
      Class Product { 
             constructor (public name: string, public quantity: number) {   } 
      } 
      // product is accessible 
      var p = new Product('mobile', 101); 
} 
// Product class is not accessible outside namespace 
var p = new Inventory.Product('mobile', 101); 

要使 Product 类在命名空间外部可用,我们在定义 Product 类时需要添加 export 关键字,如下所示:

namespace Inventory { 
      export Class Product { 
             constructor (public name: string, public quantity: number) {   } 
      } 
} 
// Product class is now accessible outside Inventory namespace 
var p = new Inventory.Product('mobile', 101); 

我们也可以通过在引用文件的代码开头添加 reference 语句来跨文件共享命名空间,如下所示:

/// <reference path="Inventory.ts" /> 

模块

TypeScript 也支持模块。由于我们处理大量的外部 JavaScript 库,这个模块将帮助我们引用和组织我们的代码。使用 import 语句,我们可以导入外部模块,如下所示:

Import { inv } from "./Inventory"; 
var p = new inv.Product('mobile', 101); 

在这里,我们只是导入了先前创建的模块 Inventory,并创建了一个分配给变量 pProduct 实例。

函数

遵循 ES5 规范的 JavaScript 不支持类和模块。但是,我们尝试使用 JavaScript 中的函数式编程来实现变量的作用域和模块化。函数是 JavaScript 应用程序的构建块。

尽管 TypeScript 支持类和模块,但函数在定义特定逻辑方面起着关键作用。我们可以在 JavaScript 中定义function匿名函数,如下所示:

//Named function 
function multiply(a, b) { 
    return a * b; 
} 
//Anonymous function 
var result = function(a, b) { return a * b; }; 

在 TypeScript 中,我们使用函数箭头符号定义参数的类型和返回类型,这也适用于 ES6;表示如下:

var multiply(a: number, b: number) => number =  
          function(a: number, b: number): number { return a * b; }; 

可选和默认参数

假设我们有一个具有三个参数的函数,并且有时在函数中可能只为前两个参数传递值。在 TypeScript 中,我们可以使用可选参数处理这种情况。我们可以将前两个参数定义为正常参数,将第三个参数定义为可选参数,如下面的代码片段所示:

function CutomerName(firstName: string, lastName: string, middleName?: string) { 
    if (middleName) 
        return firstName + " " + middleName + " " + lastName; 
    else 
        return firstName + " " + lastName; 
} 
//ignored optional parameter middleName 
var customer1 = customerName("Rajesh", "Gunasundaram");  
//error, supplied too many parameters  
var customer2 = customerName("Scott", "Tiger", "Lion", "King");   
//supplied values for all 
var customer3 = customerName("Scott", "Tiger", "Lion");   

在这里,middleName 是可选参数,在调用函数时可以忽略它。

现在,让我们看看如何在函数中设置默认参数。如果没有为参数提供值,我们可以定义它为配置的默认值:

function CutomerName(firstName: string, lastName: string, middleName: string = 'No Middle Name') { 
    if (middleName) 
        return firstName + " " + middleName + " " + lastName; 
    else 
        return firstName + " " + lastName; 
} 

在这里,middleName是默认参数,如果调用者未提供值,则默认值为'No Middle Name'

剩余参数

使用剩余参数,可以将值的数组传递给函数。这在您不确定将向函数提供多少值的场景中可以使用:

function clientName(firstClient: string, ...restOfClient: string[]) { 
   console.log(firstClient + " " + restOfClient.join(" ")); 
} 
clientName ("Scott", "Steve", "Bill", "Sergey", "Larry"); 

在这里,注意restOfClient剩余参数前缀带有省略号(...),它可以保存一个字符串数组。在函数的调用者中,只有提供的第一个参数的值将被赋给firstClient参数,并且剩余的值将被分配给restOfClient作为数组值。

泛型


当开发可对抗任何数据类型的可重用组件时,泛型非常有用。因此,使用此组件的客户端将决定它应该对哪种类型的数据进行操作。让我们创建一个简单的函数,该函数返回传递给它的任何数据类型:

function returnNumberReceived(arg: number): number { 
    return arg; 
} 
function returnStringReceived(arg: string): string { 
    return arg; 
} 

正如你所见,我们需要个别方法来处理每种数据类型。我们可以使用any数据类型在单个函数中实现相同的功能,如下所示:

function returnAnythingReceived (arg: any): any { 
    return arg; 
} 

这与泛型类似。然而,我们对返回类型没有控制。如果我们传递一个数字,并且无法预测函数是否会返回该数字,函数的返回类型可以是任意类型。

泛型提供了T类型的特殊变量。将此类型应用到函数中,如所示,使客户端能够传递他们希望该函数处理的数据类型:

function returnWhatReceived<T>(arg: T): T { 
    return arg; 
} 

因此,客户端可以为各种数据类型调用此函数,如所示:

var stringOutput = returnWhatReceived<string>("return this"); // type of output will be 'string' 
var numberOutput = returnWhatReceived<number>(101); // type of output will be number 

请注意,在函数调用中,应该将要处理的数据类型通过尖括号(<>)进行包裹传递。

泛型接口

我们还可以使用类型变量T来定义泛型接口,如下所示:

interface GenericFunc<T> { 
    (arg: T): T; 
} 
function func<T>(arg: T): T { 
    return arg; 
} 
var myFunc: GenericFunc<number> = func; 

在这里,我们定义了一个泛型接口和myFunc变量的GenericFunc类型,将数字数据类型传递给类型变量T。然后,将该变量分配给名为func的函数。

泛型类

与泛型接口类似,我们也可以定义泛型类。我们使用尖括号(<>)中的泛型类型来定义类,如下所示:

class GenericClass<T> { 
    add: (a: T, b: T) => T; 
}  
var myGenericClass = new GenericClass<number>(); 
myGenericClass.add = function(a, b) { return a + b; }; 

在这里,通过将泛型数据类型传递为number,实例化了泛型类。因此,add函数将处理并加上作为参数传递的两个数字。

装饰器


装饰器使我们能够通过添加行为来扩展类或对象,而无需修改代码。装饰器用额外功能包装类。它们可以附加到类、属性、方法、参数和访问器。在 ECMAScript 2016 中,装饰器被提议用于修改类的行为。装饰器用@符号和解析为在runtime调用的函数的装饰器名称进行前缀。

@authorize decorator on any other class:
function authorize(target) { 
    // check the authorization of the use to access the "target" 
} 

类装饰器

类装饰器是在类声明之前声明的。类装饰器可以通过应用于该类的构造函数来观察、修改和替换类的定义。TypeScript 中ClassDecorator的签名如下所示:

declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void; 

假设有一个Customer类,我们希望该类被冻结。其现有属性不应被移除,也不应添加新属性。

我们可以创建一个单独的类,可以接受任何对象并将其冻结。然后,我们可以使用@freezed装饰器来装饰Customer类,以防止从类中添加新属性或移除现有属性:

@freezed 
class Customer {  
  public firstName: string; 
  public lastName: string; 
  constructor(firstName : string, lastName : string) {  
    this.firstName = firstName; 
    this.lastName = lastName; 
  } 
} 
freezed decorator:
function freezed(target: any) { 
    Object.freeze(target); 
} 

在这里,freezed装饰器获取target,即正在被装饰的Customer类,并在执行时将其冻结。

方法装饰器

方法装饰器是在方法声明之前声明的。此装饰器用于修改、观察或替换方法定义,并且应用于方法的属性描述符。下面的示例代码显示了一个应用了方法装饰器的简单类:

class Hello { 
    @logging 
    increment(n: number) { 
        return n++; 
    } 
} 
logging function:
function logging(target: Object, key: string, value: any) { 
            value = function (...args: any[]) { 
            var result = value.apply(this, args); 
            console.log(JSON.stringify(args)) 
            return result; 
        } 
    }; 
} 

方法装饰器函数接受三个参数:targetkeyvaluetarget参数保存了正在被装饰的方法;key保存了被装饰方法的名称,value是指定对象上存在的特定属性的属性描述符。

当调用increment方法时,logging装饰器被调用,并且values参数被传递给它。logging方法将在控制台上记录有关传递的参数的详细信息。

访问器装饰器

Accessor decorators are prefixed before the accessor declaration. These decorators are used to observe, modify, or replace an accessor definition and are applied to the property descriptor. The following code snippet shows a simple class with the accessor decorator applied:

class Customer {  
  private _firstname: string; 
  private _lastname: string; 
  constructor(firstname: string, lastname: string) { 
        this._firstname = firstname; 
        this._lastname = lastname; 
  } 
  @logging(false) 
  get firstname() { return this._firstname; } 
  @logging(false) 
  get lastname() { return this._lastname; } 
} 
@logging decorator:
function logging(value: boolean) { 
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { 
        descriptor.logging = value; 
    }; 
} 

logging函数将Boolean值设置为logging属性描述符。

属性装饰器

属性装饰器是前缀到属性声明。在 TypeScript 源代码中,PropertyDecorator的签名是这样的:

declare type PropertyDecorator = (target: Object, propertyKey: string | symbol) => void; 
firstname property is decorated with the @hashify property decorator:
class Customer {  
  @hashify 
  public firstname: string; 
  public lastname: string; 
  constructor(firstname : string, lastname : string) {  
    this.firstname = firstname; 
    this.lastname = lastname; 
  } 
} 
@hashify property decorator function:
function hashify(target: any, key: string)
 { 
  var _value = this[key];
  var getter = function ()
    { 
        return '#' + _value; 
    }; 
  var setter = function (newValue)
   { 
      _value = newValue; 
    }; 
  if (delete this[key])
 { 
    Object.defineProperty(target, key,
 { 
      get: getter, 
      set: setter, 
      enumerable: true, 
      configurable: true 
    }); 
  } 
} 

_value变量保存了被装饰的属性的值。gettersetter函数都可以访问_value变量,在这里,我们可以通过添加额外行为来操纵_value变量。我在getter中连接了#来返回标记的名字。然后,使用delete操作符从类原型中删除原始属性。然后,一个新属性将会被创建,拥有原始属性名和额外的行为。

参数装饰器

参数装饰器是前缀到参数声明,并且它们应用于类构造函数或方法声明的函数。这是ParameterDecorator的签名:

declare type ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number) => void; 

现在,让我们定义Customer类,并使用参数装饰器来装饰一个参数,以使其必需,并验证其是否被提供:

class Customer { 
    constructor() {    } 
    getName(@logging name: string) { 
        return name; 
    } 
} 

在这里,name 参数已经被 @logging 装饰器修饰。参数装饰器隐式接收三个输入,即带有该装饰器的类的原型,带有该装饰器的方法的名称,以及被装饰的参数的索引。参数装饰器 logging 的实现如下所示:

function logging(target: any, key : string, index : number) {  
  console.log(target); 
  console.log(key); 
  console.log(index); 
} 

在这里,target 是带有装饰器的类,key 是函数名,index 包含参数索引。这段代码仅将 targetkeyindex 记录在控制台中。

TypeScript 和 Angular


正如你在本章中所见,TypeScript 具有强大的类型检查能力,并支持面向对象编程。由于这些优势,Angular 团队选择了 TypeScript 来构建 Angular。Angular 完全重写了核心代码,使用 TypeScript,并且它的架构和编码模式完全改变了,就像你在 第二章 和 第三章 中看到的,Angular 基本构件部分 1Angular 基本构件部分 2。因此,使用 TypeScript 编写 Angular 应用是最佳选择。

我们可以在 Angular 中实现类似 TypeScript 中的模块。Angular 应用中的组件实际上是一个带有 @Component 装饰器的 TypeScript 类。使用 import 语句可以将模块导入到当前的类文件中。export 关键字用于指示该组件可以在另一个模块中被导入和访问。使用 TypeScript 开发的示例组件代码如下所示:

import {Component} from '@angular/core' 
@Component({ 
  selector: 'my-component', 
  template: '<h1>Hello my Component</h1>' 
}) 
export class MyComponent { 
  constructor() {  } 
} 

总结


Voila! 现在你已经学会了 TypeScript 语言的基础知识。我们首先讨论了 TypeScript 是什么以及它的优势。然后,你学习了 TypeScript 中各种数据类型,并附有示例。我们还深入讲解了 TypeScript 中的面向对象编程和接口、类、模块、函数和泛型,并提供了示例。接下来,你学习了各种类型的装饰器及其实现方法,并给出了示例。最后,我们看到了为什么我们应该使用 TypeScript 来编写 Angular 应用以及使用 TypeScript 编写 Angular 应用的好处。

在下一章中,我们将讨论如何使用 Visual Studio 创建 Angular 单页面应用程序。

第五章:在 Visual Studio 中创建 Angular 单页应用程序

本章将指导您通过使用 Visual Studio 创建 Angular 单页应用程序SPA)的过程。

在本章中,我们将涵盖以下主题:

  • 创建一个 ASP.NET Core web 应用程序

  • 使用 NPM 软件包管理器添加客户端软件包

  • 使用 Gulp 运行任务

  • 添加 Angular 组件和模板

创建一个 ASP.NET Core web 应用程序


让我们从创建 ASP.NET Core web 应用程序开始这一章。我假设您在开发环境中已经安装了 Visual Studio 2017 或更新版本。按照以下步骤创建应用程序:

  1. 打开 Visual Studio,然后通过导航到 File | New | Project 来点击菜单项。

  2. 从安装模板中导航到 Visual C#,然后选择 Web

  3. 然后,选择 ASP.NET Core Web Application 并输入应用程序名称为 My Todo,如下图所示:

创建名为 My Todo 的项目

  1. 选择 ASP.NET Core Empty 模板,然后点击 Ok 创建项目,如图所示:

选择一个空的 ASP.NET Core 模板

我们创建的 My Todo 应用程序的解决方案结构如下图所示:

My Todo 的默认解决方案结构

Startup 类是 ASP.NET Core web 应用程序的入口点。 Startup 类中的 Configure 方法用于设置用于处理应用程序中所有请求的请求管道。在这里,Startup 类的默认代码被配置为默认返回 Hello World! 文本:

Startup 类的默认代码

所以,当您运行应用程序时,您将在浏览器中获得以下输出:

'My Todo' 项目的默认输出

现在,让我们让应用程序为任何请求提供默认页面。按照以下步骤进行操作:

  1. 选择 My Todo 项目下的 wwwroot 文件夹。右键单击选择项目,转到 Add,然后点击 New Item

转到添加新项目菜单

  1. Add New Item 窗口中,点击 Web 下的 Content,然后从中心窗格中选择 HTML Page。输入 index.html 作为文件名,然后点击 Add

将 HTML 文件命名为 index.html

  1. 更新 index.html 文件的内容如下:

index.html 的更新代码

  1. 打开 Startup 类并删除以下代码片段,该代码片段将 Hello World 默认文本写入每个请求的响应中:
      app.Run(async (context) =>   
      {   
            await   context.Response.WriteAsync("Hello  
            World!");   
      });   
  1. 将以下代码添加到 Configure 方法中,以便管道为请求提供默认和静态文件:

启用管道为静态和默认文件提供服务的代码

  1. 你需要添加 Microsoft.AspNetCore.StaticFiles NuGet 软件包,如图所示,以使用这些扩展:

如有需要,请向命名空间添加引用

  1. 现在,在 wwwroot 文件夹下添加一个 index.html 文件,并通过按下 F5 运行应用程序。你会注意到应用程序为请求提供了 index.html 文件作为默认文件。在这里,我已经添加了一个内容为 My Todo Landing Pageh1 标签:

在添加了 index.html 后的应用程序输出

使用 NPM 软件包管理器添加客户端软件包


在我们开发应用程序时,我们将很多框架和库添加为依赖项的引用。在 Visual Studio 中,我们有 NuGet 软件包管理工具来管理应用程序中的所有这些软件包。

在前端网络社区中,使用 Bower、Grunt、Gulp 和 NPM 来管理软件包和运行构建任务,开发现代 Web 应用已经变得广泛流行。由于这个生态系统非常丰富并且得到了广泛接受,Visual Studio 2015 已经采用了这些系统来管理客户端框架和库,如图所示。NuGet 是管理服务器端软件包的理想选择:

各种软件包管理工具

我们看到如何在 Visual Studio Code 中使用 NPM 管理客户端软件包。类似地,我们在 Visual Studio 2015 或更高版本中使用 NPM 来管理项目中的前端框架和库。让我们通过以下步骤使用 NPM 将 Angular 框架和其他所需的 JavaScript 库作为项目的依赖项添加到我们的项目中:

  1. 首先,让我们向我们的项目添加 NPM 配置文件。右键单击项目节点,导航到 Add | New Item。从左侧窗格中选择 Web 下的 General,并且从中间窗格选择 NPM 配置文件

然后,点击 Add,将默认名称保留为 package.json:

名为 package.json 的 NPM 配置文件

package.json 文件将被添加到您的项目中,其默认 JSON 代码如下:

package.json 的代码片段

  1. name 字段更新为 my-todo 并将所需的依赖项添加到 package.json 文件中,如图所示:
        "version": "1.0.0",   
        "name": "my-todo",   
        "private": true,   
        "dependencies":
        {   
          "@angular/common": "~4.0.0",   
          "@angular/compiler": "~4.0.0",   
          "@angular/core": "~4.0.0",   
          "@angular/forms": "~4.0.0",   
          "@angular/platform-browser": "~4.0.0",   
          "@angular/platform-browser-dynamic":   "~4.0.0",   

          "systemjs": "0.19.40",   
          "core-js": "².4.1",   
          "rxjs": "5.0.1",   
          "zone.js": "⁰.8.4"   
        },   
          "devDependencies": 
        {   
          "@types/node": "⁶.0.46",   
          "typescript": "~2.1.0"   
        }   
      }   
  1. 当我们保存了带有所有依赖信息的 package.json 文件时,Visual Studio 将所需的软件包添加到我们的项目下的 node_modules 隐藏文件夹中,你可以通过导航到 Dependencies 节点下的 npm 文件夹来查看加载的依赖项列表,如下图所示:

具有依赖库的 NPM 文件夹

我们的项目依赖节点中已经有了我们需要的所有客户端框架和库。但是,我们需要将依赖库添加到我们的 wwwroot 文件夹中,以便我们的应用程序引用和消耗。我们将在下一节中讨论这一点。

使用 Gulp 运行任务


Gulp 是一个在node.js上运行的任务运行器。使用 Gulp,我们可以自动化活动,如移动或复制文件,以及捆绑和最小化。在 ASP.NET Core 中,微软还将 Gulp 与 Visual Studio 集成在一起,因为它已被 Web 社区广泛接受,可以非常轻松地运行和管理复杂的任务。您可以访问官方网站了解更多信息:gulpjs.com/

让我们使用 Gulp 将解决方案中隐藏的node_modules文件夹中所需的 JavaScript 框架和库推送到项目wwwroot下的libs文件夹中。在 Visual Studio 中安装 Gulp 很容易。执行以下步骤来安装和运行 Gulp 任务:

  1. package.json的 NPM 配置文件中添加 Gulp 作为开发依赖项,如图所示,并保存文件:
      {   
            "version": "1.0.0",   
            "name": "my-todo",   
            "private": true,   
            "dependencies": {   
            "@angular/common": "~4.0.0",   
            "@angular/compiler": "~4.0.0",   
            "@angular/core": "~4.0.0",   
            "@angular/forms": "~4.0.0",   
            "@angular/platform-browser": "~4.0.0",   
            "@angular/platform-browser-dynamic":   
            "~4.0.0",   
            "systemjs": "0.19.40",   
            "core-js": "².4.1",   
            "rxjs": "5.0.1",   
            "zone.js": "⁰.8.4"   
      },   
      "devDependencies": {   
      "@types/node": "⁶.0.46",   
      "gulp": "³.9.1",   
      "typescript": "~2.1.0"   
      }   
    }   

当我们在package.json文件中添加了 Gulp 作为开发依赖项并保存时,Visual Studio 会将该包安装到我们的应用程序中的node Dependencies|npm`文件夹下,如下截图所示:

添加的 npm 文件夹下的 Gulp 依赖项

我们的应用程序中有 Gulp 包。现在,我们需要在 JavaScript 中编写一个任务,从隐藏在解决方案中的node_modules文件夹中复制所需的 JavaScript 库,如下所示:

某个node_modules隐藏文件夹

  1. 现在,让我们将Gulp 配置文件添加到我们的项目中。右键单击项目,导航到添加 | 新建项。在左侧窗格中选择Web下的General,然后在中间窗格中选择Gulp 配置文件。然后,单击添加,保持默认名称为gulpfile.js

添加 Gulp 配置文件

以下是 Gulp 配置文件gulpfile.js的默认内容:

Gulp 配置文件的默认代码片段

  1. 让我们再写一个任务,将隐藏在解决方案中的node_modules文件夹中所需的 JavaScript 库复制到项目wwwroot节点下的libs文件夹中。将以下代码片段添加到新任务的gulpfile.js中:
      var paths = {   
          sourcePath: "./node_modules",   
          targetPath: "./wwwroot/libs"   
      }   
          var librariesToMove = [   
          paths.sourcePath + '/core-
          js/client/shim.min.js',   
          paths.sourcePath + '/zone.js/dist/zone.min.js',   
          paths.sourcePath +   
         '/systemjs/dist/system.src.js',   
      ];   
          var gulp = require('gulp');   
          gulp.task('librariesToMove',   function () {   
          return           
          gulp.src(librariesToMove).pipe      
          (gulp.dest(paths.targetPath));   
      });

paths变量保存要移动的库的源和目标文件夹,librariesToMove变量保存要移动到libs文件夹的库列表。文件中的最后一条语句是在运行时将所需的 JavaScript 库复制到libs文件夹的新任务。

  1. 我们已经准备好了 Gulp 任务的代码,现在,我们需要运行 Gulp 任务来复制这些库。所以,要运行任务,右键单击gulpfile.js并打开任务运行器资源管理器,如下截图中所示:

打开任务运行器资源管理器

任务运行器资源管理器将在Tasks下列出在gulpfile.js中编写的可用任务,如下截图所示:

gulpfile.js 中可用的任务列表

  1. Task Runner Explorer 中右键点击列表中的 librariesToMove 任务,然后从菜单中选择 Run,如下所示:

在 gulpfile.js 中运行 librariesToMove 任务

您可以在 Task Runner Explorer 的右侧窗格中看到执行该任务的命令:

任务完成时没有错误

注意,库会被复制到 wwwroot 下的 libs 文件夹,如下截图所示:

创建了包含所需 JavaScript 库的 libs 文件夹

  1. 现在,我们已经在 wwwroot 节点下的 libs 文件夹中拥有了所需的库,请按照以下示例更新 index.html,向其中添加对 libs 文件夹中库的脚本引用以及配置 SystemJS 的代码:
      <!DOCTYPE html>   
      <html>   
      <head>   
          <title>My   Todo</title>   
          <script>document.write('<base   href="' + 
          document.location + 
          '" />');</script>   
          <meta charset="UTF-8">   
          <!-- Polyfills -->   
          <script src="img/shim.min.js"></script>   
          <script src="img/zone.min.js"></script>   
          <script src="img/system.src.js"></script>   
          <script src="img/systemjs.config.js"></script>   
          <script>   
            System.import('main.js').catch(function(err){          
            console.error(err); });   
          </script>   
      </head>   
      <body>   
          <my-app>Loading My Todo   App...</my-app>   
      </body>   
      </html>
  1. 添加 system.js 配置文件 systemjs.config.js,并更新其中的以下内容。这包含了运行应用程序时加载 Angular 库的映射信息:
      (function (global) {
      System.config({
      paths: {
      'npm:': 'node_modules/'
      },
      map: {
      'app': 'app',
      '@angular/common': 
      'npm:@angular/common/bundles/common.umd.js',
      '@angular/compiler':       
      'npm:@angular/compiler/bundles/compiler.umd.js',
      '@angular/core': 
      'npm:@angular/core/bundles/core.umd.js',
      '@angular/forms': 
      'npm:@angular/forms/bundles/forms.umd.js',
      '@angular/platform-browser': 'npm:@angular/platform-
      browser/bundles/platform-browser.umd.js',
      '@angular/platform-browser-dynamic': 
      'npm:@angular/platform-
      browser-dynamic/bundles/platform-browser-
      dynamic.umd.js',
      'rxjs': 'npm:rxjs'
      },
      packages: 
      {app: {
      main: './main.js',
      defaultExtension: 'js'
      },
      rxjs: {
      defaultExtension: 'js'
      }
      }
      });
      })(this);

我们创建了一个项目来开发My Todo 应用程序,并使用 NPM 包管理器管理所有客户端依赖项。我们还使用了 Gulp 运行一个任务,将 JavaScript 库复制到 wwwroot 节点。在下一节中,让我们为我们的应用程序创建所需的 Angular 组件。

添加 Angular 组件和模板


我们将使用 TypeScript 为我们的应用程序编写 Angular 组件。TypeScript 文件应该编译为 ECMAScript 5 目标的 JavaScript。

配置 TypeScript 编译器选项

我们需要告知 Visual Studio 编译 TypeScript 所需的编译器选项,以便在运行时消耗我们的应用程序。通过 TypeScript 配置文件,我们可以使用以下步骤配置编译器选项和其他详细信息:

  1. 在项目上右键点击,然后导航到 Add | New Item,保持文件名默认,添加 TypeScript Configuration File,如下截图所示:

添加 TypeScript 配置文件

将一个名为 tsconfig.json 的文件添加到项目根目录。

  1. 用以下配置替换 TypeScript 配置文件的内容:
      {   
            "compilerOptions": 
            {   
            "diagnostics": true,   
            "emitDecoratorMetadata":   true,   
            "experimentalDecorators":   true,   
            "lib": ["es2015", "dom"],   
            "listFiles": true,   
            "module": "commonjs",   
            "moduleResolution": "node",   
            "noImplicitAny": true,   
            "outDir": "wwwroot",   
            "removeComments": false,   
            "rootDir": "wwwroot",   
            "sourceMap": true,   
            "suppressImplicitAnyIndexErrors":   true,   
            "target": "es5"   
            },   
            "exclude": [   
            "node_modules"   
          ]   
      }

添加 Angular 组件

我们已经配置了 TypeScript 编译器选项。现在,让我们为我们的应用程序添加一个 Angular 根组件。按照以下步骤操作:

  1. 首先,通过右键点击 wwwroot,然后导航到 Add | New Folder,在 wwwroot 下创建一个 app 文件夹,如下截图所示:

为 Angular 应用程序文件夹添加一个名为 app 的新文件夹

  1. 我们已经准备好了app文件夹。让我们通过右键单击app文件夹并导航到Add | New Item来添加 TypeScript 文件,以创建一个根组件。从左侧面板下选择Web下的Scripts,并在中间面板选择TypeScript File。将文件命名为app.component.ts,然后单击Add

添加名为 app.component.ts 的根组件

  1. 将以下代码片段添加到app.component.ts
      import { Component } from '@angular/core';   
      @Component({   
          selector: 'my-app',   
          template: `<h1>Hello   {{name}}</h1>`   
      })   
      export class AppComponent { name   = 'My Todo App';  
      }

创建了一个名为AppComponent的根组件,并用组件元数据selectortemplateUrl进行修饰。

添加应用程序模块

在前面的部分中,我们创建了一个名为AppComponent的 Angular 组件。现在我们需要引导这个AppComponent,这样 Angular 才会将其视为应用程序的根组件。我们可以通过在AppModule类上添加NgModule元数据并将其分配给AppComponent来引导一个组件。按照以下步骤创建AppModule

  1. 通过右键单击app文件夹并导航到Add | New Item来创建一个TypeScript文件。在左侧面板下选择Web下的Scripts,并在中间面板选择TypeScript File。添加一个名为app.module.ts的文件,然后单击Add

添加名为app.module.ts的 TypeScript 文件

  1. 将以下代码片段添加到app.module.ts
      import { NgModule } from '@angular/core';
      import { BrowserModule } from '@angular/platform-
      browser';
      import { FormsModule } from '@angular/forms';
      import { AppComponent } from './app.component';
      @NgModule({
      imports: [
      BrowserModule,
      FormsModule
      ],
      declarations: [AppComponent],
      bootstrap: [AppComponent]
      })
      export class AppModule { }

在这里,我们将AppComponent添加为根组件,并导入BrowserModule,因为我们的应用将通过浏览器消耗,还有FormsModule两个绑定。

添加一个 Angular 组件

现在我们需要引导前面部分中创建的AppModule。执行以下步骤:

  1. 让我们创建一个TypeScript文件,main.ts,用于引导AppModule。在wwwroot文件夹上右键单击并导航到Add | New Item。从左侧面板下选择Web下的Scripts,并在中间面板选择TypeScript File。将文件命名为main.ts,然后单击Add

添加名为 main.ts 的 TypeScript 文件

  1. 使用这段代码更新main.ts文件:
      import { platformBrowserDynamic }   from 
      '@angular/platform-
      browser-dynamic';   
      import { AppModule } from './app/app.module';   
      platformBrowserDynamic().bootstrapModule(AppModule);

在这里,平台浏览器动态包含使应用在浏览器中运行的 Angular 功能。如果我们的应用程序不是针对在浏览器上运行的话,可以忽略这一点。

我们已经准备好验证我们的 Angular 应用程序是否正常运行。请注意 Visual Studio 如何在解决方案资源管理器中整齐地组织了模板文件、TypeScript 文件以及它们各自的已编译 JavaScript 文件,如下面的屏幕截图所示:

编译 TypeScript 文件到 JavaScript 文件

请注意,Visual Studio 在对 app 文件夹中的 TypeScript 文件进行编译并进行更改并保存文件时,将自动生成 JavaScript 文件。

  1. 通过按下F5键来运行应用程序,如果成功构建,您将看到浏览器中的输出,如下面的截图所示:

应用的输出

注意

请注意<my-app>标签的内部文本是使用app.component.html中的内容插入的。

添加 Todo 类

我们的应用处理Todo项目。因此,让我们创建一个名为Todo的类,并向其添加titlecompleted属性,如下所示:

export class Todo {   
    title: string;   
    completed: boolean;   
    constructor(title: string) {   
        this.title = title;   
        this.completed = false;   
    }   
    set isCompleted(value:   boolean) {   
        this.completed = value;   
    }   
}   

这个Todo类还有一个以title为参数的构造函数和一个设置todo项目为completed状态的方法。

添加一个 TodoService 类

todo.service.ts file:
import { Todo } from './todo'    
export class TodoService {   
    todos: Array<Todo>   
    constructor() {   
        this.todos = [new Todo('First   item'),   
        new Todo('Second item'),   
        new Todo('Third item')];   
    }   
    getPending() {   
        return   this.todos.filter((todo: Todo) => todo.completed === 
        false);   
    }   
    getCompleted() {   
        return   this.todos.filter((todo: Todo) => todo.completed === 
        true);   
    }   
    remove(todo: Todo) {   
          this.todos.splice(this.todos.indexOf(todo), 1);   
    }   

    add(title: string) {   
        this.todos.push(new   Todo(title));   
    }   
    toggleCompletion(todo: Todo)   {   
        todo.completed =   !todo.completed;   
    }   
    removeCompleted() {   
        this.todos =   this.getPending();   
    }   
}

我们创建了TodoService类,其中包含各种方法来添加、删除和返回todo项目的集合。

更新 AppComponent 类

现在我们已经拥有了TodoService类,让我们更新AppComponent类,如下所示,来消费TodoService类:

import { Component } from '@angular/core';   
import { Todo } from './todo'   
import { TodoService } from './todo.service'     
@Component({   
    selector: 'my-app',   
    templateUrl: './app/app.component.html'   
})   
export class AppComponent {   
    todos: Array<Todo>;   
    todoService: TodoService;   
    newTodoText = '';   
    constructor(todoService:   TodoService) {   
        this.todoService =   todoService;   
        this.todos =   todoService.todos;   
    }   
    removeCompleted() {   
        this.todoService.removeCompleted();   
    }   
    toggleCompletion(todo: Todo)   {   
          this.todoService.toggleCompletion(todo);   
    }   
    remove(todo: Todo) {   
          this.todoService.remove(todo);   
    }  
    addTodo() {   
        if   (this.newTodoText.trim().length) {   
              this.todoService.add(this.newTodoText);   
            this.newTodoText = '';   
        }   
    }   
}   

请注意@Component里的 metadata template已被替换为templateUrl,并且指定了一个AppComponent模板文件app.component.html。由于模板内容现在比较复杂,我们需要为AppComponent视图引入一个 HTML 文件。

更新 AppModule

app.module.ts file:
import { NgModule } from '@angular/core';   
import { BrowserModule } from '@angular/platform-browser';   
import { FormsModule } from '@angular/forms';   
import { AppComponent } from './app.component';   
import { TodoService } from './todo.service'   
@NgModule({   
    imports: [   
        BrowserModule,   
        FormsModule   
    ],   
    declarations: [AppComponent],   
    providers: [TodoService],   
    bootstrap: [AppComponent]   
})   
export class AppModule { }

添加 AppComponent 模板

AppComponent with all the mentioned features:
<section>   
    <header>   
          <h1>todos</h1>   
        <input placeholder="Add   new todo" autofocus="" [(ngModel)]="newTodoText">   
        <button type="button"   (click)="addTodo()">Add</button>   
    </header>   
    <section *ngIf="todoService.todos.length   > 0">   
        <ul>   
            <li *ngFor="let   todo of todoService.todos">   
                <input type="checkbox"   (click)="toggleCompletion(todo)" [checked]="todo.completed">   
                  <label>{{todo.title}}</label>   
                <button   (click)="remove(todo)">X</button>   
            </li>   
        </ul>   
    </section>   
    <footer *ngIf="todoService.todos.length   > 0">   
          <span><strong>{{todoService.getPending().length}}</strong>   {{todoService.getPending().length == 1 ? 'item' : 'items'}} left</span>   
        <button *ngIf="todoService.getCompleted().length   > 0" (click)="removeCompleted()">Clear completed</button>   
    </footer>   
</section>   

如你所见,我们使用ngModel在输入控件上应用了双向绑定来绑定新的todo项目title。我们将addTodo方法分配给Add按钮的点击事件,以将新的todo项目添加到todoService中的内存中的Todo项目集合。我们对<li>标签应用了ngFor来迭代todoService中的每个Todo项目。为每个Todo项目呈现的复选框有其自己的click事件,checked属性与toggleCompletion方法,以及Todo项目的completed属性分别映射。接下来,删除按钮将其click事件映射为AppComponent中的remove方法。

footer 标签有一个显示待办todo项目数量的 span,以及一个按钮来从列表中删除已完成的todo项目。该按钮的click事件与AppComponent中的removeCompleted方法相映射。

通过按下F5键来运行应用程序,您将能够执行所有操作,如添加,删除和列举todo项目:

我的 Todo 应用操作

总结


哇!你实际上在本章中学到了这本书非常重要且核心的目标。是的!我在谈论将 Angular 与.NET 应用程序集成。

我们通过创建一个新的 ASP.NET Core 空应用程序开始了这一章,并更新了Startup类以服务静态页面和默认页面。然后,您学习了如何在 Visual Studio 中使用 NPM 管理客户端包,我们还设法使用 Visual Studio 中的 Gulp 自动化和运行任务。接下来,您学习了如何添加应用程序所需的组件并将其引导。后来,我们设计了一个模型类和一个服务类来处理应用程序的核心业务逻辑。最后,我们设计了一个模板来列出Todo项目,并添加了一些控件并将它们挂钩到TodoServiceAppComponent中的某些方法。

这个应用程序只处理内存中的待办事项。然而,在实时应用程序中,我们会使用一个服务来添加、删除或获取todo项目。在下一章中,我们将讨论如何创建一个 ASP.NET Core Web API 服务来处理检索、保存和删除todo项目,并从我们刚刚构建的 Angular 应用程序中使用它。

第六章:创建 ASP.NET Core Web API 服务用于 Angular

本章将指引您创建 ASP.NET Web API 服务用于上一章中创建的 Angular 应用程序。

在本章中,我们将涵盖以下主题:

  • RESTful Web Services

  • ASP.NET Web API 概述

  • 创建 ASP.NET Web API 服务

  • 将 ASP.NET Web API 与 Angular 应用程序集成

RESTful Web Services


表征状态转移 (REST) 是一种可以应用于实现 RESTful 服务的架构风格或设计原则。REST 确保了客户端和服务之间的通信通过拥有有限数量的操作而改善。REST 帮助您以一种简单的方式组织这些独立系统之间的通信。

在 REST 中,每个资源都由自己的 通用资源标识符 (URI) 标识。它在 HTTP 的基础上使用,并利用 HTTP 动词,如 GETPOSTPUTDELETE,来控制或访问资源。

表征状态转移 (REST)是一种无状态的 Web 服务,易于扩展,工作在 HTTP 协议下,可以从任何支持 HTTP 的设备上访问。客户端不需要担心除数据格式之外的任何内容:

图片

一个 RESTful 服务

ASP.NET Web API 概述


ASP.NET Web API 是一个可以在 .NET 框架上构建 RESTful 服务的框架。ASP.NET Web API 基于 HTTP 协议,以 URI 形式公开 HTTP 动词/操作,允许客户端应用程序使用 HTTP 动词与数据交互。任何支持 HTTP 通信的客户端应用程序或设备都可以轻松访问 Web API 服务。

正如前一节所讨论的,RESTful 服务通过 URI 标识资源。例如,我们有 www.programmerguide.net/api/todos/101,并且 Angular 应用程序应用一个 GET 请求。响应这个 GET 请求的 C# 方法将在 Web API 控制器类中。路由技术将根据配置或在相应的类和方法中注释的路由来映射请求 URI 与相应的控制器和方法。

在这里,按照默认配置,请求将由 TodosController 中的 Get 方法处理。Get 方法将根据 ID 值 101 从数据库中检索 Todo 项,并将其作为 Todo 对象返回。返回的 Todo 对象将被序列化为 JSON 或 XML。

对于 Post 方法,新发布的 Todo 对象将以 JSON 形式从请求体中接收,并且将被反序列化成 Todo 对象以在 TodosControllerPost 方法中使用。

我们可以通过强大的 ASP.NET Model-View-Controller (MVC) 编程模型在 ASP.NET Web API 中创建基于 HTTP 的服务。路由、模型绑定和验证等功能提供了在使用 ASP.NET Web API 开发 RESTful Web 服务时更大的灵活性。

为什么 ASP.NET Web API 很适合 Angular

ASP.NET Web API 是一个用于构建 HTTP 服务的框架。它采用非常轻量级的架构,可以通过 RESTful 方式在 Angular 中以异步方式访问 HTTP 服务。使用 ASP.NET Web API,我们可以轻松地在 Angular 应用程序中同步数据。

创建 ASP.NET Web API 服务


让我们为上一章创建的我的待办应用添加 ASP.NET Web API 服务。我们的我的待办应用是在 Visual Studio 2015 中使用空的 ASP.NET 5 模板创建的。创建空项目时,会生成一个精简的 Web 应用程序。它不包括与 MVC 或 Web API 相关的程序集。因此,我们需要明确添加所需的程序集或模块来实现应用程序中的 Web API。

向 ASP.NET 项目添加和配置 MVC 服务

由于 ASP.NET Core 将 Web API 与 MVC 合并,我们需要添加一个 MVC 服务来实现应用程序中的 Web API:

  1. 安装 NuGetMicrosoft.AspNetCore.MVC

  2. 在 Visual Studio 中从项目的根文件夹中打开 Startup.cs 文件。

  3. ConfigureServices 方法下添加以下语句以向项目添加 MVC 服务

    public void   ConfigureServices(IServiceCollection   
    services)   
        {   
            services.AddMvc();   
        }   
  1. 我们刚刚在项目中启用了 MVC。接下来,我们将通过在 Configure 方法中添加以下语句来将 MVC 与我们的请求管道连接起来:
    app.UseMvc();

向 ASP.NET 应用程序添加 Web API 控制器

我们刚刚启用并连接了 MVC 服务到我们的应用程序。现在,让我们按照以下步骤添加一个 Web API 控制器:

  1. 我的待办 项目上右键单击,导航到 Add | New Folder,并命名文件夹为 Controllers

在我的待办项目下创建一个用于控制器的新文件夹

  1. 现在,右键单击我们刚刚创建的 Controllers 文件夹,转到 Add | New Item

将 Web API 控制器添加到控制器文件夹中

  1. 选择 Minimal Dependencies 并单击 Add 如果您收到一个添加 MVC 依赖项的弹出窗口:

添加最小的 MVC 依赖项

Visual Studio 2017 添加了一个 ScaffoldingReadMe.txt 自述文件,其中包含以下启用脚手架的说明;遵循并相应地更新您的项目代码。

ASP.NET MVC 核心依赖项已添加到项目中。但是,您可能仍然需要对项目进行以下更改:

  1. 向项目添加 Scaffolding``CLI 工具:
    <ItemGroup>   
     <DotNetCliToolReference    
     Include="Microsoft.VisualStudio.Web.CodeGeneration.  
     Tools"  Version="1.0.0" />   
    </ItemGroup>   
  1. 下面是对 Startup 类的建议更改:
    2.1 Add a constructor:   
        public IConfigurationRoot   Configuration { get; }   
        public Startup(IHostingEnvironment   env)   
        {   
            var builder = new   ConfigurationBuilder()   
                .SetBasePath(env.ContentRootPath)   
                .AddJsonFile("appsettings.json",     
                 optional: true, 
                    reloadOnChange: true)   
                .AddJsonFile($"appsettings.
                 {env.EnvironmentName}.json",   optional: 
                  true)   
                .AddEnvironmentVariables();   
            Configuration =   builder.Build();   
        }   
    2.2 Add MVC services:   
        public void   ConfigureServices(IServiceCollection  
        services)   
        {   
            // Add framework   services.   
            services.AddMvc();   
       }   
    2.3 Configure web app to use   use Configuration and 
        use MVC routing:  
        public void   Configure(IApplicationBuilder app, 
        IHostingEnvironment env, ILoggerFactory   
        loggerFactory)   
        {      
        loggerFactory.AddConsole(Configuration.GetSection       
        ("Logging"));   
              loggerFactory.AddDebug();  
            if (env.IsDevelopment())   
            {   
                  app.UseDeveloperExceptionPage();   
            }   
            else   
            {   
                  app.UseExceptionHandler("/Home/Error");   
            }   
            app.UseStaticFiles();   

            app.UseMvc(routes   =>   
            {   
                routes.MapRoute(   
                    name: "default",   
                    template: " 
            {controller=Home}/{action=Index}
                     /{id?}");   
            });   
        }
  1. 再次右键单击 Controllers 文件夹,转到 Add | Controllers,选择 API Controller with read/write actions,并将其命名为 TodosController

将控制器命名为 TodosController

注意

如果你在下面的截图中看到了错误,你需要通过编辑你的csproj文件添加给定的 XML 标签,然后再次添加控制器。

这是错误:

以下是 XML 标签的代码:

<ItemGroup>   
          <DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools"   Version="1.0.1" />   
</ItemGroup>   

这将为我们创建TodosController Web API 控制器,并提供以下模板代码,供我们根据需求进行修改:

[Produces("application/json")]   
    [Route("api/Todos")]   
    public class TodosController   : Controller   
    {   
        // GET: api/Todos   
        [HttpGet]   
        public   IEnumerable<string> Get()   
        {   
            return new string[] {   "value1", "value2" };   
        }  
        // GET: api/Todos/5   
        [HttpGet("{id}", Name = "Get")]   
        public string Get(int id)   
        {   
            return "value";   
        }    
        // POST: api/Todos   
        [HttpPost]   
        public void   Post([FromBody]string value)   
        {   
        }   
        // PUT: api/Todos/5   
        [HttpPut("{id}")]   
        public void Put(int id,   [FromBody]string value)   
        {   
        }   
        // DELETE:   api/ApiWithActions/5   
        [HttpDelete("{id}")]   
        public void Delete(int   id)   
        {   
        }   
    }   
  1. 按下F5运行应用程序,并从浏览器导航到http://localhost:2524/api/todos

注意

你的系统可能有不同的端口。

你将会在TodosController中看到以下输出,默认代码中的Get方法。如您在下面的截图中所见,它只返回了一个字符串数组:

在 TodoController 中默认的 Get 操作的输出

添加模型到 ASP.NET 应用程序

我们配置了我们的应用程序以使用 MVC 服务,并添加了 Web API 控制器。现在,让我们为我们的 My Todo 应用程序添加所需的模型。按照这些步骤添加一个名为Todo的模型:

  1. My``Todo项目上右键点击,转到Add | New Folder,并将文件夹命名为Models

在 My Todo 项目下为 Models 添加一个新文件夹

  1. 现在,右键点击刚刚创建的Models文件夹,然后转到Add | Class....:

在 Models 文件夹下为 Todo 对象添加一个类

  1. 将类命名为Todo,并将以下代码片段添加到其中:
   namespace My_Todo.Models
   {
   public class Todo
   {
   public int Id { get; set;
    }
   public string Title { get; set;
    }
   public bool Completed { get; set;
    }
   }  
  }

Todo是一个 C# POCO 类,代表一个Todo项目。它具有属性,例如Id保存着Todo项目的主键值,Title属性保存着Todo项目的标题,Completed属性保存着布尔标志,指示该项目是否已完成。

将 DBContext 添加到 ASP.NET 应用程序

我们刚刚添加了Todo模型。现在,让我们添加DBContext来管理和持久化数据库中的TodoDBContext充当您的类和数据库之间的桥梁。要添加它,请按照以下步骤操作:

  1. 右键点击Models文件夹,转到Add | Class

在 Models 文件夹下添加一个 DBContext 类

  1. 将类命名为TodoContext,并将以下代码片段添加到其中:
   public class TodoContext : DbContext
   {
     public TodoContext(DbContextOptions<TodoContext>       
     options)
     : base(options)
    {
    }
    public DbSet<Todo> Todos { get; set; }
  }

TodoContext帮助你与数据库交互,并将更改提交为一个单独的工作单元。TodoContext被配置为使用 SQL Server,并且连接字符串是从我们将在下一步添加的config.json文件中读取的。

  1. Startup.cs中添加使用语句以导入Microsoft.EntityFrameworkCore

  2. 通过将以下代码片段添加到ConfigureServices方法中来配置 SQL 服务:

    services.AddEntityFrameworkSqlServer()   
    .AddDbContext<TodoContext>(options =>   
    options.UseSqlServer(Configuration.GetConnectionString
    ("DefaultConnection")));   
    services.AddMvc();   
  1. 添加一个appsettings.json文件来保存连接字符串的值,并更新它的内容如下:
 {   
   "ConnectionStrings": 
    {   
     "DefaultConnection": "Server=(localdb)\\mssqllocaldb;
         Database=aspnet-CloudInsights-f2d509d5-468f-4bc9-  
         9c47-
         0593d0907063;Trusted_Connection=True;
         MultipleActiveResultSets=true"   
    },   
   "Logging": 
    {
     "IncludeScopes": false,   
     "LogLevel": {   
      "Default": "Warning"   
     }   
   }   
 }   

在这个JSON文件中,我们在data项下添加了连接字符串。

Startup.cs file is as follows:
public class Startup   
    {   
        public Startup(IHostingEnvironment   env)   
        {   
            var builder = new   ConfigurationBuilder()   
                  .SetBasePath(env.ContentRootPath)   
                .AddJsonFile("appsettings.json",   optional: true, reloadOnChange: true)   
                .AddJsonFile($"appsettings.{env.EnvironmentName}.json",   optional: true)   
                  .AddEnvironmentVariables();   
            Configuration =   builder.Build();   
        }   
        public IConfigurationRoot   Configuration { get; }   

        // This method gets   called by the runtime. Use this method to add services to the container.   
        // For more information   on how to configure your application, visit   https://go.microsoft.com/fwlink/?LinkID=398940   
        public void ConfigureServices(IServiceCollection   services)   
        {   
              services.AddEntityFrameworkSqlServer()   
              .AddDbContext<TodoContext>(options =>   
                  options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));   
            // Add framework   services.   
            services.AddMvc();   
        }   
        // This method gets   called by the runtime. Use this method to configure the HTTP request   pipeline.   
        public void   Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory   loggerFactory)   
        {   
              loggerFactory.AddConsole();   

            if   (env.IsDevelopment())   
            {   
                  app.UseDeveloperExceptionPage();   
            }   
              app.UseDefaultFiles();   
            app.UseStaticFiles();   
              app.UseStaticFiles(new StaticFileOptions   
            {   
                FileProvider =   new PhysicalFileProvider(   
                      Path.Combine(env.ContentRootPath, "node_modules")   
                ),   
                RequestPath = "/"   + "node_modules"   
            });   
            app.UseMvc();   
        }   
    }   

Startup.cs构造函数中,我们创建了用于从config.json文件中读取的配置。在ConfigureServices方法中,我们添加了 Entity Framework 并将 SQL Server 和TodoContext连接到了它。

使用数据库迁移


Entity Framework 中的数据库迁移帮助您在应用程序开发阶段创建数据库或更新数据库模式。现在我们已经准备好了所需的模型和数据库上下文。现在需要创建数据库。让我们使用 Entity Framework 中的数据库迁移功能在 SQL Server Compact 中创建数据库。按照以下步骤操作:

  1. 首先通过编辑将以下 XML 标签添加到csproj文件中:
  <ItemGroup>   
    <DotNetCliToolReference    
    Include="Microsoft.EntityFrameworkCore.Tools.DotNet"  
    Version="1.0.0" />   
  </ItemGroup>   
  1. 打开命令提示符并导航到项目文件夹。

  2. 执行以下命令以初始化迁移的数据库:

执行命令以添加迁移

此命令在My****Todo项目下创建Migration文件夹,并添加两个类以创建表和更新模式。

与数据库迁移相关的文件

  1. 执行以下命令以更新数据库:

执行命令以更新数据库

此命令根据上下文和模型为我们的应用程序创建database

在 Web API 控制器中使用数据库上下文

现在我们已经准备就绪,迁移也已设置好,让我们更新TodosController Web API 控制器以使用之前创建的TodoContext。按照以下步骤进行:

  1. 打开TodosController.cs

  2. 声明_db私有变量类型为TodoContext

private TodoContext _db; 
  1. 定义接受TodoContext类型的context参数并将context值赋给_dbconstructor
        public TodosController(TodoContext context) 
        { 
            _db = context; 
        } 
  1. 引入一个GET动作方法,该方法使用_db数据库上下文从数据库中返回所有Todo项的集合:
        // GET: api/todos 
        [HttpGet] 
        public IEnumerable<Todo> Get() 
        { 
            return _db.Todos.ToList(); 
        } 
  1. 引入另一个GET动作方法,该方法从数据库中移除已完成的Todo项,并使用_db数据库上下文返回所有待处理的Todo项:
        // GET: api/todos/pending-only 
        [HttpGet] 
        [Route("pending-only")] 
        public IEnumerable<Todo> GetPendingOnly() 
        { 
            _db.Todos.RemoveRange(_db.Todos.Where(x =>   
            x.Completed == true)); 
            _db.SaveChanges(); 
            return _db.Todos.ToList(); 
        }
  1. 引入一个POST动作方法,该方法在TodoContext``_db数据库中插入新的Todo项:
        // POST api/todos 
        [HttpPost] 
        public Todo Post([FromBody]Todo value) 
        { 
            _db.Todos.Add(value); 
            _db.SaveChanges(); 
            return value; 
        } 
  1. 引入PUT动作方法,使用TodoContext``_db更新具有匹配 ID 的现有Todo项:
        // PUT api/todos/id 
        [HttpPut("{id}")] 
        public Todo Put(int id, [FromBody]Todo value) 
        { 
            var todo = _db.Todos.FirstOrDefault(x => x.Id  
            == id); 
            todo.Title = value.Title; 
            todo.Completed = value.Completed; 
            _db.Entry(todo).State = 
            Microsoft.Data.Entity.EntityState.Modified; 
            _db.SaveChanges(); 
            return value; 
        } 
  1. 引入一个DELETE动作方法,使用TodoContext``_db删除具有匹配 ID 的现有Todo项:
        // DELETE api/todos/id 
        [HttpDelete("{id}")] 
        public void Delete(int id) 
        { 
            var todo = _db.Todos.FirstOrDefault(x => x.Id 
            == id); 
            _db.Entry(todo).State = 
            Microsoft.Data.Entity.EntityState.Deleted; 
            _db.SaveChanges(); 
        }     
TodosController is this:
[Produces("application/json")]   
    [Route("api/Todos")]   
    public class TodosController   : Controller   
    {   
        private TodoContext _db;   
        public   TodosController(TodoContext context)   
        {   
            _db = context;   
        }   
        // GET: api/todos   
        [HttpGet]   
        public   IEnumerable<Todo> Get()   
        {   
            return   
            _db.Todos.ToList();   
        }   
        // GET: api/todos/pending-only   
        [HttpGet]   
        [Route("pending-only")]   
        public   IEnumerable<Todo> GetPendingOnly()   
        {   
            _db.Todos.RemoveRange(_db.Todos.Where(x => 
            x.Completed == true));   
            _db.SaveChanges();   
            return   _db.Todos.ToList();   
        }   
        // POST api/todos   
        [HttpPost]   
        public Todo   Post([FromBody]Todo value)   
        {   
            _db.Todos.Add(value);   
            _db.SaveChanges();   
            return value;   
        }   
        // PUT api/todos/id   
        [HttpPut("{id}")]   
        public Todo Put(int id,   [FromBody]Todo value)   
        {   
            var todo =   _db.Todos.FirstOrDefault(x => 
            x.Id == id);   
            todo.Title =   value.Title;   
            todo.Completed =   value.Completed;   
            _db.Entry(todo).State   = 
            EntityState.Modified;   
            _db.SaveChanges();   
            return value;   
        }   
        // DELETE api/todos/id   
        [HttpDelete("{id}")]   
        public void Delete(int   id)   
        {   
            var todo =   _db.Todos.FirstOrDefault(x => 
            x.Id == id);   
            _db.Entry(todo).State   = EntityState.Deleted;   
            _db.SaveChanges();   
        }   
    }   

将 ASP.NET Core Web API 集成到 Angular 应用程序中


在上一节中,我们添加和修改了 Web API 控制器,并介绍了处理Todo项的 HTTP 动词方法。现在,让我们修改我们的 Angular 代码,以调用所有 Web API 方法来管理Todo项。

在 Angular 应用程序中更新模型

首先,我们需要在 Angular 应用程序中的Todo.ts中添加id属性来保存从 API 接收的Todo项的 ID。因此,更新后的Todo.ts如下所示:

export class Todo { 
    id: number; 
    title: string; 
    completed: boolean; 
    constructor(id: number, title: string, completed: 
    boolean) { 
        this.id = id; 
        this.title = title; 
        this.completed = completed; 
    } 
    set isCompleted(value: boolean) { 
        this.completed = value; 
    }  
}  

constructor接受三个参数:idtitlecompleted,并将它们分配给idtitlecompleted属性,分别使用this关键字访问它们。Todo类还为completed属性设置了访问器。

准备 Angular 应用程序

准备 Angular 应用程序的步骤如下:

  1. package.json中将@angular/http模块添加到依赖项中。需要使用 HTTP 模块来消费 Web API 服务。更新后的package.json如下所示:
 {   
   "version": "1.0.0",   
   "name": "my-todo",   
   "private": true,   
   "dependencies": {   
     "@angular/common": "~4.0.0",   
     "@angular/compiler": "~4.0.0",   
     "@angular/core": "~4.0.0",   
     "@angular/forms": "~4.0.0",   
     "@angular/http": "~4.0.0",   
     "@angular/platform-browser": "~4.0.0",   
     "@angular/platform-browser-dynamic":   "~4.0.0",   
     "systemjs": "0.19.40",   
     "core-js": "².4.1",   
     "rxjs": "5.0.1",   
     "zone.js": "⁰.8.4"   
   },   
   "devDependencies": {   
     "@types/node": "⁶.0.46",   
     "gulp": "³.9.1",   
     "typescript": "~2.1.0"   
   }   
 }
  1. 使用@angular/httpsystemjs.config.js中进行映射更新。更新后的systemjs.config.js如下所示:
  (function (global) {   
      System.config({           
          paths: {               
             'npm:': 'node_modules/'   
        },   
        map: {   
            'app': 'app',   
            '@angular/common':   
   'npm:@angular/common/bundles/common.umd.js',   
            '@angular/compiler': 
   'npm:@angular/compiler/bundles/compiler.umd.js',   
            '@angular/core': 
   'npm:@angular/core/bundles/core.umd.js',   
            '@angular/forms': 
   'npm:@angular/forms/bundles/forms.umd.js',   
            '@angular/http': 
   'npm:@angular/http/bundles/http.umd.js',   
            '@angular/platform-browser':   
   'npm:@angular/platform-browser/bundles/platform-
    browser.umd.js',   
            '@angular/platform-browser-dynamic':   
   'npm:@angular/platform-browser-
    dynamic/bundles/platform-browser-dynamic.umd.js',   
            'rxjs': 'npm:rxjs'   
          },   
           packages: {   
              app: {   
                  main: './main.js',   
                  defaultExtension:   'js'   
              },   
              rxjs: {   
                  defaultExtension:   'js'   
              }   
          }   
      });   
   })(this);   
  1. AppModule中导入HttpModule,如下所示:
   import { NgModule } from '@angular/core';   
   import { BrowserModule } from '@angular/platform-   
   browser';   
   import { FormsModule } from '@angular/forms';   
   import { HttpModule } from '@angular/http';   
   import { AppComponent } from './app.component';   
   import { TodoService } from './todo.service'   
   @NgModule({   
   imports: [   
        BrowserModule,   
        FormsModule,   
        HttpModule   
    ],   
       declarations: [AppComponent],   
       providers: [TodoService],   
       bootstrap: [AppComponent]   
   })   
   export class AppModule { }   
  1. 如下所示更新模型Todo
export class Todo {   
    id: number;   
    title: string;   
    completed: boolean;   
    constructor(id: number,   title: string, completed: boolean) {   
        this.id = id;   
        this.title = title;   
        this.completed =   completed;   
    }  
    set isCompleted(value:   boolean) {   
        this.completed = value;   
    }   
}   

在 TodoService 中消耗 Web API GET 操作

首先,让我们更新TodoService,以使用Http服务与 Web API 服务通信,从而获取Todo项目列表:

  1. 打开 app 文件夹中的todoService.ts文件。

  2. 添加以下import语句以导入模块,例如InjectableHttpheadersResponseObservablemapTodo

   import { Injectable } from '@angular/core'; 
   import { Http, Headers } from '@angular/http'; 
   import 'rxjs/add/operator/toPromise';
   import { Todo } from './todo'
  1. 修改constructor以注入Http服务,添加Http服务的参数:
  constructor (private http: Http) { ... } 
  1. 添加getTodos方法以使用Http标签消费 Web API 服务以获取Todo项目列表:
     getTodos(): Promise<Array<Todo>> { 
        return this.http.get('/api/todos') 
            .toPromise() 
            .then(response => response.json() as   
             Array<Todo>) 
            .catch(this.handleError); 
     }

在这里,toPromise方法将httpGet方法返回的Observable序列转换为 Promise。然后,我们在返回的 promise 上调用then方法或catch方法。我们将响应中收到的JSON转换为Todo数组。

  1. 我们刚刚添加了getTodos方法。接下来,让我们添加getPendingTodos方法来调用配置了 Web API 中pending-only路由的GET方法,从数据库中删除已完成的Todo项目,并只返回待办的Todo项目。GetPendingTodos的代码片段如下所示:
    getPendingTodos() { 
    this.http.get('http://localhost:2524/api/todos/    
    pending-only') 
         .subscribe( 
         err => console.log(err), 
         () => console.log('getTodos Complete') 
         ); 
    } 
app.component.ts:
       getPending() { 
       return this.todos.filter((todo: Todo) =>   
       todo.completed === false); 
   } 

更新后的todo.service.ts用于调用 Web API 的GET方法的代码如下:

import { Injectable } from '@angular/core'; 
import { Http, Headers } from '@angular/http'; 
import 'rxjs/add/operator/toPromise';
import { Todo } from './todo' 
@Injectable() 
export class TodoService { 
    constructor(private http: Http) {    } 
    getTodos(): Promise<Array<Todo>> { 
        return this.http.get('/api/todos') 
            .toPromise() 
            .then(response => response.json() as Array<Todo>) 
            .catch(this.handleError); 
    } 
    getPendingTodos() { 
        this.http.get('/api/todos/pending-only') 
            .subscribe( 
            err => console.log(err), 
            () => console.log('getTodos Complete') 
            ); 
    }    
    removeCompleted() { 
        this.getPendingTodos();         
    } 
    private handleError(error: any): Promise<any> { 
        console.error('An error occurred', error);  
        return Promise.reject(error.message || error); 
    } 
} 

从 TodoService 向 Web API 发布

我们刚刚更新了todo.Services.ts以调用 Web API 的GET操作并获取Todo项目。现在,让我们添加代码来将新的Todo项目发布到 Web API。按照给定的步骤进行操作:

  1. 打开todo.service.ts

  2. 添加postTodo函数以将新的Todo项目发布到 Web API 控制器:

     postTodo(todo: Todo): Promise<Todo> { 
             var headers = new Headers(); 
             headers.append('Content-Type',  
     'application/json'); 
        return this.http.post('/api/todos',  
     JSON.stringify(todo), { headers: headers }) 
            .toPromise() 
            .then(response => response.json() as Todo) 
            .catch(this.handleError); 
     } 

此函数接受Todo项目作为参数。它定义了带有JSON内容类型的header部分,并使用http服务将Todo项目异步发布到 Web API。响应被转换为Promisethen方法返回Promise<Todo>

调用 Web API 的 PUT 操作以更新 Todo 项目

我们刚刚添加了消费 Web API GET 操作的代码,并添加了代码将新的Todo项目发布到 Web API 的 POST。现在,让我们使用 Web API 的 PUT 操作来更新现有的 Todo 项目。按照以下步骤进行操作:

  1. 打开todo.service.ts

  2. 使用以下代码段添加 putTodo 函数来调用 Web API 中的 PUT 操作来更新现有的 Todo 项目:

     putTodo(todo: Todo) {
       var headers = new Headers(); 
       headers.append('Content-Type', 'application/json'); 
       this.http.put('/api/todos/' + todo.id,  
     JSON.stringify(todo), { headers: headers }) 
            .toPromise() 
            .then(() => todo) 
            .catch(this.handleError); 
     } 

此代码定义了具有 JSON 内容类型的标头,并调用了 PUT 操作方法以及 JSON 字符串化的 Todo 项目和该 Todo 项目的 id。 Web API 中的 PUT 操作方法将更新数据库中的匹配 Todo 项目。

调用 Web API 的 DELETE 操作来删除一个 Todo 项目

我们添加了一些代码,通过调用各种 Web API 操作,如GETPOSTPUT,来获取、添加和编辑 Todo 项目。现在,让我们使用 Web API 中的 DELETE 操作来删除匹配的 Todo 项目。请按照以下步骤进行:

  1. 打开 todo.service.ts

  2. 使用以下代码段添加 deleteTodo 函数,通过调用 Web API 中的 DELETE 操作来删除匹配的 Todo 项目:

      deleteTodo(todo: Todo) { 
        this.http.delete('/api/todos/' + todo.id) 
            .subscribe(err => console.log(err), 
            () => console.log('getTodos Complete') 
            ); 
         } 

此代码调用 DELETE 操作,以及被删除的 Todo 项目的 id。 Web API 中的 DELETE 操作方法将从数据库中检索匹配的 Todo 项目并删除它。

更新 TodoService 中的包装函数

我们有一些函数,如 getTodosgetPendingTodospostTodosputTododeleteTodo,这些函数与 GETPOSTPUTDELETE Web API 操作交互。 现在,让我们更新或替换作为从 app.component.ts 中使用的包装器的 removeaddtoggleCompletionremoveCompleted 函数的代码。 更新或替换函数的代码,如下所示:

    remove(todo: Todo) { 
        this.deleteTodo(todo);         
    } 
    add(title: string): Promise<Todo> { 
        var todo = new Todo(0, title, false); 
        return this.postTodo(todo); 
    } 
    toggleCompletion(todo: Todo) { 
        todo.completed = !todo.completed; 
        this.putTodo(todo); 
    } 
    removeCompleted() { 
        this.getPendingTodos();         
    } 
todo.service.ts after all the updates is this:
import { Injectable } from '@angular/core'; 
import { Http, Headers } from '@angular/http'; 
import 'rxjs/add/operator/toPromise'; 
import { Todo } from './todo' 
@Injectable() 
export class TodoService { 
    constructor(private http: Http) {    } 
    getTodos(): Promise<Array<Todo>> { 
        return this.http.get('/api/todos') 
            .toPromise() 
            .then(response => response.json() as Array<Todo>) 
            .catch(this.handleError); 
    } 
    getPendingTodos() { 
        this.http.get('/api/todos/pending-only') 
            .subscribe( 
            err => console.log(err), 
            () => console.log('getTodos Complete') 
            ); 
    }    
    postTodo(todo: Todo): Promise<Todo> { 
        var headers = new Headers(); 
        headers.append('Content-Type', 'application/json'); 
        return this.http.post('/api/todos', JSON.stringify(todo), { headers: headers }) 
            .toPromise() 
            .then(response => response.json() as Todo) 
            .catch(this.handleError); 
    } 
    putTodo(todo: Todo) { 
        var headers = new Headers(); 
        headers.append('Content-Type', 'application/json'); 
        this.http.put('/api/todos/' + todo.id, JSON.stringify(todo), { headers: headers }) 
            .toPromise() 
            .then(() => todo) 
            .catch(this.handleError); 
    } 
    deleteTodo(todo: Todo) { 
        this.http.delete('/api/todos/' + todo.id) 
            .subscribe(err => console.log(err), 
            () => console.log('getTodos Complete') 
            ); 
    }     
    remove(todo: Todo) { 
        this.deleteTodo(todo);         
    } 
    add(title: string): Promise<Todo> { 
        var todo = new Todo(0, title, false); 
        return this.postTodo(todo); 
    } 
    toggleCompletion(todo: Todo) { 
        todo.completed = !todo.completed; 
        this.putTodo(todo); 
    } 
    removeCompleted() { 
        this.getPendingTodos();         
    } 
    private handleError(error: any): Promise<any> { 
        console.error('An error occurred', error);  
        return Promise.reject(error.message || error); 
    } 
} 

更新 AppComponent

app.component.ts is as shown:
import { Component, OnInit } from '@angular/core'; 
import { Todo } from './todo' 
import { TodoService } from './todo.service' 
@Component({ 
    selector: 'my-app', 
    templateUrl: './app/app.component.html', 
    providers: [TodoService] 
}) 
export class AppComponent implements OnInit { 
    todos: Array<Todo>; 
    newTodoText = ''; 
    constructor(private todoService: TodoService) { 
        this.todos = new Array(); 
    } 
    getTodos(): void { 
        this.todoService 
            .getTodos() 
            .then(todos => this.todos = todos); 
    } 
    ngOnInit(): void { 
        this.getTodos(); 
    } 
    removeCompleted() { 
        this.todoService.removeCompleted(); 
        this.todos = this.getPending(); 
    } 
    toggleCompletion(todo: Todo) { 
        this.todoService.toggleCompletion(todo); 
    } 
    remove(todo: Todo) { 
        this.todoService.remove(todo); 
        this.todos.splice(this.todos.indexOf(todo), 1); 
    } 
    addTodo() { 
        if (this.newTodoText.trim().length) { 
        this.todoService.add(this.newTodoText).then(res =>    
   { 
            this.getTodos(); 
            }); 
            this.newTodoText = ''; 
            this.getTodos(); 
        } 
    } 
    getPending() { 
        return this.todos.filter((todo: Todo) => todo.completed === false); 
    } 
    getCompleted() { 
        return this.todos.filter((todo: Todo) => todo.completed === true); 
    } 
} 

更新 AppComponent 模板

app.component.html 的更新内容如下所示:

<section> 
    <header> 
        <h1>todos</h1> 
        <input placeholder="Add new todo" autofocus="" [(ngModel)]="newTodoText"> 
        <button type="button" (click)="addTodo()">Add</button> 
    </header> 
    <section> 
        <ul> 
            <li *ngFor="let todo of todos"> 
                <input type="checkbox" (click)="toggleCompletion(todo)" [checked]="todo.completed"> 
                <label>{{todo.title}}</label> 
                <button (click)="remove(todo)">X</button> 
            </li> 
        </ul> 
    </section> 
    <footer *ngIf="todos.length > 0"> 
        <span><strong>{{getPending().length}}</strong> {{getPending().length == 1 ? 'item' : 'items'}} left</span> 
        <button *ngIf="getCompleted().length > 0" (click)="removeCompleted()">Clear completed</button> 
    </footer> 
</section> 

TexBox 输入应用了双向绑定,使用 ngModel 来绑定新的 Todo 项目 titleAdd 按钮的点击事件与 AppComponent 中的 addTodo 方法绑定。可用的 Todo 项目将在 <li> 标签中列出,使用 ngFor 迭代 TodoService 中的每个 Todo 项目。 渲染每个 Todo 项目的复选框分别具有 click 事件和 checked 属性,与 toggleCompletion 方法和 Todo 项目的 completed 属性映射。 接下来,移除按钮的 click 事件与 AppComponent 中的 remove 方法映射。

footer 标签中有一个 span,显示待办 Todo 项目的计数以及一个按钮,用于从列表中移除已完成的 Todo 项目。这个按钮有一个点击事件,映射到 AppComponent 中的 removeCompleted 方法。

更新索引页面

index.html:
<!DOCTYPE html> 
<html> 
<head> 
    <title>My Todo</title> 
    <script>document.write('<base href="' +   
    document.location + '" />');</script> 
    <meta charset="UTF-8"> 
    <!-- Polyfills --> 
    <script src="img/shim.min.js"></script> 
    <script src="img/zone.min.js"></script> 
    <script src="img/system.src.js"></script> 
    <script src="img/systemjs.config.js"></script> 
    <script> 
      System.import('main.js').catch(function(err){ console.error(err); }); 
    </script> 
</head> 
<body> 
    <my-app>Loading My Todo App...</my-app> 
</body> 
</html> 

注意 body 标签中有一个特殊的 <my-app/> 标签, 这是 AppComponent 中的元数据。这是 AppComponent 将被实例化并使用模板渲染的地方。

运行应用程序


通过按下 F5 运行应用程序,之后,您将能够执行添加、编辑、删除和列出 Todo 项目等操作:

我的 Todo 应用程序具有所有操作

总结


我们从介绍 RESTful 服务开始本章,并为您概述了 ASP.NET Web API。我们还讨论了为什么 ASP.NET Web API 是 Angular 应用程序的最佳选择。然后,您了解了如何在 ASP.NET 5 项目中添加和配置 Entity Framework 以及使用数据库迁移来创建数据库所需的步骤。接下来,我们讲解了创建 Web API 服务和使用 Entity Framework 管理数据的过程。最后,您学会了如何从 Angular 应用程序中调用 Web API。

在本章中,我们讨论了如何从 Angular 应用程序中使用 Web API 服务来添加、更新、删除和检索数据库中的 Todo 项目,使用 Entity Framework。

在下一章中,我们将讨论如何将 Angular 应用程序与 ASP.NET MVC 和 Web API 集成。

第七章:在 Visual Studio 中使用 Angular,ASP.NET MVC 和 Web API 创建应用程序

本章将指导您通过将 Angular 应用程序与 ASP.NET MVC 和 ASP.NET Web API 集成的过程。在上一章中,我们使用 Angular 应用程序消耗了 ASP.NET Web API 服务。所有视图都由 Angular 渲染。在本章中,我们将从 ASP.NET MVC 提供视图。因此,它提供了许多机会,比如使用 Razor 语法,因为 Angular 视图将由 ASP.NET MVC 提供动力。

在本章中,我们将涵盖以下主题:

  • 使用 ASP.NET MVC 为 Angular 组件模板提供视图

  • 结合 ASP.NET MVC,ASP.NET Web API 和 Angular 的路由

使用 ASP.NET MVC


ASP.NET 包括 Web 堆栈,如 ASP.NET MVC,Razor 和 Web API。ASP.NET MVC 框架是建立在 ASP.NET 之上的。ASP.NET MVC Web 框架实现了模型-视图-控制器(MVC)模式以开发 Web 应用程序。

在 MVC 模式中,模型代表业务对象的状态。视图表示用户界面,控制器处理模型和视图之间的通信。所有请求将由控制器处理,并返回响应:

MVC 架构

ASP.NET MVC 有自己的视图引擎,称为 Razor 引擎。

结合 ASP.NET MVC,ASP.NET Web API 和 Angular 的路由


路由是将端点分解为可处理请求的模块或控制器和操作的过程。路由使 URL 可读且有意义。它还帮助隐藏用户的数据。

ASP.NET MVC 中的路由

ASP.NET MVC 路由将请求映射到控制器操作。所有路由将在路由表中定义,并由路由引擎使用来匹配请求的 URL 模式与控制器和操作。

我们可以在Startup.cs文件的 configure 方法中向路由表添加路由。以下代码片段显示了在路由表上注册的默认路由:

public void Configure(IApplicationBuilder app) 
{ 
    app.UseIISPlatformHandler(); 
    app.UseDefaultFiles(); 
    app.UseStaticFiles(); 
    app.UseMvc(config => 
    { 
        config.MapRoute( 
            name: "Default", 
            template: "{controller}/{action}/{id?}", 
            defaults: new { controller = "Home", action = "Index" } 
            ); 
    });             
} 

在这里,一个路由被注册为模板和默认值。如果在 URL 中未提供控制器或操作名称,则该请求将映射到HomeController类中的Index操作;否则,它将映射到相应的控制器操作。

在我们的应用程序中,我们有三个 MVC 控制器,分别是HomeControllerUserControllerTodoController

添加 ASP.NET MVC HomeController

Index action:
public IActionResult Index() 
{ 
    return View(); 
} 

当一个请求被路由到Index操作时,它将返回Index视图。Index.cshtml的源代码如下所示:

 @{ 
    Layout = null; 
} 
<!DOCTYPE html> 
<html> 
<head> 
    <meta name="viewport" content="width=device-width" /> 
    <title>Index</title> 
</head> 
<body> 
    <h1>index view</h1> 
</body> 
</html> 

按照给定的步骤,将 ASP.NET MVC 的HomeController及其相应视图添加到我们在早期章节中创建的My Todo应用程序中:

  1. 右键单击Controllers文件夹并添加一个新的控制器。

  2. 将新添加的控制器命名为HomeController。请注意,默认情况下HomeController已添加了Index操作。

  3. 现在,让我们为Index动作添加一个视图。右键单击My Todo应用程序,并添加一个名为Views的新文件夹。

  4. 然后,在刚刚创建的Views文件夹下添加一个名为Home的文件夹。

  5. 右键单击Home文件夹并添加一个名为Index.cshtml的新视图:

ASP.NET MVC HomeController Index 视图

ASP.NET MVC 的路由

我们刚刚创建了一个 ASP.NET MVC 控制器,并为控制器的Index动作添加了一个视图。现在我们需要为 ASP.NET MVC 应用配置路由,以便任何对Index动作的请求都由 ASP.NET MVC 路由处理。请按照以下步骤配置 ASP.NET MVC 路由:

  1. 打开Startup.cs

  2. 请注释或删除Configure方法中的app.UseDefaultFiles()语句,因为我们将使用 ASP.NET MVC 来提供视图。

  3. 用这个声明替换Configure方法中的app.UseMvc()语句:

     app.UseMvc(config =>   
            {   
              config.MapRoute(   
              name: "Default",   
              template: "{controller}/{action}/{id?}",   
     defaults: new   { controller = "Home", action =    
     "Index" }   
               );   
            });

在这里,我们已经添加了 ASP.NET MVC 的默认路由。对于 Web API 的任何请求都将与控制器中的 HTTP 谓词或动作进行映射。

通过按下F5键来运行应用程序,您将在浏览器中看到呈现的图形界面:

ASP.NET MVC HomeController Index 视图在浏览器中呈现

将内容从默认页面移动到 ASP.NET MVC 视图

在前面的部分中,我们能够运行应用程序并注意到默认视图是由 HomeController 提供的 ASP.NET MVC Index 视图。现在,让我们使用wwwroot文件夹下的Index.html文件的内容更新Index.cshtml视图。更新后的Index.cshtml如下所示:

@{   
    Layout = null;   
}   
<!DOCTYPE html>   
<html>   
<head>   
    <title>My   Todo</title>   
    <script>document.write('<base   href="' + 
    document.location + '" />');</script>   
    <meta charset="UTF-8">   
    <!-- Polyfills -->   
    <script src="img/shim.min.js"></script>   
    <script src="img/zone.min.js"></script>   
    <script src="img/system.src.js"></script>   
    <script src="img/systemjs.config.js"></script>   
    <script>   
    System.import('main.js').catch(function(err){   
    console.error(err); });   
    </script>   
</head>   
<body>   
    <h1>My Todo   App</h1>   
    <my-app>Loading My Todo   App...</my-app>   
</body>   
</html>   

现在通过按下F5来运行应用程序,并注意my-app特殊标签已被 Angular 解析为app.component.html的模板,如下所示:

Angular 应用加载到 ASP.NET MVC Index 视图

ASP.NET Web API 中的路由

任何对 Web API 的请求都将使用路由将其映射到控制器中的 HTTP 谓词或动作。Web API 2 引入了一种基于属性的路由技术,称为属性路由。可以在控制器级别和操作级别添加属性路由。可以通过传递 URL 模板来修饰 Web API 控制器的Route属性,如下所示:

[Route("api/[controller]")] 
public class TodosController : Controller 
{    
    // GET: api/todos/pending-only 
    [HttpGet] 
    [Route("pending-only")] 
    public IEnumerable<Todo> GetPendingOnly() 
    { 
    } 
} 

在这里,TodosControllerRoute修饰,并且使用api/[controller] URL 模板。这意味着如果收到的请求为www.programmerguide.net/api/todos,它将被路由到TodosController,且动作将根据应用的 HTTP 动作进行选择。

注意GetPendingOnly动作被Route修饰,并且使用pending-onlyURL 模板。这意味着如果控制器中有更多的GET动作可用,且请求 URL 为www.programmerguide.net/api/todos/pending-only,它将被映射到GetPendingOnly动作。

TodosController Web API 控制器的完整源代码如下:

[Produces("application/json")] 
    [Route("api/Todos")] 
    public class TodosController : Controller 
    { 
        private TodoContext _db; 
        public TodosController(TodoContext context) 
        { 
            _db = context; 
        } 
        // GET: api/todos 
        [HttpGet] 
        public IEnumerable<Todo> Get() 
        { 
            return _db.Todos.ToList(); 
        } 
        // GET: api/todos/pending-only 
        [HttpGet] 
        [Route("pending-only")] 
        public IEnumerable<Todo> GetPendingOnly() 
        { 
            _db.Todos.RemoveRange(_db.Todos.Where(x => 
            x.Completed == true)); 
            _db.SaveChanges(); 
            return _db.Todos.ToList(); 
        } 
        // POST api/todos 
        [HttpPost] 
        public Todo Post([FromBody]Todo value) 
        { 
            _db.Todos.Add(value); 
            _db.SaveChanges(); 
            return value; 
        } 
        // PUT api/todos/id 
        [HttpPut("{id}")] 
        public Todo Put(int id, [FromBody]Todo value) 
        { 
            var todo = _db.Todos.FirstOrDefault(x => x.Id  
            == id); 
            todo.Title = value.Title; 
            todo.Completed = value.Completed; 
            _db.Entry(todo).State = EntityState.Modified; 
            _db.SaveChanges(); 
            return value; 
        } 
        // DELETE api/todos/id 
        [HttpDelete("{id}")] 
        public void Delete(int id) 
        { 
            var todo = _db.Todos.FirstOrDefault(x => x.Id 
            == id); 
            _db.Entry(todo).State = EntityState.Deleted; 
            _db.SaveChanges(); 
        } 
  } 

Angular 中的路由

正如我们在第三章,Angular 构建模块 - 第二部分中看到的那样,Angular 引入了一个组件路由器,它深度链接 URL 请求,映射为此路由注释的组件,并渲染与该组件关联的模板或视图。Angular 路由器不是核心 Angular 框架的一部分,它作为 Angular 路由器模块的一部分。我们需要在package.json中的依赖项部分添加对此库的引用,如下所示:

"dependencies": {
"@angular/router": "~4.0.0",
}
<base> tag with the href attribute that should be added to the head tag in the index file, considering that the app folder is the root of the application:
<base href="/">

路由器通过查看浏览器请求的 URL 的RouteDefinition决定组件和模板。因此,我们需要配置路由定义。

我们的首页将有三个超链接,分别是todoaboutcontact。点击todo将导航用户到todo应用,点击about将导航到about视图,最后,点击contact将导航用户到contact视图。因此,我们需要添加另外两个组件,分别是AboutComponentContactComponent,以及它们各自的模板文件,分别是about.component.htmlcontact.component.html。按照下面的步骤创建这些组件和它们的模板:

  1. 右键单击app文件夹,并添加两个 HTML 模板:about.component.htmlcontact.component.html

  2. 将以下 HTML 片段添加为about.component.html的内容:

      <h1>This is the About   View</h1>   
  1. 将以下 HTML 片段添加为contact.component.html的内容:
      <h1>This is the Contact   View</h1>   
  1. 右键单击app文件夹,添加两个 Angular 组件:about.component.tscontact.component.ts

  2. 将以下代码片段添加到about.component.ts

    import { Component } from '@angular/core';   
    @Component({   
        selector: 'about-me',   
        templateUrl: './app/about.component.html',   
    })   
    export class AboutComponent { }   
  1. 将以下代码片段添加到contact.component.ts
    import { Component } from '@angular/core';    
    @Component({   
        selector: 'contact-us',   
        templateUrl: './app/contact.component.html',   
    })     
      export class ContactComponent { }
  1. 还要创建一个 Angular 组件,todo.component.ts,并将app.component.ts中的属性和方法移动到todo.component.ts。同时,更新TodoComponent的导入和注解。TodoComponent的完整代码片段如下所示:
     import { Component, OnInit } from   '@angular/core';   
     import { Todo } from './todo'   
     import { TodoService } from './todo.service'     
     @Component({   
         selector: 'my-app',   
         templateUrl: './app/todo.component.html',   
         providers: [TodoService]   
     })   
       export class TodoComponent   implements OnInit {   
         todos: Array<Todo>;   
         newTodoText = '';   
       constructor(private   todoService: TodoService) {   
          this.todos = new Array();   
       }   
           getTodos(): void {   
           this.todoService   
           .getTodos()   
           .then(todos =>   this.todos = todos);   
       }   
       ngOnInit(): void {   
        this.getTodos();   
       }   
        removeCompleted() {   
        this.todoService.removeCompleted();   
        this.todos =   this.getPending();   
       }   
       toggleCompletion(todo: Todo)   {   
          this.todoService.toggleCompletion(todo);   
       }   
       remove(todo: Todo) {   
          this.todoService.remove(todo);   
          this.todos.splice(this.todos.indexOf(todo), 1);   
       }   
       addTodo() {   
           if (this.newTodoText.trim().length)   {   
           this.todoService.add(this.newTodoText).then(res      
           => {   
           this.getTodos();   
           });   
           this.newTodoText = '';   
           this.getTodos();   
           }   
       }   
       getPending() {   
           return this.todos.filter((todo:   Todo) =>   
     todo.completed === false);   
       }   

        getCompleted() {   
        return   this.todos.filter((todo: Todo) =>   
        todo.completed === true);   
       }   
     }   
  1. 现在,创建todo.component.html模板,并将app.component.html的内容移动过去。更新后的todo.component.html如下所示:
    <section>   
       <header>   
          <h1>todos</h1>   
        <input placeholder="Add   new todo" autofocus=""   
        [(ngModel)]="newTodoText">   
        <button type="button"   
        (click)="addTodo()">Add</button>   
     </header>   
    <section>   
        <ul>   
            <li *ngFor="let   todo of todos">   
            <input type="checkbox"    
            (click)="toggleCompletion(todo)"  
            [checked]="todo.completed">   
            <label>{{todo.title}}</label>   
            <button   (click)="remove(todo)">X</button>   
            </li>   
        </ul>   
    </section>   
          <footer *ngIf="todos.length   > 0">   
          <span><strong>{{getPending().length}}</strong>     
          {{getPending().length == 1 ? 'item' : 'items'}}   
          left</span>   
          <button *ngIf="getCompleted().length   > 0"    
          (click)="removeCompleted()">Clear     
          completed</button>   
          </footer>   
    </section>
  1. 接下来,添加一个app.routing.ts文件,并使用下面的代码片段更新它。在这里,我们为todoaboutcontact配置了三个路由。此外,我们分配了三个组件--TodoComponentAboutComponentContactComponent--来导出NgModule属性的元数据:
    import { NgModule } from '@angular/core';   
    import { Routes, RouterModule }   from  
    '@angular/router';   
    import { TodoComponent } from './todo.component';   
    import { AboutComponent } from './about.component';   
    import { ContactComponent } from   
    './contact.component';   
    export const appRoutes: Routes =   [   
        {   
            path: '',   
            redirectTo: 'todo',   
            pathMatch: 'full',   
        },       
        { path: 'todo', component:   TodoComponent, data:    
          { title: 'Todo' } },   
        { path: 'about', component:  AboutComponent, data:   
          { title: 'About' } },   
        { path: 'contact', component: ContactComponent,   
           data: { title: 'Contact' } }   
    ];     
    export const routedComponents = [   
        TodoComponent,   
        AboutComponent,   
        ContactComponent   
    ];   
    @NgModule({   
        imports:   [RouterModule.forRoot(appRoutes)],   
        exports: [RouterModule]   
    })   
     export class AppRoutingModule { }   
  1. 更新app.module.ts如下以导入我们在上一步创建的AppRoutingModule
    import { NgModule } from '@angular/core';   
    import { BrowserModule } from '@angular/platform-  
    browser';   
    import { FormsModule } from '@angular/forms';   
    import { HttpModule } from '@angular/http';   
    import { AppComponent } from './app.component';   
    import { TodoComponent } from './todo.component';   
    import { AboutComponent } from './about.component';   
    import { ContactComponent } from   
    './contact.component';   
    import { AppRoutingModule } from './app.routing';   
    import { TodoService } from './todo.service'     
    @NgModule({   
        imports: [   
            BrowserModule,   
            FormsModule,   
            HttpModule,   
            AppRoutingModule   
        ],   
        declarations: [   
            AppComponent,    
            TodoComponent,   
            AboutComponent,   
            ContactComponent   
        ],   
        providers: [TodoService],   
        bootstrap: [AppComponent]   
    })   
    export class AppModule { }   
  1. 最后,如下更新app.component.html
    <a routerLinkActive="active"   [routerLink]="   
    ['/todo']">Todo</a>   
    <a routerLinkActive="active"   [routerLink]="
    ['/about']">About</a>   
    <a routerLinkActive="active"   [routerLink]="
    ['/contact']">Contact</a>   
    <router-outlet></router-outlet>   

注意每个超链接都有routerLink属性,并分配了路由路径。这里,routerLinkActive属性分配了active CSS 类,当该路由变为活动状态时,将添加到该元素上。换句话说,当用户点击Todo链接时,该链接将被分配active CSS 类。

routerLink属性使应用程序能够链接到应用程序的特定部分或组件。下一条语句是组件路由的<router-outlet/>特殊标记,类似于 AngularJS 1.x 中的<ng-view/>标记,用于加载与相应路由路径相关联的模板。

  1. 按下F5运行应用程序,浏览器将通过导航到Todo路径来加载应用程序,因为我们已经设置了如果是根路径,就重定向到todo

加载 todo 模板,URL 为\todo 路径

  1. 点击About链接将导航到\about 路径,并加载about的解析模板视图:

加载 about 模板,URL 为\about 路径

  1. 点击Contact链接将导航到\contact 路径,并加载 about 的解析模板视图:

加载 contact 模板,URL 为\contact 路径

注意在 URL 中路由路径的变化。

将 Angular 模板移到 ASP.NET MVC 模板

我们几乎完成了应用程序。但是,我们只使用了 Angular 视图作为 Angular 组件的模板。我们需要通过 ASP.NET MVC 提供模板。这将使我们能够根据需要添加 Razor 代码,因为这些视图是由 ASP.NET MVC 提供支持的。按照以下步骤添加 Razor 视图并更新每个 Angular 组件中的templateUrl

  1. 首先,在HomeController中添加三个动作,分别为AboutContactTodo,如下所示:
        public IActionResult   About()   
        {   
            return View();   
        }   
        public IActionResult   Contact()   
        {   
            return View();
        }      
        public IActionResult   Todo()   
        {   
            return View();   
        }   
  1. Views -> Home文件夹下添加三个视图,分别是AboutContactTodo,如下所示:

在 Home 下添加 Razor 视图

  1. 将以下 HTML 内容添加到About.cshtml
    <h1>This is the About Razor   View</h1>   
  1. 添加以下 HTML 内容到Contact.cshtml
    <h1>This is the Contact Razor View</h1>
  1. 然后,将todo.component.html的内容移动到Todo.cshtml

  2. 现在需要将AboutComponentContactComponentTodoComponenttemplateUrl的元数据更新为 HomeController 中相应操作的 URL:

     TodoComponent:   
           templateUrl: '/Home/Todo'   
     AboutComponent:   
           templateUrl: '/Home/About'   
     ContactComponent:   
           templateUrl: '/Home/Contact',   
  1. 现在,按下F5运行应用程序,并注意视图是从 ASP.NET MVC 提供的。现在你可以在视图中添加 Razor 语法,因为它们现在由 ASP.NET MVC 提供支持。

  2. 点击About链接将导航到\about 路径,并实例化相应的组件。这里,它是AboutComponent,并且会呈现适当的about Razor 视图:

关于 Razor 模板呈现

  1. 点击联系链接将导航到 \contact 路径,并启动负责呈现联系Razor 视图的ContactComponent

联系 Razor 模板呈现

点击注销将重定向到登录视图。

摘要


哇!我们刚刚创建了一个由 ASP.NET MVC 提供支持的 Angular 应用程序,具有后端 Web API。我们结合了 Angular 和 ASP.NET MVC 的路由,并演示了这些路由是如何连接在一起的。

在下一章,我们将讨论测试 Angular 应用程序。

第八章:测试 Angular 应用

本章讨论使用Jasmine框架测试 Angular 组件和服务。

在本章中,我们将涵盖以下主题:

  • 介绍 Jasmine

  • 测试 Angular 服务

  • 测试 Angular 组件

介绍 Jasmine


Jasmine 是一个开源框架,用于在不依赖于 DOM 的情况下测试 JavaScript 代码。由于 Angular 是松散耦合的,我们可以使用 Jasmine 框架来测试 Angular 组件、服务等。独立于彼此,Jasmine 的清晰语法使您能够轻松编写测试。

一个名为 describe 的全局函数是Jasmine函数的起始点。这个全局函数接受一个函数和两个 string 类型的参数。字符串参数描述了测试,函数将有测试的实际实现:

describe("short description about the test suite", function() { 
}); 

实际测试方法由名为it函数的全局函数定义,该函数接受两个参数。第一个参数是测试或规范的标题,第二个参数是通过验证代码状态来测试期望的函数。期望与 Microsoft 单元测试框架中的断言类似。如果在规范中任何一个定义的期望失败,这被称为失败的规范。以下代码说明了前述声明:

describe("short description about the test suite", function() { 
  it("a spec with single expectation", function() { 
    expect(true).toBe(true); 
  }); 
}); 

测试方法或规范方法将会有一个或多个 expect 语句,如下所示,通过链式连接到 expect 函数的匹配器函数来比较实际值和期望值;有各种默认的匹配器函数可供使用:

describe("short description about the test suite", function() { 
it("a spec with single expectation", function() { 
expect(afunction).toThrow(e); 
expect(true).toBe(true); 
expect(variable).toBeDefined(); 
expect(variable).toBeFalsy(); 
expect(number).toBeGreaterThan(number); 
expect(number).toBeLessThan(number); 
expect(variable).toBeNull(); 
expect(variable).toBeTruthy(); 
expect(value).toBeUndefined(); 
expect(array).toContain(member); 
expect(string).toContain(substring); 
expect(variable).toEqual(variable); 
expect(value).toMatch(pattern); 
  }); 
}); 

我们只看到了Jasmine框架的基础知识,还有更多的功能可以使用。你可以通过访问官方网站jasmine.github.io/了解更多信息。这个简介已经足够让我们学会如何测试 Angular 服务和组件了。

测试 Angular 服务


Todo class:
export class Todo { 
    title: string; 
    completed: boolean;
    constructor(title: string) { 
        this.title = title; 
        this.completed = false; 
    } 
    set isCompleted(value: boolean) { 
        this.completed = value; 
    } 
} 

接下来,创建一个名为todo.service.ts的服务,它在构造函数中构建Todo项目列表。todo.service.ts的完整代码如下所示:

import { Todo } from './todo' 
export class TodoService { 
    todos: Array<Todo> 
    constructor() { 
        this.todos = [new Todo('First item'), 
        new Todo('Second item'), 
        new Todo('Third item')]; 
    } 
    getPending() { 
        return this.todos.filter((todo: Todo) => todo.completed === 
        false); 
    } 
    getCompleted() { 
        return this.todos.filter((todo: Todo) => todo.completed === 
        true); 
    } 
    remove(todo: Todo) { 
        this.todos.splice(this.todos.indexOf(todo), 1); 
    } 
    add(title: string) { 
        this.todos.push(new Todo(title)); 
    } 
    toggleCompletion(todo: Todo) { 
        todo.completed = !todo.completed; 
    } 
    removeCompleted() { 
        this.todos = this.getPending(); 
    } 
} 

我们已经建立了与数据源交互的服务。现在,让我们使用 Jasmine 框架编写测试来测试TodoService。我们将测试getPending()getCompleted()两种方法。创建一个名为todo.service.spec.ts的文件。

导入TodoService应用特定的服务,如下所示:

import { TodoService } from "./todo.service"; 

定义describe方法:是 Jasmine 函数的起始点的全局函数,接受两个参数,一个描述测试的字符串和一个具有测试实际实现的函数:

describe("TodoService Testing",() => { 
}); 
describe function with beforeEach is given here:
describe('TodoService Testing', () => { 
  let service: TodoService; 
  beforeEach(() => { service = new TodoService(); });   
}); 

beforeEach函数将在运行每个测试方法之前执行,并且为每个测试提供一个TodoService的实例。

现在,让我们定义it测试方法,示例如下:

it('getPending length should return 3', () => { 
    expect(service.getPending().length).toBe(3); 
}); 
it('getCompleted length should return 0', () => { 
    expect(service.getCompleted().length).toBe(0); 
}); 

在这里,我们验证getPending()getCompleted()返回值的长度期望。

todo.service.spec.ts is this:
import { TodoService } from "./todo.service";  
describe('TodoService Testing', () => { 
  let service: TodoService; 
  beforeEach(() => { service = new TodoService(); }); 
  it('getPending length should return 3', () => { 
    expect(service.getPending().length).toBe(3); 
  }); 
  it('getCompleted length should return 0', () => { 
    expect(service.getCompleted().length).toBe(0); 
  }); 
}); 

我们已准备好要运行的测试用例或测试; 通过执行以下命令来运行它们:

npm run build:watchkarma start karma.conf.js

npm run build:watch命令将构建您的应用程序,并将 TypeScript 文件转译为 JavaScript。然后,执行karma start karma.config命令启动我们应用程序的测试运行器。

Karma 是一个测试运行器,可用于对任何 JavaScript 应用程序运行测试。karma.config.js文件是 karma 的配置文件,提供有关我们应用程序的信息,以便它能够了解并测试应用程序。karma 配置文件包含应用程序消耗的 JavaScript 库和框架的路径详细信息,还提供 karma 所使用的插件的详细信息。

Karma 配置文件包含了应用程序中basePathframeworkspluginsclient和自定义启动器的配置详细信息。我们已在 karma 中配置了 Jasmine 作为我们的测试框架,我们在运行测试时加载了所需模块的插件列表。我们还配置了具有buildPathsclearContext的客户端。buildPaths将包含查找转译后应用程序 JS 和映射文件的路径详细信息。以下是供您参考的完整 karma 配置文件:

module.exports = function(config)   {    
  var appBase    = 'src/';         // transpiled app JS and map files   
  var appAssets  = '/base/app/';   // component assets fetched by  
  Angular's compiler   
  // Testing helpers (optional)   are conventionally in a folder called 
     `testing`   
  var testingBase    = 'src/testing/';   // transpiled test JS and map  
  files   
  var testingSrcBase = 'src/testing/';   // test source TS    
   files   
  config.set({   
    basePath: '',   
    frameworks: ['jasmine'],   
    plugins: [   
      require('karma-jasmine'),   
      require('karma-chrome-launcher'),   
      require('karma-jasmine-html-reporter')   
    ],   
    client: 
  {   
      builtPaths: [appBase,   testingBase], // add more 
      spec base paths 
      as needed   
      clearContext: false //   leave Jasmine Spec Runner 
      output visible  
      in browser   
    },   
    customLaunchers: {   
      // From the CLI. Not used   here but interesting   
      // chrome setup for travis   CI using chromium   
      Chrome_travis_ci: {   
        base: 'Chrome',   
        flags: ['--no-sandbox']   
      }   
    },   
    files: [   
      // System.js for module   loading   
      'node_modules/systemjs/dist/system.src.js',   
      // Polyfills   
      'node_modules/core-js/client/shim.js',   
      // zone.js   
      'node_modules/zone.js/dist/zone.js',   
      'node_modules/zone.js/dist/long-stack-trace-
       zone.js',   
      'node_modules/zone.js/dist/proxy.js',   
      'node_modules/zone.js/dist/sync-test.js',   
      'node_modules/zone.js/dist/jasmine-patch.js',   
      'node_modules/zone.js/dist/async-test.js',   
      'node_modules/zone.js/dist/fake-async-test.js',   
      // RxJs   
      { pattern: 'node_modules/rxjs/**/*.js',   included: 
        false, 
        watched: false },   
      { pattern: 'node_modules/rxjs/**/*.js.map',   
        included: false, 
        watched: false },   
      // Paths loaded via module   imports:   
      // Angular itself   
      { pattern: 'node_modules/@angular/**/*.js',   
        included: false, 
        watched: false },   
      { pattern: 'node_modules/@angular/**/*.js.map',   
        included: 
        false, watched: false },   
      { pattern: appBase + '/systemjs.config.js',   
        included: false, 
         watched: false },   
      { pattern: appBase + '/systemjs.config.extras.js',   
        included: 
        false, watched: false },   
      'karma-test-shim.js', //   optionally extend 
       SystemJS mapping 
       e.g., with barrels   
      // transpiled application   & spec code paths loaded 
         via module 
         imports   
      { pattern: appBase + '**/*.js',   included: false, 
        watched: true   
  },   
      { pattern: testingBase + '**/*.js',   included: 
        false, watched:  
        true 
  },   
      // Asset (HTML & CSS)   paths loaded via Angular's 
         component    
         compiler   
      // (these paths need to be   rewritten, see proxies 
          section)   
      { pattern: appBase + '**/*.html',   included: false, 
        watched: 
        true 
  },   
      { pattern: appBase + '**/*.css',   included: false, 
        watched: true        
  },    
      // Paths for debugging with   source maps in dev    
         tools   
      { pattern: appBase + '**/*.ts',   included: false, 
        watched: false   
  },   
      { pattern: appBase + '**/*.js.map',   included: 
        false, watched: 
        false 
  },   
      { pattern: testingSrcBase +   '**/*.ts', included: 
        false, 
        watched: false },   
      { pattern: testingBase + '**/*.js.map',   included: 
        false, 
        watched: false}   
    ],   
    // Proxied base paths for   loading assets   
        proxies: 
  {   
      // required for modules   fetched by SystemJS   
      '/base/src/node_modules/': '/base/node_modules/'   
  },   
    exclude: [],   
    preprocessors: {},   
    reporters: ['progress', 'kjhtml'],   
    port: 9876,   
    colors: true,   
    logLevel: config.LOG_INFO,   
    autoWatch: true,   
    browsers: ['Chrome'],   
    singleRun: false   
   })   
 }   

命令karma start会以 karma 配置文件路径为参数,并启动 karma 测试运行器。npm run build命令配置在pretest中,这样它将在运行测试之前执行。它执行tsc -p src命令,这是一个 TypeScript 编译器,用于转译src文件夹中的代码。以下屏幕截图说明了根据package.jsonscripts项中的配置,在命令窗口中执行这些命令:

C:\Users\rajesh.g\Packt\Chapter8\mytodos>npm     
            test> my-todo@1.0.0 pretest   
          C:\Users\rajesh.g\Packt\Chapter8\mytodos> npm run build> my-todo@1.0.0 build   
          C:\Users\rajesh.g\Packt\Chapter8\mytodos> tsc -p src/> my-todo@1.0.0 test   
          C:\Users\rajesh.g\Packt\Chapter8\mytodos> concurrently "npm run   build:watch" "karma 
            start 
            karma.conf.js"

Karma 在浏览器中启动应用程序,并运行 specs 中的所有测试。http-server命令将启动开发服务器,以托管mytodo Angular 应用程序。测试执行结果如下所示:

TodoService 的测试结果

Angular 组件测试


我们刚刚学习了如何在 Angular 应用程序中测试服务。现在,让我们讨论如何测试 Angular 组件。执行以下步骤来为应用程序创建AppComponent

  1. 创建名为app.component.ts的文件。

  2. 导入必要的ComponentTodoServiceTodo等模块,用于AppComponent,如下所示:

        import { Component } from '@angular/core'; 
        import { Todo } from './todo'; 
        import { TodoService } from './todo.service'; 
  1. 如下所示,定义AppComponent类:
        export class AppComponent {} 
  1. 通过@Component属性装饰AppComponent类,具有selectorproviderstemplateUrl元数据:
        @Component({ 
            selector: 'my-app', 
            templateUrl: './app.component.html', 
            providers: [TodoService] 
        }) 
        export class AppComponent {     
        } 
  1. 声明todostodoServicenewTodoTexttitle变量:
        todos: Array<Todo>; 
        todoService: TodoService; 
        newTodoText = ''; 
        title = 'Test My Todo App'; 
  1. 定义构造函数,并注入todoService,如下所示。请注意,构造函数使用todoService返回的todos更新todos
        constructor(todoService: TodoService) 
        { 
              this.todoService = todoService; 
              this.todos = todoService.todos; 
        } 
  1. 引入addTodo()函数,调用TodoServiceadd()方法,并传递新todo的描述,如下图所示:
        addTodo() 
        { 
              if (this.newTodoText.trim().length) 
              { 
                  this.todoService.add(this.newTodoText); 
                  this.newTodoText = ''; 
              } 
        }
  1. 引入调用TodoServiceremove()方法通过传递要移除的todo对象来移除该对象的remove()函数,如下所示:
       remove(todo: Todo) 
       { 
              this.todoService.remove(todo); 
       } 
  1. 引入调用TodoServiceremoveCompleted()方法来删除所有已完成的待办事项的removeCompleted()函数:
      removeCompleted() 
      { 
            this.todoService.removeCompleted(); 
      } 
  1. 引入调用TodoServicetoggleCompletion()方法来切换todo项的完成状态值的toggleCompletion()函数:
      toggleCompletion(todo: Todo) 
      { 
             todo.completed = !todo.completed; 
      } 
AppComponent is this:
import { Component } from '@angular/core'; 
import { Todo } from './todo'; 
import { TodoService } from './todo.service'; 
@Component({ 
    selector: 'my-app', 
    templateUrl: './app.component.html', 
    providers: [TodoService] 
}) 
export class AppComponent { 
    todos: Array<Todo>; 
    todoService: TodoService; 
    newTodoText = ''; 
    title = 'Test My Todo App'; 
    constructor(todoService: TodoService) { 
        this.todoService = todoService; 
        this.todos = todoService.todos; 
    } 
    removeCompleted() { 
        this.todoService.removeCompleted(); 
    } 
    toggleCompletion(todo: Todo) { 
        this.todoService.toggleCompletion(todo); 
    } 
    remove(todo: Todo) { 
        this.todoService.remove(todo); 
    } 
    addTodo() { 
        if (this.newTodoText.trim().length) { 
            this.todoService.add(this.newTodoText); 
            this.newTodoText = ''; 
        } 
    } 
} 

现在我们已经准备好了AppComponent。此AppComponent的模板定义在一个模板文件app.component.html中。

编写 AppComponent 的规范

让我们使用 Jasmine 来编写测试 AppComponent 的规范:

  1. 创建一个app.component.spec.ts文件来为AppComponent编写规范或测试。

  2. 从 Angular 核心中导入模块,例如 asyncComponentFixtureTestBedFormsModuleByDebugElementAppComponent

  3. 写以下的describe全局函数并声明必要的变量:

     describe('AppComponent (templateUrl)', () => {
       let comp:    AppComponent; 
       let fixture: ComponentFixture<AppComponent>; 
       let de:      DebugElement; 
       let el:      HTMLElement; 
     });
  1. 然后,创建两个beforeEach函数:一个用于编译模板和 CSS,另一个用于获取组件的实例。代码段如下所示:
    // async beforeEach 
    beforeEach(async(() => { 
        TestBed.configureTestingModule({ 
          imports: [FormsModule], 
          declarations: [ AppComponent ], // declare the     
    test component 
        }) 
        .compileComponents();  // compile template and css 
     })); 
     // synchronous beforeEach 
     beforeEach(() => { 
        fixture = TestBed.createComponent(AppComponent); 
        comp = fixture.componentInstance; // AppComponent     
    test instance 
        // query for the title <h1> by CSS element    
           selector 
        de = fixture.debugElement.query(By.css('h1')); 
        el = de.nativeElement; 
     }); 

对于每个测试,我们可能会重复相同的代码来初始化或清除一些对象。为了简化开发者的工作,Jasmine 提供了在执行每个测试方法之前和之后运行的 beforeEachafterEach 全局功能。

  1. 最后,添加it测试或规范函数来验证期望,如下所示:
    it('no title in the DOM until manually call     
    `detectChanges`', () => { 
    expect(el.textContent).toEqual(''); 
    }); 
    it('should display original title', () => { 
        fixture.detectChanges(); 
        expect(el.textContent).toContain(comp.title); 
    });
    it('should display a different test title', () => { 
        comp.title = 'Test My Todo'; 
        fixture.detectChanges(); 
        expect(el.textContent).toContain('Test My Todo'); 
    }); 
app.component.spec.ts is as follows:
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 
import { FormsModule } from '@angular/forms'; 
import { By }              from '@angular/platform-browser'; 
import { DebugElement }    from '@angular/core'; 
import { AppComponent } from './app.component'; 
describe('AppComponent (templateUrl)', () => { 
  let comp:    AppComponent; 
  let fixture: ComponentFixture<AppComponent>; 
  let de:      DebugElement; 
  let el:      HTMLElement; 
  // async beforeEach 
  beforeEach(async(() => { 
    TestBed.configureTestingModule({ 
      imports: [FormsModule], 
      declarations: [ AppComponent ], // declare the test component 
    }) 
    .compileComponents();  // compile template and css 
  })); 
  // synchronous beforeEach 
  beforeEach(() => { 
    fixture = TestBed.createComponent(AppComponent); 
    comp = fixture.componentInstance; // AppComponent test instance 
    // query for the title <h1> by CSS element selector 
    de = fixture.debugElement.query(By.css('h1')); 
    el = de.nativeElement; 
  });
  it('no title in the DOM until manually call `detectChanges`', () => { 
    expect(el.textContent).toEqual(''); 
  });
  it('should display original title', () => { 
    fixture.detectChanges(); 
    expect(el.textContent).toContain(comp.title); 
  });
  it('should display a different test title', () => { 
    comp.title = 'Test My Todo'; 
    fixture.detectChanges(); 
    expect(el.textContent).toContain('Test My Todo'); 
  }); 
}); 

运行 AppComponent 的规范或测试

在命令行窗口中执行npm test命令,应用程序将启动,并为您运行测试。以下截图显示了针对AppComponent运行的测试或规范的结果:

AppComponent 的测试结果

单元测试一个模拟服务


通过将服务注入到组件中保持组件的精简,使我们能够使用模拟服务编写单元测试。我们可以通过模仿服务行为使用其接口来模拟注入的服务:

class MockTodoService extends TodoService   {   
    getPending() {   
        return [];   
    }   
}      

在这里,我们通过扩展和覆盖getPending方法创建了一个现有待办服务的模拟,以返回一个空数组。

我们可以使用 testBed 来测试这一点,指示如何使用模拟服务 MockTodoService 而不是实际服务 TodoService,如下所示:

beforeEach(async(() => {   
      TestBed.configureTestingModule({   
        providers: [   
        {   
            provide: TodoService,   
            useClass:   MockTodoService   
        }   
    ]})   
    .compileComponents();   
}));   

在这里,我们指示如何使用 MockTodoService 而不是 TodoService,并且可以跳过测试的结果,如下所示:

it('should return empty array   when getPending method is fired', () => {   
   let service =   fixture.debugElement.injector.get(TodoService);   
   spyOn(service, 'getPending').and.returnValue([]);        
});

这里,它从 fixture 中获取模拟服务 MockTodoService,并添加一个 spyOn 覆盖,假装列表中没有待办事项。

总结


万岁!我们学会了为 Angular 应用程序编写自动化测试的基础知识。

我们从介绍 Jasmine 框架开始这一章,学习如何编写有效的测试并自动运行它们。然后,我们学习了如何使用 Jasmine 框架测试组件和服务,并断言应用程序的行为。最后,我们讨论了模拟服务并使用 spyOn 进行测试。

在下一章中,我们将讨论一些关于 Angular 和 .NET Core 的新主题。

第九章:Angular 和 ASP.NET Core 中的新功能

我们从讨论 Angular、构建一个 Hello World 应用程序和 Angular 的核心架构开始了本书。然后,我们深入了解了 Angular 的构建块。接下来,我们讨论了 TypeScript 的基础知识以及如何在编写 Angular 应用程序时使用 TypeScript。之后,我们在 Visual Studio 中逐步创建了一个 Angular 单页面应用,并学习了 RESTful 服务以及如何使用 Web API 为 Angular 应用创建 RESTful 服务。之后,我们通过使用 Angular、ASP.NET MVC 和 Web API 在 Visual Studio 中逐步创建了一个应用程序。最后,我们学习了如何使用 Karma 和 Jasmine 测试 Angular 应用。

本章将讨论 Angular 和 ASP.NET Core 中的新功能。我们将涵盖以下话题:

  • 预编译

  • 模板更新

  • 引入标题大小写管道

  • 简化 HTTP 参数传递

  • 在测试中覆盖模板

  • Meta 服务的引入

  • 新的表单验证器

  • 在路由器中引入 ParamMap

  • 引入 .NET Core 1.0

  • .NET 执行环境下的跨平台开发

Angular 的新功能


Angular 团队已经放弃了 Angular 3,并决定遵循语义化版本控制推进 Angular 4。这有助于标准化主要、次要和补丁版本的版本号。语义化版本控制的版本号将分为三个段。在语法或概念上的任何破坏性变化将被视为主要,第一个段的版本号将会递增。任何新增功能都将被视为次要,第二个段的版本号将会递增,并且对于任何 bug 修复,第三个段的版本号会递增,视它们为补丁:

图 01:语义化版本控制

预编译

Angular 引入了一个重大改变,即在构建应用程序时生成 JavaScript 代码。这样可以在构建时了解模板中是否存在任何错误,而不是在运行时被通知。此外,它还可以让应用程序运行得更快,因为在构建阶段已经完成了代码生成。新的 Angular 视图引擎在使用 预编译AoT)时生成更少的代码。

模板更新

由于模板是 Web 组件的真实 HTML 标签,Angular 引入了一个新的 ng-template 标签来作为模板。Angular 允许我们在模板中使用 ngIfelse,如下所示:

<div *ngIf="isOld; then   content else new_content">placeholder</div>   
<ng-template   #content><h2>old content body.</h2></ng-template>   
<ng-template   #new_content><h2>body of new content.</h2></ng-template>   

如果 isOld 为 true,则会显示旧模板的内容。否则,将显示新模板的内容。

接下来让我们讨论模板语法中新增的 as 关键字。它被引入以简化 let 的语法,使我们能够将结果存储在模板变量中:

<ul>   
   <li *ngFor="let book of   books | slice:0:10 as topTenBooks; index as idx">   
      {{ topTenBooks.length - idx   }}: { book.name }}   
   </li>   
</ul>   

在这里,我们使用了as关键字来将切片的结果存储在topTenBooks变量中,并在li标签中进一步引用它。请注意,我们还给了一个别名i,用于索引,这是let i = index语法的简写。

我们也可以将as关键字和async一起使用,如下所示:

<ul>   
   <li *ngFor="let book of   books$ | async">   
      {{ book.name }}   
   </li>   
</ul>    
<h3>{{ ( books$ |   async)?.length }} books</h3>   

在这里,我们将我们的books集合作为 Observable。因此,我们已经遍历了从 Observable 返回的书籍数组。请注意,我们还显示了从 Observable 返回的书籍数量。然而,这会导致性能开销,因为使用的异步管道会在发生更改时重新运行。以下是进一步的改进,避免了这种性能开销:

<div *ngIf="books$ | async as   books">   
   <ul>   
      <li *ngFor="let book of   books">   
         {{ book.name }}   
      </li>   
   </ul>   
<div>   
<h3>{{  books.length }}   books</h3>   

在这里,我们使用了as关键字来在父组件中存储管道值。请注意,我们仅使用了一次 async。

介绍 titlecase 管道

titlecase 管道将单词的第一个字母变为大写。我们可以如下使用 titlecase:

<p>{{ 'rajesh gunasundaram'   | titlecase }}</p>      
 the parsed HTML will be    <p>Rajesh Gunasundaram</p>   

在 HTTP 中简化参数传递

sort parameter in the HTTP GET call:
http.get('http://www.programmerguide.net/api/articles`,   { params: { sort: 'ascending' } });   

在测试中重写模板

有时在测试期间需要重写模板。Angular 现在简化了重写模板,如下所示:

TestBed.overrideTemplate(BookComponent,   '<h1>{{book.title}}</h1>');   

在那之前,我们需要构建 JSON 并传递它。

介绍 Meta 服务


Angular 引入了一个名为 Meta 服务的新服务,简化了更新或获取meta标签:

@Component({   
  selector: 'book-list',   
  template: `<h1>Book   List</h1>`   
})   
export class BookComponent {   
  constructor(meta: Meta) {   
    meta.addTag({ name: 'author',   content: 'Rajesh Gunasundaram' });   
  }   
}   

新的表单验证器

新的验证器结合了现有的验证器,如requiredminLengthmaxLengthemailpattern。还介绍了一个新的指令compareWith,用于比较select控件中的选项,如下所示:

<select [compareWith]="byId"   [(ngModel)]="selectedBook">   
   <option *ngFor="let book of   books" [ngValue]="book">{{book.title}}</option>   
</select>   
byId(p1: BookModel, p2:   BookModel) {   
   return p1.id === p2.id;   
}   

介绍路由中的 ParamMap

Angular 引入了一个新的接口 ParamMap,用于映射 URL 中的参数。我们可以使用paramMapqueryParamMap来访问 URL 的参数。ParamMap具有诸如get()获取值或getAll()获取所有查询参数值的方法,如下所示:

const id =   this.route.snapshot.paramMap.get('bookId');   
this.bookService.get(id).subscribe(b   => this.book = b);   

在 observable 中,我们需要像下面这样使用ParamMap进行说明:

this.route.paramMap   
  .map((params: ParamMap) =>   params.get('bookId'))   
  .switchMap(id =>   this.bookService.get(id))   
  .subscribe(b => this.book =   b);   

介绍.NET Core 1.0


在本节中,我们将介绍.NET Core 1.0 作为一个平台的基础知识以及其中涉及的组件。

.NET Core 1.0 平台出于各种原因进行了改进。ASP.NET 的 Web 堆栈非常古老,始于.NET Framework 1.0。ASP.NET 存在大量古老和未使用的代码。即使代码不被使用,也难以避免加载它们。最大的问题是System.Web,它是老式 ASP.NET 和现在的 ASP.NET 之间的连接。MVC 和 Web API 正在试图与System.Web隔离。

ASP.NET、MVC 和 Web API 的自托管是其中一个目标,使它们能够独立于服务器平台进行托管。然而,它一直与 IIS 这个 Windows 平台绑定。当应用程序需要在更新到服务器时重新测试任何由于更新至新的.NET 版本而引入的新 bug 时,这就成了一个问题,因为它们依赖于机器级的.NET 版本,所以没有办法将.NET 版本与应用程序隔离开来,使其能够独立于新的.NET 版本运行。

由于必须加载大量代码、编译、写入磁盘、重新加载到内存中并执行,ASP.NET 团队决定从头开始重写代码,因为时间跨度受到了系统性能的影响。在 .NET Core 1.0 中,有很多事情发生了变化,它与任何其他版本的 ASP.NET 都有很大差异。这就是为什么给它起一个新的名字和新的版本号是合适的,因为它并不是一种渐进式的变化。

一个关键的区别是,.NET Core 1.0 是跨平台和开源的。.NET Core 1.0 是一个单一平台,将 MVC 和 Web API 的概念结合为一个坚实的 API 集,并且所有的遗留代码都消失了。在.NET Core 1.0 中一切都是一个依赖项。我们可以以我们想要的任何大小开发一个.NET 应用程序。.NET Core 的某些部分现在是一个 NuGet。因此,你可以从 NuGet 中仅加载所需的程序集,与之前版本的 ASP.NET 相比,这会导致内存占用更小。

在 .NET Core 1.0 中今天可以实现多个部署支持,这使我们能够部署到 Azure、AWS 和其他云服务中。你可以在 IIS 中进行托管,或者可以进行自托管,这使我们可以从命令行执行。.NET Core 1.0 支持真正的跨平台,并且可以在 Windows 和 OSX 或 Linux 上进行托管:

图 02:ASP.NET Core 1.0 的构建模块

如前述图所示,.NET Core 包括一个新的 CLR,在 OSX/Linux 和 Windows 上都得到支持。ASP.NET 也可以在 Mono 上运行。使用原生的 IIS 加载器,我们可以在 IIS 中加载和托管我们的应用程序。这个原生的 IIS 加载器直接将请求路由到 ASP.NET 而无需经过 ISAPI 过滤器等。在 Windows 平台上,你还可以使用一个叫做 dotnet.exe 的工具从命令行自托管应用程序。.NET Core 1.0 也支持在 Linux 和 OSX 上进行自托管,并且可以使用某种工具,比如 dotnet.exe,让应用程序只需使用命令行就可以运行。

自托管解决方案与 Node 很相似。在 Node 中运行,并且应用程序的根目录与 .NET Core 中的 dotnet.exe 工具的自托管方式非常相似。因此,跨平台支持,你编写的代码并不一定关心它在哪里被托管。

.NET Core 是新的跨平台.NET Framework 的子集。.NET Core 旨在尽可能小。CoreCLR 或.NET Core Framework 是.NET Framework 的子集。因此,.NET Core 中并不是所有功能都可用。例如,通过 System.Net 命名空间中的.NET Framework 内的邮件子系统来发送邮件。但是,这个功能并不存在,可以使用一些开源解决方案来实现。

.NET Core 团队希望通过 NuGet packages 来构建所有东西。因此,CLR 以及 C#和 VB 编译器之前的一切都是 NuGet 包。.NET Core 1.0 实际上是引导,CLR 并不完整。代码知道如何加载应用程序并启动它,然后 CLR 实际上管理该代码的执行。其他一切都将是 NuGet 包。MVC 查看静态文件进行日志记录、配置和身份验证;它们只是可以添加到项目的软件包。因此,在讨论创建厚或薄应用程序时,您可以决定在项目中包含什么。在 ASP.NET 中的所有内容都是可选的。

ASP.NET 5 团队已尝试采用 Node 包管理器,用于不同类型的工具支持,使用 npm 或 Bower 支持客户端库,使用 Grunt 和 Gulp 进行构建自动化,并使用 NuGet 进行.NET 包支持。

使用.NET Execution Environment 进行跨平台开发


在本节中,我们将讨论完整.NET 框架、Core CLR 和 DNX 的角色。我们将首先解释.NET 框架开发人员如何自.NET 开始就使用了执行环境。此外,我们将看到 Mono 和.NET Core。然后,我们将看到一些决定使用哪个框架的准则。最后,我们将看到 DNX 如何将一切绑在一起。

传统的.NET Framework

自.NET 开始以来,桌面和控制台应用程序已由可执行文件进行引导,传统的 ASP.NET 应用程序则通过 IIS 使用 ISAPI DLL 进行引导。在.NET 支持的任何语言中编写的应用程序都会被编译为程序集。程序集是包含中间语言IL)的 EXE 或 DLL 文件。由于操作系统和 CPU 不理解 IL,因此需要将此 IL 文件编译为本机代码,这称为即时JIT)编译。

JIT 在部署程序集的机器上执行之前,将 IL 代码编译为本机代码。JIT 功能是.NET CLR 或公共语言运行时的一部分。

CLR 负责加载程序集、检查类型和垃圾回收。因此,在应用程序运行的机器上安装.NET Framework 是必要的。大量的类和其他类型可用。它包含了所有 Windows Forms、WCF、WPF、web forms 所需的类型,以及在这些框架中可用的类型,例如文件处理、读取和操作 XML、绘图和密码。所有应用程序都使用其中的一些类。

CLR 专门设计用于在 Windows 上运行。此外,FCL 中的一些类专为 Windows 设计。System.web是一个包含与 IIS 和因此 Windows 相关联的类的程序集。传统.NET Framework 的构建模块包括以下内容:

图 03:传统.NET Framework 的构建模块

Mono 跨平台.NET Framework

Mono 是由社区开发的.NET Framework 的开源版本。它使用了与 Microsoft .NET Framework 相同的原理。它与 Microsoft .NET Framework 兼容。即使你不使用 ASP.NET 5,你也可以在 Windows 机器上使用 Microsoft .NET Framework 和 Visual Studio 创建程序集,然后在 Linux 机器上使用 Mono 运行它们。所以,与 Microsoft .NET Framework 的一个重要区别是它是跨平台的。版本适用于 Windows、macOS 和 Linux。它还被用作 Xamarin 的基础,该基础在 Android 和 iOS 上运行.NET。

NuGet 软件包管理器

Microsoft 引入了 NuGet 来管理包并方便地下载它们用于开发。NuGet 是获取库的中心位置。这些库和框架的开发人员可以轻松地向 NuGet 应用新版本或 bug 修复。Microsoft 开始在 FCL 中通常会出现的程序集中使用 NuGet。MVC 安装为应用程序中的 NuGet 包,而不像 FCL 那样在整个机器上安装。这使不同的应用程序可以使用不同版本的 MVC 而无需安装不同版本的.NET Framework。通过 NuGet 分发 MVC 使 Microsoft 能够在.NET Framework 之外"越分频"地更新 MVC,从而使 MVC 能够更快地演进并得到更频繁的更新。这是一个完全模块化的框架类库与.NET Core 的乐观预示。

.NET Core 中的 CoreFx 和 CoreCLR

多年来,.NET 已经被重新发明多次。有一个用于 Windows 桌面、Windows 商店应用和 Windows Phone 的.NET Framework。我们还有一个我们一直用于 ASP.NET 4 和更早版本应用的框架。微软发明了一种方法,可以使用可移植类库和通用应用概念在所有不同平台之间共享代码。然而,如果我们有一个所有平台通用的.NET 版本,那不是更容易吗?不仅对于微软来说,要保持所有这些堆栈更新,而且对于我们这些必须学习和维护所有这些版本的开发者来说也是如此。.NET Core 的目的是成为统治所有版本的唯一.NET 版本,这一切都始于 ASP.NET!.NET Core 的另一个动机是减少总体占用空间。从 ASP.NET 的角度来看,使用system.web实际上已经不再是一个选项。此外,在一台机器上拥有一个庞大的.NET Framework,导致版本问题并包含许多不需要的东西是很麻烦的。在这个以云为驱动的世界里,被固定在 Windows 上已经不合时宜。.NET Core 最激动人心的功能是它可以使用新的 DNX 跨操作系统运行。

就像完整的.NET Framework 一样,.NET Core 也由两部分组成:一个是普通语言运行时,现在是可移植的,名为 CoreCLR,另一个是称为 CoreFX 的类库。CoreFX 包含一组类型,这些类型是所有.NET 应用程序通用的。它不包括像完整的.NET Framework 中的 FCL 那样的完整的框架,比如 WPF 或 Web forms。例如,它包含用于操作文件的类和类似列表的集合类。CoreFX 的不同程序集都是通过 NuGet 单独分发的。除了 CoreFX 之外,你还需要从 NuGet 中获取其他所需的一切,比如 MVC 框架。不仅 CoreFX 以 NuGet 包的形式分发,CoreCLR 也是如此。.NET Core 是微软所谓的云优化的。这基本上意味着它很轻量级。它不仅比完整的.NET Framework 小得多,而且包含了一些优化:

图 04: .NET Core 的构建模块

就像任何 NuGet 包一样,.NET Core 可以在每个项目中从 NuGet 中恢复。当你将应用程序发布到服务器时,你还可以将 CoreCLR 与你的应用程序一起发布。因此,不再需要进行机器范围的安装。服务器上的每个应用程序都可以具有自己的.NET Core 版本,而不会影响其他应用程序。.NET Core 以及 DNX 都是开源软件,这意味着除了微软之外,社区也在进行相关工作,而你也可以参与其中。这还确保了如果微软决定停止工作的话,这些项目也会继续进行下去:

图 05: .NET 应用程序框架

选择一个框架

如何选择要使用的框架?在必须全局安装正确版本的.NET Framework 或 Mono 以支持应用程序的计算机上,您可以在一个服务器上开发使用不同版本的.NET Core 的应用程序。您还可以更新一个应用程序以使用较新版本的.NET Core,而不影响其他应用程序。使用.NET Framework 或 Mono,您有最好的机会使用现有代码。它提供了大量的类和其他类型。 CoreFX 是一个不同的类库,当使用现有代码时,您可能需要重构。此外,CoreFX 具有更少可用类型的选择,并且不是您习惯于使用额外包都可以提供的所有内容。它是一个仍需发展的生态系统,其中 FCL 非常完整,而且是经过验证的技术。然而,它包含了许多您可能不需要的内容。如果您的应用程序必须跨平台运行,那么使用 Mono 或.NET Core 是您的选择。.NET Framework 仅在 Windows 上运行。如果您正在为自己的业务开发组件库,或者想要商业分发它们,那么针对多个框架开发组件库是有意义的。您的库可以被使用所有选择的框架的应用程序所使用。

或者,也许您现在必须编写一个应用程序,以后再决定它必须在哪个框架上运行。在下一节中,我们将看到 DNX 在所有这些过程中扮演的角色:

图 06:选择框架的标准

Dotnet 执行环境- DNX

DNX 的主要作用是通过在框架中托管 CLR 来运行.NET 应用程序。新引入的命令行界面 dotnet cli 具有 DNX 的可执行部分。Dotnet cli 有助于运行应用程序,并还原project.json中指定的包。

DNX 命令行应用程序有自己的处理过程来执行应用程序,而不是 Windows 或 IIS 执行应用程序;DNX 托管 CLR 而不是操作系统。DNX 将寻找一个Main方法形式的入口点并从那里运行应用程序。由于独立进程运行应用程序,因此不再依赖于操作系统,DNX 命令行应用程序可以针对多个操作系统开发,如 Windows、Linux 和 macOS。现在,在 Visual Studio 中开发的应用程序可以跨平台运行。对于每个操作系统都有针对相应.NET Framework 的 DNX 版本。也可以有支持不同 CPU 架构的版本。例如,对于完整的.NET Framework,有 x86 版本和 x64 版本。

不同版本的 DNX 可以共存于一台机器上。你可以选择将它们安装在整个机器的中央位置,也可以选择将 DNX 作为应用程序部署的一部分。使用该方法完全不需要进行整机安装。如果你在 Visual Studio 中发布应用程序,你需要选择在project.json的框架部分中配置的 DNX 版本。该版本的 DNX 将随部署的应用程序一起分发:

图 07:DNX 的构建模块

DNX 是 dotnet cli 的一部分,它在 dotnet 命令行应用程序中公开其功能。当你在 Visual Studio 中运行一个 DNX 应用程序时,Visual Studio 只是执行一个命令行。例如,当你运行时,它将执行dotnet run,这将使 DNX 开始工作。在运行应用程序时,DNX 也支持调试。当你向project.json文件添加 NuGet 包时,它只会执行dotnet restore。此外,还有一些用于编译和发布应用程序的命令。Dotnet cli 只是在命令行上给出的一个命令,所以不需要使用 Visual Studio 来执行。你可以直接输入命令来执行,或者使用其他工具来调用它。这样的一个例子就是可以跨平台运行的 Visual Studio Code。dotnet命令将在 ASP.NET 5 版本 RC2 及更高版本中使用。不同的命令行应用程序,如 DNX 和 DNU,都统一在 dotnet 命令行中。例如,当你执行dotnet restore时,它将读取project.json文件,并根据需要下载和安装包。因此,DNX 不仅是一个运行时环境,它还是一个 SDK。当你在应用程序的文件夹中执行dotnet run时,运行时部分就会启动。

部署选项


有四个部署选项。所有选项都受到 dotnet 命令行界面的支持。你可以选择复制应用程序的项目,让 DNX 恢复包,并运行应用程序。必须预先安装兼容的 DNX 版本;使用 CLI 命令dotnet run

你也可以让 CLI 在你的开发机上编译项目。复制程序集并运行:

图 08:部署选项

还有一个选项是使用命令行开关本地编译应用程序。这意味着生成的不是 IL 的程序集,而是可以直接在没有 DNX 或任何 .NET Framework 帮助下直接执行的本机二进制文件。

还有一个选项是将你的应用程序打包成一个 NuGet 包,以便使用dotnet pack轻松共享你的项目。该包将包含project.json文件中配置的所有框架的支持。然后你可以将其上传到全局的 NuGet feed,或者只针对你的公司上传。

最后一个选项是使用 dotnet cli 发布应用程序,使用dotnet publish

所有所需的程序集和 DNX 本身都包括在部署中。由于部署的 DNX 是特定于操作系统的,如果您在不同操作系统上部署,则需要额外的工作才能使此选项起作用。

使用 Visual Studio 发布

我们将学习使用 Visual Studio 部署应用程序。如果我们右键单击项目的 web 节点,我们可以选择发布,然后发布将允许我们选择目标。Visual Studio 将要求您提供要创建和存储发布配置文件的名称,以备将来使用:

图 09:创建发布配置文件

我们可以选择 Azure Web 应用程序、文件系统和其他部署模式。在Settings中,我们还可以选择要使用的配置和要使用的 DNX 版本。在这里,您只会看到与您在project.json文件的 frameworks 部分中指定的匹配的选项。最后,我们可以发布应用程序:

图 10:选择 DNX 版本

发布时,它首先运行prepare语句,然后运行prepublish,以将所有内容打包到临时文件夹中,然后将其复制到文件系统。发布成功后,打开命令行界面并导航至发布文件夹。

请注意文件夹中包括在项目文件中定义的命令和 shell 脚本。还要注意approot文件夹包含应用程序所需的软件包和运行时。

如果您在 Windows 平台上,可以使用 web 命令启动应用程序。您只需打开浏览器并导航到带有5000端口的localhost,我们就可以看到我们的应用程序实际在运行。应用程序直接从命令 shell 而不是在 IIS 下运行。

使用 dotnet 命令行界面发布

让我们看看如何使用 dotnet 命令行界面进行发布。从 Visual Studio 中,在我们应用程序的项目文件夹中直接打开命令 shell。如果我们查看DOTNET命令,我们可以看到它有许多命令,您可以在其中构建您的项目并安装依赖项,尤其是基于 NuGet 的依赖项。您可以处理软件包并发布应用程序。以下是显示命令中各种选项和参数的 Windows 结果的命令:

C:\Rajesh\Packt\Angular2>dotnet.NET 命令行界面用法:dotnet [common-options] [command] [arguments]

  • 参数:[command]:要执行的命令 [arguments]:要传递给命令的参数

  • 常用选项:(在命令之前传递):-v|--verbose 启用详细输出

  • 常用命令:new: 初始化一个基本的.NET 项目restore: 恢复.NET 项目中指定的依赖项compile: 编译.NET 项目publish: 发布.NET 项目以供部署(包括运行时)run: 编译并立即执行.NET 项目repl: 启动交互式会话(读取、求职、打印、循环)pack: 创建一个 NuGet 包

dotnet 命令行界面非常有用,因为您实际上可以编写脚本来完成所有这些过程,您可以让其安装依赖项,运行构建,然后发布。因此,它提供了一种轻松的方式来自动化许多这些任务。事实上,Visual Studio 只是使用 Dotnet 工具来自动化这一切。

发布的文件夹包含应用程序的代码,所有依赖项,客户端依赖项,工具依赖项和 NuGet 依赖项,并且包含所需的整个版本的运行时。我们可以将此文件夹放在任何计算机上并运行。如果要为 Linux 或 OS 10 打包这个文件夹,您将需要适用于这些平台的运行时版本,如 CoreCLR。该文件夹包含独立的应用程序,并可以在任何浏览器上运行。这不使用安装在计算机上的任何框架;它全部包含在一个文件夹中,完全可移植。

部署到 IIS

当您部署到 IIS 服务器时,需要确保已安装作为反向代理的HttpPlatformHandler模块。当请求到来时,IIS 将其转发到另一个进程,通常是一个命令行应用程序。IIS 将根据需要启动和停止进程,并处理并发问题。

在 IIS 管理应用程序中,我们的应用程序被视为另一个网站,并且可以在 IIS 中进行配置。我们需要通知 IIS 在我们的应用程序收到请求时执行 DNX。我们可以使用项目文件夹中的web.config来实现。IIS 仍然使用web.config来使用HttpPlatformHandler

图 11:在 web.config 文件中配置 HttpPlatformHandler

HttpPlatformHandler扩展已经在 IIS 中注册,并被指示在收到请求时执行启动 DNX 进程的批处理文件。所以,让我们在 IIS 中配置应用程序。

要配置我们的应用程序,请启动 IIS 管理器。右键单击根服务器节点,选择添加网站,输入应用程序名称,应用程序池将自动生成:

图 12:向 IIS 添加网站

在物理路径文本框中设置发布应用程序的wwwroot文件夹的路径,然后点击OK。由于 CLR 正在独占进程中运行,我们需要在应用程序池下设置无托管代码

图 13:在应用程序池中设置.NET CLR 中的无托管代码

我们这样做是因为我们不需要 IIS 为我们的应用程序托管 CLR。点击OK并浏览到端口8080localhost将启动我们的应用程序。这样一来,我们就可以使用 IIS 的功能来为 DNX 应用程序提供动力,就像在 IIS 中托管任何其他 Web 应用程序一样。

部署到 Microsoft Azure

使用 Azure 的应用服务进行部署非常顺畅。当应用程序发布到 Azure 时,会创建一个新的虚拟机,或者它会托管在运行 IIS 的现有虚拟机上,该虚拟机装有HttpPlatformHandler。部署流程与在本地服务器上部署到 IIS 相同。

在 IIS 中必须创建一个新的网站,并且发布的内容必须上传到服务器。所有这些都可以通过在 Visual Studio 中创建一个发布配置文件来完成,选择Microsoft Azure App Service。我们可能需要登录 Azure 账户,选择一个订阅,然后通过给出一个名称在 Azure 中创建一个新的应用。URL 将是yourappname.azurewebsites.net。在设置下,我们需要选择 DNX 并点击发布。浏览到yourappname.azurewebsites.net将在 Azure 中运行您的应用程序:

图 14:选择发布目标

部署到 Linux 和 macOS

让我们看看如何从 Visual Studio 部署应用程序到 Linux 或 macOS 上。我们还可以使用 Visual Studio Code 在 Linux 或 macOS 平台上开发应用程序。

首先,为框架安装一个 DNX,.NET Core 或 Mono。然后,复制整个 Visual Studio 项目,包括源代码和project.json,但不包括任何程序集。

然后,使用 dotnet cli 来还原所有的 NuGet 包。这将包括 DNX 托管 CLR 在.NET Core 中所需的程序集。然而,NuGet 包可能依赖于其他包,因此在还原之前必须有某种包所需的所有包的列表。编制这样一个列表需要时间,因为所有的包都必须被检查,看看它们的依赖关系是什么。锁定文件包含了这个编制的列表,因此只需要做一次核实。所有后续的还原都使用锁定文件,前提是project.json中包的列表没有改变。

最后,指示 DNX 使用 Kestrel 作为 Web 服务器运行程序。DNX 将使用 Kestrel 作为入口点,然后 Kestrel 将托管应用程序。Kestrel 会通知我应用程序在端口5000上运行。通过使用端口5000localhost作为域名浏览将在 Linux 或 macOS 中启动我们的应用程序。

摘要


这就是全部,伙计们!我们讨论了 Angular 和 .NET Core 1.0 中的新功能。我们首先介绍了 Angular 中引入的新特性。我们探讨了对 Angular 中各种现有方法的改进。最后,我们详细了解了 .NET Core 1.0 和 .NET Execution Environment 中的跨平台开发。我们了解了完整的 .NET Framework、.NET Core 和 Mono 之间的区别。此外,我们还介绍了 DNX 以及它在以全新方式开发 .NET 应用程序中的作用。

posted @ 2024-05-18 12:02  绝不原创的飞龙  阅读(3)  评论(0编辑  收藏  举报