Loading [MathJax]/extensions/MathZoom.js

Angular5-项目教程-全-

Angular5 项目教程(全)

原文:Angular 5 Projects

协议:CC BY-NC-SA 4.0

一、Web 应用和 AJAX 通信

这本书是为对 web 开发有非常基础的知识的开发人员编写的。它不需要预先安装软件,但是在后面的章节中,您将需要安装软件来运行示例代码。该书提供了如何在需要时下载和安装软件的信息。

在我们深入 Angular 之前,我想介绍一些 web 开发的基本概念。本章介绍了 web 应用的基本架构,以及它如何将数据从服务器传递到 web 浏览器。它还介绍了一些工具,在调试服务器和 web 浏览器之间的通信时,这些工具可能会让您的工作变得更加轻松。

更有经验的开发人员可以跳过这一章。

客户端和服务器简介

Web 应用基本上包括两台相互通信的计算机,称为服务器和客户端。该概念如图 1-1 所示。

A458962_1_En_1_Fig1_HTML.jpg

图 1-1

Client/server architecture

该服务器位于公司办公室或数据中心,监听 HTTP 请求,并通过应答进行响应。服务器还访问 web 应用使用的数据(存储在数据库中)。

用户使用他们的 web 浏览器与 web 应用进行交互。用户的计算机与服务器通信,发送 HTTP 请求并接收回答。客户端电脑可能是各种机器,从智能手表到手机到平板电脑再到电脑。

在 web 上,客户端和服务器使用 HTTP(超文本传输协议)进行通信。HTTP 是客户端和服务器之间的请求-响应协议。第二十章详细介绍 HTTP。

服务器端 Web 应用

在服务器端 web 应用中,大部分应用在服务器上执行,而客户端仅用于一次显示一个 HTML 页面。当用户在 web 应用中执行一个操作时,客户机向服务器发送一个请求,服务器执行一些操作,并返回一个全新的 HTML 页面作为响应显示在客户机上。每次都重新生成网页,并发回显示在客户端的 web 浏览器上,如图 1-2 所示。

A458962_1_En_1_Fig2_HTML.jpg

图 1-2

Server-side web application

客户端 Web 应用

客户端 web 应用(也称为单页应用,或简称为 SPAs)是一个较新的现象,计算行业正在更多地向这种模式发展。这里,许多应用仍然在服务器上执行,但是一些代码也在客户机(web 浏览器)上执行,以避免频繁地重新生成页面。当用户在客户端执行一个操作时,它向服务器发送一个请求,服务器执行一些操作并返回结果信息——而不是一个全新的 HTML 页面。客户端代码监听来自服务器的回答,并在不生成新页面的情况下自己决定如何响应。客户端 web 应用往往更具交互性和灵活性,因为它们可以更快地响应用户交互——它们不必等待服务器发回同样多的数据。他们只需要等待服务器返回结果,而不是整个 HTML 页面。该架构如图 1-3 所示。

A458962_1_En_1_Fig3_HTML.jpg

图 1-3

Client-side web application

取得平衡

因此,基本上有两种类型的 web 应用:服务器端和客户端(SPA)。如果这些被认为是黑白的,你的 web 应用应该在中间的某个地方,在“灰色”区域。

服务器端应该保留聪明东西的存储库——业务规则、数据存储和设置应该保留在服务器上,并在需要时从客户端调用或检索。

客户端(浏览器)应该使用更现代的客户端技术来避免整页刷新。但是,不能太聪明,也不能太臃肿。它应该知道足够多的信息来完成与用户交互的工作,仅此而已。它应该调用服务器端的代码来做智能的事情或执行业务流程。它不应该有太多的业务逻辑、内部系统数据(除了用户可以查看或修改的数据之外的数据)或硬编码信息,因为这些在服务器上管理更好。

Caution

你必须避免把“除了厨房水槽以外的所有东西”都扔给客户。

用 AJAX 创建 Web 应用

AJAX 代表异步 JavaScript 和 XML。AJAX 是一种借助 XML、HTML、CSS 和 JavaScript 创建更好、更快、更具交互性的 web 应用的技术。

当客户端 web 应用需要与服务器通信时,它使用 AJAX 发送一些东西,并等待结果返回。请记住,它返回的结果只包含数据,而不是一个全新的网页。此外,客户端代码在等待时不会停止运行,因为它仍然需要显示用户界面并响应用户。这是 AJAX 的异步部分。

客户端 web 应用使用 JavaScript 来调用 AJAX 请求并对其做出响应。这是 AJAX 的 JavaScript 部分。

AJAX 请求过去使用 XML(可扩展标记语言)作为在客户机和服务器之间来回传递的请求和结果数据的数据格式。如今,AJAX 倾向于使用 JSON (JavaScript 对象表示法)作为数据格式,而不是 XML。这是因为 JSON 更紧凑,更直接地映射到现代编程语言中使用的数据结构。但是 XML 和 JSON 都是以文本形式传输数据的常用格式。

前面,我使用了术语异步。你可以这样看待异步:你打电话给你的配偶请求帮助。他们的电话占线,所以你留言让他们在超市停一下,给你买一箱啤酒。同时,你继续看电视——因为这些事情是异步发生的。这一进程的成果将包括以下内容:

  • 成功:配偶给你回电话,告诉你啤酒在路上。
  • 失败:配偶打电话给你,告诉你商店关门了。

在 AJAX 中,客户端代码在等待服务器响应时不会停止运行,就像你在等待配偶回电时不会停止看电视一样。

回收

通常,当您进行 AJAX 调用时,您必须告诉它在收到服务器响应时该做什么。AJAX 系统代码在收到响应时应该触发的这段代码称为回调。

当您执行 AJAX 操作时,您用参数和一两个函数调用 AJAX 代码——回调。回调有两种类型:

  • Success:如果服务器响应成功,并且客户机收到正确的回答,则调用 success(或 done)回调。
  • failure:fail 或 error 回调是可选的,如果服务器返回一个错误(或者 AJAX 调用无法与服务器通信),就会调用这个回调。

承诺

有时你调用 AJAX 代码,它会返回一个承诺或延期。承诺是来自 AJAX 操作的“响应承诺”的对象。当您收到一个承诺时,您可以向该承诺注册您的成功或失败回调,使该承诺能够在成功或失败发生时调用回调。

编码

当您使用 AJAX(或者客户端和服务器之间的其他通信)时,您需要确保信息以适合传输的形式发送。你可以通过编码来实现。如果不使用编码,很有可能某些信息不会像发送时那样被准确接收。对于一些特殊的字符信息尤其如此,例如空格、引号等等。

表 1-1 列出了信息编码的三种主要方法。

表 1-1

Three Main Methods of Encoding Information

| 方法 | 笔记 | | :-- | :-- | | 编码器(编码器)(编码器)(编码器)(编码器)(编码器)(编码器)(编码器)(编码器)(编码器)(编码器)(编码器)(编码器)(编码器)(编码器)(编码器)(编码器)(编码器)(编码器)(编码器)(编码器)(编码器)(编码器)(编码器) | 这对于用特殊字符的转义序列将整个 URL 编码成 UTF 8 非常有用。它以与 encodeURIComponent 相同的方式对字符串进行编码(见下一项),只是它不涉及组成 URL 路径的字符(如斜杠)。例如: [`http://www.cnn.com`](http://www.cnn.com) 转换为`http://www.cnn.com%0A`。 | | 中文 | 这对编码参数很有用。它不适合编码整个 URL,因为它可以用转义序列替换重要的 URL 路径信息。例如: [`http://www.cnn.com`](http://www.cnn.com) 转换为`http` %3A%2F%2F www.cnn.com%0A。 | | 逃跑 | 这将返回一个包含[参数]内容的字符串值(Unicode 格式)。使用时要小心,因为默认情况下,服务器不会接收 Unicode 格式的数据。例如: [`http://www.cnn.com`](http://www.cnn.com) 转换为`http%3A//www.cnn.com%0A`。 |

为了测试这些方法,请前往 http://pressbin.com/tools/urlencode_urldecode/ 。图 1-4 显示了这个网络界面的样子。

A458962_1_En_1_Fig4_HTML.jpg

图 1-4

Web page that displays different encodings for what you type

哈尔和哈特奥斯

为了与服务器通信,客户端需要知道服务器在哪个 URL 上可用。这些信息不应该硬编码在客户端上。相反,服务器应该告诉客户端使用什么 URL 来获取信息。将这些信息发送回客户端的格式有多种标准,包括 HAL 和 HATEOAS。

例如,如果客户机向服务器发送一个 AJAX 请求来检索客户列表,那么返回的信息应该包括每个客户的 AJAX 请求的 URL。这避免了在客户机上硬编码客户 AJAX 请求 URL。你可以分别在 https://martinfowler.com/articles/richardsonMaturityModel.htmlhttps://en.wikipedia.org/wiki/HATEOAS 阅读更多关于 HAL 和 HATEOAS 的内容。

监控数据流量

您的网络浏览器内置了开发工具。其中一个工具是网络工具,它允许您监控客户端和服务器之间的数据流量。该数据流量以带有时间线的列表形式呈现,如图 1-5 所示。您可以选择列表上的一个项目来更详细地查看它,并确切地看到哪些数据被发送到服务器,哪些数据被返回。您可以过滤想要跟踪的网络流量类型。例如,您可以选择“XHR”来查看 AJAX 请求。

A458962_1_En_1_Fig5_HTML.jpg

图 1-5

Viewing data traffic with the network developer tool in the Google Chrome browser

Fiddler 是一个免费的 web 调试代理,其工作方式类似于浏览器开发工具中的网络选项卡(见图 1-6 )。Fiddler 有一些额外的功能,比如创建自己的 AJAX 请求和运行脚本。在 www.telerik.com/fiddler 阅读更多关于提琴手的信息。

A458962_1_En_1_Fig6_HTML.jpg

图 1-6

Viewing data traffic with Fiddler

邮差很像提琴手( www.getpostman.com )。两者都很有用。

分析 JSON

您经常会收到来自服务器的很长的 JSON 响应,并且需要遍历响应数据来提取您需要的数据。您的响应数据通常会作为参数传递给 AJAX 成功回调函数。以下是检查这些数据的一些提示:

  • 将其转换为字符串:可以调用 JSON.stringify 函数将响应数据转换为字符串。这将使您能够在成功回调时将其输出到控制台,如下所示:

    function success(data){
     console.log('success - data:' + JSON.stringify(data));
     //
     // do something with data
     //
    }
    
    
  • 将 JSON 数据复制到控制台之外:要将 JSON 数据复制到剪贴板,请执行以下操作:

    1. 打开你的浏览器。
    2. 转到开发者工具菜单。
    3. 单击控制台操作。
    4. 选择 JSON 文本。
    5. 右键单击并选择复制。
  • 格式化 JSON 数据,使其更具可读性:现在,您已经将 JSON 数据放在剪贴板中,您可以将它复制并粘贴到网站中,使其更具可读性:

    1. 打开你的浏览器。

    2. Go to https://jsonformatter.curiousconcept.com (or a similar service—there are lots of these). Figure 1-7 shows what this website looks like.

      A458962_1_En_1_Fig7_HTML.jpg

      图 1-7

      Formatting JSON data

    3. 将 JSON 粘贴到大文本框中。

    4. Click the Process button. The website will show you the JSON data in a validated, formatted, easy-to-read web page, as shown in Figure 1-8. You can even view the JSON full-screen.

      A458962_1_En_1_Fig8_HTML.jpg

      图 1-8

      Formatted JSON data

  • 复制 JSON 数据并将其粘贴到您的编辑器中:然后您可以应用编辑器的格式命令。您可能需要首先将该文件保存为. js 文件,以确保将其格式化为 JavaScript。

摘要

近年来,web 应用的世界已经发生了很大的变化。客户端应用(也称为 spa)变得越来越普遍。在本章中,我们看到 SPA 最重要的方面之一是客户端(浏览器)和服务器之间的 AJAX 通信。

作为一名开发人员,了解如何使用 web 浏览器开发工具的网络部分非常重要,这样您就可以调试 AJAX 通信。您可能还需要知道如何使用其他工具,如 Postman 或 Fiddler。

在下一章,我将介绍 Angular,并展示它是如何随着版本的变化而变化的。

二、Angular 旧的和新的

在学习 Angular 之前,了解一下称为 AngularJS 的原始版本,并谈谈第一个版本和后来的版本之间最重要的差异,会有所帮助。

以下是一些关于 AngularJS 和 Angular 的基本事实:

  • AngularJS 于 2009 年发布,是最初的 Angular。
  • 这是一个动态 web 应用的 JavaScript 框架——不需要重新加载页面。动态 web 应用也称为 SPAs(单页应用)。
  • 它很流行用在任何浏览器上都能快速运行的小部件来创建网页。
  • 它允许用户扩展 HTML 来添加特定领域的标签,比如<CAR>
  • 它允许用户将数据从模型绑定到特定于 HTML/域的标签。
  • Angular 2 于 2009 年和 2014 年开发。
  • 谷歌于 2014 年 9 月宣布开发 Angular 4,并于 2015 年 1 月进入测试阶段。
  • Angular 4 发布于 2017 年 3 月。
  • Angular 5 于 2017 年 11 月发布。

AngularJS 像野火一样迅速发展起来,因为它是快速构建应用原型的一个很好的工具。它也很灵活,因为你可以使用 HTML 来创建页面,并在此基础上快速构建,将静态的 HTML 变成一个移动的、反应灵敏的、性感的应用。以下是如何:

  1. 获取一个 HTML 模板,修改一些 HTML 代码元素来添加数据绑定。数据绑定允许可视控件(如文本框、选择框等)将其值与变量同步。例如,您可以将一个city变量绑定到一个“城市”文本框。如果用户在文本框中输入内容,那么city变量的值将被更新。如果代码改变了city变量的值,“城市”文本框会更新以匹配
  2. 添加 JavaScript Angular 控制器:
    1. 为要绑定到的 HTML 标记添加变量。
    2. 添加行为 JavaScript 代码(响应按钮点击等事件的代码)。

搞定了。

很明显,您可以做更多的事情,但是关键是开发人员可以很快地将原始的 HTML 转换成一个有效的、响应迅速的应用。

语义版本控制

Angular 2、4 和 5 非常相似,它们都与原始 AngularJS 非常不同。很奇怪,我们有了好几年的 AngularJS,然后在很短的时间内有了 Angular 2,4 和 5。这是因为 Google 的人决定从版本 2 开始实现语义版本化。语义版本化是软件版本化的新标准,它现在如此流行的原因是版本号(或版本号的变化)提供了关于自上一版本以来所做的变化的信息。

使用语义版本化,版本号被分成三个部分,每个部分用句点分隔。

[主要版本号]。[次要版本号]。[补丁版本号]

因此,当 Angular 从 4 更改为 5 时,这是主版本号的更改。

主要版本号的变化表明软件在很大程度上发生了变化,这意味着您过去可以工作的代码可能不再工作,因为 api 已经发生了变化。

次要版本号更改表示软件被更改,但更改的方式允许您的代码仍然可以工作。

补丁版本号是用来修正错误的,一切都应该正常。

Angular 5 是 Angular 4 的基础,有许多小的改进,其中一些改进导致了 api 的修改。如语义主要版本号变化所示,当从 4 转换到 5 时,您的代码可能需要修改。从 4 到 5 最重要的变化包括:

  • http 模块的修改(这已经包含在 Angular 版本中)。
  • 构建优化器已经过修改,可以生成更小、更高效的部署模块。当您从 Angular 项目中部署文件时,这些文件将会更小。
  • 有新的工具可以从浏览器和服务器传输状态数据(反之亦然)。
  • 编译器被重新编写得更快更彻底。以前 Angular 写的是在运行你的 app 时使用 jit(即时编译)。当您加载组件和对象时,它们会在需要时被编译。Angular 现在更倾向于 aot 模型,在这种模型中,你的代码是提前编译的,而不是在需要的时候。5 中的这些编译器更新推进了向 aot 的转移,这将使您的应用运行得更快,因为它在运行应用时将执行更少的编译。
  • 改进了对多语言应用的国际化支持。

平台

AngularJS 运行在 web 浏览器上,web 浏览器运行 JavaScript,所以,JavaScript 是 AngularJS 和 Angular 的平台。

术语 evergreen 浏览器指的是自动升级到未来版本的浏览器,而不是像旧浏览器那样通过制造商发布的新版本进行更新。这个术语反映了浏览器的设计和交付在过去几年中的快速变化。现在广泛使用的浏览器都是常青树,自己更新。

浏览器使用 JavaScript 引擎运行 JavaScript

我们曾经认为网络浏览器和它运行 JavaScript 的能力是一回事。自从 Node(它使用 Google Chrome 的 JavaScript 引擎在远离浏览器的地方运行程序)以来,这种情况已经发生了变化,你可以在远离浏览器的地方独立运行这些引擎。

JavaScript 引擎是一个执行 JavaScript 的程序或解释器,可以利用 JIT(实时)编译成字节码。自 AngularJS 以来,JavaScript 引擎随着 ECMA JavaScript 的新版本(ECMA 指的是版本)而稳步改进。AngularJS 运行在运行名为 ECMA5 的 JavaScript 版本的网络浏览器上。现在大多数浏览器运行的都是更高版本。随着 ECMA6(也称为 ECMA 2016),JavaScript 朝着成为像 Java 或. NET 一样的结构化、类型化语言迈出了一大步。两个更重要的变化是用于创建类和模块的新语法,这对于本书来说很重要也很相关。

您可能知道,客户端 JavaScript 的世界变化很快。ECMA 维基百科页面定期更新最新信息: https://en.wikipedia.org/wiki/ECMAScript

垫片和聚合填料

填充和聚合填充是软件组件,旨在允许旧浏览器运行更现代的代码。shim 是一段代码,它拦截浏览器上的现有 API 调用并实现不同的行为,从而实现跨不同环境的标准化 API。因此,如果两个浏览器以不同的方式实现相同的 API,您可以使用一个填充程序来拦截其中一个浏览器中的 API 调用,并使其行为与另一个浏览器保持一致。polyfill 是一段 JavaScript,它可以将缺失的 API“植入”旧浏览器。例如,填充和聚合填充使旧的 ECMA5 浏览器能够运行 ECMA6 代码。

以打字打的文件

在 AngularJS 和 Angular 的出现之间,JavaScript 得到了改进,变得更像一种结构化语言。但是您可以更进一步,使用 TypeScript 语言,它是结构化的,甚至更像 Java 之类的语言。NET 和 C#。事实上,TypeScript 是由微软开发的,是 JavaScript 的改进版。图 2-1 简洁地表达了 TypeScript。

A458962_1_En_2_Fig1_HTML.jpg

图 2-1

How to think of TypeScript

为什么 TypeScript 很重要?Google 使用 TypeScript 开发 Angular。因此 Angular 和 TypeScript 语言是一个很好的组合,我们将在本书中对此进行大量讨论。

蒸发

TypeScript 如何在 web 浏览器上运行?嗯,没有,至少目前没有。使用一个称为 transpilation 的过程将 TypeScript 转换回兼容的 JavaScript。翻译程序是将一种语言的源代码转换成另一种语言的源代码的软件。例如,TypeScript、CoffeeScript、Caffeine、Kaffeine 和二十多种其他语言都被转换成了 JavaScript。

如果你想直接看到 transpilation,请查看 www.typescriptlang.org/play/ 并查看一些示例。如果您从该网页的弹出框中选择 Using Classes,您可以看到现代的 TypeScript 类是如何转换成兼容的 JavaScript 的。

清单 2-1 显示了您将为一个 TypeScript 类编写的代码,清单 2-2 显示了到 JavaScript 的转换。

class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        return "Hello, " + this.greeting;
    }
}

Listing 2-1TypeScript Class

var Greeter = (function () {
    function Greeter(message) {
        this.greeting = message;
    }
    Greeter.prototype.greet = function () {
        return "Hello, " + this.greeting;
    };
    return Greeter;
}());
Listing 2-2Transpiled to Browser-Compatible JavaScript

调试和映射文件

因此,您以一种方式编写代码,以另一种方式部署它——这一定是调试的噩梦,对吗?是的,如果你没有地图文件,调试将是一场噩梦。映射文件由 transpiler 自动生成,并为浏览器提供将原始(TypeScript)代码映射到已部署(JavaScript)代码所需的信息。这意味着 JavaScript 调试器可以让您调试源代码,就像浏览器正在运行它一样。这有多酷?如果你让。地图文件在您的浏览器中,它会自动寻找它们,拿起它们,并使用它们。我用。我在 Chrome 调试的时候一直在映射文件。

地图文件执行以下操作:

  • 将合并/缩小/传输的文件映射回未构建状态。
  • 将浏览器中的 JavaScript 代码行映射回 TypeScript 代码行
  • 使浏览器和调试器能够显示您在 TypeScript 中编写的原始代码并对其进行调试

蒸腾作用和

有许多方法可以设置您的项目,将您的 TypeScript 代码转换成浏览器友好的 JavaScript。这完全取决于您的项目设置。在这方面你有很多选择,这可能会变得复杂和混乱。

我建议您开始使用 Angular CLI 工具。这个工具可以非常简单地生成具有简单构建过程设置的现成项目,包括 transpilation。它在大型项目中也很有用。

模块

模块这个词指的是独立的、可重用的软件代码的小单元,例如,执行动画的代码。我想到的是乐高积木之类的模块(图 2-2 )。每个块都有自己的用途,但是被插入到一个更大的结构(应用)中。

AngularJS 有自己的模块系统,使用简单。那时候 JavaScript 还没有自己的模块化代码的体系。Angular 有自己的模块系统将 Angular 代码打包成模块,还有现代的 JavaScript 模块。

A458962_1_En_2_Fig2_HTML.jpg

图 2-2

Modules are like software LEGO blocks

不要担心,稍后会更详细地介绍这些内容。

控制器和组件

AngularJS 使用控制器来表示 HTML 页面上用户界面中的小部件。

Angular(从版本 2 开始)用Component对象替换控制器。组件可以有自己的标签,比如<Component1>。组件有一个包含数据和代码的类。

第八章更详细地介绍了组件。组件是 Angular 5 应用的构建块。

依赖注入和构造函数注入

正如我提到的,作为 Java Spring 的一员,我喜欢依赖注入,因为它让生活变得更简单。我们可以在这个主题和依赖注入提供的好处上花费大量的篇幅。

AngularJS 提供了依赖注入。现代 Angular 还提供依赖注入。因为您的组件有类,所以依赖项现在通常通过构造函数注入,使用构造函数注入模式。这种软件模式是另一种服务器端技术,现在正用于客户端。让我们看一个 Java Spring 使用构造函数注入的例子。以下配置指定了一个构造函数参数—一个字符串消息,"Spring is fun":

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans

http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="message"
          class="org.springbyexample.di.xml.ConstructorMessage">
        <constructor-arg value="Spring is fun." />
    </bean>

</beans>

以下 bean 类期望在构造函数中接收消息:

public class ConstructorMessage {

    private String message = null;

    /**
     * Constructor
     */
    public ConstructorMessage(String message) {
        this.message = message;
    }

    /**
     * Gets message.
     */
    public String getMessage() {
        return message;
    }

    /**
     * Sets message.
     */
    public void setMessage(String message) {
        this.message = message;
    }

}

这有什么了不起的?在这种情况下,这是一个字符串的简单例子。但是它展示了一个软件对象(在本例中是一个字符串对象)是如何使用构造函数“插入”另一个软件对象的。

例如,在 Angular 中,您可以创建一个可重用的软件对象来处理与服务器的通信。您可以通过构造函数将它传递给每个需要它的对象(类)。这样,在课堂上,你就有了与服务器对话的现成方法。

一次编写一个服务,在许多地方多次使用它。

范围、控制器和组件

在 AngularJS 中,Scope ( $scope)曾经是控制器的“数据容器”。您的变量将包含在$scope对象中。例如,如果您有一个地址输入表单的控制器,地址的每一行可能是控制器的$scope中的一个变量。

在 Angular 中,您不再拥有控制器,而是拥有组件,并使用这些组件来构建用户界面。您可以使用 composition 将组件嵌套在其他组件中。组件有一个类,类似于 Java 或. NET。这个类是“数据容器”,包含你的变量。这更像是传统的服务器端编码。例如,如果您有一个带有地址的输入表单组件,地址的每一行可能是组件类中的一个变量,类似于 Java swing(或 Windows Form)类。

  • 组件使用类来包含它们的变量和应用代码。
  • 类有实例变量、构造函数和方法。
  • 可以使用构造函数将依赖项注入到类中。
  • 实例变量可以绑定到模板,以创建一个响应用户界面。

第八章详细介绍了组件。

形式

编写代码来处理表单上的数据输入是很重要的。编写处理表单、数据输入和验证的 AngularJS 代码很容易,但 Angular 有新的表单模块,可以更容易地完成以下任务:

  • 动态创建表单
  • 用通用验证器验证输入(必需)
  • 用自定义验证器验证输入
  • 测试表格

模板

AngularJS 和 Angular 都使用 HTML 模板(见图 2-3 和 2-4 )。模板中的 HTML 被绑定到数据变量和代码上,以便创建一个工作的应用。不幸的是,模板语法出现了分歧。第十二章详细介绍了新的语法。

A458962_1_En_2_Fig4_HTML.jpg

图 2-4

Angular template

A458962_1_En_2_Fig3_HTML.jpg

图 2-3

AngularJS template

摘要

读完这一章后,你应该对 Angular 的不同版本有更好的理解。最初的 AngularJS 像野火一样迅速发展,因为它是编写跨浏览器应用的一种快捷方式,但它有一些不一致之处,需要更新才能使用更新的浏览器提供的功能。

更现代的 Angular 类似于 AngularJS,只是开发环境更简单,支持使用更新的 JavaScript 和 TypeScript。

Angular 依赖于 JavaScript 和 TypeScript。第三章介绍了 JavaScript 以及它是如何从一个版本变化到另一个版本的。

三、JavaScript 语言

当网景公司在 1995 年 4 月雇用布伦丹·艾希时,他被告知他有 10 天的时间来创造和制作一种可以在网景浏览器上运行的编程语言的工作原型。

十天来创造我们现在所知的 JavaScript!考虑到他被给予的时间,我会说他做得相当好。

JavaScript 在不断发展。目前,大多数 web 浏览器支持 JavaScript ES5,但 ES6 将在未来一两年内成为标准。

JavaScript ES5:局限性和缺点

这一节讨论了当前版本 ES5 之前的 JavaScript 中的限制和缺点。这些缺点在 ES6 中已经解决了,将在本章后面介绍。

类型

当对变量进行运算时,计算机可能知道也可能不知道所涉及的每个变量的类型。

如果类型是已知的,那么操作就很简单,因为操作非常具体。

示例:

const a: number = 123;
const b: number = 456;
const c: number = a + b;

如果不知道类型,事情就会变得更加复杂。计算机必须试图找出正在使用的变量的类型,或者将它们强制转换成预期的类型。逻辑可能会变得复杂。

示例:

var foo = 123 + "Mark";

答案是什么?

  • 123Mark
  • 错误—因为 123 是数字而"Mark"是字符串?

JavaScript 仅支持六种类型:

  • 不明确的
  • 布尔代数学体系的
  • 线
  • 数字
  • 目标

没错,只有一个号码类型。然而,有这么多不同类型的数字,包括整数和小数。我不认为我在类型方面做了什么,JavaScript 并没有削减它。

快速失败行为

代码要么应该准确地工作,要么应该快速(立即)失败。因为 JavaScript 的类型和规则很少,所以它经常会继续运行而不是失败,并带来奇怪的副作用。你认为不起作用的东西会起作用。

例如,以下代码不会失败:

alert((![]+[])[+[]]+(![]+[])[+!+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]);

值/对象比较

当你在 Java 或. NET 语言中比较两个变量时,你不需要成为一个火箭专家就能知道如何比较它们。您实现了一个.equals()方法。但是,因为 JavaScript 的类型很少,所以它使用复杂的逻辑来比较值或对象。要了解 JavaScript 如何比较变量,请看图 3-1 所示的等式算法。你可能想先吃一片泰诺。

A458962_1_En_3_Fig1_HTML.jpg

图 3-1

Abstract equality comparison algorithm

A458962_1_En_3_Figa_HTML.jpg

辖域

在 JavaScript 中,未声明的变量被隐式提升为全局变量。对我来说,这似乎是不合逻辑和危险的,因为肯定要有一个全局变量,你应该这样声明它?

在图 3-2 中,变量foo1是全局变量,变量foo2不是。当这段代码运行时,您只会看到一个提示框,显示“hello”你看不到第二个,因为foo2没有设置,因为它超出了范围,不是全局变量。

A458962_1_En_3_Fig2_HTML.jpg

图 3-2

Only one variable is shown in an alert

JavaScript 严格模式

JavaScript 严格模式在 ES5 中发布。它不会影响旧代码——换句话说,例如,如果在 ES4 中运行,使用strict mode 命令不会破坏 JavaScript 代码。严格模式旨在通过实施更好的编程实践来防止意外错误。

祈祷

"use strict"指令仅在脚本或函数的开头被识别。这种模式可以在两个不同的范围内运行:文件和函数。如果将该指令放在脚本文件的开头,该文件中的所有代码都将以该模式运行。如果将它放在函数的开头,函数中的所有代码都将以该模式运行。

我不能涵盖严格模式的每一个方面,但我会在本节中讨论其中的主要方面。

赋给未声明的变量或对象

当用户将一个值赋给一个未赋值的变量或对象时,严格模式抛出一个错误,防止创建一个非预期的全局变量(我将在本章后面详细讨论这个主题)。以下代码在严格模式下抛出错误:

"use strict";
pie = 3.14;

"use strict";
obj = {str:10, zip:30350};

删除变量或对象

严格模式不允许使用delete关键字删除变量或对象。以下代码在严格模式下抛出错误:

"use strict";
var pie = 3.14;
delete pie;

复制函数参数

严格模式不允许一个函数中有多个同名的参数。以下代码在严格模式下抛出错误:

"use strict";
function concat(word1, word1) {};

复制对象属性

严格模式不允许一个函数在一个对象中有多个同名的属性。以下代码在严格模式下抛出错误:

"use strict";
var obj = {
  prop1 : 0,
  prop2 : 1,
  prop1 : 2
};

只读属性

在 ES5 中,用户可以使用函数Object.defineProperties定义对象属性。该函数允许开发人员将一些属性定义为不可写的(即只读的)。在正常模式下,当代码试图写入只读属性时,代码不会引发错误。但是,在严格模式下,代码会在这种情况下抛出一个错误:

var obj = Object.defineProperties({}, {
              prop1 : {
                value : 1,
                writable : false
              }
            });

obj.prop1 = 2;

不可扩展的变量或对象

在 ES5 中,用户可以使用函数Object.preventExtensions来防止对象被扩展。在正常模式下,当代码试图扩展对象时,代码不会引发错误,但是在严格模式下,代码会在以下情况下引发错误:

"use strict";
var obj = {prop1 : 1};
Object.preventExtensions(obj);
obj.prop2 = 2;

关键词

严格模式引入了以下保留关键字,这些关键字不能在该模式下的代码中使用:

  • implements
  • interface
  • let
  • package
  • private
  • protected
  • public
  • static
  • yield

JavaScript ES6:变化和改进

JavaScript ES6 比 ES5 改进了很多。我不打算涵盖 ES5 和 ES6 之间的所有改进——只是主要的改进。涵盖所有的改进将需要几个章节。注意,如果你想玩玩 ES6,但不确定该做什么,请访问 www.es6fiddle.net 并尝试一下。

常数

常量适用于不能重新分配新值的变量:

const TAX = 0.06;

块范围的变量和函数

在 ES6 之前,JavaScript 在变量方面有两大缺陷。首先,在 JavaScript 中,未声明的变量被隐式提升为全局变量。正如我之前提到的,在我看来,这似乎是不合逻辑和危险的。如果脚本试图将赋值给未声明的变量,JavaScript 中的严格模式会引发错误,如下例所示:

"use strict";
mark = true; // no ‘var mark’ to be found anywhere....

此外,当您用var语句声明变量时,这会将变量的范围缩小到最接近的整函数。以下示例分配了两个x变量:一个在函数内部,但在if块外部,另一个在函数内部,但在if块内部。注意代码是如何运行的,就好像只有一个x变量一样。这是因为它的作用域是整个函数。即使它离开了if语句的范围,它仍然保持相同的值:

function varTest() {
  var x = 31;
  if (true) {
    var x = 71;  // same variable!
    console.log(x);  // 71
  }
  console.log(x);  // 71
}

现在 ES6 允许开发者在块范围内声明变量和函数。ES6 有一个新的用于声明变量的let语句。它类似于var语句,只是变量的作用域是最近的封闭块,就像在{' and '}中一样。

下一个例子展示了内部变量x如何作用于if语句中最近的块。当代码退出if语句时,内部x变量超出了作用域。因此,当控制台日志打印在if下面的语句中时,它显示的是外部x变量的值:

function letTest() {
  let x = 31;
  if (true) {
    let x = 71;  // different variable
    console.log(x);  // 71
  }
  console.log(x);  // 31
}

ES6 还允许您定义块内的函数。当块终止时,这些函数立即超出范围。例如,以下代码在带有 ES5 的 Plunker 上运行良好,但在 Es6fiddle.net 上运行时会抛出“未捕获的引用错误:日志未定义”:

if (1 == 1){
    function log(){
      console.log("logging");
    }
    log();
}
log();

箭头功能

箭头函数是一种用于编写 JavaScript 函数的新的 ES6 语法(见图 3-3 )。箭头函数是一个匿名函数,您可以在源代码中内嵌它(通常是为了传递给另一个函数)。您不需要通过使用function关键字来声明箭头函数。关于箭头函数需要记住的一件非常重要的事情是,this变量的值保存在函数内部。

A458962_1_En_3_Fig3_HTML.jpg

图 3-3

Arrow function

函数参数现在可以有默认值

如果某些参数未定义,您可以指定默认值。

例如,以下内容

function multiply(a = 10, b = 20){
  return a * b;
}
console.log(multiply(1,2));
console.log(multiply(1));
console.log(multiply());

产生以下输出:

2
20
200

函数现在接受 Rest 参数

这个参数语法使我们能够将无限数量的参数表示为一个数组。

例如,以下内容

function multiply(...a){
  var result = 1;
  for (let arg in a){
    result = result * a[arg];
  }
  return result;
}
console.log(multiply(5,6));
console.log(multiply(5,6,2));

产生以下输出:

30
60

字符串插值

字符串插值使变量能够被数据绑定到文本字符串中。注意,插值只对用于模板文字的新引号字符```ts 起作用。模板文字允许用户使用多行字符串和字符串插值。字符串插值不适用于普通引号"'中的字符串。

例如,以下内容:

var person = {name: "julie", city: "atlanta"};
console.log(person.name);
// works
console.log(`${person.name} lives in ${person.city}`);
// doesnt work
console.log("${person.name} lives in ${person.city}");
console.log('${person.name} lives in ${person.city}');

```ts

产生以下输出:

julie
julie lives in atlanta
${person.name} lives in \({person.city} \){person.name} lives in ${person.city}


### 模块

模块化编程是一种软件设计技术,它强调将程序的功能分成独立的、可互换的模块,每个模块都包含执行所需功能的一个方面所需的一切。

目前,大多数网络浏览器运行的是 JavaScript 版本 ECMA 5,它不是为模块化编程而编写的。然而,ECMA 6 旨在与模块一起工作,其规范于 20156 月达成一致。ES6 JavaScript 允许你将代码编写成模块,并像乐高积木一样使用它们。

例如,您可以拥有一个包含大量国际化代码的国际化实用程序模块,包括为不同地区加载资源包的代码等等。然而,您只需要其他代码来访问这个代码的一个方法,一个叫做`getI18N(locale, key)`的方法,它将返回一个地区和一个键的文本。JavaScript 模块为您提供了这种能力,让您编写一个只能通过公共接口访问的代码“黑盒”——在本例中是一个导出函数。

#### 一条命

在 ES6 中,每个模块都写在自己的 JavaScript 文件中——一个。js 文件。每个文件只有一个模块,每个模块只有一个文件。有两种方法可以从一个模块中导出东西,使它们在外部可用,这两种方法都使用了`export`关键字。您可以在同一个模块中混合使用这两种导出方式,但如果不这样做,会更简单。随便选一个。一旦导出了代码,就可以通过导入在其他地方使用它。

#### 导出方法 1:命名导出

如果希望模块导出多个对象(例如,常数、函数、对象等),请使用命名导入。命名导入使您能够导出带有名称的代码。

模块`mymath.js`:

export const sqrt = Math.sqrt;
export function square(x) {
return x * x;
}
export function diag(x, y) {
return sqrt(square(x) + square(y));
}


导入和使用模块代码:

import { square, diag } from 'mymath';
console.log(square(11));
console.log(diag(4, 3));


注意导出不需要分号,并且名称必须与模块中的原始名称相匹配。

#### 导出方法 2:默认导出

每个模块只能有一个默认导出。如果您只想让您的模块导出一个东西,这是很有用的。

模块`mymath.js`:

export default function square(x) {
return x * x;
}


导入和使用模块代码:

import sq from 'mymath';
sq();


再次注意导出不需要分号,名字`sq`与模块中的函数不匹配。使用默认导出允许您使用“昵称”,因为它知道将要使用的对象,因为只有一个。

Note

如果需要编写可以部署到运行 ES5 的浏览器上的现代代码(ES6 或更高版本),可以使用 transpilation 来转换代码。

### 以打字打的文件

ES6 是 ES5 的一大进步,但它仍然缺少 Java 和 C#等现代结构化语言提供的一些功能,例如,强类型、装饰器、枚举等。

别担心。已经有一些构建在 ES6 之上的东西,并把它推进了一步。这叫打字稿。我们将在 ES6 和 TypeScript 中编写现代代码,并使用 transpilation 将其转换为可在主流 web 浏览器上部署的兼容代码。

TypeScript 由微软编写,是一种非常现代的结构化语言,类似于 Java 和 C#。谷歌与微软在 TypeScript 上合作,用它自己编写 Angular。这使得在 Angular 中使用 TypeScript 成为一个非常好的主意!

## 摘要

本章讨论了 JavaScript 的许多陷阱,并指出当前大多数 web 浏览器仍然运行 ES5,而不是 ES6。我提到过,虽然 ES6 是对 ES5 的改进,但 TypeScript 扩展并改进了它,提供了打字和许多其他功能。打字稿是下一章的主题。

# 四、TypeScript

TypeScript 是 JavaScript(由微软编写)的超集,主要提供可选的静态类型、类和接口。它是开源的,正在 GitHub 上开发。编译器在 TypeScript 中实现,可以在任何 JavaScript 主机上工作。

作为 JavaScript 的严格超集意味着 JavaScript 程序也是有效的 TypeScript 程序,TypeScript 程序可以无缝地使用 JavaScript。TypeScript 编译成兼容的 JavaScript。TypeScript Java/非常相似。NET 有一些不同,例如,构造函数和接口。

您不需要下载或安装 TypeScript。当您使用 Angular CLI 时(在第七章中介绍),它会自动为您的项目设置 TypeScript。

简而言之,您可以这样看待 TypeScript:

*   TypeScript = JavaScript +类型+类+模块+更多

这些增加中最重要的是类型,我们将在本章中讨论。类型使 ide 能够提供一个更丰富的环境,以便在您键入代码时发现常见错误。

Note

浏览器不能直接运行 TypeScript 至少现在还不能。TypeScript 代码被编译成 JavaScript。

微软学习打字稿的网站是 [`www.typescriptlang.org`](http://www.typescriptlang.org) ,其操场如图 4-1 所示。

请注意,您可以在左侧输入 TypeScript,并在右侧看到它被转换为 JavaScript。

![A458962_1_En_4_Fig1_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-frontend-framework-zh/raw/master/docs/ng5-proj/img/A458962_1_En_4_Fig1_HTML.jpg)
4-1

Playground area of [`www.typescriptlang.org`](http://www.typescriptlang.org)

本章的其余部分集中在 JavaScript 和 TypeScript 语言之间的主要区别。

## 强力打字

TypeScript 提供了强类型,类型非常有用,因为它使开发人员能够指定如何使用变量(它将存储什么类型的信息)。这使编译器能够验证情况是否如此。如果您的代码没有像预期的那样以有效的方式使用变量,它将无法编译。

使用 TypeScript 进行相等性比较比使用 ECMA5 JavaScript 更容易,因为您可以很容易地检测到所比较的两项是否属于同一类型。如果不是,就会产生一个错误。类型检查完成后,相等检查会更容易,因为两个项属于同一类型。代码中包含类型给了 ide 更多的信息。例如,如果 IDE 知道某个变量是字符串,它可以将自动完成选择范围缩小到字符串。

TypeScript 提供了以下基本类型:

*   布尔代数学体系的
*   数字
*   线
*   排列
*   列举型别
*   任何的
*   空的

## 班级

ECMAScript 5 没有类,但是 TypeScript 和 ECMAScript 6 有。

类具有以下格式的构造函数:

```ts
class Animal {
    private name:string;
    constructor(theName: string) { this.name = theName; }
}

Note that the code below will do the same thing as the code above (ie assign a value to the 'name' instance variable):

class Animal {
    constructor(private name: string) {}
}

类可以扩展其他类:

class Animal {
    name:string;
    constructor(theName: string) { this.name = theName; }
    move(meters: number = 0) {
        alert(this.name + " moved " + meters + "m.");
    }
}

class Snake extends Animal {
    constructor(name: string) { super(name); }
    move(meters = 5) {
        alert("Slithering...");
        super.move(meters);
    }
}

class Horse extends Animal {
    constructor(name: string) { super(name); }
    move(meters = 45) {
        alert("Galloping...");
        super.move(meters);
    }
}

类可以实现接口(见下一节)。和类可以对成员变量或方法使用公共和私有修饰符。如果没有为变量或方法指定 public 或 private,编译器会假定该成员是公共的。

接口

把接口想象成做某事(例如以某种方式实现某个功能)或存储某种数据(例如属性、数组)的承诺。TypeScript 接口可以应用于函数:

interface SearchFunc {
  (source: string, subString: string): boolean;
}
var mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
  var result = source.search(subString);
  if (result == -1) {
    return false;
  }
  else {
    return true;
  }
}

TypeScript 接口也可以应用于属性。接口可以强制属性,但也可以有可选属性(例如,下面代码中的color):

interface LabelledClothing {
  label: string;
  size: number;
  color? : string;
}

function printLabel(labelled: LabelledClothing) {
  console.log(labelled.label + " " + labelled.size);
}

var myObj = {size: 10, label: "Dress"};
printLabel(myObj);

Typescript 接口可以应用于数组:

interface StringArray {
  [index: number]: string;
}

var myArray: StringArray;
myArray = ["Bob", "Fred"];

类可以实现接口:

interface ClockInterface {
    currentTime: Date;
    setTime(d: Date);
}

class Clock implements ClockInterface  {
    currentTime: Date;
    setTime(d: Date) {
        this.currentTime = d;
    }
    constructor(h: number, m: number) { }
}

您可以拥有扩展其他接口的接口:

interface Shape {
    color: string;
}

interface Square extends Shape {
    sideLength: number;
}

var square = <Square>{};
square.color = "blue";
square.sideLength = 10;

模块

模块不包含在 ECMAScript 5 中,但它们包含在 TypeScript 和 ECMAScript 6 中。关键字export允许你在一个模块中导出你的 TypeScript 对象,这样它们就可以在其他地方使用。

有两种主要类型的 TypeScript 模块:内部模块和外部模块。在 Angular 中,大多数时候你将使用外部模块。

内部模块

内部模块是 TypeScript 自己模块化代码的方法。您使用module关键字来创建一个模块。内部模块可以跨多个文件,有效地创建一个名称空间。在浏览器中,你使用<script/>标签加载模块,因为没有运行时模块加载机制。或者你可以把 TypeScript 文件编译成一个 JavaScript 文件,用一个<script/>标签包含进来。

您可以像这样声明内部模块:

module mymod {

  export function doSomething() {
    // this function can be accessed from outside the module
  }

  export class ExportedClass {
    // this class can be accessed from outside the module
  }

  class AnotherClass {
    // this class can only be accessed from inside the module
  }
}

要使用内部模块,您可以使用它们的完全限定名:

var exportedClassInstance = new mymod.ExportedClass();

或者您可以导入它们:

import ExportedClass = mymod.ExportedClass;
var exportedClassInstance = new ExportedClass();

外部模块

这些是在 Angular 中开发时最常用的模块类型。外部模块使用运行时模块加载机制。我们将在第九章中讨论模块加载机制。

要使用外部模块,您需要决定是使用 AMD 还是 CommonJS(您的两种模块系统选择),然后使用带有值amdcommonjs–module编译器标志编译您的源代码。

在计算中,名称空间是用来组织各种对象的一组符号。对于外部模块,文件的名称和路径将创建名称空间,用于标识该项。

下面是一个名为 projectdir/ExportedClass.ts 的文件的示例:

class ExportedClass {
  // code ....
}
export = ExportedClass;

要使用外部模块:

import ExportedClass = require("projectdir/ExportedClass");
var exportedClassInstance = new ExportedClass();

枚举和泛型

枚举用于设置常数值列表。他们将熟悉 Java 和。NET 开发人员:

enum Color {Red, Green, Blue};
var c: Color = Color.Green;

仿制药也是如此:

interface LabelledClothing {
  label: string;
  size: number;
}
var arr: Array<LabelledClothing> = new Array<LabelledClothing>();

构造器

TypeScript 使用constructor关键字来声明构造函数,而不是类名。另一个区别是,TypeScript 自动将构造函数参数作为属性进行赋值。您不需要在您的构造函数中分配实例变量——这已经为您完成了。

这个:

class Person {
    constructor(private firstName: string, private lastName: string) {
    }
}

等于这个:

class Person {
    private firstName: string;
    private lastName: string;

    constructor(firstName: string, lastName: string) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
}

功能

ECMAScript 5 中不存在箭头函数,但在 TypeScript 和 ECMAScript 6 中存在。箭头函数是可以在源代码中内联编写的函数(通常是为了传递给另一个函数)。图 4-2 至 4-4 显示箭头功能。

A458962_1_En_4_Fig4_HTML.jpg

图 4-4

The functions in the preceding two figures could be written in shorter form, like this

A458962_1_En_4_Fig3_HTML.jpg

图 4-3

The function in Figure 4-2 could be written into an arrow function in this manner

A458962_1_En_4_Fig2_HTML.jpg

图 4-2

Regular function Note

语法不是开发人员在 TypeScript 中使用箭头函数的主要原因。主要原因是变量this的值保存在箭头函数中。这对开发人员有很大的好处,因为常规 JavaScript 函数有一种称为装箱的机制,它在进入被调用函数的上下文之前包装或更改this对象。在匿名函数中,this对象代表全局窗口。在其他函数中,它代表其他的东西。许多开发人员在绝对希望确保this变量是他们所期望的时,会使用箭头函数。

图 4-5 显示了一个常规函数的例子。

A458962_1_En_4_Fig5_HTML.jpg

图 4-5

A regular function

运行图 4-5 中的代码后,person.age的值为 1。它的值应该是 2,因为Person函数中的this变量实际上并不代表Person函数。

图 4-6 显示了一个箭头功能的例子。

A458962_1_En_4_Fig6_HTML.jpg

图 4-6

An arrow function

运行图 4-6 中的代码后,person.age的值为 2,这是正确的。那是因为Person函数中的this变量按照预期代表了Person函数。

省略号运算符(用...表示)允许方法接受一组参数作为数组,如下例所示:

function sum(...numbers: number[]) {
    var aggregateNumber = 0;
    for (var i = 0; i < numbers.length; i++)
        aggregateNumber += numbers[i];
    return aggregateNumber;
}

console.log(sum(1, 5, 10, 15, 20));

Getters 和 Setters

如果您的目标是使用 ECMAScript 5 的浏览器,这个脚本版本支持Object.defineProperty()特性。如果您使用 TypeScript getters 和 setters,那么您可以用.符号定义和直接访问属性。如果您习惯于 C#,那么您已经非常习惯于:

class foo {
  private _bar:boolean = false;

  get bar():boolean {
    return this._bar;
  }
  set bar(theBar:boolean) {
    this._bar = theBar;
  }
}

...

var myBar = myFoo.bar;
myFoo.bar = true;

类型

TypeScript 中可以有变量类型,但这是可选的。它之所以是可选的,是因为 TypeScript 是向后兼容的,这意味着它可以运行所有的 JavaScript 代码。在 Typescript 中声明变量时,可以通过在变量名称后添加: [type]来指定变量类型。

例如,我们声明一个类型为number的标记变量:

var mark: number = 123;

如果我们编辑代码将一个字符串赋给这个变量,我们会得到如图 4-7 所示的语法错误高亮显示。请注意,这不会在 Plunker 中发生——只会在像 Visual Studio Code 这样的编辑器中发生。

A458962_1_En_4_Fig7_HTML.jpg

图 4-7

Syntax error highlighting

如果我们保存这个(坏的)代码并编译 TypeScript,我们会得到以下错误:

Type 'string' is not assignable to type 'number'.

原始类型

TypeScript 提供了以下基本类型:

  • 任何的
  • 空的
  • 数字
  • 线
  • 布尔代数学体系的

原始类型不是从Object类继承的,也不可扩展(不能子类化)。基本类型通常以小写首字母命名,例如,number

对象类型

对象类型不是基本类型。它们继承自Object类,并且是可扩展的。对象类型通常以大写首字母命名,例如,Number

这种类型的对象可以访问它们的prototype,因此您可以向对象添加额外的功能:

String.prototype.Foo = function() {
    // DO THIS...
}

对象类型也让你使用instanceof来检查类:

myString instanceof String

工会类型

有时您希望变量是多种类型中的一种,例如,字符串或数字。为此,您可以使用 union ( |)类型。以下变量可以是字符串或数字,并且代码有效:

var name: string|number;

...

constructor(){
      this.name = 'abc';
      this.name = 22;
}

这是另一个例子:

var action = ActionNew | ActionSave | ActionDelete ;
...

if (action instanceof ActionNew){
      ...do something...
}

联合类型也可以应用于函数参数和结果:

function format(value: string, padding: string | number) { // ... }

function getFormatted(anyValue:any): string | number { // ... }

别名类型

您也可以使用type关键字来定义类型别名:

type Location = string|number;
var loc: Location;

元组类型

元组是元素的有限有序列表,例如:姓名、地址、数字邮政编码。TypeScript 允许您使用使用类或元组的变量来访问这些数据。元组类型允许您将变量定义为一系列类型:

var contactInfo: [string, string, number];

contactInfo = ['Mark', '12 Welton Road', 30122];

编译选项

您可以根据自己的喜好配置 TypeScript:源代码位于何处、编译有多严格、您想要(或不想要)什么编译检查、生成的 transpiled 代码位于何处等等。您可以通过在 JSON 格式的文件中指定配置选项来配置 TypeScript。通常,这个文件被称为“tsconfig.json ”,当您使用 CLI 生成 Angular 项目时,您会发现这样一个文件。我有时会编辑这个文件,如果你要做一些不寻常的事情,让不相关的编译检查进行下去。例如,如果您在 TypeScript 类中使用一些常规的 JavaScript 代码。

摘要

现在,您应该对 TypeScript 如何改进 JavaScript 有了基本的了解。TypeScript 有助于开发人员更好地声明和指定代码,允许您用指定的类型声明变量并启用更多的编译时检查。它还帮助您使用注释向编译器提供关于您正在编写的有 Angular 的对象的信息。TypeScript 确实使 Angular 2 和 4 更容易使用。

我们很快就要开始编码了,但是首先我们需要设置我们的代码编辑器。第六章封面编辑。

五、Visual Studio Code

许多编辑器都可以使用 TypeScript,包括 Visual Studio、Visual Studio Code、WebStorm、WebEssentials、Eclipse 等等。

不同的人喜欢不同的编辑器,不同的开发人员可以在同一个项目中使用不同的编辑器,而不会引起很多问题。没有“正确”或“错误”的编辑器。

我将介绍 Visual Studio Code,因为它非常好用,而且是免费的。我建议你去 https://code.visualstudio.com 点击下载链接安装这个编辑器。如果您最终不喜欢 Visual Studio Code,您可以轻松地删除它并选择不同的代码。

我选择 Visual Studio Code 是因为它是微软开发的开源源代码编辑器,可用于 Windows、Linux 和 macOS。它包括对调试、嵌入式 Git 控件、语法高亮、智能代码完成、代码片段和代码重构的支持。它是由编写 TypeScript 的同一批人编写的,所以我们知道它可以很好地与它一起工作。它还可以很好地与 JavaScript、PHP 等等一起工作。而且比较紧凑。

有时我会转而使用 Webstorm,因为它能更好地重构代码。然而,Visual Studio Code 90%的时间都工作得很好。

Visual Studio Code 入门

如果您还没有这样做,请转到 https://code.visualstudio.com ,下载 Visual Studio Code,并安装它。图 5-1 显示下载页面。

A458962_1_En_5_Fig1_HTML.jpg

图 5-1

Download page for Visual Studio Code

安装程序后,要启动 shell,请双击 Visual Studio Code 图标将其打开。单击文件➤打开文件夹,然后选择项目的根文件夹。

导航到项目的根文件夹。输入命令code .(代码空格周期)。

查看文件、命令和热键

按 Ctrl+P 会在顶部的文本框下面列出文件,如图 5-2 所示。当您键入时,它会过滤列表。

A458962_1_En_5_Fig2_HTML.jpg

图 5-2

Filtering the list of files

按 Ctrl+Shift+P 会在顶部文本框的下方列出顶部的命令,如图 5-2 所示。当您键入时,它会过滤列表。

A458962_1_En_5_Fig3_HTML.jpg

图 5-3

Filtering the list of commands

开始构建

编辑项目根文件夹中的 tasks.json 文件(见图 5-4 )。这个配置文件指定了我们将在示例项目中使用的构建命令。它在命令行上运行npm run build来调用构建。有关 Webpack 和构建过程的更多信息,请参见第十章。

A458962_1_En_5_Fig4_HTML.jpg

图 5-4

Editing tasks.json

若要开始生成,请按 Ctrl+Shift+B。生成输出将显示在输出窗格中。通常运行需要 10-30 秒。见图 5-5 。

A458962_1_En_5_Fig5_HTML.jpg

图 5-5

Build output will appear in the Output pane

要查看构建错误,请按 Ctrl+Shift+M。错误列在屏幕顶部(参见图 5-6 )。单击错误以导航到错误的来源。

A458962_1_En_5_Fig6_HTML.jpg

图 5-6

Any errors would be listed at the top of the screen

介绍界面

Visual Studio Code 在左侧显示了一个侧边栏,提供不同的模式(图 5-7 )并在右侧显示了一个编辑区域。

A458962_1_En_5_Fig7_HTML.jpg

图 5-7

The Visual Studio sidebar has four modes: Explorer, Search, Git, and Debug (from top to bottom).

您可以使用 Ctrl+B 键盘快捷键来显示和隐藏侧栏。

您可以在四种主要的侧边栏模式之间轻松切换:Explorer、Search、Git 和 Debug。有不同的方式来切换模式:

  • 单击侧边栏图标。
  • 单击查看并选择您的模式。
  • 使用热键(在以下章节中给出)。

探险家

浏览器窗格(图 5-8 )是侧边栏之后的第一个窗格。它分为两个部分:工作文件(上面的)和项目文件(下面的—在这种情况下,称为 Temp)。单击文件列表中的文件,将其显示在右侧进行编辑。

A458962_1_En_5_Fig8_HTML.jpg

图 5-8

Explorer pane

要激活或聚焦浏览器窗格,请单击边栏中的文件图标,单击查看➤浏览器,或按 Ctrl+E

工作文件

编辑文件时,它们会出现在工作文件中。如果您一次只编辑几个项目文件,将这些文件列在工作文件部分的顶部会很方便。当你将鼠标悬停在“工作文件”标题上时,它会显示一个 X,允许你根据需要清除列表。

项目文件

项目文件是项目中所有文件以及文件夹的列表。

搜索

搜索(图 5-9 )的工作方式就像它在大多数程序中一样(见图 5-9 )。要激活或聚焦搜索窗格,请单击边栏中的放大镜图标,单击查看➤搜索,或按 Ctrl+Shift+F

A458962_1_En_5_Fig9_HTML.jpg

图 5-9

Search

饭桶

要激活或聚焦 Git 窗格,单击侧栏中的 Git 图标,单击查看➤ Git,或按 Gtrl+Shift+G。参见图 5-10 。

A458962_1_En_5_Fig10_HTML.jpg

图 5-10

Git

  • “视图”菜单“选项”“Git”。
  • control–Shift–G

调试

要激活或聚焦调试窗格,请单击边栏中的调试图标,单击查看➤调试,或按 Ctrl+Shift+D。参见图 5-11 。

A458962_1_En_5_Fig11_HTML.jpg

图 5-11

Debug

调试对于调试服务器端代码比调试浏览器端代码更有用,所以对于 Angular 来说可能没有太大用处。如果您在浏览器上启用远程调试并连接到它,您可以使用该调试器来调试浏览器代码,但使用可用的(优秀的)浏览器调试器可能更容易,如 Google Chrome 中的调试器。

要调试您的服务器端代码,您必须首先设置一个调试启动任务。这使您能够设置调试启动配置,用于启动服务器端代码并开始调试它。

要进行调试,请执行以下操作:

  1. 单击边栏中的调试图标,或者使用已经提到的另一个选项。
  2. 单击齿轮图标打开调试配置设置(在中)。settings/launch.json)。
  3. 选择您的调试配置(在齿轮图标旁边),然后单击 play 启动它。

扩展ˌ扩张

要激活或聚焦扩展窗格,单击左侧的扩展图标(图 5-12 ),单击查看➤扩展,或按 Ctrl+Shift+X。图 5-13 显示了扩展窗格。

A458962_1_En_5_Fig13_HTML.jpg

图 5-13

Extensions pane

A458962_1_En_5_Fig12_HTML.jpg

图 5-12

Extensions icon

将扩展安装到 Visual Studio Code 中非常容易。我工作的 Angular 5 项目有一个包含林挺的构建过程,它检查代码以确保它遵循风格指南。如果用户添加了太多空白,构建通常会失败。这变得很烦人,在代码中安装 linter 扩展是一个好主意,这样它可以在林挺问题发生时突出显示这些问题(在左下角有一个警告)。

要查看与扩展相关的命令,请输入以下内容:

extensions

这将显示如图 5-14 所示的可用扩展命令列表。

A458962_1_En_5_Fig14_HTML.jpg

图 5-14

Extensions commands

要在代码中安装扩展,请输入以下命令并按照说明进行操作:

ext install

要在代码中设置 TypeScript linter,请输入以下命令并遵循说明(参见图 5-15 ):

A458962_1_En_5_Fig15_HTML.jpg

图 5-15

Installing the TypeScript linter

ext install tslin

A Few Other Handy Things To Note

作为一个丰富的编辑环境,虚拟工作室代码提供了智能感知代码选择和完成。如果语言服务知道可能的完成方式,IntelliSense 建议将在您键入时弹出。您可以通过按 Ctrl+空格键来手动触发它。

要保存您的工作,可以使用普通的“文件”菜单“保存”命令,就像 Ctrl+S 快捷键一样。

Visual Studio Code 允许您自由地进出代码。例如,您可以按住 Ctrl 并单击来“钻取到”代码,例如钻取到一个方法。当您需要详细查看某个东西时,这非常有用,但是您需要能够回到您所在的位置。这就是向后和向前导航的用武之地。要向后导航,请按 Ctrl+或单击查看➤向后导航。要向前导航,请按 Ctrl+Shift+或单击查看➤向前导航。

摘要

希望现在您已经安装了 Visual Studio Code 并检查了它。请注意,您安装的版本中显示的屏幕可能与本章中的屏幕截图略有不同。这个程序经常更新。

还要记住,你没有被“锁定”在任何特定的编辑器中。我选择 Visual Studio Code 与本书一起使用,因为它简单易用,易于上手。如果您想使用另一个编辑器来处理本书中的代码示例,请继续。

现在您已经安装了一个编辑器,我们可以继续我们的开发环境,并准备开始编码。当你用 Angular 开发代码时,你最终会使用大量的第三方代码——也就是其他人写的代码。你尽量不要从头开始写!

因此,您的项目将依赖于其他人的代码。Node 的目的是管理这些依赖关系,所以我们将在下一章讨论 Node。

六、Node

我们需要尽快编码,但我们需要一个项目。要创建一个项目,我们需要使用 CLI (Angular 的命令行界面,将在下一章介绍)。CLI 需要节点才能工作。因此,在开始编码之前,我们需要讨论节点。

Node 是安装在计算机上的 JavaScript 运行时。它是开发工具和服务器(或其他任何东西)的平台。Node 易于使用,并且已经为它编写了数百个模块——这意味着您可以重用大量代码。

Node 使用谷歌为 Chrome 浏览器编写的 V8 JavaScript 引擎代码,结合额外的模块来完成文件 I/O 和其他在浏览器中无法完成的有用任务。Node 本身什么也不做——它是一个可以运行许多有用的 JavaScript 代码模块的平台,包括 web 服务器、传输器等等。

Node 还提供了节点包管理器的依赖管理,我们将在本章中介绍。它将使你能够管理你的项目对第三方 JavaScript 库的依赖。掌握 npm 的运作是非常重要的。节点包管理器也允许你发布你的 Angular 代码作为一个模块,以及使用其他人。

你需要 Node。要下载它,去 nodejs.org/download 为你的电脑安装核心节点软件。图 6-1 所示为节点网址。

A458962_1_En_6_Fig1_HTML.jpg

图 6-1

The Node website

您可以选择下载并安装最推荐的版本或最新版本。显然,前者更稳定——这就是为什么推荐它。

设置和运行节点

以下命令在项目中设置节点。它会向您提出一些问题,然后生成 package.json 文件(稍后会介绍)。在项目的根文件夹中运行此命令:

npm init

Note

“npm”命令用于调用节点程序包管理器。

一旦安装了 Node,您就可以通过命令行访问命令node。输入不带参数的命令将允许您键入 JavaScript 并按 Enter 键运行它:

$ node
> console.log('Hello World');
Hello World

使用命令node更有用的方法是输入这个命令加上一个文件名作为参数。这将把文件的内容作为 JavaScript 执行。在这种情况下,我们将创建文件 hello.js:

setTimeout(function() {
   console.log('Hello World!');
}, 2000);

现在我们可以运行它了:

node hello.js

该程序等待两秒钟,然后将“Hello World”写入控制台。

节点程序包管理器(npm)

现在我们知道了如何通过 Node 运行 JavaScript 代码,我们需要看看如何安装这些有用的模块。您可能认为这很简单,但这并不是因为许多节点模块依赖于其他节点模块来工作。因此,当您安装一个节点模块时,node 需要确保所有依赖的节点模块也被安装。这就是为您发明节点包管理器(npm)的原因——添加、更新和删除项目中的节点模块,并管理这些相互依赖关系。

为此,Node 提供了对命令npm的命令行访问。这个命令有许多不同的参数,允许您安装、更新或卸载模块。

网站 http://docs.npmjs.com 是关于国家预防机制的详细文献的一个很好的资源。而 www.npmjs.com 是可用节点包的绝佳资源。

节点模块安装级别

节点模块安装有两个级别:

  • 全局:如果您要在命令行上安装某个东西,可以通过在命令行上将-g添加到npm install来进行全局安装:

  • 本地:如果你正在安装你想在你的程序中使用的东西(不是从命令行),使用本地级别。要在本地安装一个模块,可以在命令行中省略npm install中的-g:

npm install -g typescript

npm install express

运行 NPM Install[模块名称]来安装模块

如果您正在做一些简单的事情,比如向项目中添加一个额外的节点模块,这将非常有用。例如,最有用的模块之一是 Express,一个有能力的 web 服务器。要安装 Express,我们可以在命令行中输入以下内容:

npm install express

这不会更新您的节点依赖文件 package.json(稍后会详细介绍)。如果您需要将该模块保存为该文件中的项目依赖项,请在命令中添加--save--save-dev参数:

  • save 参数--save:这将添加您将要安装的节点模块,作为您的项目在生产中所需的节点模块。您的 package.json 文件被修改为包含这个依赖项。

    npm install express --save
    
    
  • save 参数--save-dev:这将添加您将要安装的节点模块,作为您的项目在开发中只需要的节点模块(也就是说,在生产中不需要)。您的 package.json 文件再次被修改以包含这个依赖项。

    npm install express --save-dev
    
    

更新节点模块

有时你的依赖会改变。您希望添加一个附加模块,但是添加该模块要求其他模块具有更高的版本号。节点提供以下命令来检查您的模块是否过期:

npm outdated

有两种不同的方式来更新节点中的模块。

  • 您可以运行命令npm update来指定要更新的模块。如果您希望您的 package.json 文件更新为更高的版本,还可以添加--save选项。如果指定了-g标志,这个命令将更新全局安装的软件包。
  • 可以编辑 package.json 文件,更新模块依赖关系,然后运行npm update。这将更新您的模块以匹配该文件中的规范。

Checking Your Node Version

如果已经安装了 Node,可以通过运行以下命令来检查其版本:

npm –v

卸载节点模块

您可以运行命令npm uninstall,指定要卸载的模块。如果您希望您的 package.json 文件用从依赖项列表中删除的模块进行更新,也可以添加--save选项。如果指定了-g标志,这个命令将删除全局安装的软件包。

package.json 文件

节点旨在从项目文件夹中的命令行运行。它允许开发人员将与项目相关的信息存储在 package.json 文件中,该文件应该位于项目的根文件夹中。该文件指定了许多关于项目的有用信息:

  • 项目的名称和版本。
  • 您的项目依赖于哪些节点模块(以及您需要这些模块的哪些版本)。
  • 您的项目在生产中需要哪些节点模块。
  • 你的项目在开发中需要哪些节点模块(也就是生产中不需要)。

更新 package.json

您可以通过两种方式更新这个“packages.json”文件:

  • 通过使用安装/更新/删除节点模块和更新该文件的节点命令(在命令行上)。
  • 通过自己编辑这个文件。然后运行节点命令来安装/更新/删除节点模块以匹配该文件。

版本号

package.json 文件允许开发人员指定项目所需的节点模块。当您在此文件中指定依赖项时,您也可以指定这些依赖项的版本,例如 1.0.1。Node 允许您灵活地以许多不同的方式指定您需要的版本号,如表 6-1 中所总结的。

表 6-1

Ways of Specifying Version Numbers

| 1.2.1 | 必须与版本 1.2.1 匹配 | | >1.2.1 | 必须高于版本 1.2.1 | | >=1.2.1 | 必须是版本 1.2.1 或更高版本 | | <1.2.1 | 必须在版本 1.2.1 之前 | | <=1.2.1 | 必须早于或等于版本 1.2.1 | | ~1.2.1 | 必须大致相当于版本 1.2.1 | | ¹.2.1 | 必须与版本 1.2.1 兼容 | | 1.2.x | 必须是从 1.2 开始的任何版本。 | | * | 任何版本 |

文件夹节点 _ 模块

当您安装一个节点模块时,它被下载并放置在项目文件夹的子文件夹 node_modules 中。通常情况下,您会得到比您预想的更多的东西,因为您安装的节点模块有许多依赖项,所以您最终会得到一个巨大的 node_modules 文件夹,里面有几十个模块子目录。有时 npm 需要很长时间来下载和安装项目节点模块。另外,注意不要把这个文件夹从一个地方复制到另一个地方,因为这可能会花去你似乎永远也不会有的时间。如果要将项目从一台计算机复制到另一台计算机,请先删除“node_modules”文件夹,然后在目标计算机上运行“npm install”。

有两种不同的方式将模块安装到节点中。您可以运行命令npm install来指定模块(来安装它),或者您可以编辑 package.json 文件,然后运行npm install

编辑 package.json 文件并运行 npm install

当您的项目依赖于多个模块时,手动编辑 package.json 文件是安装多个模块的最佳方式。首先,您必须在项目的根文件夹中建立一个 package.json 文件,该文件包含应用的概述。有许多可用的字段,但在下面的示例 package.json 文件中,您看到的是最少的字段。dependencies部分描述了您想要安装的模块的名称和版本。在这种情况下,我们还将依赖于快速模块:

{
  "name" : "MyStaticServer",
  "version" : "0.0.1",
  "dependencies" : {
    "express" : "3.3.x"
  }
}

要安装 package.json 文件中概述的依赖项,请在项目根文件夹的命令行中输入以下内容:

npm install

Installing The Latest Angular

转到您的项目文件夹,发出以下命令:

npm install @angular/{common,compiler,compiler-cli,core,forms,http,platform-browser,platform-browser-dynamic,platform-server,router,animations}@latest typescript@latest --save

摘要

现在您已经知道了什么是 Node,以及如何使用它来管理项目对第三方代码的依赖,是时候开始使用 Angular CLI(命令行界面)进行编码了。让我们在下一章开始吧!

七、开始使用 CLI 编码

当我第一次在 Angular 2 中开始开发时,我发现一开始有一个尖锐的学习曲线。因为没有一个标准的 Angular 2 项目蓝图来简单地处理项目的构建和运行,所以很难让项目进行下去。您必须在 Node 中设置您依赖项(稍后将详细介绍),设置您的构建过程,并设置部署过程。这使得 Angular 2 一开始很难,因为你必须同时学习概念和语法。

输入 CLI。Angular CLI(命令行界面)的开发是为了让开发人员能够快速使用 Angular。这很棒——它可以生成结构良好、设计良好的项目。我怎么强调都不为过,这是一个多么好的工具。难怪这么快就被采用了。

Angular CLI 是一个开源项目。你可以在 https://github.com/angular/angular-cli/ 看它的代码。官方 Angular CLI 文档可通过 https://cli.angular.io/ 在线获取。你可以在 https://angular.io/docs/ts/latest/cli-quickstart.html 查看快速入门页面。

本章的目的是让您使用 CLI 创建项目。现在还不打算详细介绍 CLI,因为还没有必要——在后面的章节中会有更多关于 CLI 的内容。然而,如果你现在想要大量的信息,我推荐你在 www.sitepoint.com/ultimate-angular-cli-reference/ 的优秀文章。

Angular CLI 名副其实,它使用命令行界面。您在终端中使用ng命令,它就开始工作了。使用命令行可能会让您想起“糟糕的旧时光”,那时您必须记住一堆命令,但是当您看到 CLI 所做的事情时,您会忘记这一切:

  • 它允许您创建新的 Angular 应用。
  • 它允许您运行一个开发服务器,并实时重载变更。
  • 它允许您向 Angular 应用添加更多代码
  • 它运行您的应用的测试。
  • 它为部署构建您的应用。
  • 它部署您的应用。

如前一章所述,要运行 CLI,首先需要安装 node . js 4 . 0 . 0 版或更高版本。如果您还没有这样做,请返回并阅读如何做到这一点。

要安装 CLI,请在终端中输入以下命令,这将启动各种节点下载:

npm install -g angular-cli

请注意,-g参数将 Angular CLI 作为全局包安装。这将把ng命令放在 path 中,使它可以在任何目录中使用。

您可以通过运行以下命令来检查您的 CLI 版本:

ng --version

要更新 CLI 版本,您应该卸载它,然后使用以下命令重新安装:

npm uninstall -g angular-cli
npm cache clean
npm install -g angular-cli

创建一个开始项目

最后,我们要做一些编码!不完全是。还没有。让我们创建基本项目并运行它。请遵循以下步骤:

  1. 打开终端窗口。

  2. 导航到合适的文件夹,例如“文档”。

  3. 输入以下命令,这将在一个名为 start 的文件夹中创建一个新的 Angular 应用,并会产生大量它创建的文件:

    ng new start
    
    
  4. 导航到开始文件夹。

    cd start
    
    
  5. 输入以下命令启动应用:

    ng serve
    
    
  6. Open your web browser and browse to localhost:4200. You should see the text “welcome to app!” as shown in Figure 7-1. That means your app is running.

    A458962_1_En_7_Fig1_HTML.jpg

    图 7-1

    The app is working

现在,您可以对项目中的文件进行更改,只要您运行ng serve,项目就会自动重新编译代码,并在 web 浏览器中刷新应用。这造就了一个高效的开发环境。

现在,让我们来看看这个项目以及其中的内容。启动 Visual Studio Code 并打开文件夹 start。表 7-1 显示了内部的内容及其结构。

表 7-1

What’s in the Root Folder?

| 文件或文件夹 | 事实真相 | | :-- | :-- | | e2e | 用于测试文件的文件夹(本书后面会有更多关于测试、Karma 和量角器的内容) | | 节点 _ 模块 | 项目节点相关性的文件夹 | | 科学研究委员会 | 项目源代码的文件夹 | | .editorConfig | 编辑器配置文件 | | 。吉蒂尔 | Git 忽略文件 | | angular angular-CLI . JSON | CLI 配置文件。您可以在此文件中更改 CLI 选项 | | 因果报应日 json | Karma 配置文件(本书后面会有更多关于测试、Karma 和量角器的内容) | | package.json | 节点相关性配置文件 | | 量角器-conf.js | 量角器配置文件(本书后面会有更多关于测试、Karma 和量角器的内容) | | README.md | 自述信息文件,包含有关 CLI 命令的信息 | | 林顿. json | Lint 配置文件 |

表 7-2 显示了源代码。这是真正重要的东西 CLI 为您的项目生成的源代码。这是您编码的起点。

表 7-2

CLI-Generated project code

| 文件或文件夹 | 事实真相 | | :-- | :-- | | 应用 | 应用源代码文件的文件夹,当前包含应用组件的源代码(稍后将详细介绍) | | 资产 | 应用图像和 CSS 文件的文件夹 | | 环境 | 环境配置文件的文件夹,例如,开发和生产配置 | | favicon.ico | 应用图标 | | index.html | Angular 单页应用的 HTML 页面 | | 主页面 | 启动应用的代码(稍后将详细介绍) | | 样式. css | 全局样式定义 | | 测试. ts | 运行应用测试的代码 | | tsconfig.json 文件 | TypeScript/编译器配置文件 |

修改开始项目

让我们修改开始项目,看看会发生什么。请遵循以下步骤:

  1. 打开终端窗口。

  2. 导航到 start 文件夹,确保ng start命令正在运行,并且导航到 localhost:8080 会产生“欢迎使用 app!”如预期的网页。保持ng start命令运行。

  3. 编辑文件 src/app/app.component.ts,将其更改为以下内容:

    import { Component } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      templateUrl: './app.component.html',
      styleUrls: ['./app.component.css']
    })
    export class AppComponent {
      title = 'app works! and has been modified....';
    }
    
    
  4. Go back to your web browser . It should now display what you see in Figure 7-2.

    A458962_1_En_7_Fig2_HTML.jpg

    图 7-2

    The app has been modified

请注意,当您在编辑器中单击“保存”时,应用是如何自动重新编译和重新加载的。这是因为 CLI 项目包括 Watchman,它可以监视更改的文件,并在您更改应用时对其进行重建和重新加载。我很快会说更多关于 Watchman 的内容。

启动项目:编译错误

让我们在项目中引入一个编译错误,看看会发生什么。

编辑文件 src/app/app.component.ts,并将其更改为以下内容(记住省略“app works”中的引号):

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

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = app works;
}

请注意,应用不会改变或重新加载,并且您会在终端窗口中得到错误信息。浏览器控制台中也会出现错误消息。在 Chrome 中,您可以通过选择更多工具,然后在菜单中选择开发者工具来查看浏览器控制台。

启动项目:运行时错误

让我们在项目中引入一个运行时错误,看看会发生什么:

  1. 编辑文件 src/app/app.component.ts,改回原代码:

    import { Component } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      templateUrl: './app.component.html',
      styleUrls: ['./app.component.css']
    })
    export class AppComponent {
      title = 'app works!';
    }
    
    
  2. 编辑文件 src/app/app.component.html 并将其更改为以下内容(以创建一个错误):

    <h1>
      {{title.test.test}}
    </h1>
    
    

应用变成空白。如果你检查终端,它说“webpack:编译成功。”所以,编译成功了。然而,页面没有加载,因为我们(故意)引入了一个运行时错误(只有在应用运行时才会出现)。要查找错误,请转到浏览器控制台(参见图 7-3 )。在谷歌浏览器中,你可以通过打开“汉堡”菜单,选择更多工具,然后在菜单中选择开发者工具来查看浏览器控制台。

A458962_1_En_7_Fig3_HTML.jpg

图 7-3

The app has been modified

文件监视器和网络服务器

如前所述,如果让ng serve运行,它会监视我们的文件(必要时执行编译和重新部署),并在 localhost:4200 上运行一个本地 web 服务器。当您更改某项内容并单击“保存”时,观察器会执行以下操作:

  • 创建一个 Webpack 构建,包括编译兼容的 JavaScript 和捆绑代码(本书后面会详细介绍 Webpack)
  • 生成一个新的 index.html 文件,根据需要添加脚本引用来引用 Webpack 捆绑的 JavaScript 文件
  • 在本地 web 服务器上执行新的部署
  • 刷新网页

拔靴带

自举通常是指一个自启动的过程,它应该在没有外部输入的情况下进行。在这种情况下,它指的是 Angular 应用如何启动。这一节看一下 starter 项目如何启动。

当我们转到 localhost:4200 时,会发生以下情况:

  1. 默认情况下,web 浏览器会打开文件 index.html。

  2. 浏览器在最后加载脚本文件。这包括 main.bundle.js,它是 typescript 文件 main.ts 的 transpiled 版本。这是我们的主要应用入口点。

  3. Main.bundle.js 加载一些模块然后调用下面的 Angular 系统代码:

    platformBrowserSpecific().bootstrapModule(AppModule)
    
    
  4. AppModule 已加载——它是用于引导应用的根 Angular 模块。这是一个 Angular 模块,不是 JavaScript 模块——它们是不同的东西(我将在本书的后面介绍 Angular 模块)。如果您查看 AppModule.ts,您会看到它包含以下代码行,告诉模块使用 AppComponent 进行引导:

    @NgModule({
    
        ...
    
      bootstrap: [AppComponent]
    })
    
    
  5. AppModule 用 AppComponent 引导,将组件注入开始和结束标记app-root :

    <app-root>Loading...</app-root>
    
    

    之间的空间

有用的 CLI 选项

我们经常在本书中使用 CLI,以下是我们在示例中使用的一些 CLI 选项:

--flat

生成具有平面文件结构的 cli 项目,而不是在自己的目录中生成每个组件。

--inline-template

使用内联模板生成组件(稍后将详细介绍)。组件模板标记将在组件中生成,而不是在单独的文件中生成。

--inline-style

生成具有内联样式的组件(稍后将详细介绍)。组件样式将在组件中生成,而不是在单独的文件中生成。

--spec false

生成不带单元测试“规范”文件的组件,默认情况下通常会为您生成这些文件。

自从我编写了示例之后,又添加了一个非常有用的新 CLI 选项:

--minimal

生成一个包含内联模板、样式且不包含测试的最小 cli 项目。

提前编译

正如在第二章中提到的,Angular 现在更倾向于 aot 模型,在这种模型中,你的代码是提前编译的,而不是在需要的时候。5 中的这些编译器更新推进了向 aot 的转移,这将使您的应用运行得更快,因为它在运行应用时将执行更少的编译。

如果您正在处理一个 CLI 项目,并且希望提前执行 aot 编译,那么您可以在 CLI 命令中添加'—aot '选项。例如,您可以使用以下命令运行带有 aot 编译的应用:

ng serve -aot

这对于提前发现模板中的错误非常有用。如果你的组件行为异常,而你不知道为什么,试着用 aot 编译来编译或运行你的应用!这在很多场合都帮了我大忙!在运行 ng serve 时使用'—aot '选项可以提前发现我们在 3 页前引入模板的运行时错误:

<h1>
  {{title.test.test}}
</h1>

摘要

本章向您介绍了 Angular CLI。除了创建一个启动项目之外,您还可以做更多的事情:

  • 将不同类型的对象添加到项目中
  • 测试您的代码
  • 构建您的代码
  • 部署您的代码
  • 更加

我们将在所有的编码示例中使用 CLI,所以不要担心:我将会更多地介绍它,并且我们将用它做更多的事情。

下一章非常重要:它介绍了组件,有 Angular 的用户界面的构建块。

八、组件介绍

Angular 组件类似于 AngularJS 控制器。组件基本上是标记、元数据和一个类(包含数据和代码),它们组合在一起创建一个 UI 小部件。组件是我们用来构建交互式 UI 的主要工具。所有 Angular 应用都有一个根组件,通常称为应用组件。

Angular 为组件之间传递数据和响应彼此的事件提供了方法。我们将在第十二章讨论组件输入和输出。

您可以编写一个组件,并在其他几个组件中将它作为子组件使用——出于这个目的,它们被设计为自包含和松散耦合的。每个组件都包含关于自身的有价值的数据:

  • 它需要什么数据作为输入
  • 它可能向外界发射什么事件
  • 怎么画自己
  • 它的依赖关系是什么

通常当你开发组件时,三个文件中的每一个都有一个组件,因为一个组件有三个部分:模板、类和样式。默认情况下,CLI 就是这样工作的。例如,当您使用命令ng new [project name]在 CLI 中创建一个应用时,CLI 会为该应用组件生成三个文件(如果您包括. spec.ts 测试文件,则会生成更多文件):

  • app.component.css:样式
  • app.component.html:模板
  • 应用组件:类

然而,这不是你唯一的选择。以下是更多选项:

  • 将样式包含在。ts 类文件:这被称为内联样式,它使你不必为组件准备一个样式文件。如前一章所述,使用 CLI --inline-style参数生成具有内联样式的组件。
  • 将模板包含在。ts 类文件:这被称为内联模板,它使你不必为组件准备模板文件。如前一章所述,使用 CLI --inline-template参数生成具有内联样式的组件。
  • 在同一个文件中包含多个组件类:您可以在同一个文件中组合多个组件,如下所示:
import { Component } from '@angular/core';

@Component({
  selector: 'Paragraph',
  template: `
  <p><ng-content></ng-content></p>
  `,
  styles: ['p { border: 1px solid #c0c0c0; padding: 10px }']
})
export class Paragraph {
}

@Component({
  selector: 'app-root',
  template: `
  <p>
  <Paragraph>Lorem ipsum dolor sit amet, consectetur adipiscing elit. </Paragraph>
  <Paragraph>Praesent eget ornare neque, vel consectetur eros. </Paragraph>
  </p>
  `,
  styles: ['p { border: 1px solid black }']
})
export class AppComponent {
  title = 'welcome to app!';
}

您可能会在同一个文件中找到包含多个组件的代码示例。这样做是有意的,以便您可以将更多的代码复制并粘贴到更少的文件中。

当您在应用中使用组件时,您需要确保每个组件都在模块中声明。第九章更详细地介绍了模块。以下是声明两个组件的模块示例:AppComponent 和 Paragraph:

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

@NgModule({
  declarations: [
    AppComponent,
    Paragraph
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

组件的剖析

注释提供了将所有部分组合成一个组件的元数据。模板通常是 HTML 标记,用于在浏览器中呈现组件,即模型-视图-控制器(MVC)中的视图。它可以包含嵌套组件的标记。一个类有添加元数据的注释并包含数据(was$scope)——MVC 中的模型。它包含行为代码 MVC 中的控制器。

@组件注释

注释位于类的顶部附近,是类中最重要的元素。这是一个将类标记为组件并接受对象的函数。它使用对象向 Angular 提供关于组件以及如何运行它的元数据。注释也称为装饰器。

如果您使用 CLI 生成一个项目,并检查生成的组件 app.component.ts,您将看到下面的@Component注释:

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})

表 8-1 显示了可以添加到@Component注释中的基本元素。

表 8-1

Basic Elements for the @Component Annotation

| 注释元素 | 笔记 | | :-- | :-- | | `selector` | 该组件对应的标记、元素。 | | `template/templateUrl` | 指定包含组件标记的模板。您有两种选择:您可以使用`template`在引号块中指定内联模板。这对简单的模板非常有用。或者您可以使用`templateUrl`来指定外部模板文件的相对路径。这对于更大或更复杂的模板更好。如果模板长度超过 10 行,我通常会将其放在外部模板文件中。 | | `styles/styleUrls` | 指定模板标记的 CSS 信息。您有两种选择:您可以使用`styles`来指定一个内联样式数组。这对于一些样式定义来说非常有用。或者您可以使用`styleUrls`来指定样式定义文件的相对路径数组。当你使用多种风格时,这样更好。如果组件中使用的样式超过 5 种,我通常会将它们放在一个外部样式文件中。 |

selector语法类似于 JQuery 选择器,如表 8-2 所示。

表 8-2

selector Syntax

| 类型 | 例子 | 所选标记的示例 | 笔记 | | :-- | :-- | :-- | :-- | | 名字 | `welcome` | `` | 这是使用选择器最常见的方式。只要确保这个标签是唯一的,并且永远不会被 HTML 使用。为您的项目和其中的所有组件使用一个公共前缀通常是一个好主意。例如,奖励计划项目可以有前缀`rp_`。 | | 身份 | `#welcome` | `
` |   | | CSS 类 | `.welcome` | `'
` |   |

选择器和 DSL

在 Angular 中,您可以创建映射到特定标签(或属性)的 can 组件和指令。例如,如果你正在创建一个销售汽车的应用,你可以像这样使用标签和属性:<CarSearch></CarSearch><CarList></CarList><CarDetail></CarDetail>等等。实际上,通过 Angular 组件和指令,我们正在为我们的应用创建 DSL(特定领域语言)。DSL 是一种专用于特定应用领域的计算机语言。DSL 非常强大,因为它们允许代码特定于应用的领域(它的使用),并以语言形式表示所代表的业务实体。

其他元素

表 8-3 显示了你可以添加到@Component注释中的其他更高级的元素。我们将在后面详细讨论其中的许多内容。

表 8-3

Advanced Elements

| 注释元素 | 笔记 | | :-- | :-- | | `animations` | 该组件的动画列表 | | `changeDetection` | 此组件使用的更改检测策略 | | `encapsulation` | 此组件使用的样式封装策略 | | `entryComponents` | 动态插入到该组件视图中的组件列表 | | `exportAs` | 在模板中导出组件实例时使用的名称 | | `hosts` | 将类属性映射到事件、属性和特性的宿主元素绑定 | | `Inputs` | 要作为组件输入进行数据绑定的类属性名称列表 | | `interpolation` | 此组件模板中使用的自定义插值标记 | | `moduleId` | 定义该组件的文件的 ES/CommonJS 模块 ID | | `outputs` | 公开其他人可以订阅的输出事件的类属性名称列表 | | `providers` | 此组件及其子组件可用的提供程序列表 | | `queries` | 配置可以注入组件的查询 | | `viewProviders` | 此组件及其视图子级可用的提供程序列表 |

组件模板

该模板包含在 web 浏览器中显示组件的标记代码。关于组件模板的信息由注释提供。

模板位置

模板标记可以包含在与Component类相同的文件中,也可以包含在单独的文件中:

以下是@ Component注释中内嵌的模板标记:

@Component({
  selector: 'app-root',
  template: `
  <div class="app">
  [app]
  <app-customer-list>
  </app-customer-list>
  </div>
  `,
  styles: ['.app {background-color:#d5f4e6;margin:10px;padding:10px;}']
})

下面是包含在一个单独文件中的模板标记:

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})

脚本标签

组件模板中不允许使用<script>标记。为了消除脚本注入攻击的风险,这是被禁止的。实际上,<script>被忽略,浏览器控制台中会出现一条警告。

换句话说,永远不要这样做:

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

@Component({
  selector: 'app-root',
  template: `
  <h1>
    {{title}}
  </h1>
  <script>
    alert('app works');
  </script>
  `,
  styles: []
})
export class AppComponent {
  title = 'welcome to app!';
}

Elvis 操作员

该操作员也被称为“安全导航操作员”。

Angular 经常出现空值问题,尤其是模板表达式。通常你会有一个模板突然停止工作,因为你添加的代码引用了一个未初始化的变量。例如,假设我们有一个为空的对象x,我们有以下代码:

Total {{x.totalAmt}}

这将导致 JavaScript 和 Zone 问题(稍后会有更多关于 Zone 的内容),您的组件将突然无法呈现。我希望我每次遇到这种事都能得到一块钱。

幸运的是,“猫王”接线员帮助了我们。简单来说,Elvis 操作符就是模板表达式中可能为空的变量旁边的一个问号。一旦发现该变量为空,Elvis 操作符就告诉代码退出,留下一个空白。这将停止对属性的评估,并绕过 JavaScript 问题:

Total {{x?.totalAmt}}

有时在模板表达式中需要多个 Elvis 运算符:

Total {{x?.amt?.total}}

组件样式

样式包含更改组件样式所需的 CSS 规则。关于组件模板的信息由style注释提供。您可以在元件或外部文件中指定元件的样式。创建 Angular CLI 项目时,其样式文件在。angular-cli.json 文件。

样式可以包含在与Component类相同的文件中,也可以包含在单独的文件中。

以下是@ Component注释中内联包含的样式标记:

@Component({
  selector: 'app-root',
  template: `
  <div class="app">
  [app]
  <app-customer-list>
  </app-customer-list>
  </div>
  `,
  styles: ['.app {background-color:#d5f4e6;margin:10px;padding:10px;}']
})

下面是包含在一个单独文件中的样式标记:

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})

组件类

此 TypeScript 类包含组件的数据和代码。数据包含在变量中,变量可以绑定到模板中的 HTML 标记。代码可以响应用户事件(比如单击按钮)或者调用自身来开始做事情。

别担心,这只是一个介绍——我们将在第十二章更详细地讨论组件类。

MVC: Model View Controller

MVC 是一种编写程序的方式,主要用于在计算机上实现用户界面。它将给定的软件应用分成三个相互联系的部分:模型(数据)、视图(用户看到的)和控制器(更新模型的命令)。在 Angular 的上下文中,可以说模型是Component类中的数据,视图是组件模板,控制器可以是Component类中的代码。

数据绑定简介

数据绑定是 Angular 如此受欢迎的原因——组件 UI 小部件的元素与组件类中的数据的同步,反之亦然,由 Angular 为您管理。您在组件类中设置变量来存储数据,并在组件模板中编辑 HTML 来添加数据绑定。现在,您的 HTML 不再是静态的—它会随着您的数据而变化!如果您希望您的组件与用户交互,您必须在其中使用数据绑定。

就 MVC 而言,在运行时 Angular 使用变化检测来确保组件视图总是反映组件模型。使用数据绑定,您可以通过更改变量来控制组件的用户界面,并且可以接受用户输入,允许他们更改某些变量的值。数据绑定可以控制用户界面的每个方面:隐藏东西、关闭东西、显示结果、接受用户输入等等。它非常强大而且易于使用。

示例:登录组件中的数据绑定

假设你有一个带有两个字段的登录表单,如图 8-1 所示。每个字段在组件模板的 HTML 中都有一个文本框,每个字段在Component类中都有一个对应的实例变量。文本框和实例变量相互绑定。如果有人输入用户名,username实例变量会用新值更新。当开发人员编写提交按钮时,他们从实例变量中获得用户名和密码,而不是从 HTML 中提取它们。

A458962_1_En_8_Fig1_HTML.jpg

图 8-1

Login form with two fields

示例:数据绑定和客户数据输入

您可以拥有一个表单,该表单允许您使用字段输入客户信息。每个字段在组件模板的 HTML 中都有一个文本框,每个字段在Component类中都有一个对应的实例变量。Angular 数据绑定使您能够在用户向 HTML 字段输入信息时自动更新实例变量。它还使您能够在更改实例变量的值时自动更新 HTML 字段,以及在用户向文本框中键入内容时自动更新实例变量。当开发人员想要默认输入字段的值时,他们需要做的就是设置实例变量值。HTML 文本框将自动更新。

有两种主要类型的数据绑定—单向和双向:

A458962_1_En_8_Fig2_HTML.jpg

图 8-2

Two-way data binding

  1. 单向数据绑定:当模板(视图)自动与类实例变量(模型)中的最新值保持同步时,就会出现这种情况。更新只向一个方向流动。当类实例变量(模型)自动与从模板(视图)输入的值保持同步时,也会发生单向数据绑定。更新仍然是单向的。
  2. 双向数据绑定:这是类实例变量(模型)和模板(视图)互相保持最新的时候。更新流向两个方向,如图 8-2 所示。

单向数据绑定

本节重点介绍 Angular 中单向数据绑定的各个方面。

与{{和}}的单向数据绑定

那些双花括号也被称为小胡子或插值。双花括号用于单向绑定模板表达式,根据模型中的可用数据进行计算,并将其包含在视图中。表达式产生一个值,它包含在视图中(来自组件模板的标记)。模型——即Component类中的数据——从不更新。

模板表达式通常是一个简单的 JavaScript 表达式。通常模板表达式只是模型中一个属性的名称(即Component类中的一个实例变量)。Angular 用属性的字符串值(实例变量的字符串值)替换该属性名称。

有时模板表达式会变得更复杂。Angular 试图对表达式(可以包含数学、属性名、方法调用等等)求值,并将求值结果转换为字符串。然后用结果替换内容和花括号。

以下是花括号和模板表达式的一些示例:

  • {{2+2}}
  • {{firstName}}
  • {{1 + 1 + getVal()}}

单向数据绑定:示例代码组件-ex100

下面的讨论将是关于示例组件-ex100:

  1. 使用 CLI 构建应用:输入以下命令,这将在一个名为 Start 的文件夹中创建一个新的 Angular 应用,还将创建并输出大量文件:

    ng new components-ex100 --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd components-ex100
    ng serve
    
    
  3. Open app: Launch your web browser and browse to localhost:4200. You should see the text “welcome to app!” as shown in Figure 8-3. That means your project is running.

    A458962_1_En_8_Fig3_HTML.jpg

    图 8-3

    Your project is running

  4. 编辑组件:编辑文件 src/app/app.component.ts,并将其更改为:

    import { Component } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      template: `
      <h1>
        {{title}}
      </h1>
      <p>
        Length: {{title.length}}
      </p>
      <p>
        Reversed: {{getReversed(title)}}
      </p>
      `,
      styles: []
    })
    export class AppComponent {
      title = 'welcome to app!';
    
      getReversed(str: string){
        let reversed = '';
        for (let i=str.length-1;i>=0;i--){
          reversed += str.substring(i,i+1);
        }
        return reversed;
      }
    }
    
    

你的应用应该工作在本地主机:4200。请注意模板如何使用两个表达式:一个显示标题的长度,另一个使用类中的方法反转标题。

A458962_1_En_8_Figa_HTML.jpg

使用[ and ]或*的单向数据绑定

方括号可用于单向绑定。通过这些,您可以绑定一个模板表达式,根据模型中的可用数据进行计算,并将其包含在数据绑定目标中。

您也可以使用前缀*来代替双方括号:

[Data Binding Target] = "Template Expression"

或者:

*Data Binding Target = "Template Expression"

数据绑定目标是 DOM 中的一些东西(包括元素属性、组件属性和指令属性),可以绑定到目标右侧的表达式的结果,如表 8-4 所示。

表 8-4

Data Binding Target Markup

| 利润 | 描述 | | :-- | :-- | | `` | 将图像源设置为模型中的属性`imageUrl`。 | | `
` | 根据模型中的属性`isSelected`设置 CSS 类。 | | `` | 将模型中`car-detail`的`car`属性设置为属性`selectedCar`。`car-detail`可以是一个组件,它会使用`car`属性将信息从当前模板传递给那个组件。 | | `

下面的讨论将是关于示例组件-ex200:

  1. 使用 CLI 构建应用:输入以下命令:

    ng new components-ex200 --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd components-ex200
    ng serve
    
    
  3. 打开应用:启动 web 浏览器,浏览到 localhost:4200。您应该看到文本“欢迎使用应用!”

  4. 编辑组件:编辑文件 src/app/app.component.ts,并将其更改为:

    import { Component } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      template: `
      <h1>Doesnt work:</h1>
      <img src="starUrl">
      <h1>Works:</h1>
      <img [src]="starUrl">
      `,
      styles: []
    })
    export class AppComponent {
      starUrl = 'https://developer.mozilla.org/samples/cssrimg/starsolid.gif';
    }
    
    

你的应用应该工作在本地主机:4200。注意以下事项(并参见图 8-4 ):

A458962_1_En_8_Fig4_HTML.jpg

图 8-4

Author please add caption

  • 第一个图像标签失败,因为它没有换行src。它从字面上理解startUrl,而不是从一个实例变量中将它作为一个表达式来计算。
  • 第二个 image 标签可以工作,因为它将src放在方括号中,这意味着这是一个需要计算startUrl实例变量的值的表达式。

有时,您需要在模板生成的 HTML 元素中动态创建属性。如果您希望在 HTML 中包含数据,稍后可以使用 JavaScript 代码提取这些数据,这将非常有用。例如:

<li
  id="12345"
  data-make="bmw"
  data-model="m3"
  data-parent="cars">
...
</li>

在这种情况下,id标签用于标识元素(对 JavaScript 和 CSS 非常有用),还有各种存储信息的数据元素。

在 Angular 中,你可以使用[attr.***name***]语法在生成的 HTML 中设置属性。

单向数据绑定:示例代码组件-ex250

这个组件会列出一些汽车,让你点击查看按钮查看一篇关于汽车的文章,如图 8-5 所示。

A458962_1_En_8_Fig5_HTML.jpg

图 8-5

Component that lists cars

该组件有趣的地方在于它在每个<li>元素中存储属性数据。这将是关于示例组件-ex250:

  1. 使用 CLI 构建应用:输入以下命令:

    ng new components-ex250 --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd components-ex250
    ng serve
    
    
  3. 打开应用:打开 web 浏览器,浏览到 localhost:4200。您应该看到文本“欢迎使用应用!”这意味着您的项目正在运行。

  4. 编辑组件:编辑文件 src/app/app.component.ts,并将其更改为:

    import { Component } from '@angular/core';
    import { Car } from './car';
    @Component({
      selector: 'app-root',
      template: `
        <ul>
          <li *ngFor="let car of _cars">
            <span [attr.id]="car.id" [attr.data-desc]="car.make + ' ' + car.model" [attr.data-article]="car.article">
              {{car.year}}&nbsp;{{car.make}}&nbsp;{{car.model}}&nbsp;<button (click)="showCar($event)">View</button></span>
          </li>
        </ul>
      `,
      styles: []
    })
    export class AppComponent {
      _cars = [
        new Car('car1', 2002, 'bmw', 'm3', 'https://en.wikipedia.org/wiki/BMW_M3'),
        new Car('car2', 2017, 'acura', 'nsx', 'https://en.wikipedia.org/wiki/Honda_NSX'),
        new Car('car3', 2016, 'chevy', 'camaro', 'https://en.wikipedia.org/wiki/Chevrolet_Camaro')
      ];
    
      showCar(event){
        const desc = event.target.parentElement.dataset.desc;
        if (window.confirm('If you click "ok" you would be redirected to an article about the ' +
            desc + '. Cancel will load this website '))
          {
          window.location.href=event.target.parentElement.dataset.article;
          };
      }
    }
    
    
  5. 创建类:创建文件 src/app/car.ts,并将其更改为:

    export class Car {
        constructor(
            private _id: string,
            private _year: number,
            private _make: string,
            private _model: string,
            private _article: string){
        }
    
        public get id() : string {
            return this._id;
        }
    
        public get year() : number {
            return this._year;
        }
    
        public get make() : string {
            return this._make;
        }
    
        public get model() : string {
            return this._model;
        }
    
        public get article() : string {
            return this._article;
        }
    
    }
    
    

您已经到达练习的末尾。请注意以下几点:

  • desc数据属性是如何产生的:

    [attr.data-desc]="car.make + ' ' + car.model"
    
    
  • 如何使用 JavaScript 获取desc数据属性:

    const desc = event.target.parentElement.dataset.desc;
    
    

双向数据绑定

本节重点介绍双向数据绑定。

使用[(和)]的双向数据绑定

[()]又称盒中香蕉。实际上,你已经看过了。[()]格式用于双向绑定属性——换句话说,从模型中读取和写入属性。格式如下:

[(Data Binding Target)] = "Property"

“数据绑定目标”是 DOM(包括ComponentDirective标签)中可以绑定到目标右侧表达式的属性的东西。对于输入框,数据绑定目标是ngModel,它对应于输入框中的文本。

这是模型中的一个属性(Component类中的一个实例变量)。

双向数据绑定:示例代码组件-ex300

这是一个在用户改变输入时改变前景色和背景色的组件,如图 8-6 所示:

  1. 使用 CLI 构建应用:输入以下命令:

    ng new components-ex300 --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd components-ex300
    ng serve
    
    
  3. 打开应用:打开 web 浏览器,浏览到 localhost:4200。您应该看到文本“欢迎使用应用!”这意味着您的项目正在运行。

  4. 编辑模块:编辑文件 src/app/app.module.ts,并将其更改为:

    import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';
    
    import { AppComponent } from './app.component';
    import { FormsModule } from '@angular/forms';
    
    @NgModule({
      declarations: [
        AppComponent
      ],
      imports: [
        BrowserModule,
        FormsModule
      ],
      providers: [],
      bootstrap: [AppComponent]
    })
    export class AppModule { }
    
    
  5. Edit component: Edit the file src/app/app.component.ts and change it to the following:

    A458962_1_En_8_Fig6_HTML.jpg

    图 8-6

    Changing foreground and background colors

    import { Component } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      template: `
      <p>
      Foreground: <input [(ngModel)]="fg" />
      </p>
      <p>
      Background: <input [(ngModel)]="bg" />
      </p>
      <div [ngStyle]="{'color': fg, 'background-color': bg, 'padding': '5px'}">
      Test
      </div>
      `,
      styles: []
    })
    export class AppComponent {
      fg = "#ffffff";
      bg = "#000000";
    }
    
    

你的应用应该工作在本地主机:4200。当用户更改颜色值时,这会更新模型,然后更新模板的 HTML:

  • 从输入字段到模型发生绑定(当用户改变颜色值时)。当输入字段改变时,模型更新以匹配。
  • 从模型到模板的 HTML 发生绑定。当模型更新时,模板的 HTML 也随之更新。

事件处理

用户界面需要响应用户输入。这就是为什么我们在组件模板中有事件处理。我们指定一个目标事件,以及当该事件发生时应该发生哪个语句。格式是这样的:

(Target Event) = "Template Statement"

“目标事件”是括号中事件的名称。“模板语句”是关于目标事件发生时该做什么的指令。通常,这是对Component类中的一个方法的调用,该方法执行某些操作——通常,修改绑定到模板的实例变量,导致 UI 发生变化。事件信息在$event变量中可用,它可能被利用也可能不被利用。例如,如果您正在观察文本框中的输入,您可以使用来自$event的信息将文本框中的文本值传递给该方法。您将在下一个示例中看到这一点。

事件处理:示例代码组件-ex400

该组件接受文本框中的输入,捕获输入事件,并以大写和小写显示输入,如图 8-7 所示:

  1. 使用 CLI 构建应用:输入以下命令:

    ng new components-ex400 --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd components-ex400
    ng serve
    
    
  3. 打开应用:启动 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”

  4. Edit class: Edit app.component.ts and change it to the following:

    A458962_1_En_8_Fig7_HTML.jpg

    图 8-7

    Displaying the input

    import { Component, AfterViewInit, ViewChild } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      template: `
      <input #input type="text" (input)="textInput($event)" value=""/>
      <hr>
      Upper-Case: {{upperCase}}
      <br/>
      Lower-Case: {{lowerCase}}
    
      `,
      styles: []
    })
    export class AppComponent implements AfterViewInit{
      upperCase: string= '';
      lowerCase: string = '';
      @ViewChild('input') inputBox;
    
      textInput(event){
        this.upperCase = event.target.value.toUpperCase();
        this.lowerCase = event.target.value.toLowerCase();
      }
    
      ngAfterViewInit() {
        this.inputBox.nativeElement.focus()
      }
    }
    
    

你的应用应该工作在本地主机:4200。请注意以下几点:

  • 模板变量#inputviewChild用于获取对输入框的引用。在视图初始化之后(生命周期方法ngAfterViewInit被触发),焦点被设置到输入框。

  • 该模板使用下面的代码来监听input事件,当事件发生时触发方法textInput(传入事件对象):

    (input)="textInput($event)"
    
    
  • 该类有一个由input事件触发的方法textInput。它计算用户输入的大写和小写版本,并将其设置为从类绑定(单向)到模板的实例变量。

CDK

Angular CDK(组件开发套件)于 2017 年与 Angular 5 一起发布。其目的是使开发人员能够创建高质量的 Angular 定制组件。CDK 包含服务、指令、组件、类和模块。CDK 包含组件可访问性、文本方向性、平台检测和动态组件实例化的代码。如果你真的想开始构建自己的定制可重用组件库,那么你需要安装“@angular/cdk”节点模块并开始。

摘要

本章涵盖了一些重要的概念,所以我强烈建议在继续学习之前先学习这些例子。阅读完本章后,你应该对组件有一些基本的了解,包括它们是由什么组成的。您还知道组件由注释、模板和类组成。我们还讨论了事件处理。

既然我们可以编写用户界面组件,我们将在下一章把注意力转向模块化我们的 Angular 代码。如果您还没有完全理解组件,请不要担心——后面的章节将会详细介绍。

九、模块简介

模块这个词指的是独立的、可重用的代码的小单元。典型的模块是专用于单一目的的内聚代码块。模块在代码中导出一些有价值的东西,通常是一样东西,比如一个对象。

本章主要介绍不同模块的概念。它没有包括很多编码示例——您将在后面编写模块。

JavaScript 给了你做很多糟糕事情的自由——你没有义务写可重用的代码。你可以把你的代码散布在任何地方。随着 JavaScript 及其环境的成熟,这种情况必须改变。您需要通过隐藏对象的内部工作方式并让公共接口从外部可用来简化对象。您需要能够将代码打包成可重用的块,这些块可以彼此独立地打包和部署。你还需要能够按需加载它们,而不是在应用启动时(缓慢地)加载所有内容。

不同类型的模块

本章介绍 AngularJS、Angular 和 JavaScript 模块化代码的三种方式:

A458962_1_En_9_Fig1_HTML.gif

图 9-1

Angular applications are made up of Angular modules and JavaScript modules

  • Angular 的原始版本中包含的 AngularJS 模块系统:这使您能够在粗粒度级别模块化代码。
  • ES6 和 TypeScript 中现在可用的 JavaScript 模块:这些模块使您能够在细粒度级别模块化代码。记住,每个源代码文件都有一个模块。ts 或者。js)。
  • Angular 模块系统:这使您能够在粗粒度级别模块化代码。您可以将 Angular 代码单元捆绑到模块中。例如,如果你正在用 Angular 编写一个系统,其中包含一个销售应用、一个人力资源应用和一个税务应用,你可以将这三个应用分割成独立的功能模块和一个共享公共代码的共享模块,如图 9-1 所示。

AngularJS 模块系统

AngularJS 有自己的模块系统,非常简单。您有一个 Angular 模块,其中可能包含 Angular 控制器、指令等。

在图 9-2 中,我们声明了模块 xxx,它依赖于许多其他模块:ngCookies、ngRoute、ngResource、ngSanitize、angularSpinner、ui.bootstrap.demo、ui.bootstrap、ui.select、wj 和 angularModalService。在代码之后,我们将声明这个模块 xxx 中的项目。

A458962_1_En_9_Fig2_HTML.jpg

图 9-2

Declaring module xxx

JavaScript 模块

JavaScript 过去常常使用库,这些库对于开发人员在某个领域的开发非常有用。例如,JQuery 曾经帮助开发人员进行 UI 开发。这些库写得很好,但没有作为模块实现。相反,它们是作为 JavaScript 脚本实现的(如。js 脚本文件),它将创建 JavaScript 对象来做事情。这是在 JavaScript 模块系统存在之前。

现在 ES6 和更高版本支持模块。在 Javascript 模块中,每个文件都是一个模块。你可以自己编模块,也可以用别人的模块。您可以使用 Node 将依赖模块拉入您的项目中(进入 node_modules 文件夹)。

当我们在 TypeScript 中用 Angular 5 编码时,我们使用两个 JavaScript 模块关键字:

  • Export:导出模块代码
  • Import:导入模块代码

导出代码

您将应用编写为小模块的集合。您的代码使用export关键字将对象从模块导出到外部世界。例如,下面的代码用于告诉 TypeScript 您正在导出类App以便在其他地方使用:

export class App {...}

下面是如何从模块中导出默认对象:

module "foo" {
  export default function() { console.log("hello!") }
}

导入代码

语句告诉 TypeScript 从某处获取模块代码。某处可以来自其他人的模块,也可以来自同一项目中的本地代码。

从其他人的模块中导入代码

当您使用import语句从其他人的模块中获取代码时,您需要标识模块名和您想要导入的项的名称,在from之后指定模块名。这通常是从节点模块导入代码的方式。例如,从 Angular 导入元件:

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

以下是如何从 ngx-bootstrap 导入日期选择器:

import { DatepickerModule } from 'ngx-bootstrap/datepicker';

导入您的项目代码

当从同一项目中的本地代码导入代码时,需要指定该代码的相对路径。下面的例子指定了一个相对路径(./)。这告诉 TypeScript 该代码与将要使用该模块的代码在同一个文件夹中:

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

以下是一些更重要的语法:

  • 全部导入:

    import * as myModule from 'my-module';
    
    
  • 使用命名导入时,名称需要与模块中导出的对象名称完全匹配:

    import { myMember } from 'my-module';
    
    
  • 对于来自一个模块的多个命名导入,名称需要与模块中导出的对象名称完全匹配:

    import { foo, bar } from 'my-module';
    
    
  • 对于从模块的默认导入,名称不需要与模块中导出的任何对象匹配。它可以是一个别名。它知道它必须从模块

    import myDefault from 'my-module';
    
    

    导入默认对象

Angular 模块系统

Angular 模块系统是 Angular bundles 编码成可重用模块的方式。使用该模块系统,Angular 系统代码本身被模块化。许多第三方使用模块为 Angular 提供了额外的功能,您可以轻松地将这些功能包含到您的应用中。

Angular 为什么不直接用 JavaScript 模块?为什么强迫开发者使用自己的模块系统?首先,它确实使用了标准的 JavaScript 模块,但是还不够。它们使得 Angular 不容易声明由不同对象捆绑在一起组成的长块 Angular 代码——例如,组件、服务和管道。在 Angular 2 的早期,开发人员没有 Angular 模块的选项,开发人员使用模块加载器来加载和启动应用(System.js)。对我来说,它在实践中并不奏效。它很难学,很容易坏,而且太复杂了。

当 Angular2 处于测试阶段时,我参与了它的工作,并喜欢这个产品,但我讨厌它在模块加载方面的复杂性,因为我不得不学习 System.js 等。我后来回到它身边,发现您可以快速而简单地使用 Angular CLI 来构建一个使用 Webpack 进行部署的模块化应用。我欢迎 Angular 模块系统,并认为它与 CLI 和 Webpack 配合得很好。

开始项目中的模块

你已经用过角模块系统了,即使你没有意识到。如果您打开使用 CLI 创建的启动项目,您将看到已经有一个文件 app.module.ts。模块。让我们打开来看看:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';

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

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

让我们看看这里的一些元素:

  • @NgModule注解:这个注解是这个类最重要的部分。这是一个函数,它接受一个对象,并使用该对象向 Angular 提供关于模块的元数据:如何编译它以及如何运行它。所以,@NgModule是 Angular 的声明方式,让你告诉 Angular 如何把这些部分组合在一起。注意,@NgModule本身需要从顶部的@angular/core导入。
  • declarations:这应该是你的模块使用的 Angular 组件、指令和管道的数组,而不是其他任何东西——没有 ES6 类或其他任何东西。当您使用 CLI 命令ng generate component添加一个组件时,它会导入该组件并将其添加到这个声明列表中。如果您添加并使用一个组件,而没有在这里声明它,您将在浏览器控制台收到一条错误消息。
  • import:这应该是这里应用需要的 Angular 模块数组。这些模块必须使用@NgModule来定义。Angular 本身有许多有用的系统模块,默认情况下,CLI 为您提供了其中的几个模块,包括浏览器模块、表单模块和 http 模块。
  • providers:这应该是应用所需的 Angular 对象的数组provider。这些provider对象是服务类和值,它们通过依赖注入被注入到你的类中。如果您有一个公共服务对象供组件用来与服务器通信,那么您可以在这里将其添加为提供者。
  • 你可以使用模块来包含你的应用的代码。要运行,您的应用需要知道如何启动以及应该从哪个组件启动(根)。这是您指定根组件的地方,当应用启动时,它将被创建并装载到 HTML 中。这个根组件通常被称为 AppComponent。

根模块

您的 Angular 应用可以包含多个模块。但是它总是有一个起点,一个用来引导自己的模块。这是根模块,通常称为 AppModule。

路由模块

我们稍后将讨论路由,但路由对于 Angular 应用非常重要。它允许用户将组件映射到 URL 并导航用户界面。当我们使用 CLI 构建 Angular 应用时,它会为您的应用路由构建一个单独的模块,通常在 app-routing.ts 文件中。这看起来可能是多余的,但它非常巧妙地将 Angular 路由对象与您的应用的路由设置打包到一个模块中,该模块为您的应用处理所有路由。

功能模块

领域驱动设计(DDD)是一种软件开发方法,通过将实现与演进的模型相联系来解决复杂的需求。DDD 经常不得不处理非常大、复杂的业务需求建模,它的方法是将这些需求分解到不同的环境中。有界上下文是业务需求中可以逻辑分离的领域,如图 9-3 所示。

A458962_1_En_9_Fig3_HTML.jpg

图 9-3

Bounded contexts

如你所见,在图 9-3 中有两个上下文:销售和支持。每一个都可以是你 Angular 应用的一个独立部分。事实上,每一个都可以包含在它自己独立的模块中,称为特性模块。每个模块可以包含特定的代码,以满足其他地方不需要的特定需求。例如,销售模块可以包含一个 Angular UI 来管理销售渠道,这在其他任何地方都不会使用。因此,功能模块通常包含不打算在该模块之外使用的代码。

需要时,根模块可以包括所需数量的功能模块。例如,当用户点击销售菜单时,特征模块甚至可以按需加载。

共享模块

您可以将功能模块视为不共享的代码块。共享模块则相反——它们包含最常用的模块化代码,因此可以尽可能多地重用。当需要时,根模块可以包括所需数量的共享模块。

Angular 模块系统:示例模块-ex100

这个例子是一个关于如何一起使用根模块、特性模块和共享模块的非常基本的练习。

该示例的组件顶部有两个链接:销售和支持。您可以单击每个链接,在两个组件之间浏览应用。销售和支持这两个组件都是独立的功能模块。

这个例子有一个根模块,App,特性模块(已经提到过),以及一个来自共享模块的组件 shared,如图 9-4 所示。

A458962_1_En_9_Fig4_HTML.gif

图 9-4

Example of Angular module system

让我们看一下这个例子:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new modules-ex100
    
    
  2. 开始ng serve:使用以下代码:

    cd modules-ex100
    ng serve
    
    
  3. 打开应用:启动 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”

  4. 生成模块:让我们使用 CLI 来生成附加模块:

    ng generate module shared
    ng generate module routing --routing
    ng generate module sales
    ng generate module support
    
    
  5. 生成组件:让我们使用 CLI 来生成附加组件:

    ng generate component sales
    ng generate component support
    ng generate component shared
    
    
  6. 编辑组件样式:编辑文件 sales.component.css 并将其更改为以下内容:

    div {
        background-color: #bdcebe;
        border: 1px solid #000000;
        padding: 10px;
        margin: 10px;
    }
    
    

    编辑文件 support.component.css 并将其更改为以下内容:

    div {
        background-color: #eca1a6;
        border: 1px solid #000000;
        padding: 10px;
        margin: 10px;
    }
    
    

    编辑文件 shared.component.css 并将其更改为以下内容:

    div {
        background-color: #d6cbd3;
        border: 1px solid #000000;
        padding: 10px;
        margin: 10px;
    }
    
    

    编辑文件 app.component.css 并将其更改为以下内容:

    div {
        background-color: #e3eaa7;
        border: 10px;
        padding: 10px;
    }
    
    
  7. 编辑组件模板:编辑文件 sales.component.html,更改为:

    <div>
      sales module!
      <app-shared></app-shared>
    </div>
    
    

    编辑文件 support.component.html,更改为:

    <div>
      support module!
      <app-shared></app-shared>
    </div>
    
    

    编辑文件 shared.component.html,更改为:

    <div>
      shared module!
    </div>
    
    

    编辑文件 app.component.html,更改为:

    <div style="text-align:center">
      <h1>
        Welcome!!
      </h1>
      <a [routerLink]="['sales']">Sales</a>
      <a [routerLink]="['support']">Support</a>
       <router-outlet></router-outlet>
    </div>
    
    
  8. 编辑路由模块:编辑 routing.module.ts 文件,并将其更改为:

    import { NgModule } from '@angular/core';
    import { CommonModule } from '@angular/common';
    import { Routes, RouterModule } from '@angular/router';
    import { SalesComponent } from '../sales/sales.component';
    import { SupportComponent } from '../support/support.component';
    
    const routes: Routes = [
      {
        path: 'sales',
        component: SalesComponent
      },
      {
        path: 'support',
        component: SupportComponent
      },
      {
        path: '**',
        component: SalesComponent
      }
    ];
    
    @NgModule({
      imports: [RouterModule.forRoot(routes)],
      exports: [RouterModule],
      providers: []
    })
    export class RoutingModule { }
    
    
  9. 编辑销售模块:编辑文件 sales.module.ts,并将其更改为:

    import { NgModule } from '@angular/core';
    import { CommonModule } from '@angular/common';
    import { SalesComponent } from './sales.component';
    
    @NgModule({
      imports: [
        CommonModule
      ],
      declarations: [SalesComponent]
    })
    export class SalesModule { }
    
    
  10. 编辑共享模块:编辑 shared.module.ts 文件,并将其更改为:

```ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SharedComponent } from './shared.component';

@NgModule({
  imports: [
    CommonModule
  ],
  exports: [
    SharedComponent
  ],
  declarations: [SharedComponent]
})
export class SharedModule { }

```
  1. 编辑支持模块:编辑 support.module.ts 文件,并将其更改为:
```ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SupportComponent } from './support.component';

@NgModule({
  imports: [
    CommonModule
  ],
  declarations: [SupportComponent]
})
export class SupportModule { }

```
  1. 编辑 App 模块:编辑 app.module.ts 文件,修改为:
```ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';
import { RoutingModule } from './routing/routing.module';
import { SalesComponent } from './sales/sales.component';
import { SupportComponent } from './support/support.component';
import { SharedModule } from './shared/shared.module';

@NgModule({
  declarations: [
    AppComponent,
    SalesComponent,
    SupportComponent
  ],
  imports: [
    BrowserModule,
    RoutingModule,
    SharedModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

```

你的应用应该工作在本地主机:4200。请注意以下几点:

  • 路由模块提供应用路由的代码。根应用导入这个模块,所有路由代码都准备好了,可以使用了。
  • 共享模块提供共享组件。根应用只需要导入这个模块就可以访问它的组件。
  • 销售和支持模块不必导入共享模块或共享组件,即使它在销售和支持组件中使用。
  • 销售和支持模块只导入 Angular 公共模块。这个公共模块与我们的代码无关。这是 Angular 提供基本 Angular 指令代码的方式,如NgIfNgFor等等。

部署:独立模块

从一个根模块访问特性模块、共享模块等等的能力听起来很棒,但是问题是您可能需要单独更新特性模块,特别是如果您有一个单独的团队在处理每个特性。例如,“销售”人员可能与“支持”人员有不同的发布日期。不幸的是,这个例子被部署在一组 Webpack 模块中。

如果您希望销售可以与支持分开部署,那么每一个都应该在自己的文件夹中有自己的单页应用。这使得部署工作变得更加轻松。

部署:使用节点管理对公共代码的依赖

部署的另一个问题是公共代码。销售人员可能需要与支持人员不同的通用代码版本。一个可能使用新的公共对象,另一个可能不使用。这是考虑使用 Node 来管理每个项目对公共代码的依赖的好时机。

您可以从 Angular 项目创建节点模块。记住,Angular 有通过节点模块部署的模块。这有点超出了本书的范围,但我自己已经做到了这一点,这要感谢下面这篇高超的文章:https://medium.com/@cyrilletuzi/how-to-build-and-publish-an-angular-module-7ad19c0b4464

要做到这一点,您需要从公共代码库中设置一些代码,比如 GitHub。这里有一个我放在 GitHub 上的非常简单的例子: https://github.com/markclowisg/sharedcomponents

有用的节点命令

当一起使用 Angular 和 Node 时,您可能还想考虑使用节点包管理器命令npm linknpm scope:

  • 当构建节点模块时,这非常有用。它允许您设置一个链接,这样依赖项目就可以使用您的节点代码,而不必不断地重新构建和重新部署到您的存储库中。在本地做所有事情并在以后复制到您的存储库中要容易得多。
  • 当你有几个 npm 公共代码项目,并且你想把它们分组在一个名字前缀下时,这是很有用的。Angular 用它的@angular npm 包前缀来做这件事。你可以考虑一下这个。如果您为“abc”公司工作,并且您有两个用于组件和服务的通用 npm 包,您可能想要使用范围,这样它们可以是@abc/components@abc/services

摘要

本章涵盖了与模块化相关的广泛主题。它向您介绍了模块化的概念以及如何在 JavaScript 和 Angular 中实现它。

如果你觉得这一章太难了,不要担心。您可以跳过“部署”部分,稍后再回到这个部分。模块化可能看起来很复杂,有点迟钝,但是它有价值,可以使你的项目更容易维护。

下一章将介绍 Webpack,Angular CLI 使用它将您的代码打包成可部署的文件。

十、Webpack 简介

如今,你可以在现代浏览器中做更多的事情,这在未来会增加更多。多亏了 Angular 5 这样的技术,页面重载会更少,每个页面会有更多的 JavaScript 代码,客户端会有很多代码。您需要一种方法来有效地部署所有这些代码,以便快速加载。

复杂的客户端应用可能包含模块,有些模块可以同步加载,有些可以异步加载。因此,我们如何打包并最有效地部署它–我们使用 Webpack!

Webpack 和 Angular CLI

Angular CLI 使用 Webpack 来传输、编译和部署项目代码。默认情况下,它还使用 webpack-dev-server 作为它的 web 服务器。在这一章的后面,我会谈到 Webpack 配置和 webpack.config.js。你会在你的项目中寻找它,并注意到它不见了。这是故意的,因为编写 Angular CLI 的人希望隐藏尽可能多的配置细节,以使事情变得更简单,这包括 Webpack 配置。

以下 Angular CLI 命令使 Webpack 配置文件可用:

ng eject

但是,使用这个命令时要小心,因为可能会有一些意想不到的副作用。参见 https://github.com/angular/angular-cli/wiki/ejecthttp://stackoverflow.com/questions/39187556/angular-cli-where-is-webpack-config-js-file-new-2017-feb-ng-eject 了解更多关于这个主题的信息。

Webpack 是一个模块捆绑器。它获取具有依赖关系的模块,并生成表示这些模块的静态资产。图 10-1 所示。

A458962_1_En_10_Fig1_HTML.jpg

图 10-1

Webpack generates static assets representing modules

模块和依赖关系

如果您使用 Node 进行开发,Webpack 将读取您的节点配置文件 packages.json,并自动将您的依赖项作为静态资产包含在构建中。这消除了配置模块加载和部署的痛苦——您不需要解决任何问题。我在参与的每个 Angular 5 项目中都使用了 Webpack,因为它让生活变得更简单。

Webpack 适用于大型项目,因为它支持开发和生产模式。开发模式可以利用像 JavaScript 这样的非最小化资产,使您的应用可以在这种模式下进行调试。生产模式可以使用最少的资产,因此占用空间更小。

您的代码库可以分成多个块,这些块可以按需加载,从而减少应用的初始加载时间。结果:更快的装载时间。作为一名开发人员,您还可以控制这些块的配置(稍后将详细介绍)。

开发过程是这样的:

  1. 编写项目代码。
  2. 将 Webpack 作为构建过程的一部分运行(或者通过 CLI 为您运行)。
  3. 构建完成后,您的静态资产就可以部署到服务器上了。

安装和配置 Webpack

如果您正在使用 CLI,则不需要安装 Webpack。Webpack 在节点下运行。但是如果您想单独试验 Webpack,您可以使用下面的命令来安装它(从项目的根文件夹中):

npm install webpack -g

如果您运行前面提到的ng eject命令,您的 Webpack 选项将包含在项目根文件夹的 webpack.config.js 文件中。在该文件中,您会发现以下内容:

  • 输出路径:您可以指定捆绑资产的放置位置——输出路径。

  • 入口点:你的应用可以在不同的地方使用不同的代码启动。Webpack 将打包用于部署的代码,以便它可以从这些不同的代码开始,但共享公共的打包块。

  • Loaders: A loader is a Node function that takes a type of file and converts files of this type into a new source for bundling (see Figure 10-2). Loaders are Node packages used by Webpack.

    A458962_1_En_10_Fig2_HTML.jpg

    图 10-2

    Loaders

  • Plugins: I use the CommonsChunk plugin in the book’s example project to split our code into deployable chunks that can be loaded separately. The CommonsChunk plugin checks which chunks of code (modules) you use the most and puts them in a file. This gives you a common file that has the CSS and JavaScript needed by every page in your application.

    A458962_1_En_10_Figa_HTML.jpg

图 10-3 中的代码用于创建以下内容:

  • app.js
  • app.map
  • common.js
  • common.map
  • 供应商. js
  • 供应商.地图

摘要

这个简短的章节向您介绍了 Webpack 的基础知识。Webpack 为开发人员提供了惊人的控制能力,我们可以花大量的时间来研究它的可配置性。

但是我们需要继续学习 Angular。第十一章介绍了另一个非常重要的元素:指令。

十一、指令简介

指令是 DOM 元素(如属性)上的标记,它告诉 Angular 将指定的行为附加到现有元素上。

自安古拉吉斯以来,指令就一直存在。它们使用起来相当复杂,尽管在 Angular 中使用起来要容易得多,尤其是在将数据传递到指令中的时候。指令曾经是在 AngularJS 应用中创建自定义标签的主要方式;现在它已经被指令和组件取代了。

Angular 本身提供了许多指令来帮助你编码。你也可以自己编码。

正如第八章所述,组件有三个主要元素:

  • 注释为 Angular 提供了元数据,以便将所有部分组合成一个组件。
  • 模板包含用于在浏览器中呈现组件的标记(通常是 HTML)。
  • 该类包含组件的数据和代码。代码实现组件的预期行为。

如您所见,模板用于为组件的显示生成标记。该标记可以包括用于其他 Angular 组件的标签(或其他选择器),从而允许从其他组件合成组件。这种标记还可以包括实现某些行为的指令。

例如,您可能有一个显示升级请求的升级详细信息的组件。但是,想要查看升级请求的人可能没有查看信息的权限,这意味着某些元素应该隐藏。您可以使用 Angular ngIf指令来评估用户的权限,并基于这些权限隐藏或显示元素。

指令的类型

现在我们知道组件模板使用指令,但是它们可能以不同的方式影响模板的输出。有些指令可能会完全改变模板输出的结构。这些指令可以通过添加和删除视图 DOM 元素来更改 DOM 布局。让我们称这些为结构指令。有些指令可能只是改变模板输出的项目的外观。让我们称这些为非结构化指令。

Angular 包括几个可供您在模板中使用的结构指令:

  • NgIf
  • NgFor
  • NgSwitchNgSwitchWhenNgSwitchDefault

Angular 还包括几个在模板中使用的非结构化指令:

  • NgClass
  • NgStyle
  • NgControlName
  • NgModel

ngIf

这是您添加到标记中的元素的指令,通常是添加到类似于div的容器元素。如果ngIf的模板表达式为真,那么在绑定完成后,元素内部的内容将包含在视图 DOM 中。如果ngIf的模板表达式为 false,那么在绑定完成后,元素内部的内容将从视图 DOM 中排除。因此,ngIf指令用于包含或排除 UI 的元素,包括元素的子元素。被 ngIf 排除的标记不会是不可见的,它只是根本不在 DOM 中。

在本例中(指令-ex100),我们在显示姓名和地址之间切换,如图 11-1 所示。

A458962_1_En_11_Fig1_HTML.jpg

图 11-1

Toggling between name and address

让我们使用ngIf来隐藏和显示元素:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new directives-ex100
    
    
  2. 开始ng serve:使用以下代码:

    cd directives-ex100
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”

  4. 编辑类:编辑 app.component.ts,修改为:

    import { Component } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      templateUrl: './app.component.html',
      styles: ['div.box { width: 200px;padding:20px;margin:20px;border:1px solid black;color:white;background-color:green }']
    })
    export class AppComponent {
      showName: boolean = true;
    
      toggle(){
        this.showName = !this.showName;
      }
    }
    
    
  5. 编辑模板:编辑 app.component.html,更改如下:

    <div *ngIf="this.showName" class="box">
      Name: Mark
    </div>
    <div *ngIf="!this.showName" class="box">
      Address: Atlanta
    </div>
    <button (click)="this.toggle()">Toggle</button>
    
    

为了什么

这是一个处理 iterable 对象的每一项的指令,为每一项输出一个标记。这被称为结构化指令,因为它可以通过添加和删除视图 DOM 元素来更改 DOM 布局。

ndFor对于生成重复的内容很有用,比如客户列表、下拉列表的元素等等。

iterable 的每个处理项在其模板上下文中都有可用的变量,如表 11-1 所示。

表 11-1

ngFor Variables

| 可变的 | 描述 | | :-- | :-- | | 项目本身 | 例子:`ngFor="#name of names"`。在这种情况下,项目具有变量`name`。 | | `Index` | 每个模板上下文的当前循环迭代。 | | `last` | 布尔值,指示该项是否是迭代中的最后一项。 | | `even` | 指示此项是否有偶数索引的布尔值。 | | `odd` | 指示此项是否有奇数索引的布尔值。 |

这将是示例指令-ex200,如图 11-2 所示。

A458962_1_En_11_Fig2_HTML.jpg

图 11-2

ngFor showing a list

让我们使用ngFor来显示一个列表:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new directives-ex200
    
    
  2. 开始ng serve :

    cd directives-ex200
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”

  4. 编辑类:编辑 app.component.ts,修改为:

    import { Component } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      templateUrl: './app.component.html',
      styleUrls: ['./app.component.css']
    })
    export class AppComponent {
      names = [
        'Peter Falk', 'Mary-Ann Blige', 'Eminem'];
    }
    
    
  5. 编辑模板:编辑 app.component.html,更改如下:

    <div *ngFor="let name of names; let i = index;">
      <div>{{i}}:&nbsp;{{name}}</div>
    </div>
    
    

ngSwitch、ngSwitchWhen 和 ngSwitchDefault

ngSwitch是当 DOM 元素匹配switch表达式时添加或删除 DOM 元素的指令。它被称为结构化指令,因为它可以通过添加和删除视图 DOM 元素来更改 DOM 布局。

这将是示例指令-ex300,如图 11-3 所示。

A458962_1_En_11_Fig3_HTML.jpg

图 11-3

ngSwitch hiding and showing elements

让我们根据您的选择使用ngSwitch来隐藏和显示元素:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new directives-ex300
    
    
  2. 开始ng serve:使用以下代码:

    cd directives-ex300
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”

  4. 编辑类:编辑 app.component.ts,修改为:

    import { Component } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      templateUrl: './app.component.html',
      styles: ['.block1 {background-color:#d5f4e6;margin:10px;padding:10px;}',
      '.block2 {background-color:#d5f4ff;margin:10px;padding:10px;}',
      '.block3 {background-color:#d5cce6;margin:10px;padding:10px;}']
    })
    export class AppComponent {
      selection = 'name';
      options = ['name','address','other'];
    }
    
    
  5. 编辑模板:编辑 app.component.html,更改如下:

    <select [(ngModel)]="selection">
      <option *ngFor="let option of options">{{option}}</option>
    </select>
    <div [ngSwitch]="selection">
      <div class="block1" *ngSwitchCase="options[0]">name</div>
      <div class="block2" *ngSwitchCase="options[1]">address</div>
      <div class="block3" *ngSwitchDefault>other</div>
    </div>
    
    
  6. 编辑模块:编辑 app.module.ts,修改为:

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

ngClass

我们可以通过使用这个指令添加或删除类来改变 DOM 元素的外观。它的参数是一个对象,该对象包含下列对象对:

  • CSS 类名
  • 一种表达

如果表达式为真,CSS 类名将被添加到目标 DOM 元素中,否则将被忽略。它不仅对设置 CSS 类有用。使用如下代码可能更容易:

<div [class]="classNames">Customer {{name}}.</div>

在下一个例子中,n gClass让用户单击动物列表中的动物来选择它。选定的动物以红色突出显示。这将是示例指令-ex400,如图 11-4 所示。

A458962_1_En_11_Fig4_HTML.jpg

图 11-4

ngClass highlighting in a list

让我们来做指令示例-ex400:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new directives-ex400
    
    
  2. 开始ng serve:使用以下代码:

    cd directives-ex400
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”

  4. 编辑类:编辑 app.component.ts,修改为:

    import { Component } from '@angular/core';
    @Component({
      selector: 'app-root',
      templateUrl: './app.component.html',
      styles: [
        '.selected { color: white; background-color:red; padding: 10px; margin: 10px }',
        '.unselected { background-color: white; padding: 10px; margin: 10px}'
        ]
    })
    export class AppComponent {
      selectedAnimal = 'cat';
      animals = ['cat', 'dog', 'zebra', 'giraffe'];
    
      onAnimalClicked(event:Event){
        const clickedAnimal = event.srcElement.innerHTML.trim();
        this.selectedAnimal = clickedAnimal;
      }
    }
    
    
  5. 编辑模板:编辑 app.component.html,更改如下:

    <div *ngFor="let animal of animals">
      <div [ngClass]="{'selected': animal === selectedAnimal, 'unselected' : animal !== selectedAnimal}"
      (click)="onAnimalClicked($event)">{{animal}}</div>
    </div>
    
    

你的应用应该工作在本地主机:4200。

ngStyle

这是用于设置元素的 CSS 样式的指令。如果您只想设置一种样式,使用如下代码可能更容易:

<div [style.fontSize]="selected ? 'x-large' : 'smaller'" >
  Some text.
</div>

但是如果你想设置多种风格,ngStyle是正确的选择。此指令需要一个计算结果为包含样式属性的对象的表达式。该表达式可以是如下所示的内联代码:

[ngStyle]="{'color': 'blue', 'font-size': '24px', 'font-weight': 'bold'}"

或者像这样的函数调用:

[ngStyle]="setStyles(animal)"

... later on in the class ...

setStyles(animal:String){
    let styles = {
      'width' : '50px'
    }
    return styles;
  }

它让用户点击动物列表中的动物来选择它。选中的动物以红色高亮显示,如图 11-5 所示。

A458962_1_En_11_Fig5_HTML.jpg

图 11-5

ngStyle highlighting in a list

让我们做指令示例-ex500:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new directives-ex500
    
    
  2. 开始ng serve:使用以下代码:

    cd directives-ex500
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”

  4. 编辑类:编辑 app.component.ts,修改为:

    import { Component } from '@angular/core';
    @Component({
      selector: 'app-root',
      templateUrl: './app.component.html'
    })
    export class AppComponent {
      selectedAnimal = 'cat';
      animals = ['cat', 'dog', 'zebra', 'giraffe'];
    
      onAnimalClicked(event:Event){
        const clickedAnimal = event.srcElement.innerHTML.trim();
        this.selectedAnimal = clickedAnimal;
      }
    
      getAnimalStyle(animal){
        const isSelected = (animal === this.selectedAnimal);
        return {
          'padding' : '10px',
          'margin' : '10px',
          'color' : isSelected ? '#ffffff' : '#000000',
          'background-color' : isSelected ? '#ff0000' : '#ffffff',
        }
      }
    }
    
    
  5. 编辑模板:编辑 app.component.html,更改如下:

    <div *ngFor="let animal of animals">
      <div [ngStyle]="getAnimalStyle(animal)" (click)="onAnimalClicked($event)">{{animal}}</div>
    </div>
    
    

你的应用应该工作在本地主机:4200。

Note

Angular 还使用其他指令来处理表单。我将在后面的章节中介绍这些内容。

创建指令

指令和组件都是有 Angular 的对象,对应于标记中的元素,可以修改生成的用户界面。它们都有选择器。选择器用于标识与网页或模板中的标记相关联的组件或指令。对于组件,通常使用标签名——例如CustomerList。对于指令,通常使用一个标记属性名,它使用方括号——例如,[tooltip]

指令和组件都有注释。指令有@Directive注释,组件有@Component注释。它们都有类,这些类可以通过构造函数以相同的方式使用依赖注入。

然而,整流罩和组件并不完全相同。例如,组件需要视图,而指令不需要。指令没有模板。没有用于呈现元素的捆绑 HTML 标记。

指令向现有 DOM 元素添加行为。例如,您可以为工具提示添加指令。您创建了指令,将指令选择器添加到 HTML 或使用它的模板中,它就交付了功能(您还需要添加导入)。

创建指令类似于创建组件:

  1. 导入Directive装饰器。
  2. 添加@Directive注释,包括一个 CSS 属性选择器(在方括号中),它将指令标识为一个属性。您还可以向@Directive注释添加其他元素,包括输入属性和主机映射。
  3. 指定用于绑定的公共输入属性的名称(如果需要)。
  4. Directive类。这个类将使用构造函数注入,并可能操作注入的元素和渲染器。
  5. 将装饰器应用于将要使用它的组件或指令。

如前所述,指令是 DOM 元素(如属性)上的标记,它告诉 Angular 将指定的行为附加到现有元素上。这意味着我们需要一种方法来访问指令所应用到的 DOM 元素,以及修改 DOM 元素的方法。

Angular 为我们提供了两个非常有用的对象:ElementRefRenderer

  • 通过nativeElement属性,ElementRef对象让您可以直接访问指令的 DOM 元素。小心使用ElementRef对象。允许直接访问 DOM 会使您的应用更容易受到 XSS 攻击。
  • Renderer对象给了我们许多帮助方法,使我们能够修改 DOM 元素。

我们可以把两者都注入到我们的课堂中。下面的代码通过构造函数接受ElementRef(允许您使用 DOM 元素的nativeElement属性访问 DOM 元素)和Renderer,并使每个变量成为私有实例变量:

constructor(private element: ElementRef, private renderer: Renderer) {
}

创建简单指令:示例指令-ex600

这是一个简单的指令,用于更改它所添加到的 HTML 元素的大小:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new directives-ex600
    
    
  2. 导航到目录;使用以下代码:

    cd directives-ex600
    
    
  3. 使用 CLI 创建指令:使用 CLI 创建文件并修改模块 app.module.ts:

    ng generate directive sizer
    
    

    这将生成一些文件,包括 sizer.directive.ts。

  4. 编辑 sizer.directive.ts:将其改为:

    import { Directive, Input, Component, ElementRef, Renderer, |OnInit } from '@angular/core';
    
    @Directive({
      selector: '[sizer]'
    })
    export class SizerDirective implements OnInit {
      @Input() sizer : string;
    
      constructor(private element: ElementRef, private renderer: Renderer) {
      }
    
      ngOnInit() {
        this.renderer.setElementStyle(this.element.nativeElement, 'font-size', this.sizer);
      }
    }
    
    

    注意该指令如何在初始化后触发的ngOnInit方法中工作。如果您要将setElementStyle代码移动到构造函数中,这是可行的,因为sizer输入变量没有立即设置它的值——它是在 app 组件初始化时设置的。

  5. 编辑模板:编辑 app.component.html,更改如下:

    <div sizer="72px">
      {{title}}
    </div>
    
    
  6. 查看应用:打开 web 浏览器并导航到 localhost:4200。它应该显示“应用工程”在大文本。

你的应用应该工作在本地主机:4200。请注意如何使用渲染器来更新样式和更改大小。

在指令中访问 DOM 事件

我们可能还需要一种方法来访问链接到指令的元素的 DOM 事件。Angular 提供了不同的方法来访问这些事件。

使用指令元素宿主

这可用于指定与host元素相关的事件、动作、属性和特性。它可用于将事件绑定到类中的代码:

@Directive({
  selector: 'input',
  host: {
    '(change)': 'onChange($event)',
    '(window:resize)': 'onResize($event)'
  }
})
class InputDirective {
  onChange(event:Event) {
    // invoked when the input element fires the 'change' event
  }
  onResize(event:Event) {
    // invoked when the window fires the 'resize' event

  }
}

主机监听器

Angular HostListener是允许您将类中的方法绑定到 DOM 事件的注释:

@HostListener('mouseenter') onMouseEnter() {
  this.highlight('yellow');
}

@HostListener('mouseleave') onMouseLeave() {
  this.highlight(null);
}

private highlight(color: string) {
  this.el.nativeElement.style.backgroundColor = color;
}

在指令中访问 DOM 属性

您可能希望修改链接到指令的元素的属性。您可以使用元素ref来做到这一点。然而,还有另一种方法。您可以使用@HostBinding指令将元素的 DOM 属性绑定到 Angular 指令中的实例变量。然后您可以更新变量的值,DOM 属性将自动更新以匹配。

例如,在下面的代码中,您可以通过修改backgroundColor实例变量的值来控制元素的背景颜色:

@Directive({
    selector: '[myHighlight]',
})
class MyDirective {
  @HostBinding('style.background-color') backgroundColor:string = 'yellow';
}

创建带有事件的指令:示例指令-ex700

这是一个处理宿主事件的示例指令。主机事件映射到host元素中的 DOM 事件。当您需要一个指令来响应 DOM 上发生的事情时,它们很有用:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new directives-ex700
    
    
  2. 导航到目录:使用此命令:

    cd directives-ex700
    
    
  3. 使用 CLI 创建指令:使用 CLI 创建文件并修改模块 app.module.ts:

    ng generate directive hoverer
    
    

    这将生成一些文件,包括 hoverer.directive.ts。

  4. 编辑 hoverer.directive.ts:改为如下:

    import { Directive, Input, ElementRef, Renderer } from '@angular/core';
    
    @Directive({
      selector: '[hoverer]',
      host: {
        '(mouseenter)': 'onMouseEnter()',
        '(mouseleave)': 'onMouseLeave()'
      }
    })
    
    export class HovererDirective {
      @Input() hoverer;
    
      constructor(
        private elementRef:ElementRef,
        private renderer:Renderer) { }
    
      onMouseEnter(){
        this.renderer.setElementStyle(
          this.elementRef.nativeElement, 'color', this.hoverer);
      }
    
      onMouseLeave(){
        this.renderer.setElementStyle(
          this.elementRef.nativeElement, 'color', 'black');
      }
    }
    
    
  5. 编辑模板:编辑 app.component.html,更改如下:

    <h1 hoverer="red">{{title}}</h1>
    
    
  6. 查看应用:打开 web 浏览器并导航到 localhost:4200。当你悬停在“欢迎使用应用!”上时,它应该会变成红色

你的应用应该工作在本地主机:4200。

摘要

学习完这一章后,你应该知道如何编写指令,并理解它们与组件的不同之处。

指令在被重用来向用户界面添加通用行为时非常有用。它们通常被放在共享模块中,以便可以跨应用重用。例如,您可以编写一个指令,根据用户的设置(或其他状态)在整个应用中启用或禁用按钮。该指令可以由元素属性指定。您可以将该指令添加到共享模块(或主模块)中,然后修改应用的模板以在按钮上包含该指令的属性。

我们暂时完成了指令。下一章回到组件,更详细地看它们。

十二、更多组件

本章的目的是通过更高级的主题进一步增强您的组件知识。

组件和子组件

众所周知,组件是用户界面中的一个构建块。Angular 应用总是有一个应用(或根)组件。这个组件(像其他组件一样)在 HTML 中有一个标签,并且有 Angular 地引导到那个组件中。这个应用组件(像其他组件一样)可以包含其他(子)组件。

因此,组件可以包含其他组件。这就是所谓的构图。正如我在前面的章节中提到的,组件就像是 UI 的乐高积木。组合是用这些乐高积木组合成一个应用的艺术。我用一个例子介绍一下作文。

当您编写单页应用时,惯例是您有一个组件层次结构—一个组合。图 12-1 给出了一个例子。

A458962_1_En_12_Fig1_HTML.gif

图 12-1

Hierarchy of components

当你用一个组合编码时,你必须非常小心地把数据(称为状态)存储在正确的位置,这样它就不会重复(存储两次)。脸书大学的皮特·亨特在 ?? 写了一篇关于这个的精彩文章。这篇文章是关于 React 的,但是同样的规则也适用于 Angular。

数据向下流动

数据应该从较高级别的组件向下流到较低级别的组件。当您创建一个从外部接收数据的组件时,您必须使用@Input decorator 显式地告诉 Angular 期望该数据作为输入。您将@Input装饰器放在数据将从外部注入的实例变量旁边。

当您从外部将数据传递到组件中时,您使用输入属性将数据传递到组件中。

有时,您可能希望输入属性的名称不同于它将被注入到的实例变量的名称。这时您需要使用一个alias,它允许您指定输入属性名。在@Input装饰器中,alias可以在圆括号内指定。图 12-2 给出了一个例子。

A458962_1_En_12_Fig2_HTML.jpg

图 12-2

Passing data to car components

该组件将数据从应用传递到汽车组件。这将是更多组件示例-ex100:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new more-components-ex100 --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd more-components-ex100
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”

  4. 编辑 app 类:编辑 app.component.ts,修改为:

    import { Component } from '@angular/core';
    import { ICar } from './icar';
    
    @Component({
      selector: 'app-root',
      template: `
      <car *ngFor="let car of cars" [theCar]="car"></car>
      `,
      styles: []
    })
    export class AppComponent {
      cars:Array<ICar> = [
        {make: 'bmw', model: 'm3'},
        {make: 'porsche', model: '911'},
        {make: 'bmw', model: 'm3'}
      ];
    }
    
    
  5. 创建ICar界面:使用以下命令:

    ng generate interface ICar
    
    
  6. 编辑 ICar 界面:编辑 icar.ts,更改为:

    export interface ICar {
        make: string,
        model: string
    }
    
    
  7. 创建Car类:使用下面的代码:

    ng generate component Car --inline-template --inline-style --flat
    
    
  8. 编辑Car类:编辑 car.component.ts,修改为:

    import { Component, Input } from '@angular/core';
    import { ICar } from './icar';
    
    @Component({
      selector: 'car',
      template: `
        <p>
          {{car.make}} : {{car.model}}
        </p>
      `,
      styles: []
    })
    export class CarComponent {
      @Input('theCar') car: ICar;
    }
    
    

你的应用应该工作在本地主机:4200。请注意以下几点:

  • 应用组件有一个三辆汽车的列表。我们使用ngFor指令遍历汽车列表,为每辆汽车生成一个汽车组件。我们使用theCar输入属性将汽车传递给汽车组件。
  • 我们有一个汽车组件来显示每辆汽车。在 car 组件中,我们使用theCar别名输入属性从外部接受 Car 实例变量。

Warning

您可以通过@Input()属性和@Component注释的inputs元素传递包含字段的对象。例如,您可以执行一个 HTTP 请求来获取一个包含名称和地址的 customer 对象,然后通过一个属性将它传递给一个子组件来显示它。这样做很好,但是请记住,在服务器返回响应之前,您传递的属性可能为空。因此,子组件可能试图显示像空对象的名称和地址这样的元素,这可能导致 Angular 抛出异常,并且在数据从服务器返回时不显示数据。这让我好几次措手不及。解决这个问题的方法是使用 Elvis 操作符。

向上流动的事件

有时您需要组合包含子组件的父组件并控制它们。父组件需要有响应子组件上发生的事情(事件)的代码。事件应该向上流动,从较低级别的组件向上发出,并由较高级别的组件响应。

下面是如何设置子组件以将自定义事件传递给父组件:

  1. 导入EventEmitter类。
  2. 通过使用@Component指令的events元素来指定您的组件将发出的定制事件。你一定要记得这样做!
  3. 在类中创建一个事件发射器作为实例变量。
  4. 当您想要发出一个事件时,调用事件发射器方法emit

下面是如何设置一个父组件来接收子组件的自定义事件:

  1. 将带有自定义事件的组件添加到另一个组件中。记得导入它并在@Component注释的directive元素中指定它。
  2. 将带有自定义事件的组件添加到另一个组件的模板标记中。编辑模板中的标记,使用圆括号中的事件名称和它将触发的模板语句来响应自定义事件,例如:(wordInput)="wordInputEvent($event)"。请注意,这使用了与非自定义事件相同的语法。

通过@Output()发出输出

您在子组件/指令中创建了一个类型为EventEmitter@Output()实例变量。您可以修改子组件/指令,以便在需要时使用该实例变量来发出事件。您可以修改父组件,用 template 语句绑定其模板中同名的事件属性。Angular 将从子组件/指令向父组件发出事件,并将调用模板语句。图 12-3 给出了一个例子。

A458962_1_En_12_Fig3_HTML.jpg

图 12-3

Emitting output

图 12-3 显示了事件如何从一个组件向上流向另一个组件。这将是更多组件示例-ex200:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new more-components-ex200 --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd more-components-ex200
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”

  4. 编辑 app 类:编辑 app.component.ts,修改为:

    import { Component } from '@angular/core';
    import { ICar } from './icar';
    
    @Component({
      selector: 'app-root',
      template: `
      <car *ngFor="let car of cars" (carDelete)="deleteCar(car)" [theCar]="car">
      </car>
      `,
      styles: []
    })
    
    export class AppComponent {
      cars:Array<ICar> = [
        {make: 'bmw', model: 'm3'},
        {make: 'porsche', model: '911'},
        {make: 'ford', model: 'mustang'}
      ];
    
      deleteCar(car: ICar){
        alert('Deleting car:' + JSON.stringify(car));
      }
    }
    
    
  5. 创建ICar接口:使用下面的代码:

    ng generate interface ICar
    
    
  6. 编辑ICar界面:编辑 icar.ts,更改为:

    export interface ICar {
        make: string,
        model: string
    }
    
    
  7. 创建Car类:使用下面的代码:

    ng generate component Car --inline-template --inline-style --flat
    
    
  8. 编辑Car类:编辑 car.component.ts,修改为:

    import { Component, Input, Output, EventEmitter } from '@angular/core';
    import { ICar } from './icar';
    
    @Component({
      selector: 'car',
      template: `
        <p>
          {{car.make}} : {{car.model}}
          <button (click)="delete(car)">Delete</button>
        </p>
      `,
      styles: []
    })
    export class CarComponent {
      @Input('theCar') car: ICar;
      @Output() carDelete = new EventEmitter();
    
      delete(car: ICar){
        this.carDelete.emit(car);
      }
    }
    
    

你的应用应该工作在本地主机:4200。请注意以下几点:

  • 应用组件有一个包含三个Car的列表。它监听carDelete事件,当事件发生时触发deleteCar方法。
  • 我们有一个汽车组件来显示每辆汽车。它包含一个删除按钮,当用户单击它时会发出一个carDelete事件。

构图:示例

让我们使用 Angular CLI 创建一个包含其他组件的组件的简单示例。我们将编写一个包含客户列表的应用,该列表包含三个客户,如图 12-4 所示。

A458962_1_En_12_Fig4_HTML.jpg

图 12-4

Customer list with three customers

这将是更多组件示例-ex300:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new more-components-ex300 --inline-template --inline-style
    
    

    注意--inline-template--inline-style参数。这些命令告诉 CLI 将模板和样式合并到组件的类中,使组件定义成为一个文件,而不是三个文件——当您有很少样式的小模板时,这要容易得多。当您开始编写更大的组件时,您可能需要重新考虑这一点。当您使用这个命令(以及下面的命令)时,您可以添加--spec参数来告诉 CLI 不要为应用和组件创建. spec.ts 文件。我只是留下了spec文件单独生成。

  2. 开始ng serve:使用以下代码:

    cd more-components-ex300
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”

  4. 创建客户列表组件:使用下面的代码:

    ng generate component customer-list --flat --inline-template --inline-style
    
    

    注意我们如何再次使用--inline-templateinline-style参数将组件合并到一个文件中。我们还使用参数--spec false告诉 CLI 不要生成. spec.ts 测试文件。

  5. 创建客户组件:使用下面的代码:

    ng generate component customer --flat --inline-template --inline-style
    
    

    我们再次使用--inline-templateinline-style参数将组件合并到一个文件中。我们使用参数--spec false告诉 CLI 不要生成. spec.ts 测试文件。

  6. 编辑 app 组件:将下面的代码复制粘贴到 app.component.ts:

    import { Component } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      template: `
      <div class="app">
      [app]
      <app-customer-list>
      </app-customer-list>
      </div>
      `,
      styles: ['.app {background-color:#d5f4e6;margin:10px;padding:10px;}']
    })
    
    export class AppComponent {
    }
    
    
  7. 编辑客户列表组件:将下面的代码复制粘贴到 customer-list.component.ts:

    import { Component, OnInit } from '@angular/core';
    
    @Component({
      selector: 'app-customer-list',
      template: `
        <div class="customerList">
        <p>
        [customer list]
        </p>
        <app-customer>
        </app-customer>
        <app-customer>
        </app-customer>
        <app-customer>
        </app-customer>
        </div>
      `,
      styles: ['.customerList {background-color:#80ced6;margin:10px;padding:10px;}']
    })
    export class CustomerListComponent implements OnInit {
    
      constructor() { }
    
      ngOnInit() {
      }
    
    }
    
    
  8. 编辑客户组件:将以下代码复制并粘贴到 customer.component.ts:

    import { Component, OnInit } from '@angular/core';
    
    @Component({
      selector: 'app-customer',
      template: `
        <div class="customer">
          [customer]
        </div>
      `,
      styles: ['.customer {background-color:#fefbd8;margin:10px;padding:10px}']
    })
    export class CustomerComponent implements OnInit {
    
      constructor() { }
    
      ngOnInit() {
      }
    
    }
    
    

你的应用应该工作在本地主机:4200。您已经编写了一个由不同组件组成的应用。请注意以下几点:

  • 每个组件的顶部都有一个指定选择器的@Component指令。例如,customer 组件:

    @Component({
      selector: 'app-customer',
    
    

    当您需要将该组件包含在另一个组件中时,您可以使用选择器作为标记。例如,客户列表组件使用客户组件的标记三次将其包含在模板中:

    <app-customer>
    </app-customer>
    <app-customer>
    </app-customer>
    <app-customer>
    </app-customer>
    
    
  • 文件 app.module.ts 已被 CLI 修改。每个组件都作为一个声明添加到模块中(稍后将详细介绍模块)。

数据向下流动:示例

让我们修改示例 more-components-ex300,将数据从客户列表组件向下传递到客户组件,如图 12-5 所示。

A458962_1_En_12_Fig5_HTML.jpg

图 12-5

Customer list with three customers

这将是更多组件示例-ex400。

编辑客户组件

我们编辑客户组件以接受来自外部的输入数据:

  • 修改导入以包含来自 Angular core:

    import { Component, OnInit, Input } from '@angular/core';
    
    

    Input

  • 更改模板以包含实例变量customer : {{customer.name}}{{customer.city}}的字符串插值。这将输出customer实例变量

      template: `
        <div class="customer">
          {{customer.name}} | {{customer.city}}
        </div>
      `,
    
    

    namecity属性的内容

  • 将实例变量customer声明为输入变量:

    @Input() customer;
    
    

编辑客户列表组件

我们编辑客户列表组件,使用单向数据绑定将数据传递给客户组件(稍后将详细介绍):

  • 将以下标签

    <app-customer>
    </app-customer>
    <app-customer>
    </app-customer>
    <app-customer>
    </app-customer>
    
    

    替换为:

    <app-customer *ngFor="let customer of customerList" [customer]="customer">
    </app-customer>
    
    
  • 声明实例变量customerList并用数据填充它。在export之后、constructor之前添加代码:

      private customerList = [
        { name: 'Brian', city: 'Atlanta'},
        { name: 'Peter', city: 'San Francisco'},
        { name: 'Janet', city: 'Colorado'},
      ];
    
    

你的应用应该工作在本地主机:4200。您修改了组件列表以包含客户列表数据,并使用单向(向下)数据绑定将该数据向下传递给客户。请注意以下几点:

  • 客户列表组件将客户列表数据设置为实例变量,模板引用该变量。
  • 客户列表组件在模板中使用了一个ngFor。这允许模板遍历客户列表,为每个客户创建一个customer变量,并通过一个绑定属性将其传递给客户组件。
  • 客户组件声明了一个名为customer的实例变量。它使用注释@Input()告诉 Angular 从外部自动设置它的值。注意,Input类必须在客户组件类的顶部导入。
  • 客户组件使用模板中的{{customer.name}}{{customer.city}}输出名为customer的实例变量的namecity属性。

向上流动的事件:示例

让我们修改前面的示例,从客户组件向客户列表组件触发事件,如图 12-6 所示。这将是示例 ex300,它基于示例更多组件-ex500。

A458962_1_En_12_Fig6_HTML.jpg

图 12-6

Firing events

这将是示例 ex300。

编辑客户组件

我们编辑客户组件,以便在用户单击客户时输出一个单击事件:

  • 修改import以包括来自 Angular 核心的OutputEventEmitter类:

    import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
    
    
  • 修改模板以在用户点击客户:

    template: `
        <div class="customer"(click)="onClicked()">
          {{customer.name}} | {{customer.city}}
        </div>
      `,
    
    

    时调用方法onClicked

  • 将事件发射器的实例变量添加到 TypeScript 类:

    @Output() clicked: EventEmitter<String> = new EventEmitter<String>();
    
    
  • onClicked方法添加到 TypeScript 类:

    onClicked(){
      this.clicked.emit(this.customer.name);
    }
    
    

编辑客户列表组件

我们编辑客户列表组件来响应客户组件发出的clicked事件:

  • 修改模板以绑定到clicked事件,当事件发生时调用onCustomerClicked方法:

    <app-customer *ngFor="let customer of customerList" [customer]="customer" (clicked)="onCustomerClicked($event)">
    
    
  • 添加方法onCustomerClicked来接收事件数据,并显示一个带有客户名称:

    onCustomerClicked(customerName:String){
      alert('Customer Clicked:' + customerName);
    }
    
    

    的警告框

你的应用应该工作在本地主机:4200。当你点击一个客户时,应用现在应该显示一个警告框。请注意事件是如何从多个客户组件流向单个客户列表组件的。还要注意以下几点:

  • customer 组件为输出带有字符串数据的事件的事件发射器设置一个实例变量。它使用一个注释@Output()告诉 Angular 其他组件应该能够绑定到这个事件。
  • 客户组件模板包括 Angular 指令(click),用于监听和响应用户点击div。它触发一个使用事件发射器输出事件的方法。
  • 客户列表组件包括一个事件处理程序,用于监听和响应客户组件中的定制clicked事件。
  • 客户列表组件包含在onCustomerClicked方法中的代码,用于接收来自事件的数据并显示一个警告框。

模板参考变量

模板引用变量是对模板中一个或多个元素的引用。可以用ref-前缀代替#

一旦声明了模板引用变量,就可以在模板或代码中使用它。然而,你需要知道这个变量不是由 Angular 设置的,直到ngAfterViewInit生命周期方法已经完成(本书后面会有更多关于 Angular 生命周期的内容)。

ViewChild:示例

ViewChild声明对组件中子元素的引用。当声明实例变量时,在括号中指定一个选择器,用于将子元素绑定到实例变量。

该示例显示了文本(参见图 12-7 ),它看起来类似于 CLI 默认应用。这是更多组件示例-ex600。

A458962_1_En_12_Fig7_HTML.jpg

图 12-7

ViewChild example

如果您检查代码,您会看到它使用一个模板变量来引用h1元素:

<h1 #title></h1>

在组件加载完视图后,它有代码来设置它的内部 HTML:

  ngAfterViewInit(){
    this.title.nativeElement.innerHTML = 'app works differently!'
  }

还要注意,模板变量由模板中的一些插值引用:

The title is {{title.innerHTML}}

我们来看更多示例-组件-ex600:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new more-components-ex600 --inline-template
    
    

    记住--inline-template告诉 CLI 在生成新应用时使用内联模板。

  2. 开始ng serve:使用以下代码:

    cd more-components-ex600
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”

  4. 编辑类:编辑 app.component.ts,修改为:

    import { Component, ElementRef, ViewChild, AfterViewInit } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      template: `
      <h1 #title></h1>
      The title is {{title.innerHTML}}
      `,
      styleUrls: ['./app.component.css']
    })
    export class AppComponent implements AfterViewInit  {
      @ViewChild('title') title: ElementRef;
    
      ngAfterViewInit(){
        this.title.nativeElement.innerHTML = 'app works differently!'
      }
    }
    
    
  5. 查看应用:您应该看到“应用的工作方式不同!”

ViewChildren:示例

ViewChildren声明对组件中多个子元素的引用。当声明实例变量时,在括号中指定一个选择器,用于将子元素绑定到实例变量。

该选择器可以是子类型(即ChildAngular 元素的类)或模板引用(#name)。

本例使用ViewChildren来访问段落列表(见图 12-8 )。我们使用ViewChildren和一系列子引用名,用逗号分隔。这是更多组件示例-ex700。

A458962_1_En_12_Fig8_HTML.jpg

图 12-8

Accessing a list of paragraphs

我们来看更多示例-组件-ex700:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new more-components-ex700 --inline-template
    
    
  2. 开始ng serve:使用以下代码:

    cd more-components-ex700
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”

  4. 编辑类:编辑 app.component.ts,修改为:

    import { Component, ViewChildren, AfterViewInit } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      template: `
      <p #paragraph1>Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. </p>
      <p #paragraph2>At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr,  sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.</p>
      <p *ngIf="note">{{note}}</p>
      `,
      styles: ['p { background-color: #FFE5CC; padding: 15px; text-align: center}']
    })
    export class AppComponent implements AfterViewInit{
      @ViewChildren('paragraph1, paragraph2') paragraphs;
      note: string = '';
    
      ngAfterViewInit(){
        setTimeout(_ => this.note = 'Number of Paragraphs:' + this.paragraphs.length);
      }
    }
    
    

你的应用应该工作在本地主机:4200。您应该会看到两段文本,下面有一个段落计数,如图 12-8 所示。

NgContent 和 Transclusion:示例

Transclusion 是将内容从组件标签内的区域包含和转移到组件的模板中。NgContent标签用于传输。它甚至有一个选择器,允许您选择要包含的内容。如果您在选择器中使用了一个[(如在[test]中),那么它可以用来选择具有该属性的内容(例如,<div test>hejwejgwegrhj</div>)。

这个例子非常简单,没有使用选择器。它只包括组件标签之间的文本。这是 more-components-ex800 的示例(见图 12-9 )。

A458962_1_En_12_Fig9_HTML.jpg

图 12-9

Text between component tags

我们来看更多示例-组件-ex800:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new more-components-ex800 --inline-template
    
    
  2. 开始ng serve:使用以下代码:

    cd more-components-ex800
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”

  4. 编辑类:这次我们将把两个Component类添加到同一个文件中。编辑 app.component.ts 并将其更改为以下内容:

    import { Component } from '@angular/core';
    
    @Component({
      selector: 'Paragraph',
      template: `
      <p><ng-content></ng-content></p>
      `,
      styles: ['p { border: 1px solid #c0c0c0; padding: 10px }']
    })
    export class Paragraph {
    }
    
    @Component({
      selector: 'app-root',
      template: `
      <p>
      <Paragraph>Lorem ipsum dolor sit amet, consectetur adipiscing elit. </Paragraph>
      <Paragraph>Praesent eget ornare neque, vel consectetur eros. </Paragraph>
      </p>
      `,
      styleUrls: ['./app.component.css']
    })
    export class AppComponent {
      title = "welcome to app!";
    }
    
    
  5. 编辑模块:我们在同一个文件中有两个Component类。我们需要确保两个Component都在模块定义中声明——否则,它们将不可用。编辑 app.module.ts 并将其更改为以下内容:

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

你应该会看到两段文字,如图 12-9 所示。

ContentChild:示例

您可以使用ngContent来包含附加内容。Transclusion 指的是将内容注入 DOM 中的特定元素。您可以使用ContentChild来声明对被包含的附加内容中的child元素的引用。

这个例子与前一个例子相似,只是它使用了ContentChild来获取对被交叉包含的内容中的title元素的引用(参见图 12-10 )。然后,它包含来自该元素的内部 HTML。这是更多组件示例-ex900。

A458962_1_En_12_Fig10_HTML.jpg

图 12-10

Text between component tags

我们来看更多示例-组件-ex900:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new more-components-ex900 --inline-template
    
    
  2. 开始ng serve:使用以下代码:

    cd more-components-ex900
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”

  4. 编辑类:编辑 app.component.ts 并将其更改为:

    import { Component, ContentChild } from '@angular/core';
    
    @Component({
      selector: 'Paragraph',
      template: `
      <div>
      <b>{{title.nativeElement.innerHTML}}</b>
      <p><ng-content></ng-content></p>
      </div>
      `,
      styles: ['p { border: 1px solid #c0c0c0 }']
    })
    export class Paragraph {
      @ContentChild('title') title;
    }
    
    @Component({
      selector: 'app-root',
      template: `
      <p>
      <Paragraph><title #title>Paragraph 1</title>Lorem ipsum dolor sit amet, consectetur adipiscing elit. In pulvinar egestas massa sit amet scelerisque.</Paragraph>
      <Paragraph><title #title>Paragraph 2</title>Praesent eget ornare neque, vel consectetur eros. Morbi gravida finibus arcu, vel mattis justo dictum a.</Paragraph>
      </p>
      `,
      styleUrls: ['./app.component.css']
    })
    export class AppComponent {
      title = 'welcome to app!';
    }
    
    
  5. 编辑模块:我们在同一个文件中有两个Component类。我们需要确保两个Component都在模块定义中声明——否则,它们将不可用。编辑 app.module.ts 并将其更改为以下内容:

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

您应该会看到两段文字。

内容儿童:示例

您可以使用ContentChildren在包含的附加内容中声明对多个子元素的引用。

这个例子与前一个例子相似,只是它使用ContentChild来获取对被交叉包含的内容中的title元素的引用(参见图 12-11 )。然后,它包含来自该元素的内部 HTML。这是更多组件示例-ex1000。

A458962_1_En_12_Fig11_HTML.jpg

图 12-11

List of people and people count

我们来看更多示例-组件-ex1000:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new more-components-ex1000 --inline-template
    
    
  2. 开始ng serve:使用以下代码:

    cd more-components-ex1000
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”

  4. 编辑类:编辑 app.component.ts 并将其更改为:

    import { Component, ContentChildren } from '@angular/core';
    
    @Component({
      selector: 'Person',
      template: `
      <div>&nbsp;-&nbsp;<ng-content></ng-content></div>
      `,
      styles: ['']
    })
    export class Person {
    }
    
    @Component({
      selector: 'Paragraph',
      template: `
      <div>
      <ng-content></ng-content>
      <p *ngIf="people">Number of people: {{people.length}}</p>
      </div>
      `,
      styles: ['div { border: 1px solid #c0c0c0; margin:10px; padding:10px }', 'p { margin: 5px 0 }']
    })
    export class Paragraph {
      @ContentChildren(Person) people;
    }
    
    @Component({
      selector: 'app-root',
      template: `
      <Paragraph>Lorem ipsum dolor sit amet, consectetur adipiscing elit.
        <Person>Albertus Falx</Person>
        <Person>Godefridus Turpilius</Person>
        <Person>Demipho Renatus</Person>
      </Paragraph>
      <Paragraph>Praesent eget ornare neque, vel consectetur eros.
        <Person>Hanno Grumio</Person>
        <Person>Lycus Auxilius</Person>
      </Paragraph>
        `,
      styleUrls: ['./app.component.css']
    })
    export class AppComponent {
      title = 'welcome to app!';
    }
    
    
  5. 编辑模块:我们在同一个文件中有三个Component类。我们需要确保两个Component都在模块定义中声明——否则,它们将不可用。编辑 app.module.ts 并将其更改为以下内容:

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

您应该会看到两段文字。每一段都要有一个人数列表,底部有一个人数,如图 12-11 。

组件类生命周期

像 AngularJS 一样,Angular 为您管理组件——当它创建组件、更新组件、销毁组件时,等等。每个组件都有所谓的生命周期事件:出生和生活事件,如变化和死亡。有时你需要添加额外的代码,当这些事件发生时,Angular 会为你触发这些代码。

构造函数与 OnInit

有时你需要设置你的组件并初始化它。这里有两种选择:可以使用构造函数或OnInit生命周期方法。当组件第一次初始化时,触发OnInit生命周期方法。

Tip

你应该使用哪一个取决于你,但是许多人遵循这个一般的经验法则。我们主要使用ngOnInit进行初始化/声明,避免在构造函数中工作。构造函数应该只用于初始化类成员,但不应该做实际的“工作”

一旦组件加载并可见,您可能需要添加一些代码来做一些事情。例如,将输入焦点放在第一个字段上,这样用户就可以开始输入了。您可能认为可以将这段代码添加到组件类构造函数中,但这是不正确的,因为构造函数是在组件可见之前触发的。事实上,您可能会在视图初始化之后将这段代码添加到ngAfterViewInit

接口

要挂钩到生命周期方法,组件的类应该实现所需的接口。然后,该接口将强制您实现相应的方法。

例如,要实现一个在视图初始化后触发的方法,您应该实现接口AfterViewInit,它需要方法ngAfterViewInit。表 12-1 有更多的细节。

表 12-1

Interfaces and Methods

| 连接 | 方法 | 描述 | | :-- | :-- | :-- | | `OnChanges` | `ngOnChanges` | 当输入或输出绑定值更改时调用 | | `OnInit` | `ngOnInit` | 第一个`ngOnChanges`之后 | | `DoCheck` | `ngDoCheck` | 开发人员的自定义变更检测 | | `AfterContentInit` | `ngAfterContentInit` | 组件内容初始化后 | | `AfterContentChecked` | `ngAfterContentChecked` | 每次检查成分含量后 | | `AfterViewInit` | `ngAfterViewInit` | 组件视图初始化后 | | `AfterViewChecked` | `ngAfterViewChecked` | 每次检查组件视图后 | | `OnDestroy` | `ngOnDestroy` | 就在指令被销毁之前 |

NgOnChanges:示例

当绑定属性的值更改时,将调用此回调。每当输入属性的值改变时,它就执行。它将接收一个包含绑定的当前和先前值的变更映射,该映射被封装在一个SimpleChange中(参见图 12-12 )。这是更多组件示例-ex1100。

A458962_1_En_12_Fig12_HTML.jpg

图 12-12

Component with a text box that lets you enter text

当您进行更改时,更改会记录在下面。我们来看更多示例-组件-ex1100:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new more-components-ex1100 --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd more-components-ex1100
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”

  4. 编辑类:编辑 app.component.ts 并将其更改为:

    import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
    
    @Component({
      selector: 'name',
      template: `
      <p *ngFor="let change of changes">
      {{change}}
      </p>
      `,
      styles: []
    })
    export class NameComponent implements OnChanges{
      @Input('name') nm;
      changes: Array<string> = [''];
    
      ngOnChanges(changes: SimpleChanges){
        this.changes.push(JSON.stringify(changes));
      }
    }
    
    @Component({
      selector: 'app-root',
      template: `
      Change this field: <input [(ngModel)]="name" />
      <hr/>
      History
      <name [name]="name"></name>
      `,
      styles: []
    })
    export class AppComponent{
      name: string = '';
    }
    
    
  5. 编辑模块:编辑 app.module.ts,修改为:

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

你的应用应该工作在本地主机:4200。请注意以下几点:

  • 这两个组件驻留在同一个文件中,但是它们必须在模块中分别导入和声明。
  • app 组件使用双向绑定到name实例变量。它将name实例变量传递给Name组件。
  • Name组件使用生命周期方法ngOnChanges来监听输入属性的变化(在本例中是name)。当这个方法被触发时,它使用 JSON.stringify 将一个表示更改的字符串转储到下面的更改列表中。

NgOnInit:示例

一旦 Angular 创建完组件并初始化它,这个回调就被调用。它在构造函数之后和第一次触发ngOnChange之后被直接调用。这是一个显示日志的组件,如图 12-13 所示。这将是更多组件示例-ex2000。

A458962_1_En_12_Fig13_HTML.jpg

图 12-13

Displaying logs

组件初始化和调用生命周期方法ngOnInit的日志。我们来看更多示例-组件-ex2000:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new more-components-ex1200 --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd more-components-ex1200
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”

  4. 编辑类:编辑 app.component.ts,修改为:

    import { Component, OnInit } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      template: `
      <p *ngFor="let log of logs">
      {{log}}
      </p>
      `,
      styles: []
    })
    export class AppComponent implements OnInit{
      logs: Array<string> = [ new Date()+''];
    
      constructor(){
        for (let i=0;i<1000;i++){
          console.log(i);
        }
      }
    
      ngOnInit(){
        this.logs.push(new Date()+'');
      }
    }
    
    

你的应用应该工作在本地主机:4200。请注意以下几点:

  • 定义实例变量时,日志被初始化。
  • 构造函数有一些代码来减缓组件的创建。
  • 当 Angular 完成创建组件时,日志会增加。

NgDoCheck:示例

每次检查组件或指令的输入属性时,都会调用此回调。我们可以使用这个生命周期挂钩,用我们自己的定制检查逻辑来扩展检查。

这是一个让你创建一个数组的组件,它会计算出你改变了什么,如图 12-14 所示。这将是更多组件示例-ex1300。

A458962_1_En_12_Fig14_HTML.jpg

图 12-14

Creating an array and figuring out what you change

让我们做得更多-组件-ex1300:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new more-components-ex1300 --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd more-components-ex1300
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”

  4. 编辑类:编辑 app.component.ts 并将其更改为:

    import { Component, Input, DoCheck, IterableDiffers } from '@angular/core';
    
    @Component({
      selector: 'numbers',
      template: `
      {{numbers}}
      <br/>
      <p *ngFor="let change of changes">
      {{change}}
      </p>
      `,
      styles: ['p{padding:0;margin:0}']
    })
    export class NumbersComponent implements DoCheck {
      @Input('numbers') numbersArray: Array<string>;
      changes: Array<string> = [];
      differ;
    
      constructor(private differs: IterableDiffers) {
        this.differ = differs.find([]).create(null);
      }
    
      ngDoCheck() {
        const differences = this.differ.diff(this.numbersArray);
        if (differences) {
          if (differences.forEachAddedItem) {
            differences.forEachAddedItem((item) => {
              if ((item) && (item.item)){
                this.changes.push('added ' + item.item);
    
              }
            });
          }
          if (differences.forEachRemovedItem) {
            differences.forEachRemovedItem((item) => {
              if ((item) && (item.item)){
                this.changes.push('removed ' + item.item);
              }
            });
          }
        }
      }
    }
    
    @Component({
      selector: 'app-root',
      template: `
      Enter Array (comma-separated): <input [(ngModel)]="numbers" (onModelChange)="onModelChange"/>
      <br/>
      <numbers [numbers]="numbers.split(',')"></numbers>
      `,
      styles: []
    })
    export class AppComponent {
      numbers = '';
    }
    
    
  5. 编辑模块:编辑 app.module.ts,修改为:

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

你的应用应该工作在本地主机:4200。请注意以下几点:

  • app 组件将input字符串解析成一个数组,并将其传递给Numbers组件。
  • Numbers组件有一个通过构造函数注入的Iterable Differ,因此它可以是一个实例变量并在以后使用。
  • 当输入发生变化并且Numbers组件的input属性发生变化时,该组件使用differ来分析这些变化,并将每个变化添加到变化日志中。

NgAfterContentInit:示例

这个回调在 ngOnInit 之后被调用:当组件或指令的内容已经被初始化并且绑定已经被第一次检查时。

在本例中,app 组件声明了一个 crew 结构,其内容中包含成员,如图 12-15 所示。稍后,此生命周期回调用于选择列表中的第一个组员。这将是更多组件示例-ex1400。

A458962_1_En_12_Fig15_HTML.jpg

图 12-15

Declaring a crew structure and selecting a member

是时候做更多示例了-组件-ex1400:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new more-components-ex1400 --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd more-components-ex1400
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”

  4. 编辑类:编辑 app.component.ts 并将其更改为:

    import { Component, Input, AfterContentInit, ContentChildren, QueryList } from '@angular/core';
    
    @Component({
      selector: 'member',
      template: `
      <p [style.backgroundColor]="getBackgroundColor()"><ng-content></ng-content></p>
      `,
      styles: ["p{padding: 5px}"]
    })
    export class MemberComponent {
    
      selected = false;
      getBackgroundColor(){
        return this.selected ? "#FFCCCC" : "#CCFFFF";
      }
    }
    
    @Component({
      selector: 'crew',
      template: `
      <p><ng-content></ng-content></p>
      `,
      styles: []
    })
    export class CrewComponent implements AfterContentInit {
      @ContentChildren(MemberComponent) members: QueryList<MemberComponent>;
    
      ngAfterContentInit() {
        this.members.first.selected = true;
      }
    }
    
    @Component({
      selector: 'app-root',
      template: `
      <crew>
        <member>Captain Kirk</member>
        <member>Spock</member>
        <member>Sulu</member>
        <member>Bones</member>
        <member>Checkov</member>
      </crew>
      `,
      styles: []
    })
    export class AppComponent {}
    
    
  5. 编辑模块:编辑 app.module.ts,修改为:

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

你的应用应该工作在本地主机:4200。请注意以下几点:

  • app 组件声明了一个成员在其内容中的 crew 结构。
  • Crew 组件声明实例变量members(使用@ContentChildren声明)以映射到内容中自己的crew标签内的船员列表。这个变量属于QueryList类型,因此可以更容易地查询它。
  • 一旦 Angular 为您设置了 members 的实例变量,Crew 内容就会使用生命周期方法ngAfterContentInit来访问它。然后,这个生命周期方法设置第一个memberselected实例变量,以便突出显示他或她。
  • 成员组件显示了member标签中的内容,并根据selected实例变量设置了组件的背景颜色。

NgAfterContentChecked:示例

每次检查完组件或指令的内容后,都会执行该回调,当检查完组件的所有绑定时,该回调会有效;即使他们没有改变。这个例子允许你挑选一张牌,如图 12-16 所示。这将是更多组件示例-ex1500。

A458962_1_En_12_Fig16_HTML.jpg

图 12-16

Picking a card

我们来看更多示例-组件-ex1500:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new more-components-ex1500 --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd more-components-ex1500
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”

  4. 编辑类:编辑 app.component.ts 并将其更改为:

    import { Component, ContentChild, AfterContentChecked } from '@angular/core';
    
    @Component({
      selector: 'card',
      template: `
      <ng-content></ng-content>
      `,
      styles: []
    })
    export class CardComponent {
    }
    
    @Component({
    
      selector: 'app-root',
      template: `
      <card>{{card}}</card>
      <button (click)="pickCard($event)">Pick a Card</button>
      `,
      styles: []
    })
    export class AppComponent implements AfterContentChecked {
      card = CARD_ACE_OF_SPADES;
    
      @ContentChild(CardComponent) contentChild: CardComponent;
    
      ngAfterContentChecked() {
        console.log("content inside card has been checked: " + this.card);
      }
    
      pickCard() {
        this.card = this.card === CARD_ACE_OF_SPADES ? CARD_TEN_OF_CLUBS : CARD_ACE_OF_SPADES;
      }
    }
    
    const CARD_ACE_OF_SPADES = 'ace of spades';
    const CARD_TEN_OF_CLUBS = 'ten of clubs';
    
    
  5. 编辑模块:编辑 app.module.ts,修改为:

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

你的应用应该工作在本地主机:4200。请注意以下几点:

  • app 组件使用卡片组件来显示当前卡片。当前 card 是一个实例变量,其值作为内部内容放在 Card 组件中。
  • app 组件有一个按钮可以让你翻到另一张卡。它会更改实例变量的值。
  • app 组件有一个ngAfterContentChecked方法,当 Card 组件中的内容改变时会自动触发该方法。当当前卡片改变时触发。

NgAfterViewInit:示例

这个回调在组件视图及其子视图被创建并初始化后被调用。这对于执行组件初始化很有用。请注意,@ViewChild@ViewChildren实例变量此时已经设置好并可用(与组件生命周期的早期不同)。本示例向您展示如何设置初始输入焦点,如图 12-17 所示。这将是更多组件示例-ex1600。

A458962_1_En_12_Fig17_HTML.jpg

图 12-17

Setting initial inpur focus

我们来看更多示例-组件-ex1600:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new more-components-ex1600 --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd more-components-ex1600
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”

  4. 编辑类:编辑 app.component.ts,修改为:

    import { Component, AfterViewInit, ViewChild } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      template: `
        First Input Field: <input #firstInput />
      `,
      styles: []
    })
    export class AppComponent implements AfterViewInit{
      @ViewChild('firstInput') firstInput;
    
      ngAfterViewInit(){
        // ViewChild variables are available in this method.
        // Set initial focus.
        this.firstInput.nativeElement.focus();
      }
    }
    
    

你的应用应该工作在本地主机:4200。请注意以下几点:

  • app 组件设置firstInput实例变量来引用声明为# firstInput的模板变量。
  • app 组件有一个ngAfterViewInit方法,一旦视图被初始化并且firstInput实例变量可用,就会触发该方法。此方法设置初始输入焦点。

NgAfterViewChecked:示例

每次检查组件视图后都会调用这个回调。它只适用于组件,当所有子指令的绑定都被检查时,即使它们没有改变。如果组件正在等待来自其子组件的东西,这可能会很有用。

不要在这里设置任何绑定到模板的变量。如果这样做,您将收到“检查后表达式已更改”错误。

该示例允许您输入一些内容,并显示一条消息,提示您输入的内容是否为数字,如图 12-18 所示。这将是更多组件示例-ex1700。

A458962_1_En_12_Fig18_HTML.jpg

图 12-18

Displaying whether input is numeric or not

我们来看更多示例-组件-ex1700:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new more-components-ex1700 --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd more-components-ex1700
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”

  4. 编辑类:编辑 app.component.ts,修改为:

    import { Component, ViewChild, AfterViewChecked } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      template: `
      <input [(ngModel)]="input"/>
      <br/>
      {{input}}
      <br/>
      <div #message></div>
      `,
      styles: []
    })
    export class AppComponent implements AfterViewChecked {
      input: string = '';
    
      @ViewChild('message') message;
    
      ngAfterViewChecked(){
        console.log('AfterViewChecked');
        if (isNaN(parseInt(this.input))){
          this.message.nativeElement.innerHTML = "Input not numeric.";
        }else{
          this.message.nativeElement.innerHTML = "Input is numeric.";
        }
      }
    }
    
    
  5. 编辑类:编辑 app.module.ts,修改为:

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

你的应用应该工作在本地主机:4200。请注意以下几点:

  • app 组件设置message实例变量来引用声明为# message的模板变量。
  • app 组件有一个ngAfterViewChecked方法,一旦检查了视图的绑定,就会触发该方法。这是我们检查输入并设置message来指示用户输入是否为数字的方法。

NgOnDestroy:示例

当组件、指令、管道或服务被销毁时,将调用此回调。在这里添加代码以销毁任何可能作为实例变量保留的引用(也就是说,清理您的引用)。

这个例子使用一个时间间隔计时器来计数,当组件被破坏时,这个计时器就会被破坏,如图 12-19 所示。这是准备处理组件的最佳位置,例如,取消后台任务。这将是更多组件示例-ex1800。

A458962_1_En_12_Fig19_HTML.jpg

图 12-19

Counting up with an interval timer

现在我们来看更多示例-组件-ex1800:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new more-components-ex1800 --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd more-components-ex1800
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”

  4. 编辑类:编辑 app.component.ts,修改为:

    import { Component, OnInit, OnDestroy } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      template: `
      <h1>
        {{count}}
      </h1>
      `,
      styles: []
    })
    export class AppComponent implements OnInit, OnDestroy{
      interval;
      count = 0;
    
      ngOnInit(){
        this.interval = setInterval(() => {
          this.count++;
        })
      }
    
      ngOnDestroy(){
        clearInterval(this.interval);
        delete this.interval;
      }
    }
    
    

你的应用应该工作在本地主机:4200。请注意以下几点:

  • app 组件有一个ngOnInit方法,一旦加载就被触发。它初始化时间间隔,该时间间隔递增计数。
  • app 组件有一个在被销毁时触发的ngOnDestroy方法。它清除间隔,停止向上计数。

摘要

这是一个重要的章节,因为你将花费大部分时间编写组件。你需要知道你的用户界面是由一系列组件组成的,每一个组件从创建到销毁都有自己的生命周期。

注意您的数据是如何向下流动的,从高级组件向下到低级组件,这也是非常重要的。你需要知道事件如何向相反的方向发展。

在下一章,我们将讨论如何创建服务对象来执行非可视任务,并使用依赖注入将这些对象插入到组件中。

十三、依赖注入

在软件工程中,依赖注入是一种软件设计模式,它实现了用于解决依赖的控制反转。依赖是可以使用的对象(服务)。注入是将依赖关系传递给使用它的依赖对象(客户机)。在习惯了 Angular 之后,你会认为依赖注入是理所当然的,因为它太容易使用了。

例如,这段代码

var svc = new ShippingService(new ProductLocator(),
   new PricingService(), new InventoryService(),
   new TrackingRepository(new ConfigProvider()),
   new Logger(new EmailLogger(new ConfigProvider())));

可以被这样的东西代替:

var svc = container.Resolve<IShippingService>();

依赖注入的一些优点包括:

  • 你的代码更干净,可读性更强。
  • 对象是松散耦合的。
  • 有可能消除或至少减少组件不必要的依赖。
  • 减少组件的依赖性通常会使它更容易在不同的上下文中重用。
  • 提高组件的可测试性。
  • 将依赖关系移动到组件的接口,这样您就不用显式地引用依赖关系,而是通过接口来引用它们。

服务和供应器

Angular 提供的服务列于表 13-1 中。

表 13-1

Angular’s Provided Services

| 服务 | 描述 | | :-- | :-- | | 超文本传送协议(Hyper Text Transport Protocol 的缩写) | 对于与服务器的 HTTP 通信 | | 形式 | 表单处理程序代码 | | 路由器 | 页面导航代码 | | 动画 | 用户界面动画 | | 用户界面库 | 例如,`NgBootstrap` |

Tip

您可以从 www.ngmodules.org 下载其他服务。

您可能希望编写以下服务的特定实现:

  • 服务器通信
  • 安全
  • 审计
  • 记录
  • 会议

请记住,您的实现可以“包装”其他服务。例如,您的服务器通信服务本身可以使用 Angular Http 服务并添加更多的功能,实现不同的东西,或者只是具有不同的配置。

编写服务时,通常将它们编写为 TypeScript 类,每个类一个文件(filename.service.ts)。使用@Injectable()注释将这些类标记为可注入的是一个好主意。@Injectable()将一个类标记为可用于实例化的注入器。一般来说,当试图实例化一个没有标记为@Injectable()的类时,注入器会报告一个错误。

提供者用于注册类、函数或值,以便依赖注入可以使用它们。Injector类使用提供者来提供信息,这样它就可以创建一个对象的实例来注入另一个对象。因此,提供者基本上是如何创建对象实例的信息源。该信息包括令牌,即可能需要创建的对象的标识符。当你在 Angular 代码中看到provider()时,你看到的是一个 Angular 函数的调用,用来注册如何创建一个对象的信息。

有三种类型的提供者:类提供者、工厂提供者和值提供者。在这一章的后面,我将首先介绍类提供者,因为它们是最常用的。

每个组件都有自己的注入器,用于提供者为组件创建对象。当组件有子组件时,注射器为子组件创建子注射器。

当依赖注入需要将一个对象注入到一个组件中时,它会尝试使用get方法在本地注入器(即组件的注入器)中解析该对象。如果不能解决这个问题(换句话说,如果对象不存在于注入器中),它将尝试在父组件的注入器中解决该对象,依此类推,一直到应用组件。这确保了优先使用最近的(本地)注射器提供者,而不是更高级别的提供者。这就是所谓的隐藏,类似于同名的局部变量优先于同名的全局变量。

通常 Angular 会为您处理从属关系的解析和创建。然而,Injector类提供了您自己调用它的方法——例如,resolveAndCreate

创建服务:示例

这是一个简单的组件,它使用服务来提供关于汽车的信息。这将是依赖注入示例 ex100。它将在汽车组件中提供服务,如图 13-1 所示。

A458962_1_En_13_Fig1_HTML.gif

图 13-1

Component using a service to provide information

让我们试试这个练习:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new dependency-injection-ex100 --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd dependency-injection-ex100
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“应用工作!”

  4. 创建服务类:创建 car.service.ts 并将其更改为:

    import { Injectable } from '@angular/core';
    
    @Injectable()
    export class CarService {
        constructor(){
            console.log('CarService: constructor');
        }
        // Some dummy method.
        isSuperCharged(car: string){
            return car === 'Ford GT' ? 'yes' : 'no';
        }
    }
    
    
  5. 编辑类:编辑 app.component.ts,修改为:

    import { Component, OnInit, Input } from '@angular/core';
    import { CarService } from './car.service';
    
    @Component({
      selector: 'car',
      template: `
      <h3>
        {{name}} Is Supercharged: {{supercharged}}
      </h3>
      `,
      styles: [],
      providers: [CarService]
    })
    export class CarComponent implements OnInit{
      @Input() name;
      supercharged: string = '';
      constructor(private service: CarService){}
      ngOnInit(){
        this.supercharged = this.service.isSuperCharged(this.name);
      }
    }
    
    @Component({
    
      selector: 'app-root',
      template: `
      <car name="Ford GT"></car>
      <car name="Corvette Z06"></car>
      `,
      styles: []
    })
    export class AppComponent {
      title = 'app works!';
    }
    
    
  6. 编辑模块:编辑 app.module.ts,修改为:

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

你的应用应该工作在本地主机:4200。请注意以下几点:

A458962_1_En_13_Fig2_HTML.jpg

图 13-2

Returning a yes or no

  • car服务在构造函数中输出一个日志。该服务包含一个方法isSuperCharged,该方法接收汽车名称作为参数,并相应地返回 yes 或 no,如图 13-2 所示。
  • app 组件有一个使用了两次的 car 组件。car 组件将car服务指定为提供者。汽车组件调用service方法isSuperCharged,方法ngOnInit. ngOnInit在组件初始化后被触发。

为什么会创建同一服务的多个实例?打开控制台,你会看到类似图 13-3 的东西。

A458962_1_En_13_Fig3_HTML.jpg

图 13-3

Constructor invoked twice

正如您所看到的,构造函数被调用了两次,因为服务被创建了两次。这是因为CarService是在汽车组件中提供的,并且汽车组件被创建了两次。以下是汽车部件的摘录:

@Component({
  selector: 'car',
  ...
  providers: [CarService]
})
export class CarComponent implements OnInit{
  ...
  constructor(private service: CarService){}
  ...
}

我们想要的是创建服务的单个实例,如图 13-4 所示。

A458962_1_En_13_Fig4_HTML.jpg

图 13-4

We want one instance of a service created

要做到这一点,我们只需将提供者转移到应用级别或只使用一次的类。在代码示例中,我们可以在

如果我们想要共享服务的单例,该怎么办?我们没有指定我们在Car对象上需要它,因为有多个Car。我们需要指定我们在应用级别的其他地方需要该服务。

让我们转换我们的应用来共享服务的一个实例。

转换应用以共享服务的一个实例:示例依赖项-注入-ex200

这是一个使用服务提供汽车信息的简单组件,如图 13-5 所示。

A458962_1_En_13_Fig5_HTML.jpg

图 13-5

Service providing information about cars

示例 dependency-injection-ex200 与 dependency-injection-ex100 相同,只是它在 app 组件中提供服务,因此只创建一个CarService实例,如图 13-6 所示。

A458962_1_En_13_Fig6_HTML.gif

图 13-6

One instance of CarService provided in the app component

让我们做练习依赖-注射-ex200:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new dependency-injection-ex200 --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd dependency-injection-ex200
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“应用工作!”

  4. 创建服务类:这与前面的例子相同。编辑 car.service.ts 并将其更改为以下内容:

    import { Injectable } from '@angular/core';
    
    @Injectable()
    export class CarService {
        constructor(){
            console.log('CarService: constructor');
        }
        // Some dummy method.
        isSuperCharged(car: string){
            return car === 'Ford GT' ? 'yes' : 'no';
        }
    }
    
    
  5. 编辑类:这与前面的例子不同。编辑 app.component.ts 并将其更改为以下内容:

    import { Component, OnInit, Input } from '@angular/core';
    import { CarService } from './car.service';
    
    @Component({
      selector: 'car',
      template: `
      <h3>
        {{name}} Is Supercharged: {{supercharged}}
      </h3>
      `,
      styles: [],
      providers: []
    })
    export class CarComponent implements OnInit{
      @Input() name;
      supercharged: string = '';
      constructor(private service: CarService){}
      ngOnInit(){
        this.supercharged = this.service.isSuperCharged(this.name);
      }
    }
    
    @Component({
      selector: 'app-root',
      template: `
      <car name="Ford GT"></car>
      <car name="Corvette Z06"></car>
      `,
      styles: [],
      providers: [CarService]
    })
    export class AppComponent {
      title = 'app works!';
    }
    
    
  6. 编辑模块:这与前面的例子相同。编辑 app.module.ts 并将其更改为以下内容:

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

你的应用应该工作在本地主机:4200。请注意,CarService构造函数在控制台中只记录一次。这是因为它只需要在 app 组件中创建一次,就可以被所有子组件使用。

转换应用以共享服务的一个实例:示例依赖项-注入-ex300

这是一个使用服务提供汽车信息的简单组件,如图 13-7 所示。

A458962_1_En_13_Fig7_HTML.jpg

图 13-7

Service providing information about cars

示例 dependency-injection-ex300 与 dependency-injection-ex200 相同,只是它在模块中提供服务,因此只创建了一个CarService实例,可以在应用中的任何地方使用,如图 13-8 所示。

A458962_1_En_13_Fig8_HTML.gif

图 13-8

One instance of CarService provided in the module

让我们做练习依赖-注射-ex300:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new dependency-injection-ex300 --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd dependency-injection-ex300
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“应用工作!”

  4. 创建服务类:这与前面的例子相同。编辑 car.service.ts 并将其更改为以下内容:

    import { Injectable } from '@angular/core';
    
    @Injectable()
    export class CarService {
        constructor(){
            console.log('CarService: constructor');
        }
        // Some dummy method.
        isSuperCharged(car: string){
            return car === 'Ford GT' ? 'yes' : 'no';
        }
    }
    
    
  5. 编辑类:这与前面的例子不同。编辑 app.component.ts 并将其更改为以下内容:

    import { Component, OnInit, Input } from '@angular/core';
    import { CarService } from './car.service';
    
    @Component({
      selector: 'car',
      template: `
      <h3>
        {{name}} Is Supercharged: {{supercharged}}
      </h3>
      `,
      styles: []
    })
    export class CarComponent implements OnInit{
    
      @Input() name;
      supercharged: string = '';
      constructor(private service: CarService){}
      ngOnInit(){
        this.supercharged = this.service.isSuperCharged(this.name);
      }
    }
    
    @Component({
      selector: 'app-root',
      template: `
      <car name="Ford GT"></car>
      <car name="Corvette Z06"></car>
      `,
      styles: []
    })
    export class AppComponent {
      title = 'app works!';
    }
    
    
  6. 编辑模块:这不同于前面的例子。编辑 app.module.ts 并将其更改为以下内容:

    import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';
    import { FormsModule } from '@angular/forms';
    import { HttpModule } from '@angular/http';
    
    import { AppComponent, CarComponent } from './app.component';
    import { CarService } from './car.service';
    
    @NgModule({
      declarations: [
        AppComponent, CarComponent
      ],
      imports: [
        BrowserModule,
        FormsModule,
        HttpModule
      ],
      providers: [CarService],
      bootstrap: [AppComponent]
    })
    export class AppModule { }
    
    

你的应用应该工作在本地主机:4200。请注意,CarService构造函数在控制台中只记录一次。这是因为它只需要在 App 模块中创建一次,就可以被所有子组件使用。

类提供者:示例依赖注入 ex350

如前所述,有三种类型的提供者:类提供者、工厂提供者和值提供者。类提供者允许我们告诉提供者哪个类用于依赖项。

图 13-9 显示了一个依赖于Watch服务的组件。

A458962_1_En_13_Fig9_HTML.jpg

图 13-9

Component relying on a Watch service

让我们看看依赖注入示例 ex350:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new dependency-injection-ex350 --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd dependency-injection-ex350
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“应用工作!”

  4. 编辑类:编辑 app.component.ts,修改为:

    import { Component } from '@angular/core';
    
    class Watch {
      getTime(): string {
        return new Date() + "";
      }
    }
    
    class Seiko extends Watch {
      getTime(): string{
        return "Seiko Time:" + super.getTime();
    
      }
    }
    
    @Component({
      selector: 'app-root',
      template: `
      <h1>
        {{watch.getTime()}}
      </h1>
      `,
      styles: [],
      providers: [{
        provide: Watch,
        useClass: Seiko
      }]
    })
    export class AppComponent {
      constructor(private watch:Watch){}
    }
    
    

你的应用应该工作在本地主机:4200。注意,当我们使用@Component注释的Provider元素来创建依赖关系时,我们指定了Watch的子类(一个精工)。

工厂提供者:示例依赖项-注入-ex400

工厂提供程序使用函数为 Angular 提供对象的实例。当您需要基于某些数据动态地更改您想要创建的对象时,这很有用。

这是一个使用日志服务的简单组件:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new dependency-injection-ex400 --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd dependency-injection-ex400
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“应用工作!”

  4. 创建服务类:这与前面的例子相同。创建 logging.service.ts 并将其更改为以下内容:

    import { Injectable } from '@angular/core';
    
    @Injectable()
    export class LoggingService {
        constructor(private dateAndTime: boolean){
            console.log('LoggingService: constructor');
        }
        log(message){
            console.log((this.dateAndTime ? new Date() + ': ' : '') + message);
        }
    }
    
    
  5. 编辑类:编辑 app.component.ts,修改为:

    import { Component } from '@angular/core';
    import { LoggingService } from './logging.service';
    
    @Component({
      selector: 'app-root',
      template: `
      <h1>
        {{title}}
      </h1>
      `,
      styles: [],
      providers: [provideLoggingService()]
    })
    export class AppComponent {
      constructor(private logging: LoggingService){
        logging.log('test log');
      }
      title = 'app works!';
    }
    export const LOGGING_USE_DATE = false;
    export function provideLoggingService() {
      return {
        provide: LoggingService,
        useFactory: () => new LoggingService(LOGGING_USE_DATE)
      }
    }
    
    

你的应用应该工作在本地主机:4200。请注意,该日志服务可以选择包含日志日期和时间。您可以使用日志记录服务的构造函数进行设置。工厂提供者用于提供日志服务的实例。

图 13-10 显示了测井包含的日期;

A458962_1_En_13_Fig10_HTML.jpg

图 13-10

Date included in logging

export const LOGGING_USE_DATE = true;
export function provideLoggingService() {
  return {
    provide: LoggingService,
    useFactory: () => new LoggingService(LOGGING_USE_DATE)
  }
}

图 13-10 显示了不包括在测井中的日期;

A458962_1_En_13_Fig11_HTML.jpg

图 13-11

Date not included in logging

export const LOGGING_USE_DATE = false;
export function provideLoggingService() {
  return {
    provide: LoggingService,
    useFactory: () => new LoggingService(LOGGING_USE_DATE)
  }
}

工厂提供者:示例依赖注入 ex500

这是一个显示扑克牌的简单组件,如图 13-12 所示。

A458962_1_En_13_Fig12_HTML.jpg

图 13-12

Displaying a playing card

让我们以依赖注入 ex500 为例:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new dependency-injection-ex500 --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd dependency-injection-ex500
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“应用工作!”

  4. 创建Card类:这与前面的例子相同。编辑 card.ts 并将其更改为以下内容:

    import { Injectable } from '@angular/core';
    
    @Injectable()
    export class Card {
        constructor(public suite: string, public rank: string) {}
        toString(): string {
            return "Card is " + this.rank + " of " + this.suite;
        }
    }
    
    
  5. 编辑类:编辑 app.component.ts,修改为:

    import { Component } from '@angular/core';
    import { Card } from './card';
    @Component({
      selector: 'app-root',
      template: `
      <h1>
        {{title}}
      </h1>
      `,
      styles: [],
      providers: [{
        provide: Card,
        useFactory: () => {
          const suite: number = Math.floor(Math.random() * 4);
          const suiteName: string =
            suite == 0 ? "Clubs" :
            suite == 1 ? "Diamonds" :
            suite == 2 ? "Hearts" : "Spades";
          const rank: number = Math.floor(Math.random() * 15);
          const rankName: string =
            rank == 0 ? "Ace" :
            rank == 1 ? "Joker" :
            rank == 2 ? "King" :
            rank == 3 ? "Queen" :
            (rank - 3).toString();
          return new Card(suiteName, rankName);
        }
      }]
    })
    
    export class AppComponent {
      title = 'app works!';
      constructor(card:Card){
        this.title = card.toString();
    
      }
    }
    
    

价值提供者:示例依赖关系-注入-ex600

您已经看到了类提供者和工厂提供者的代码和示例。现在让我们看看价值提供者。值提供者只是提供一个对象的值,如图 13-13 所示。

A458962_1_En_13_Fig13_HTML.jpg

图 13-13

Value of an object

让我们看一下依赖注入示例 ex600:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new dependency-injection-ex600 --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd dependency-injection-ex600
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“应用工作!”

  4. 编辑类:编辑 app.component.ts,修改为:

    import { Component, Injector } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      template: `
      <h1>
        {{title}}
      </h1>
      `,
      styles: [],
      providers: [{
        provide: 'language',
        useValue: 'en'
      }]
    })
    export class AppComponent {
      title: string = '';
      constructor(private injector: Injector){
        this.title = 'Language is: ' + injector.get('language');
      }
     }
    
    

注射器 API

您还不需要非常详细地了解注射器 API。如果你在本章的这一点上感到不知所措,请随意跳到下一章,稍后再回到这一章。

然而,如果您想要对创建依赖关系有更多的控制,您可以直接访问Injector对象。Injector是角芯包中的一类。它是一个依赖注入容器,用于实例化对象和解析依赖关系。

如果您试图解析和创建(使用Injector)的类本身有依赖关系,Injector会自动尝试为您解析和创建这些依赖关系。你也可以使用Provider类中的附加选项。

这里有一个例子:

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

const injector = Injector.resolveAndCreate([Car, Engine, Tires, Doors]);

const car = injector.get(Car);

另一个例子是:

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

const injector = Injector.resolveAndCreate(
  [
    provide(Car, useClass: Car)),
    provide(Engine, useClass: Engine)),
    provide(Tires, useClass: Tires)),
    provide(Doors, useClass: Doors))
  ]
);

const car = injector.get(Car);

摘要

本章涵盖了很多内容,从依赖注入的概念一直到Injector本身。此时,您不需要了解关于注射器 API 的一切。

您需要知道的是 Angular 中依赖注入的基础——如何设置您的提供者和使用构造器注入。我希望您遵循了这些示例,并且它们帮助您理解依赖注入是如何工作的。

通过依赖注入来使用服务是你会一直用到的。大多数情况下,您的应用将使用每个服务的一个实例。每个服务有一个实例非常有用,因为您可以使用这些服务来保存多个组件访问的状态信息(例如客户列表)。此外,有时您将使用单实例服务作为组件之间的“通信”桥梁。

在下一章,我们将开始讨论第三方部件库。当大公司为你提供经过良好测试和设计精美的 UI 小部件库时,为什么要花费宝贵的时间来开发定制的外观呢?

十四、Angular 和 UI 小部件

Angular 是许多新 JavaScript 应用的核心。但是,你需要把 Angular 和一个前端 UI 框架耦合起来,比如 Bootstrap 或者 Material Design。在这一章中,我首先介绍 Bootstrap,因为它是目前两者中比较常见的,然后我将讨论材料设计。

将 UI 小部件库与 Angular 一起使用

您可以通过两种方式使用 UI 小部件库:

  • 前 Angular 方式:以正常方式使用 HTML 标记和 JavaScript。
  • 使用自定义标记指令。您利用第三方定制组件和指令模块来生成 UI 小部件 HTML 标记。

预 Angular 方式

您可以使用 HTML 和 JavaScript 创建组件,这些组件的样式与您在 JQuery 或另一个早期的 JavaScript 库中使用的样式相同。

当然,HTML 是一种标记语言,也是 web 上最常见的文档格式。标记语言注释文档。包括 HTML 在内的一些标记语言有确定如何显示结构化数据的规范,它告诉计算机如何显示某些内容。在 Angular 中,我们编写动态用户界面,Angular 组件使用 HTML 标记来告诉计算机如何显示事物。

标记在模板中,模板在组件的@Component注释中指定。它有时也在@View注释中指定(稍后会详细介绍)。

Angular 的方式

您可以使用预构建和样式化的 Angular 组件和指令的模块来创建组件,这些组件作为一个模块交付,以便您可以重用它们。这是一个组件对象(就像你在前面章节中写的那些)和指令的模块,使你能够使用标签来创建一个引导 UI。这需要您使用其他人的代码,但是通过提供预构建的组件和使用它们的指令,可以节省您的时间。第十一章涵盖指令。

在这一章(和其他章节)中,我使用了 ng2-bootstrap 模块( http://valor-software.github.io/ng2-bootstrap/ ),这是 Angular 的一个 bootstrap 实现。

使用 NgBootstrap 时的预 Angular 与 Angular

图 14-1 显示了一个常见的 UI 元素:选项卡。我们将在有和没有 ng2-bootstrap 模块的情况下为同一个选项卡编写 HTML 标记。

A458962_1_En_14_Fig1_HTML.jpg

图 14-1

Common UI element: a tab

以下是使用 HTML、CSS 和 JavaScript 的预 Angular 方法:

<div class="tabbable tabs-left" style="margin-top: 100px;">
  <ul class="nav nav-tabs">
    <li class="active"><a href="#pane1" data-toggle="tab" rel="popover" id="tab">Homee</a></li>
    <li><a href="#pane2" data-toggle="tab" title="blah blah" id="tab1">Profile</a></li>
    <li><a href="#pane3" data-toggle="tab" id="tab2">Messages</a></li>
    <li><a href="#pane4" data-toggle="tab">Settings</a></li>
  </ul>
  <div class="tab-content">
    <div id="pane1" class="tab-pane active">...</div>
    <div id="pane2" class="tab-pane">...</div>
    <div id="pane3" class="tab-pane">...</div>
    <div id="pane4" class="tab-pane">...</div>
  </div>
</div>

这是 ng2 自举模块的 Angular:

<ngb-tabset>
  <ngb-tab title="Home">
    <ng-template ngbTabContent>
      ...
    </ng-template>
  </ngb-tab>
  <ngb-tab title="Profile">
    <ng-template ngbTabContent>
      ...
    </ng-template>
  </ngb-tab>
  <ngb-tab title="Messages">
    <ng-template ngbTabContent>
      ...
    </ng-template>
  </ngb-tab>
  <ngb-tab title="Settings">
    <ng-template ngbTabContent>
      ...
    </ng-template>
  </ngb-tab>
</ngb-tabset>

该模块使代码更小。注意它调用标签ngb-tab s 而不是div s。

引导程序

Bootstrap 是一个开源的工具组,拥有 HTML 和 CSS 设计模板,用于表单、按钮、排版和导航等界面元素,以及可选的 JavaScript 扩展。Bootstrap 使开发动态网站和 web 应用变得更加容易。它兼容大多数浏览器的最新版本,包括 Firefox、Internet Explorer、Google Chrome、Opera 和 Safari,但不是在所有平台上。

Bootstrap 及更高版本还支持响应式 web 设计,它可以根据正在使用的任何设备(无论是手机、平板电脑还是台式机)的特征动态调整网页的布局。从 3.0 版本开始,Bootstrap 拥有移动优先的设计理念,默认采用响应式设计。它提供了一个网格系统,允许开发人员(他们可能缺乏响应性设计方面的技能)编写在所有设备上都同样适用的代码。没有启用响应功能的默认 12 列网格系统使用 940 像素宽的容器。添加了响应 CSS 文件后,网格将变为 724×1170 像素宽,具体取决于您的视口。在 767 像素视口下,列变得流畅并垂直堆叠。

更多关于 Bootstrap 的信息,请访问 http://getbootstrap.com 。图 14-2 展示了一个用 Bootstrap 制作的网页。

A458962_1_En_14_Fig2_HTML.jpg

图 14-2

Web page made with Bootstrap

安装 ng 引导程序

ng-bootstrap 是 bootstrap 库的 Angular 版本,您可以使用 Bootstrap 小部件快速构建应用。源代码在 https://github.com/ng-bootstrap/ng-bootstrap 可用,演示在 https://ng-bootstrap.github.io/#/components/accordion/examples 可用。

下面是安装 ng-bootstrap 的方法:

  1. 以通常的方式使用 CLI 构建应用。

  2. 使用 npm 安装 ng-bootstrap 和 bootstrap 模块:

    npm install --save @ng-bootstrap/ng-bootstrap bootstrap
    
    
  3. 告诉 CLI 项目使用引导 CSS 文件中的样式。编辑. angular.json 并在 styles 下添加以下条目:

    "../node_modules/bootstrap/dist/css/bootstrap.css",
    
    
  4. 编辑你的模块文件(app.module.ts)并将NgbModule指定为import。这将使NgbModule中的代码可用于该 Angular 模块:

    imports: [
        NgbModule.forRoot(),
        BrowserModule
      ],
    
    

引导程序:示例小部件-ex100

该组件允许用户使用一组按钮来选择披萨,这些按钮的作用类似于一组单选按钮,如图 14-2 所示。

A458962_1_En_14_Fig3_HTML.jpg

图 14-3

Selecting a pizza

让我们看一下这个例子:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new widgets-ex100 --inline-template --inline-style
    
    
  2. 安装ng-bootstrapbootstrap:使用以下代码:

    cd widgets-ex100
    npm install --save @ng-bootstrap/ng-bootstrap bootstrap
    
    
  3. 将引导程序样式安装到项目中:编辑。angular-cli.json 并在 styles 下添加以下条目:

    "../node_modules/bootstrap/dist/css/bootstrap.css",
    
    

    样式块应该如下所示:

    "styles": [
            "../node_modules/bootstrap/dist/css/bootstrap.css",
            "styles.css"
          ],
    
    
  4. 开始ng serve:使用该命令:

    ng serve
    
    
  5. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”

  6. 编辑模块:编辑 app.module.ts,修改为:

    import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';
    import { FormsModule } from '@angular/forms';
    import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
    
    import { AppComponent } from './app.component';
    
    @NgModule({
      declarations: [
        AppComponent
      ],
      imports: [
        NgbModule.forRoot(),
        BrowserModule,
        FormsModule
      ],
      providers: [],
      bootstrap: [AppComponent]
    })
    export class AppModule { }
    
    
  7. 编辑类:编辑 app.component.ts,修改为:

    import { Component } from '@angular/core';
    import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
    
    @Component({
      selector: 'app-root',
      template: `
        <div style="padding:10px">
        <h2>Please select your pizza:</h2>
        <div [(ngModel)]="model" ngbRadioGroup name="radioBasic">
          <label ngbButtonLabel class="btn btn-primary">
          <input ngbButton type="radio" value="Hawaiian"> Hawaiian
          </label>
          <label ngbButtonLabel class="btn btn-primary">
          <input ngbButton type="radio" value="Peperoni"> Peperoni
          </label>
          <label ngbButtonLabel class="btn btn-primary">
          <input ngbButton type="radio" value="Everything"> Everything
          </label>
        </div>
        <hr>
        Your Selection: {{model}}
        </div>
      `,
      styles: []
    })
    
    export class AppComponent {
      model = 'Hawaiian';
    }
    
    

你的应用应该工作在本地主机:4200。

材料设计

材料设计也使用基于网格的布局,如 Bootstrap。它支持快速响应的动画和过渡、填充、深度效果、灯光和阴影。材料具有基于纸张和墨水的颜色。

Polymer 是 web 应用用户界面材料设计的实现。它包含 Polymer library,该库为浏览器和 elements 目录提供了一个 Web 组件 API,包括一个以可视元素为特色的 paper elements 集合。图 14-4 显示一个材料设计网页。你可以在 www.material-ui.com 了解更多关于这款产品的信息。

A458962_1_En_14_Fig4_HTML.jpg

图 14-4

Web page of Material Design

安装 Angular 材料

Angular Material 是一个 Angular 版本的素材库,您可以使用材质组件快速构建应用。源代码可在 https://github.com/jelbourn/material2-app 处获得,示例在此: https://material2-app.firebaseapp.com

以下是安装 Angular 材料的方法:

  1. 以通常的方式使用 CLI 构建应用。

  2. 使用 npm 安装 Angular 材质、Angular 动画和 CDK(组件开发工具包)。

    npm install --save @angular/material
    npm install --save @angular/animations
    npm install --save @angular/cdk
    
    
  3. 将图标添加到您的项目中,方法是将它们包含在 index.html 文件中:

    <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
    
    
  4. 将样式文件 styles.css 重命名为 styles.scss,并将其更改为:

    @import '~@angular/material/prebuilt-themes/deeppurple-amber.css';
    
    
  5. 更改对中样式文件的引用。angular-cli.json:

    "styles": [
        "styles.scss"
    ],
    
    
  6. 编辑 CLI 生成的模块文件 app.module.ts,确保它导入了小部件模块(如 MdButtonModule、MdCheckboxModule)、动画模块(BrowserAnimationsModule)和 hammerjs:

    import {MdButtonModule, MdCheckboxModule} from '@angular/material';
    import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
    import { hammerjs } from 'hammerjs';
    
    @NgModule({
      ...
      imports: [MdButtonModule, MdCheckboxModule], [BrowserAnimationsModule]
      ...
    }
    
    

Angular 的材料设计:示例部件-ex200

该组件允许用户使用材料样式的日期选择器弹出窗口选择日期,如图 14-5 所示。

A458962_1_En_14_Fig5_HTML.jpg

图 14-5

Date picker popup

让我们来看看这个例子:

  1. 使用 CLI 构建应用:

    ng new widgets-ex200 --inline-template --inline-style
    
    
  2. 安装 Angular 材料,动画和组件开发工具包。

    cd widgets-ex200
    npm install --save @angular/material
    npm install --save @angular/animations
    npm install --save @angular/cdk
    
    
  3. 将图标添加到您的项目中,方法是将它们包含在 index.html 文件中:

    <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
    
    
  4. 将样式文件 styles.css 重命名为 styles.scss,并将其更改为:

    @import '~@angular/material/prebuilt-themes/deeppurple-amber.css';
    
    
  5. 更改对中样式文件的引用。angular-cli.json:

    "styles": [
        "styles.scss"
    ],
    
    
  6. 编辑 app.module.ts 并将其更改为以下内容:

    import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';
    import { AppComponent } from './app.component';
    import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
    import {FormsModule, ReactiveFormsModule} from '@angular/forms';
    import {
      MatNativeDateModule,
      MatFormFieldModule,
      MatInputModule,
      MatDatepickerModule
    } from '@angular/material';
    @NgModule({
      declarations: [
        AppComponent
      ],
      imports: [
        BrowserModule, BrowserAnimationsModule, MatNativeDateModule, MatFormFieldModule, MatInputModule, MatDatepickerModule
      ],
      providers: [],
      bootstrap: [AppComponent]
    })
    export class AppModule { }
    
    
  7. 编辑 app.component.ts 并将其更改为以下内容:

    import { Component } from '@angular/core';
    
    @Component({
    selector: 'app-root',
    template: `
      <mat-form-field>
      <input matInput [matDatepicker]="picker" placeholder="Choose a date">
      <mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
      <mat-datepicker #picker></mat-datepicker>
      </mat-form-field>
    `,
    styles: []
    })
    
    export class AppComponent {
      title = 'app';
    }
    
    
  8. 开始ng serve :

    ng serve
    
    
  9. 打开 web 浏览器,导航到 localhost:4200。您应该会看到应用正在运行。

你的应用应该工作在本地主机:4200。

摘要

如果你真的想构建一个完美的 Angular 应用,你应该使用一个预建的小部件库模块。许多应用使用自举和材料。我目前正在开发一个使用 Bootstrap 的应用,它的优势在于提供了一个网格系统,这消除了响应式设计的许多痛苦。

UI 小部件库无疑使用更易维护的标准代码编写漂亮的用户界面变得更加容易。不要浪费你的时间重新发明轮子写你自己的部件库!请记住,您可以在小部件库中设置主题,以便根据您的需求定制它们。

十五、路由和导航

在大多数 web 应用中,用户在执行应用任务时会从一个页面导航到下一个页面。用户可以通过以下方式导航:

  • 在地址栏中输入 URL
  • 跟随链接、点击按钮等等
  • 在浏览器历史中后退或前进

在 Angular 应用中,用户可以以同样的三种方式导航,但他们是通过组件(Angular 应用的构建块)导航。我们可以导航,因为我们有角路由器。路由器可以将浏览器 URL 解释为导航到组件的指令,并将可选参数(包含信息)传递给组件,为其提供上下文信息,并帮助其决定要呈现的特定内容或需要做的事情。

我们可以将路由器绑定到页面上的链接,当用户单击链接时,它将导航到适当的组件。当用户点击一个按钮,从一个下拉菜单中选择,或者响应来自任何来源的其他刺激时,我们可以强制性地导航。

路由器将活动记录在浏览器的历史记录中,因此后退和前进按钮也可以工作。

客户端的路由器路由

任何包含#字符的 URL 都是片段 URL。#左边的 URL 部分标识可以访问的资源(从服务器),右边的部分称为片段标识符,指定资源内的位置(在客户机上)。例如,在 URL www.cnn.com/index.html#section2 中,片段名是 section2,它指定了文档 index.html 中的一个位置。

片段的最初目的是允许用户跳转到当前页面指定部分的链接,向上或向下滚动。现在,片段经常用于客户端导航,因为从本质上讲,它们不会调用从服务器获取资源的请求。

HTML5 浏览器可以处理 URL 的客户端和非客户端路由,包括有哈希的和没有哈希的。但是一些老的浏览器不支持未经哈希处理的 URL 的客户端路由。散列意味着 URL 的客户端部分需要在#符号之后(也就是说,它是一个片段)。

如果您正在将单页面应用部署到生产环境中,您可能需要执行以下操作:

  1. 在路由器上打开哈希路由。这将使您的单页应用与 HTML5 以前的浏览器更加兼容。当您在您的模块中导入路由器模块时,您应该执行以下操作:

    @NgModule({
      imports: [
        RouterModule.forRoot(appRoutes, {useHash: true})
      ],
      ...
    })
    export class ...
    
    
  2. 确保您在服务器上的 404 页面重新路由到包含单页应用的网页(例如,index.html)。如果由于某种原因,浏览器试图错误地获取服务器资源,这将把页面返回到单页应用。

路由匹配

如果您在 web 浏览器的地址栏中指定了一个 URL,并且加载了 Angular 应用,路由器将尝试查找与该 URL 匹配的路由。它检查可能的路由组合的每一个排列,直到它匹配完整的 URL。如果有多条路由可能匹配同一个 url,路由器将使用第一条可用的路由,即使第二条路由看起来更完整。

路由器 DSL

指定路由时,可以使用文本字符串来指定,例如“/customers/123”。DSL 代表“领域特定语言”,使用该术语是因为您的路由文本字符串可以用多种方式解释和匹配。您的路由字符串可以指定:

  • 绝对路由。
  • 从你现在所在的位置出发的相对路由。
  • 引用服务器资源的路由。
  • 引用客户端资源的路由(使用片段 URL)。

“DSL”是一个令人生畏的术语,但是不要担心,我们将在这一章中介绍不同类型的路由,您将很快上手!

路由器模块

在开始使用组件路由器之前,您应该知道该模块包含在节点包依赖项中,但默认情况下不包含在 Angular CLI 项目中。应用模块中不包括路由。

但是,您可以通过在ng命令的末尾添加--routing参数来改变这种情况。例如:

ng new router-ex300 --inline-template --inline-style --routing

表 15-1 列出了路由器模块中的对象。表格中有许多对象,但不要担心,经过一些练习后,它们会变得更容易理解。

表 15-1

Objects in the Router Module

| 目标 | 类型 | 描述 | | :-- | :-- | :-- | | `RouterModule` | 组件 | 一个独立的 Angular 模块,提供必要的服务提供者和在应用视图中导航的指令。 | | `Router` |   | 显示活动 URL 的应用组件。管理从一个组件到下一个组件的导航。 | | `Routes` |   | 定义一个路由数组,每个路由将一个 URL 路径映射到一个组件。 | | `Route` |   | 定义路由器应该如何根据 URL 模式导航到组件。大多数管线由路径和元件类型组成。 | | `RouterOutlet` | 管理的 | 标记路由器显示视图位置的指令(``)。 | | `RouterLink` | 管理的 | 将可点击的 HTML 元素绑定到路由的指令。单击带有绑定到字符串或链接参数数组的`RouterLink`指令的元素会触发导航。 | | `RouterLinkActive` | 管理的 | 当包含在元素上或元素内的相关联的`RouterLink`变为活动/非活动时,从 HTML 元素添加/移除类的指令。 | | `ActivatedRoute` |   | 提供给每个路由组件的服务,包含路由特定的信息,如路由参数、静态数据、解析数据、全局查询参数和全局片段。 | | `RouterState` |   | 路由器的当前状态,包括当前激活的路由树以及遍历路由树的便利方法。 |

简单路由:示例

这是一个披萨选择组件,它使用路由允许用户单击链接来显示不同类型的披萨,每种披萨都在自己的组件中。这个例子还展示了路由参数的使用:您可以使用smalllargesize参数路由到everything比萨饼。图 15-1 所示。

A458962_1_En_15_Fig1_HTML.jpg

图 15-1

Pizza selection

这是示例路由器-ex100:

  1. 使用 CLI 构建应用:使用以下代码:

    ng new router-ex100 --routing --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd router-ex100
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”

  4. 编辑路由类:编辑 app-routing.module.ts,修改为:

    import { NgModule } from '@angular/core';
    import { Routes, RouterModule } from '@angular/router';
    import { PepperoniComponent, EverythingComponent} from './app.component';
    
    const routes: Routes = [
      { path: '',
        redirectTo: '/pepperoni',
        pathMatch: 'full'
      },
      {
        path: 'pepperoni',
        component: PepperoniComponent
      },
      {
        path: 'everything/:size',
        component: EverythingComponent
      }
    ];
    
    @NgModule({
      imports: [RouterModule.forRoot(routes)],
      exports: [RouterModule]
    })
    export class AppRoutingModule { }
    
    
  5. 步骤 5–编辑组件类编辑“app.component.ts”并将其更改为以下内容:

    import { Component } from '@angular/core';
    import { Router, ActivatedRoute, ActivatedRouteSnapshot} from '@angular/router';
    
    @Component({
      selector: 'pepperoni',
      template: `
        <h2>Pepperoni</h2>
        <img src="https://thumb1.shutterstock.com/display_pic_with_logo/55755/161642033/stock-photo-single-slice-of-pepperoni-meat-isolated-on-white-with-path-shot-from-above-161642033.jpg">
      `,
      styles: []
    })
    export class PepperoniComponent {
    }
    
    @Component({
      selector: 'everything',
      template: `
        <h2>Everything</h2>
        Size:{{size}}
        <img src="https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcR0UXyx2jQrCBBBw2N4ofFVw2oWz7keZjDVUB4UDrASE9JHwQdi">
      `,
      styles: []
    })
    export class EverythingComponent {
      private size: String = '';
      constructor(private route: ActivatedRoute){
        route.params.subscribe(
          (params: Object) =>
          this.size = params['size']);
      }
    }
    
    @Component({
      selector: 'app-root',
      template: `
        <h1>
          Pizzas
        </h1>
        <a [routerLink]="['pepperoni']">Pepperoni</a>
        <a [routerLink]="['everything','small']">Everything Small</a>
        <a [routerLink]="['everything','large']">Everything Large</a>
        <router-outlet></router-outlet>
      `,
      styles: []
    })
    export class AppComponent {
      title = 'app';
    }
    
    
  6. 编辑模块:编辑 app.module.ts,修改为:

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

你的应用应该工作在本地主机:4200。请注意以下几点:

  • 文件 app-routing.module.ts 由 CLI 生成。它为路由定义了一个模块 AppRoutingModule。这个模块包含一个数据结构,该结构设置带有伴随组件的 URL。请注意第一个 URL 如何将默认 URL 映射到另一个 URL,并注意 EverythingComponent 的路径如何指定一个size参数。

    {
      path: 'everything/:size',
      component: EverythingComponent
    }
    
    
  • app.component.ts 文件包含所有组件。它使用RouterLink指令来修改链路,以便与 Angular 路由器一起工作。EverythingComponent 用于显示everything披萨,并可以接受一个size参数。注意它是如何订阅路由参数对象来接收参数更新的。如果用户从everything的一个尺寸切换到另一个尺寸,更新size参数,这是必要的。

    constructor(private route: ActivatedRoute){
        route.params.subscribe(
          (params: Object) =>
          this.size = params['size']);
      }
    
    
  • 文件 app.module.ts 声明了所有组件,因此可以在 app 模块中访问它们。它还导入我们在 app.routing.module.ts 中设置的 AppRoutingModule。

嵌套路由:示例

嵌套路由意味着能够路由和导航到被导航到的其他组件内部的子组件。这在 Angular 中是完全可能的,正如你在这个例子中看到的。

嵌套路由链接 URL 具有多个“级别”,因为现在存在路由及其子级的层次结构,而不仅仅是路由。

这将是示例路由器-ex200,表 15-2 将本示例的 URL 与前一示例的 URL 进行了比较。

表 15-2

router-ex100 URLs vs. router-ex200 URLs

| 路由器-ex100 | 路由器-ex200 | | :-- | :-- | | /意大利香肠 | /意大利香肠 | | /一切 | /其他/意大利面 | | /其他/canzone |

这是另一个使用路由的披萨选择组件。但是,这一次它为“other”菜单和组件使用了嵌套路由。当您点击“其他”链接时,您可以从 pasta 或 calzone 的子菜单中进行选择,如图 15-2 所示。这些菜单项的显示由嵌套路由处理。

A458962_1_En_15_Fig2_HTML.jpg

图 15-2

Pasta or calzone ?

让我们以路由器-ex200 为例:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new router-ex200 --routing --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd router-ex200
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”

  4. 编辑路由类:编辑 app-routing.module.ts,修改为:

    import { NgModule } from '@angular/core';
    import { Routes, RouterModule } from '@angular/router';
    import { PepperoniComponent } from './app.component';
    import { OtherComponent } from './app.other-component';
    import { NestedPastaComponent, NestedCalzoneComponent } from './app.other-component';
    
    const routes: Routes = [
      {
        path: '',
        redirectTo: '/pepperoni',
        pathMatch: 'full'
      },
      {
        path: 'pepperoni',
        component: PepperoniComponent
      },
      {
        path: 'other',
        component: OtherComponent,
        children: [
          {
            path: '',
            redirectTo: 'pasta',
            pathMatch: 'full'
          },
          {
            path: 'pasta',
            component: NestedPastaComponent
          },
          {
            path: 'calzone',
            component: NestedCalzoneComponent
          }
        ]
      }
    ];
    
    @NgModule({
      imports: [RouterModule.forRoot(routes)],
      exports: [RouterModule]
    })
    export class AppRoutingModule {}
    
    
  5. 编辑组件类:编辑 app.component.ts 并将其更改为:

    import { Component } from '@angular/core';
    import { Router, ActivatedRoute, ActivatedRouteSnapshot} from '@angular/router';
    
    @Component({
      selector: 'pepperoni',
      template: `
        <div>
          <h2>Pepperoni</h2>
          <img src="https://thumb1.shutterstock.com/display_pic_with_logo/55755/161642033/stock-photo-single-slice-of-pepperoni-meat-isolated-on-white-with-path-shot-from-above-161642033.jpg">
        </div>
      `,
      styles: []
    })
    export class PepperoniComponent {
    }
    
    @Component({
      selector: 'app-root',
      template: `
        <div>
          <h1>
            Delivery Menu
          </h1>
          <a [routerLink]="['pepperoni']" routerLinkActive="router-link-active">Pepperoni Pizza</a>
          <a [routerLink]="['other']" routerLinkActive="router-link-active">Other Menu Items</a>
          <router-outlet></router-outlet>
        </div>
      `,
      styles: []
    })
    export class AppComponent {
      title = 'app';
    }
    
    
  6. 创建其他组件:创建 app.other-component.ts 并将其更改为:

    import { Component } from '@angular/core';
    
    @Component({
      selector: 'pasta',
      template: `
        <div>
          <h2>Pasta</h2>
          <img src="https://capetowncafe.files.wordpress.com/2015/04/spaghetti-recipe-wikipedia.jpg">
        </div>
      `,
      styles: []
    })
    export class NestedPastaComponent {
    }
    
    @Component({
      selector: 'calzone',
      template: `
        <div>
          <h2>Calzone</h2>
          <img src="https://upload.wikimedia.org/wikipedia/commons/5/54/Calzone_fritto.jpg">
        </div>
      `,
      styles: []
    })
    
    export class NestedCalzoneComponent {
    }
    
    @Component({
      selector: 'other',
      template: `
        <div>
          <h2>Other Menu Items</h2>
          <a [routerLink]="['pasta']" routerLinkActive="router-link-active">Pasta</a>
          <a [routerLink]="['calzone']" routerLinkActive="router-link-active">Calzone</a>
          <router-outlet></router-outlet>
          <br/>
          <br/>
        </div>
      `,
      styles: []
    })
    export class OtherComponent {
    }
    
    
  7. 编辑模块:编辑 app.module.ts,修改为:

    import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';
    
    import { AppRoutingModule } from './app-routing.module';
    import { AppComponent, PepperoniComponent } from './app.component';
    import { OtherComponent, NestedCalzoneComponent, NestedPastaComponent } from './app.other-component';
    
    @NgModule({
      declarations: [
        AppComponent,
        PepperoniComponent,
        OtherComponent,
        NestedCalzoneComponent,
        NestedPastaComponent
      ],
      imports: [
        BrowserModule,
        AppRoutingModule
      ],
      providers: [],
      bootstrap: [AppComponent]
    })
    export class AppModule { }
    
    
  8. 编辑样式:编辑 styles.css 并将其更改为以下内容:

    /* You can add global styles to this file, and also import other style files */
    img {
        width:200px;
        border: 1px solid #000000;
    }
    a {
        background-color: #0066CC;
        color: #ffffff;
        border: 1px solid #000000;
        padding: 10px;
        margin: 10px;
    }
    .router-link-active {
        background-color: #C14242;
    }
    div {
        border: 1px dotted #000000;
        margin: 10px;
        padding: 10px;
    }
    
    

你的应用应该工作在本地主机:4200。请注意以下几点:

  • 文件 app-routing.module.ts 由 CLI 生成。它为路由定义了一个模块 AppRoutingModule。这个模块包含一个数据结构,该结构设置带有伴随组件的 URL。注意,这次数据结构包含子路由,使用了children属性:

    {
        path: 'other',
        component: OtherComponent,
        children: [
            {
            path: '',
            redirectTo: 'pasta',
            pathMatch: 'full'
            },
            {
            path: 'pasta',
            component: NestedPastaComponent
            },
            {
            path: 'calzone',
            component: NestedCalzoneComponent
            }
        ]
    }
    
    
  • 文件 app.component.ts 用于定义 app 组件和意大利香肠组件(非嵌套)。它还包含非嵌套路由器链接和路由器出口,非嵌套组件被注入到其中。

  • app.other-component.ts 文件用于定义其他组件(非嵌套)以及 pasta 和 calzone 嵌套组件。另一个组件包含嵌套路由器链路和路由器出口,嵌套组件注入其中。

  • 文件 app.module.ts 声明了所有组件,以便可以在 app 模块中访问它们。它还导入我们在 app.routing.module.ts 中设置的 AppRoutingModule。

  • 文件 styles.css 声明了一些用于链接和选项卡效果的样式(我承认很糟糕)。注意如何在路由器链路上设置routerLinkActive样式来突出显示当前活动的链路(这适用于嵌套和非嵌套链路)。路由器链接:

    <a [routerLink]="['pepperoni']" routerLinkActive="router-link-active">Pepperoni Pizza</a>
    
    

    活动路由器链接的样式:

    .router-link-active {
        background-color: #C14242;
    }
    
    

路由配置

Angular 应用通过使用路由器服务的单个实例进行路由。导航时,路由器会尝试解析新位置的路由。要解析路由,必须为路由器配置路由。路由被配置为一组route对象。每个route对象都需要一个路径(来解析它),通常还需要一个组件,该组件将显示在路由器出口中,用于解析路由。路由对象还可以有更多的属性(稍后将详细介绍)。

您可以配置重定向到其他路径的路由路径。例如,以下代码将空路由重定向到意大利香肠路由。在空 URL 的情况下,我们还需要添加pathMatch: 'full'属性,这样 Angular 就知道它应该完全匹配空字符串,而不是部分匹配空字符串:

const routes: Routes = [
  {
    path: '',
    redirectTo: '/pepperoni',
    pathMatch: 'full'
  },
  ...
];

您还可以使用路径**添加一个总括路径,如果该 URL 与任何其他路径都不匹配,它将与该路径匹配:

const routes:Routes = [
    ...
    {path: '**', component: CatchAllComponent}
];

当您配置您的路由时,您使用一组route对象来配置它们。每个route对象可以有一个data属性,该属性包含其他属性,这些属性可以在以后由该路由的目标组件提取。

以下代码设置了一个包含数据的路由,包括一条“未找到”路径的消息:

{ path: '500', component: ErrorPageComponent, data: {message: 'Unexpected Server Error'}}

下面的代码访问该数据,因此它可以用于显示消息。要么这样:

this.errorMessage = this.snapshot.data['message'];

或者这个:

this.route.data.subscribe(
  (data: Data) => { this.errorMessage = data['message']; }
);

请注意,这允许您将同一组件用于不同的目的和不同的数据。例如,您还可以为路径 401 设置一个路由,该路由将重用错误页面组件,但这一次带有消息“未授权”

路由路径参数

您可以将数据参数作为 URL 路径的一部分传递给路由中的组件,例如 customer/123。

当您为接收参数的组件编写代码时,有两种不同的实现可供选择:

  • 您可以从路由snapshot(路由的一次性快照)中读取参数。当您只路由到父组件中的子组件一次并且该参数从不改变时,这很有用:

    constructor(route: ActivatedRoute) {
      this.customerId = route.snapshot.paramMap.get('id');
    }
    
    
  • 您可以通过订阅可观察的参数图来读取参数。当您路由到父组件中的子组件时,这是很有用的,并且当某些事情发生变化时,子组件可能会重新路由(被传递一个新参数:

    constructor(route: ActivatedRoute) {
      route.paramMap.subscribe(
        params => this.customerId = params.get('id')
      );
    }
    
    

路由查询参数:示例

您可以使用查询字符串将数据参数传递给路由中的组件,例如,customer?id=123。

这与路径参数的工作方式类似。当您为接收参数的组件编写代码时,有两种不同的实现可供选择:

  • 您可以从路由快照中读取查询参数,这在路由到父组件中的子组件时非常有用,并且此参数永远不会改变:

    constructor(route: ActivatedRoute) {
      this.customerId = route.snapshot.queryParams['id'];
    }
    
    
  • 您可以通过订阅一个可观察的查询参数映射来读取参数——这在路由到父组件中的子组件时非常有用,并且子组件可以在发生变化时重新路由:

    constructor(route: ActivatedRoute) {
      route.queryParams.subscribe(
        params => this.customerId = params.get('id')
      );
    }
    
    

本例是一个在顶部显示客户列表的组件,下面是所选客户的详细信息,如图 15-3 所示。这将是示例路由器-ex300。

  1. 使用 CLI 构建应用:使用以下命令:

    ng new router-ex300 --routing --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd router-ex300
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”

  4. 创建客户类:创建客户

  5. 创建CustomerService类:创建 customerService.ts:

    import { Injectable } from '@angular/core';
    import { Customer } from './customer';
    
    @Injectable()
    export class CustomerService {
      private _customers: Array<Customer> = [
          new Customer(1, 'Mark', 'Atlanta', 'GA', 12000),
          new Customer(2, 'Peter', 'Belvue', 'CA', 5000),
          new Customer(3,'Jill', 'Colombia', 'SC', 2000),
          new Customer(4, 'Brian', 'Augusta', 'GA', 2000)
      ];
    
      get customers() {
        return this._customers;
      }
    
      getCustomerById(id: number){
        for (let i=0,ii=this._customers.length;i<ii;i++){
          const customer = this._customers[i];
          if (customer.id == id){
            return customer;
          }
        }
        return null;
      }
    }
    
    
  6. 编辑 app 路由模块:编辑 app-routing.module.ts,修改为:

    import { NgModule } from '@angular/core';
    import { Routes, RouterModule } from '@angular/router';
    import { DetailComponent, PleaseSelectComponent } from './app.component';
    
    const routes: Routes = [
      {
        pathMatch: 'full',
        path: '',
        component: PleaseSelectComponent,
        children: []
      },
      {
        pathMatch: 'full',
        path: 'detail',
        component: DetailComponent,
        children: []
      }
    ];
    
    @NgModule({
      imports: [RouterModule.forRoot(routes)],
      exports: [RouterModule]
    })
    export class AppRoutingModule { }
    
    
  7. 编辑 app 组件:编辑 app.component.ts,修改为:

    import { Component } from '@angular/core';
    import { ActivatedRoute } from '@angular/router';
    import { CustomerService } from './customerService';
    import { Customer } from './customer';
    
    @Component({
      selector: 'pleaseSelect',
      template: `
        <div>
        <h2>Please make a selection.</h2>
        </div>
      `,
      styles: ['div { background-color: #FFFFFF; padding: 10px; border: 1px solid #000000 }']
    })
    export class PleaseSelectComponent {
    }
    
    @Component({
      selector: 'detail',
      template: `
        <div>
        <h2>Customer Detail {{id}}</h2>
        <p>{{customer.name}}<p>
        <p>{{customer.city}}, {{customer.state}}</p>
        <p>Balance: &#36;{{customer.balance}}</p>
        </div>
      `,
      styles: ['div { background-color: #FFE4E1 }']
    })
    export class DetailComponent {
      customer: Customer;
      constructor(
        private customerService: CustomerService,
        private route: ActivatedRoute) {
          route.queryParams.subscribe(
            (queryParams: Object) =>
            this.customer = customerService.getCustomerById(queryParams['id']));
      }
    }
    
    @Component({
      selector: 'app-root',
      template: `
        <div>
        <h1>
          Customer List
        </h1>
        <p *ngFor="let customer of _customerService.customers">
          <a [routerLink]="['detail']" [queryParams]="{id: customer.id}" routerLinkActive="active">{{customer.name}}</a>
        </p>
        </div>
        <router-outlet></router-outlet>
      `,
      styles: ['div { background-color: #faebd7 }',]
    })
    export class AppComponent {
      constructor(private _customerService: CustomerService){
      }
    }
    
    
  8. 编辑 app 模块:编辑 app.module.ts,修改为:

    import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';
    
    import { AppRoutingModule } from './app-routing.module';
    import { AppComponent, DetailComponent, PleaseSelectComponent } from './app.component';
    import { CustomerService } from './customerService';
    import { Customer } from './customer';
    
    @NgModule({
      declarations: [
        AppComponent,
        DetailComponent,
        PleaseSelectComponent
      ],
      imports: [
        BrowserModule,
        AppRoutingModule
      ],
      providers: [CustomerService],
      bootstrap: [AppComponent]
    })
    export class AppModule { }
    
    
  9. 编辑样式:编辑 styles.css 并将其更改为以下内容:

    div {
        padding: 10px; border: 1px solid #000000;
    }
    h1,h2 {
        margin: 0px;
    }
    .active {
        font-weight: bold;
    }
    .active::before {
        content: ">>> ";
    }
    .active::after {
        content: " <<<";
    }
    
    

A458962_1_En_15_Fig3_HTML.jpg

图 15-3

List of customers and detail

你的应用应该工作在本地主机:4200。请注意以下几点:

  • 文件 customer.ts 设置了customer类。

  • 文件 customerService.ts 是注入到 app 组件和 Detail 组件中的服务。它包含一个客户列表,以及访问客户数据的方法。

  • app-routing.module.ts 文件为 Please Select 组件和 Detail 组件设置路由。

  • app.component.ts 文件设置组件。请注意它如何使用不同的语法来指定路由器链接的查询参数:

    <a [routerLink]="['detail']" [queryParams]="{id: customer.id}" routerLinkActive="active">{{customer.name}}</a>
    
    
  • 文件 app.module.ts 声明所使用的组件,导入 app routing 模块,并将CustomerService类设置为 Detail 和 app 组件中客户依赖注入的提供者。

  • 文件“styles.css”用于设置 h1、h2 和 div 标签的通用样式。

路由器命令式导航:示例

到目前为止,我们已经编写了代码,为用户提供了单击链接进行导航的能力。命令式导航是不同的。这不是生成链接;它只是告诉路由器去某个地方,在你的代码中执行导航。导航是一个异步事件;它不会锁定代码,直到完成。本节讨论的命令式导航方法在完成时返回一个Promise对象,这是一个对成功或失败的回调。这两种方法是Router.navigateRouter.navigateByUrl。要使用命令式导航,首先需要使用构造函数注入将路由器注入到您的类中。

  • Router.navigate:根据一组命令或路由元素,相对(相对于当前路由)或绝对导航到一个组件。它返回一个承诺,在导航完成时解决。它使用先前在路由器链路 DSL 格式中指定的链路 DSL。基本上和点击路由器链接一样。
  • Router.navigateByUrl:这导航到一个完整的绝对 URL 字符串。它返回一个承诺,在导航完成时解决。通常更喜欢用navigate导航,而不是这种方法,因为 URL 更脆弱。如果给定的 URL 以/开头,路由器将绝对导航。如果给定的 URL 不是以/开头,路由器将相对于该组件导航。

两种导航方法都返回一个承诺,这使用户能够添加两个回调方法来处理导航结果:第一个用于成功处理程序,第二个用于错误处理程序。下面的例子中就有这样的例子。两种导航方法都能够接受一个NavigationExtras对象的附加参数。此对象允许您将附加信息传递给路由器,以进一步指定所需的路由。

该组件允许用户在组件之间导航,也可以返回,如图 15-4 所示。它还记录导航完成的时间。这是示例路由器-ex400。

A458962_1_En_15_Fig4_HTML.jpg

图 15-4

Navigating between components

让我们来看看这个例子:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new router-ex400 --routing --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd router-ex400
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”

  4. 编辑路由模块:编辑 app-routing.module.ts,修改为:

    import { NgModule } from '@angular/core';
    import { Routes, RouterModule } from '@angular/router';
    import { AppComponent, Component1, Component2 } from './app.component';
    
    const routes: Routes = [
      {
        path: 'component1',
        component: Component1
      },
      {
        path: 'component2',
        component: Component2
      },
      {
        path: '**',
        component: Component1
      },
    ];
    
    @NgModule({
      imports: [RouterModule.forRoot(routes)],
      exports: [RouterModule]
    })
    export class AppRoutingModule { }
    
    
  5. 编辑Component类:编辑 app.component.ts,修改为:

    import { Component } from '@angular/core';
    import { Router } from '@angular/router';
    import { Location } from '@angular/common';
    @Component({
      selector: 'component1',
      template: `
        <h1>
          {{title}}
        </h1>
        <router-outlet></router-outlet>
      `,
      styles: []
    })
    export class Component1 {
      title = 'Component 1';
    }
    
    @Component({
      selector: 'component2',
      template: `
        <h1>
          {{title}}
        </h1>
        <router-outlet></router-outlet>
      `,
      styles: []
    })
    export class Component2 {
      title = 'Component 2';
    }
    
    @Component({
      selector: 'app-root',
      template: `
        <h1>
          {{title}}
        </h1>
        <button (click)="component1()">Component 1</button>
        <button (click)="component2()">Component 1</button>
        <button (click)="back()"><- Back</button>
        <router-outlet></router-outlet>
      `,
      styles: []
    })
    export class AppComponent {
      title = 'App Component';
      constructor(private router: Router, private location: Location){}
      component1(){
        this.router.navigate(['component1']).then(result => { console.log("navigation result: " + result)});
      }
      component2(){
        this.router.navigateByUrl("/component2");
      }
      back(){
        this.location.back();
      }
    }
    
    
  6. 编辑模块:编辑 app.module.ts,修改为:

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

你的应用应该工作在本地主机:4200。请注意以下几点:

  • app 组件注入路由器和位置。
  • 它包含当用户单击按钮时强制导航的代码。
  • 它包含导航完成时触发的回调。
  • 它还在后退按钮的位置包含代码。

路由器:提取数据

您可以从注入到您的类中的Router对象中提取表 15-3 中显示的信息。

表 15-3

Extracting Data from Router

| 财产 | 描述 | | :-- | :-- | | `errorHandler` | 导航出错时调用的错误处理程序 | | `navigated` | 指示是否发生了至少一次导航 | | `urlHandlingStrategy` | URL 处理策略 | | `routeReuseStrategy` | 路由重用策略 | | `routerState` | 当前路由器状态 | | `url` | 当前 URL | | `events` | 一个可观察的路由器事件,允许您向路由器事件添加回调 |

您通常使用配置对象来定义路由器路由,这一点不会改变。然而,您可以随时使用resetConfig方法将不同的配置对象重新加载到路由器中。如果您想从服务器或其他数据源加载路由,这将非常有用。

路由守卫:示例

路由使用户能够在应用中导航。有时,用户需要做一些事情才能被允许访问应用的某个部分,例如,登录。路由守卫可用于控制对某些路由的访问。

有两种主要类型的路由守卫:

  • 用户能导航到一条路由吗?在这堂课上,你可以注入路由器。如果不允许用户导航到某个路由,这有助于将用户导航到另一个资源。
  • 用户可以离开某条路由吗?对于提示保存更改非常有用。

该示例组件显示菜单链接。有些链接只有在用户登录后才能使用。这将是示例路由器-ex500:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new router-ex500 --routing --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd router-ex500
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”

  4. 创建激活服务:创建 activate.service.ts 并将其更改为:

    import { Injectable } from '@angular/core';
    import { UserService } from './user.service';
    import { CanActivate } from '@angular/router';
    
    @Injectable()
    export class ActivateService implements CanActivate{
        constructor(private _userService: UserService){}
        canActivate() {
            return this._userService.authenticated;
      }
    }
    
    
  5. 编辑路由模块:编辑 app.routing.module.ts,修改为:

    import { NgModule } from '@angular/core';
    import { Routes, RouterModule } from '@angular/router';
    import { AuthenticatedComponent, NonAuthenticatedComponent} from './app.component';
    import { UserService } from './user.service';
    import { ActivateService } from './activate.service';
    
    const routes: Routes = [
      {
        path: 'authenticated',
        component: AuthenticatedComponent,
        canActivate: [
          ActivateService
        ]
      },
      {
        path: '**',
        component: NonAuthenticatedComponent
      }
    ];
    
    @NgModule({
      imports: [RouterModule.forRoot(routes)],
      exports: [RouterModule],
      providers: [UserService, ActivateService]
    })
    export class AppRoutingModule { }
    
    
  6. 编辑组件:编辑 app.component.ts,修改为:

    import { Component, ViewChild } from '@angular/core';
    import { UserService } from './user.service';
    
    @Component({
      selector: 'non-authenticated-component',
      template: `
        <div>
          <h2>Non-authenticated</h2>
          <p>This component can be accessed without authentication.</p>
        </div>
      `,
      styles: []
    })
    export class NonAuthenticatedComponent {
    }
    
    @Component({
      selector: 'authenticated-component',
      template: `
        <div>
          <h2>Authenticated</h2>
          <p>This component cannot be accessed without authentication.</p>
        </div>
      `,
      styles: []
    })
    export class AuthenticatedComponent {
    }
    
    @Component({
      selector: 'app-root',
      template: `
        <span *ngIf="!_userService.authenticated">
        User:<input type="input" #name />
        Password:<input type="input" #password />
        <input type="button" (click)="login()" value="Login" />"
        </span>
        <hr/>
        Authenticated:{{_userService.authenticated}}
        <hr/>
        <a [routerLink]="['non-authenticated']">Non-Authenticated</a>
        <a [routerLink]="['authenticated']">Authenticated</a>
        <router-outlet></router-outlet>
      `,
      styles: []
    })
    export class AppComponent {
      loggedIn: boolean = false;
      @ViewChild('name') name;
      @ViewChild('password') password;
      constructor(private _userService: UserService){}
      login(){
          this._userService.authenticate(
            this.name.nativeElement.value,
            this.password.nativeElement.value);
      }
    }
    
    
  7. 编辑 app 模块:编辑 app.module.ts,修改为:

    import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';
    
    import { AppRoutingModule } from './app-routing.module';
    import { AppComponent, AuthenticatedComponent, NonAuthenticatedComponent } from './app.component';
    import { UserService } from './user.service';
    
    @NgModule({
      declarations: [
        AppComponent,
        AuthenticatedComponent,
        NonAuthenticatedComponent
      ],
      imports: [
        BrowserModule,
        AppRoutingModule
      ],
      providers: [UserService],
      bootstrap: [AppComponent]
    })
    export class AppModule { }
    
    
  8. 创建用户服务:创建 user.service.ts 并将其更改为:

    import { Injectable } from '@angular/core';
    
    @Injectable()
    export class UserService {
        private _authenticated: boolean = false;
        public get authenticated(): boolean{
            return this._authenticated;
        }
        public set authenticated(value: boolean){
            this._authenticated = value;
        }
        public authenticate(name, password){
            if ((name === 'user') && (password === 'password')){
                this._authenticated = true;
            }
        }
    }
    
    

你的应用应该工作在本地主机:4200。请注意以下几点:

  • 服务 activate.service.ts 是一个路由保护,它允许或禁止路由被激活。调用canActivate方法:true允许激活,false不允许激活。该服务被注入到路由模块中,因此可以在路由配置中使用。
  • 服务 user.service.ts 是一种跟踪用户状态的服务——无论他们是否经过身份验证。这个服务被注入到服务 activate.service.ts 和 app 组件中。

摘要

希望这一章会对你非常有用,你会用它来写你的 Angular 应用的路由。请记住,您的路由可能会变得非常复杂,可能会有多个路由器模块和路由器插座。您可以尝试将所有路由放在一个路由模块中,也可以尝试将其分散到多个模块中。一个路由模块可能更简单,但当开发人员不断地更改这个文件时,这可能会导致更多的合并冲突。请记住,您可以使用 route guards 来加强安全性,允许或阻止对各种组件的访问。

下一章将讨论反应式编程,以及 Angular 应用如何使用新技术来处理应用中的数据流。

十六、观察者、反应式编程和 RxJS

Reactive Extensions for JavaScript(RxJS)是一个 Reactive streams 库,允许您处理异步数据流,它包含在 Angular。该项目是微软与开源开发者社区合作开发的。

本章的目的是介绍 RxJS 的基本概念,并涵盖该库的一些功能。我将在另一章介绍 RxJS 和 Angular 的结合使用。

反应式编程是一种专注于数据流和变化的编程范式。它允许您轻松地表达静态或动态数据流,并且执行模型将通过数据流自动传播更改。Reactive Extensions 代码几乎可以在每个计算平台上使用,不仅仅是 JavaScript,它的目的是将 Reactive 编程的能力带到计算平台上。

异步数据流

RxJS 库使用 JavaScript 中的可观察集合组成异步和基于事件的反应式程序。什么是异步数据流?让我们来分解一下:

  • 异步:在 JavaScript 中,这意味着我们可以调用一个函数并注册一个回调函数,以便在结果可用时得到通知,这样我们就可以继续执行并避免网页无响应。这用于 AJAX 调用、DOM 事件、承诺、web workers 和 WebSockets。
  • 数据:JavaScript 数据类型形式的原始信息,比如数字、字符串、对象(数组、集合、映射)。
  • 流:一段时间内可用的数据序列。例如,与 to 数组相比,您不需要所有的信息都存在就可以开始使用它们。

异步数据流的例子包括您正在观看的内容:

  • 股票报价
  • 小鸟叫声
  • 计算机事件,例如鼠标点击
  • Web 服务请求

可观察序列(可观察的)

在 RxJS 中,您使用可观察序列来表示异步数据流,也称为可观察序列。你可以使用 observables 来观察股票行情或鼠标点击。可观测量是灵活的,可以与推或拉模式一起使用:

  • Push:当使用 push 模式时,我们订阅源数据流,并在新数据可用(发出)时立即对其做出反应。你可以听一个流,然后做出相应的反应。
  • Pull:当使用 pull 模式时,我们使用相同的操作,但是是同步的。使用数组、生成器或可迭代对象时会发生这种情况。

因为可观测量是数据流,所以您可以使用由可观测量类型实现的操作符来查询它们。以下是使用可观察运算符可以做的许多事情中的一些:

  • 过滤掉你不拥有的股票的变化
  • 聚合—在前五秒钟内获得所有输入内容
  • 对多个事件执行基于时间的操作

观察者:示例

如果说可观察物是可以观察的东西,那么观察者就是观察它们的东西,如图 16-1 所示。

A458962_1_En_16_Fig1_HTML.gif

图 16-1

Observables and observers

观察者是可以对事件或正在发生的事情做出反应的类。要做出响应,他们必须实现以下方法:

  • onNext:当被观察对象发出一个物品时,被观察对象调用这个方法。onNext将被观测者发射的物品作为参数。
  • onError:当一个可观察对象未能创建预期的数据或遇到其他错误时,调用这个方法,停止可观察对象。被观察对象不会再呼叫onNextonCompletedonError方法将导致错误的原因作为其参数。
  • onCompleted:如果没有任何错误,observable 在最后一次调用onNext后调用该方法。

在这个例子中,我们将用两个事件创建一个可观察对象,然后观察它。图 16-2 显示了您将看到的控制台。这将是示例 rxjs-ex100。

A458962_1_En_16_Fig2_HTML.jpg

图 16-2

Console showing events

让我们来看看这个例子:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new rxjs-ex100 --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd rxjs-ex100
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”

  4. 编辑类:编辑 app.component.ts,修改为:

    import { Component } from '@angular/core';
    import * as Rx from 'rxjs';
    @Component({
      selector: 'app-root',
      template: `
      `,
      styles: []
    })
    export class AppComponent {
      constructor(){
        const array: Array<string> = ['event1', 'event2'];
        const observable: Rx.Observable<string[]> = Rx.Observable.of(array);
        const subscription: Rx.Subscription = observable.subscribe(
          // Observer
          function (x) {
              console.log('Next: ' + x);
          },
          function (err) {
              console.log('Error: ' + err);
          },
          function () {
              console.log('Completed');
          }
        );
      }
    }
    
    

你的应用应该工作在本地主机:4200。在浏览器中打开开发者工具,重新加载页面并查看控制台输出。请注意,app 组件执行以下操作:

  • 创建一个数组。
  • 从数组中创建一个可观察值。
  • 订阅是从对可观察对象的订阅中创建的。该订阅实现了处理事件的观察者代码。

捐款

订阅就像是可观察对象和观察者之间的联系。图 16-3 说明了这种关系。

A458962_1_En_16_Fig3_HTML.gif

图 16-3

A subscription connects observable and observer

您使用订阅将可观察对象和观察者联系在一起:

const subscription: Rx.Subscription = observable.subscribe(
      // Observer
      function (x) {
          console.log('Next: ' + x);
      },
      function (err) {
          console.log('Error: ' + err);
      },
      function () {
          console.log('Completed');
      }
    );

要取消可观察对象和观察者的链接,请在订阅中调用方法dispose:

subscription.dispose();

Observables, Observers, And Javascript ES7

ES7 是一个即将提出的 JavaScript 标准,将包含Object.observe,它将允许观察者接收一个按时间排序的变化记录序列,这些记录描述了一组被观察对象发生的一组变化。这和 RxJS 做的事情差不多,只是原生在浏览器里。它已经在一些浏览器中实现了——比如 Chrome 36.ss。

运算符:示例

操作员执行各种任务。他们的目的是为了更方便地观察一个可观察的现象。操作员执行以下操作:

  • 创造可观的
  • 组合可观测量
  • 过滤可观测量
  • 处理错误
  • 执行实用程序

大多数操作符操作一个可观察值并返回一个可观察值。这允许您在一个链中一个接一个地应用操作符。链中的每一个算子都会修改前一个算子的运算所产生的可观测值。

在这个例子中,我们将创建一个带有两个事件的可观察对象,然后我们将观察它。如果您查看控制台,您会看到事件日志。这将是示例 rxjs-ex200。

让我们来看看这个例子:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new rxjs-ex200 --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd rxjs-ex200
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”

  4. 编辑类:编辑 app.component.ts,修改为:

    import { Component } from '@angular/core';
    import * as Rx from 'rxjs';
    @Component({
      selector: 'app-root',
      template: `
      `,
      styles: []
    })
    export class AppComponent {
      constructor(){
        const observable: Rx.Observable<number> = Rx.Observable.range(0,100);
        const subscription: Rx.Subscription = observable.subscribe(
          // Observer
          val => { console.log(`Next: ${val}`) },
          err => { console.log(`Error: ${err}`) },
          () => { console.log(`Completed`) }
        );
      }
    }
    
    

你的应用应该工作在本地主机:4200。在浏览器中打开开发者工具,重新加载页面并查看控制台输出。请注意以下几点:

  • range操作符创建一系列事件。
  • 观察者使用箭头函数来处理事件。

产生可观测量的算子

有许多运算符仅用于创建可观测量。本节将讨论其中的几个。

该运算符从发出多个值的其他对象创建一个可观察对象。下面的代码从发出两个值的数组中创建一个可观察对象:

const array: Array<string> = ['event1', 'event2'];
const observable: Rx.Observable<string> = Rx.Observable.from(array);

间隔

interval创建一个在每个周期后发出一个值的可观察值,例如 0,1,2,3,4。图 16-4 显示了每 1/2 秒(500 毫秒)发射一个可观测值。

A458962_1_En_16_Fig4_HTML.jpg

图 16-4

Observable emitting a value every half second

代码如下:

const observable: Rx.Observable<number> = Rx.Observable.interval(500);
var observable: Rx.Observable<number> = new Rx.Observable.interval(500);

(刚刚)的

将一个物品转换成一个只发射该物品的可观察物。下面的代码创建一个只发出一次 500 的可观察对象:

const observable: Rx.Observable<number> = Rx.Observable.of(500);

范围

创建一个发出整数范围的可观察对象。下面的代码创建一个发出 1 到 100 的可观察对象:

const observable: Rx.Observable<number> = Rx.Observable.range(0,100);

重复

创建一个可观察对象,该对象发出给定元素的特定次数的重复。下面的代码创建了一个发出1 2 3 1 2 3 1 2 3 1 2 3的可观察对象:

const observable: Rx.Observable<number> = Rx.Observable.range(1,3).repeat(4);

计时器

timer创建一个可观察对象,在到期时间过后和每个周期后发出一个值:

const observable: Rx.Observable<number> = Rx.Observable.timer(2000,500);

转换由可观察对象发出的项目的运算符

您已经看到了如何创建发出值的可观测量。现在让我们看看如何修改这些值。

缓冲器

buffer是一个操作符,它周期性地将可观察对象中的项目聚集成束并发射这些束,而不是一次发射一个项目。下面的代码创建一个每隔 100 毫秒发出一个值的可观察对象。然后,它每隔 5000 毫秒将发射捆绑起来:

const observable: Rx.Observable<any> = Rx.Observable
    .timer(0,100)
    .buffer( Rx.Observable.timer(0, 5000) );

图 16-5 显示了结果。

A458962_1_En_16_Fig5_HTML.jpg

图 16-5

Gathering observables into bundles

地图

map是一种算子,常用于通过对每一项应用函数来变换一个可观察对象所发出的项。下面的代码只是在发出的值周围放了一个管道,结果如图 16-6 所示。

A458962_1_En_16_Fig6_HTML.jpg

图 16-6

Putting pipes around emitted values

const observable: Rx.Observable<string> = Rx.Observable.range(0,100)
    .map((val) =>  '|' + val + '|' );

扫描

scan是一个运算符,用于将一个函数顺序应用于一个可观察对象发出的每一项,并发出每一个连续值。就像map一样,只是第一个函数调用的结果被输入到第二个函数调用中,以此类推。以下代码的结果如图 16-7 所示:

A458962_1_En_16_Fig7_HTML.jpg

图 16-7

Emitting values of functions applied

const observable: Rx.Observable<number> = Rx.Observable.range(1,5)
    .scan((val) =>  { val++; return val * val } );

过滤由可观察对象发出的项目的运算符

你不需要什么都看。有时候你只需要看某些东西。

去抖:示例

debounce是用来保证观察者在一定时间内只发射一个物品的操作符。这在观察 UI 元素时很有用——例如,如果您有一个filter框,并且您不希望它响应太快,并且在多个请求中超越自己。debounce将阻止网络和计算机因过多的搜索请求而超载。

这将是示例 rxjs-ex300,其结果如图 16-8 所示。

A458962_1_En_16_Fig8_HTML.jpg

图 16-8

Emitting only one item

这个例子有一个搜索框,它使用debouncedistinctUntilChanged方法过滤用户的输入:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new rxjs-ex300 --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd rxjs-ex300
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”

  4. 编辑类:编辑 app.component.ts,修改为:

    import { Component } from '@angular/core';
    import * as Rx from 'rxjs';
    
    @Component({
      selector: 'app-root',
      template: `
        Search: <input type="text" (keyup)="onChange($event.target.value)"/>
        <div *ngFor="let log of _logs">Search:&nbsp;{{log}}</div>
      `,
      styles: []
    })
    export class AppComponent {
      _searchText: string;
      _searchSubject: Rx.Subject<string>;
      _logs: Array<string> = [];
    
      constructor() {
    
        // Create new Subject.
        this._searchSubject = new Rx.Subject<string>();
    
        // Set the Subject up to subscribe to events and filter them by
        // debounce events and ensure they are distinct.
        this._searchSubject
                .debounceTime(300)
                .distinctUntilChanged()
                .subscribe(
                  // Handle event. Log it.
                  searchText => this._logs.push(searchText)
                );
      }
      public onChange(searchText: string) {
    
        // Emit an event to the Subject.
        this._searchSubject.next(searchText);
      }
    }
    
    

你的应用应该工作在本地主机:4200。请注意以下几点:

  • 构造函数设置 Rxjs 主题_searchSubject来订阅事件并过滤它们。过滤后,每个事件都会添加到日志中,显示在组件中。主体是既能充当观察者又能充当被观察者的客体。因为它是观察者,所以它可以订阅一个或多个可观察的,因为它是可观察的,所以它可以通过重新发射它们来穿过它所观察的项目,它也可以发射新的项目。
  • 当用户在搜索框中键入一个键时,触发onChange方法。该方法中的代码向_searchSubject Rxjs 主题发送一个字符串事件。

明显的

distinct是用于抑制重复项发射的运算符。以下示例每 1/2 秒生成一个新值。然后,我们使用 map 将其转换为字符串“不变值”。然后我们添加distinct来抑制重复值。因此,我们只能发出一个值:

const observable: Rx.Observable<string> = Rx.Observable.interval(500)
      .map((val) => 'unchanging value').distinct();

过滤器

filter是一个运算符,用于从一个可观察对象中仅发出第一个项目,或满足某个条件的第一个项目。以下代码每 1/2 秒生成一个新值,然后过滤掉任何不能被 7 整除的值:

const observable: Rx.Observable<number> = Rx.Observable.range(0,100)
    .filter((val) => val % 7 === 0);

结果如图 16-9 所示。

A458962_1_En_16_Fig9_HTML.jpg

图 16-9

Generating a new value and filtering

take是一个运算符,用于只发射一个可观察对象发射的前 n 个项目。以下代码发出从 0 到 100 的新值,但只取前三个值:

const observable: Rx.Observable<number> = Rx.Observable.range(0,100)
    .take(3);

结合其他可观测量的算子

表 16-1 列出了组合其他运算符的运算符。

表 16-1

Operators That Combine Other Operators

| 操作员 | 描述 | | :-- | :-- | | `And` / `Then` / `When` | 使用模式和计划中介组合由多个可观察对象发出的项目 | | `CombineLatest` | 使用指定的函数组合每个可观察对象发出的最新项目,并根据函数的结果发出项目 | | `Join` | 当一个可观察物的物品在由另一个可观察物发射的物品定义的时间范围内发射时,组合由两个可观察物发射的物品 | | `Merge` | 通过合并它们发出的光,把几个可观测的东西结合成一个 | | `StartWith` | 在从源可观察物发射物品之前发射特定的物品序列 | | `Switch` | 将发出可观察物的可观察物转换为发出由最近发出的那些可观察物发出的项目的单个可观察物。 | | `Zip` | 通过一个函数将多个可观测值的发射组合起来,并根据函数的结果为每个组合发射单个项目。 |

分享

share操作符允许您将一个订阅的实例共享给一个或多个观察者。share当观察器的数量从 0 变到 1 时创建一个订阅,然后与所有后续的观察器共享该订阅,直到观察器的数量返回到 0,此时该订阅被释放。如果你想从多个地方观看同一个东西,这很有用。图 16-10 显示了一个例子,图 16-11 显示了第二个屏幕产生的控制台日志。

A458962_1_En_16_Fig11_HTML.jpg

图 16-11

Console logs produced by the share operator

A458962_1_En_16_Fig10_HTML.jpg

图 16-10

The share operator in action

摘要

这一重要章节介绍了异步数据流和 RxJs。我希望您已经完成了练习,因为很快我们将使用事件,这是一种异步数据流的形式,您将在应用中观察到。在处理 Angular 事件时,我们将使用主体、可观察对象和观察者,并且我们将使用本章中介绍的操作符。

下一章将详细介绍 RxJs 与 Angular 的结合使用。

十七、Angular 和 RxJS

在前一章中,我们浏览了反应式扩展的核心概念,并学习了 RxJS 中的可观察对象、观察者、订阅和操作符。现在,我们将看看如何在 Angular 中使用反应式扩展。

在写 AngularJS 的时候,反应式扩展还没有出现,但是 promises 出现了。AngularJS 使用了很多 promise 对象,包括\(http、\)interval 和$timeout 模块。Promise 对象可以用来表示异步结果:成功(返回值)或失败(返回错误)。Promise 对象用于与服务器和许多其他对象的 HTTP 通信。图 17-1 给出了一个例子。

A458962_1_En_17_Fig1_HTML.jpg

图 17-1

Promise objects in AngularJS

在 Angular,承诺正在消失,取而代之的是可观的。不过,它们并没有完全消失。

可观察到的东西比承诺有一些优势:

  • Promises 只发出一个值/错误。随着时间的推移,可观测量可以发出多个值。例如,使用 observable,您可以在一段时间内监听 web 套接字上的事件。有承诺只能听一次。
  • 您可以使用带有观察点的运算符进行映射、过滤等操作。
  • 你可以取消可观测量。

可观测量和 Angular

Angular 在 DOM 事件和 HTTP 服务中使用异步数据流的可观察对象。在监听 DOM(文档对象模型)事件的过程中,您可以观察到用户在用户界面中正在做什么的稳定数据流,比如击键、鼠标事件等等。对于 Http 服务,您可以监听服务器响应,打开连接并响应传入的数据。

Observables 和 DOM 事件:示例

DOM 是表示 HTML 文档中的对象并与之交互的一种方式。文档节点被组织在一个称为 DOM 树的结构中,树中的对象使用对象上的方法进行寻址和操作。

Angular DOM 事件是可以观察到的。为了使用 DOM 事件,我们将使用模块 Rx。DOM(RxJS 的 HTML DOM 绑定)到 rx.angular。

您可以过滤事件,并将观看多个不同的事件和在一个地方观察结合起来。

这个例子检测用户在 5 秒钟内没有做任何事情。当这种情况发生时,我们在组件的显示中添加一行“idle”,如图 17-2 所示。这将是示例 rxjs-和-angular-ex100。

A458962_1_En_17_Fig2_HTML.jpg

图 17-2

Displaying idle users

让我们来看看这个例子:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new rxjs-and-angular-ex100 --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd rxjs-and-angular-ex100
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”

  4. 编辑类:编辑 app.component.ts,修改为:

    import { Component } from '@angular/core';
    import * as Rx from 'rxjs';
    
    @Component({
      selector: 'app-root',
      template: `
        Search: <input type="text">
        <div *ngFor="let log of _logs">Search:&nbsp;{{log}}</div>
      `,
      styles: []
    })
    export class AppComponent {
      _logs: Array<string> = [];
      constructor(){
        const observable: Rx.Observable<any> = Rx.Observable.merge(
          Rx.Observable.fromEvent(document,'keydown'),
          Rx.Observable.fromEvent(document,'click'),
          Rx.Observable.fromEvent(document,'mousemove'),
          Rx.Observable.fromEvent(document,'scroll'),
          Rx.Observable.fromEvent(document,'touchstart')
        );
        const idleEventObservable = observable.bufferTime(5000)
          .filter(function(arr) {
            return arr.length == 0;
          })
        .subscribe(idleEvent => this._logs.push('idle'));
      }
    }
    
    

你的应用应该工作在本地主机:4200。请注意,在构造函数中,我们将来自文档事件keydownclickmousemovescrolltouchstart的排放合并为一个可观察值。我们将其缓冲为每 5 秒一次,并过滤掉这段时间内发生的事件。然后我们订阅结果,当结果出现时,我们添加一个“空闲”日志,显示在组件中。

可观察对象和 HTTP 服务

$http 和 http 模块

AngularJS 有自己的 Http 模块。$http 服务是一个核心的 AngularJS 服务,它通过浏览器的XMLHttpRequest对象或 JSONP 促进了与远程 http 服务器的通信。

Angular 2 & 4 Http 模块(@angular/http)类似于 Angular 第一版中的 Http 模块,除了它使用了反应式扩展——换句话说,就是 observables。反应式扩展带来了很多好处,提供了前面章节中提到的所有操作符。

当 Angular 5 发布时,它包含了一个新的 httpClient 模块(@angular/common/http)来取代之前的 Http 模块。您仍然可以使用旧的 Http 模块(@angular/http),但它已被弃用,并将在未来的版本中被删除。

下一章将介绍新的 Angular HttpClient 模块。

摘要

本章非常短,但您现在应该了解以下内容:

  • Angular 使用 observables 来处理 DOM 事件和 HTTP 服务调用的结果(调用服务器上的 HTTP 服务并接收结果)。
  • 可观测量使用户能够使用 RxJS 处理数据流。例如,您可以发出一个 HTTP 调用来获取一些数据,并使用 RxJS map操作符来转换结果。

我使用 RxJs 处理过几次 DOM 事件,但是我经常在 HTTP 服务中使用 RxJs 操作符。我将在下一章介绍 HTTP 服务以及如何在 RxJs 中使用它们。

十八、HTTP 和 HttpClient 模块

99%的 Angular 项目涉及客户端(浏览器)和一些远程服务器之间的通信。通常这是通过 HTTP 完成的。因此,了解 HTTP 通信的工作原理以及如何为其编写代码非常重要。这就是本章的内容。

超文本传输协议(HTTP)旨在实现客户端和服务器之间的通信。HTTP 是客户端和服务器之间的请求-响应协议。我们将在本章中更详细地讨论这一点。

HTTP 方法已经存在很长时间了(在 AJAX 和不同类型的 web 应用之前)。HTTP 方法可以用于传统的服务器端 web 应用,也可以用于客户端 AJAX web 应用。

每当客户端使用 HTTP 与 web 服务器通信时,它都会包含有关请求方法的信息。该方法描述了客户端希望服务器做什么——请求的意图。最常用的方法是GETPOSTGET方法用于从服务器请求数据。POST方法用于向服务器发送数据,以便保存或更新数据。

最常用的 HTTP 方法如下:

  • POST
  • GET
  • PUT
  • PATCH
  • DELETE

HTTP 头允许客户机和服务器在请求或响应中传递附加信息。请求头由其名称组成,名称不区分大小写,后跟一个冒号(😃,再后跟其值(没有换行符)。

如果您在浏览器中使用开发工具,您可以看到网络通信,包括 HTTP 调用。如果您使用 web 浏览器的开发工具来检查 HTTP 请求,您将看到向服务器发出的请求和返回的请求。

图 18-1 和 18-2 显示了 HTTP 请求和响应头。

A458962_1_En_18_Fig2_HTML.jpg

图 18-2

HTTP response headers

A458962_1_En_18_Fig1_HTML.jpg

图 18-1

HTTP request headers

Http 正文

http 主体允许客户端和服务器在请求或响应的报头后传递附加信息。Http 主体并不总是必需的,因为信息主体并不总是必需的。例如,Http“get”请求不需要在正文中包含信息——所有信息都已经包含在报头中了。

以下是服务器响应的 http 正文示例:

A458962_1_En_18_Figa_HTML.jpg

用 HTTP 传递信息

从浏览器向服务器传递信息有多种方式。服务器通常在正文中返回信息,尽管它可以通过在 HTTP 头中返回数据来传递信息。

查询参数

Angular Http 客户端允许您使用查询参数将 URL 中的信息传递给服务器。比如 http://localhost:4200/sock js-node/info?t=1498649243238。

某些字符不允许作为 URL 的一部分(例如空格),而其他字符在 URL 中可能有特殊的含义。为了解决这个问题,URL 语法允许对参数进行编码,以确保 URL 有效。例如,以下 URL 中大西洋和城市之间的空格字符编码为%20: https://trailapi-trailapi.p.mashape.com/?q[city_cont]=Atlantic%20City

当使用 JavaScript 方法encodeURIComponent构建带有字符串连接的 URL 时,可以执行这种编码。如果您使用一个有 Angular 的对象(比如URLSearchParams)来构建查询参数字符串,它会自动为您完成这项工作。

在浏览器中导航时,用户可以在地址栏中看到查询参数。但是,当使用 Angular Http 客户端执行 AJAX 请求时,它们是不可见的。

查询参数不能用来传递像请求体那样多的信息。

矩阵参数

Angular Http 客户端允许您使用矩阵参数在 URL 中将信息传递给服务器,例如 Http://localhost:4200/sock js-node/info;t=1498649243238。矩阵参数类似于查询字符串,但使用不同的模式。他们的行为也不同,因为没有?,它们可以被缓存。此外,矩阵参数可以有多个值。通过指定包含矩阵参数的 URL,可以在 Angular 中使用矩阵参数。但是,Angular 目前没有任何内置对象来创建带有矩阵参数的 URL。

矩阵参数不能用来传递像请求体那样多的信息。

路径参数

Angular Http 客户端允许您使用路径参数将信息传递到 URL 中的服务器,例如 Http://localhost:4200/API/badges/9243238。

在请求正文中传递数据

在过去,HTML 表单(带有form标签和input字段)是向服务器发送数据的最佳方式。用户将填写一个表单并点击 Submit,数据将在请求体中被发送(使用 HTTP POST方法)到服务器。

现在 Angular Http 客户端允许您以编程方式做同样的事情:使用 Http 客户端的POST方法在请求体中将信息传递给服务器。

与使用查询或矩阵参数在 URL 中传递数据相比,在请求正文中可以传递更多的数据。

休息

RESTful 应用是一个服务器应用,它将其状态和功能公开为一组客户机(浏览器)可以操作的资源,并且符合一组特定的原则。资源的例子可能是客户列表或他们的订单。

所有资源都是唯一可寻址的,通常通过 URIs,尽管也可以使用其他寻址方式。例如,您可以使用 orders/23 来访问订单编号 23,或者使用 orders/24 来访问订单编号 24。

所有资源都可以通过一组受约束的众所周知的动作来操作,通常是 CRUD(创建、读取、更新、删除),最常见的是通过 HTTP 方法POSTGETPUTDELETE来表示。有时只使用这些 HTML 方法中的一部分,而不是全部。例如,您可以使用一个 HTTP DELETE to orders/23 来删除该订单。

所有资源的数据都是通过一定数量的众所周知的表示形式进行传输的,通常是 HTML、XML 或 JSON。JSON 是最常见的。

数据

JSON 代表 JavaScript 对象符号。这是一种用于在客户端和服务器之间双向传递数据的数据格式。JSON 与 JavaScript 语言使用的数据格式相同。它使用逗号来分隔项目,使用冒号来分隔属性名称和该属性的数据。它使用不同类型的括号来表示对象和数组。

下面是传递包含数据的对象的 JSON。注意如何使用{}括号来表示对象的开始和结束:

{ "name":"John", "age":31, "city":"New York" }

这里是 JSON 传递一个数组。注意如何使用[]括号来表示数组的开始和结束:

[ "Ford", "BMW", "Fiat"]

这是 JSON,用于传递对象数组。请注意括号是如何组合起来创建一个具有两个属性的cars对象的:NissanFord。每个属性都有一系列模型:

{
    "cars": {
        "Nissan": [
            {"model":"Sentra", "doors":4},
            {"model":"Maxima", "doors":4}
        ],
        "Ford": [
            {"model":"Taurus", "doors":4},
            {"model":"Escort", "doors":4}
        ]
    }
}

Angular Http 客户端

Angular Http client 是一个服务,您可以将它注入到您的类中,以执行与服务器的 Http 通信。这项服务可以通过新的 Angular 5 Http 客户端模块@angular/common/http获得,它取代了旧的 Angular 4 Http 模块@angular/common/http。您需要修改您的模块类(项目的模块类)来导入这个模块:

@NgModule({
  imports: [
        ...
        HttpClientModule,
        ...
  ],
  declarations: [ AppComponent ],
  bootstrap: [ AppComponent ]
})

您可以通过以下方式将 Angular Http 服务直接注入到您的组件中:

@Injectable()
class CustomerComponent {
  ...
  constructor(private http: HttpClient) {
  ...
  }
}

这对原型设计来说很好,但从长远来看对代码的可维护性来说是不可取的。实际上,您不应该在服务类之外直接使用 HttpClient 进行数据访问。相反,您应该编写使用 Http 客户端的服务类,然后将这些类注入到您的代码中需要数据访问的地方。如果你查看 angular.io 上的官方 Angular 文档,你会看到以下内容:这是一条黄金法则:总是将数据访问委托给支持服务类。

下面是一个使用 HttpClient 的服务类的示例:

@Injectable()
class CustomerCommunicationService {
  ...
  constructor(private http: HttpClient) {
  ...
  }
}

class CustomerComponent {
  ...
  constructor(private http: CustomerCommunicationService) {
  ...

  // perform data access
  }
}

无商标消费品

在 Angular 5 中,新的 HttpClientModule 允许我们在调用 HTTP 请求时使用泛型。泛型使我们能够告诉 Angular 我们期望从 HTTP 请求中收到的响应的类型。响应类型可以是“任何”(允许任何类型的响应)、变量类型(例如字符串)、类或接口。例如,下面的代码执行一个 http“get ”,将预期的响应指定为一个语言对象数组:

this._http.get<Array<Language>>('https://languagetool.org/api/v2/languages');

这使得 Angular 能够为我们解析响应,这样我们就不必这么做了。不再需要调用 JSON.parse 来将响应字符串转换为对象。

异步操作

在 JavaScript 中,发出 HTTP 请求是一个异步操作。它向 API 发送 HTTP 请求,在继续下一行代码之前不等待响应。当 API 在几毫秒、几秒或几分钟后做出响应时,我们会得到通知,并可以开始处理响应。

在 Angular 中,有两种方法来处理这些异步操作:我们可以使用承诺或可观察值(在几章前已经讨论过)。

通常我们调用我们的支持服务类,它们返回异步结果,我们在组件中处理。

请求选项

很快我将介绍您可以进行的每种类型的 HTTP 调用,但是首先让我们讨论一下请求选项。当您调用与服务器的 HTTP 通信时,您有许多配置通信的方法。你应该使用什么标题?您应该从服务器接受什么媒体?您应该向服务器传递什么凭证?您在名为RequestOptionsArgs的 Angular 对象中设置这些选项,并将其作为参数传递给 Angular Http 客户端方法调用。

这里有一个打GET电话时使用RequestOptionsArg的例子。请注意如何使用该对象来指定 URL、HTTP 方法、参数、身份验证令牌和主体:

var basicOptions:RequestOptionsArgs = {
  url: 'bankInfo',
  method: RequestMethods.Get,
  params: {
    accountNumber: accountNumber
  },
  headers: new HttpHeaders().set('Authentication': authenticationStr),
  body: null
};

HTTP GET 方法:示例

GET方法非常常用于从服务器“获取”数据。它通常不使用请求体。比如对于 URL /customers/getinfo.php?id=123,没有请求体。下面是GET的一些方面:

  • 这是幂等的——多次调用同一个PUT与调用一次效果相同。
  • 它可以保留在浏览器历史中。
  • 可以收藏。
  • 它有长度限制。
  • 请求使用 HTTP 头。
  • 作为 HTTP 正文返回的响应。

GET方法应该在服务器上以独立的方式实现。换句话说,发出多个相同的请求与发出一个请求具有相同的效果。注意,虽然幂等操作在服务器上产生相同的结果(没有副作用),但是响应本身可能不相同(例如,资源的状态可能在请求之间发生变化)。

图 18-3 显示了一个从 Snapchat 获取语言和语言代码列表的组件。

A458962_1_En_18_Fig3_HTML.jpg

图 18-3

Getting a list of languages and language codes from Snapchat

这将是示例 http-ex100:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new http-ex100 --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd http-ex100
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”

  4. 编辑模块:编辑 app.module.ts,修改为:

    import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';
    import { AppComponent } from './app.component';
    import { SwaggerService } from './swagger.service';
    import { HttpClientModule } from '@angular/common/http';
    
    @NgModule({
      declarations: [
        AppComponent
      ],
      imports: [
        BrowserModule, HttpClientModule
      ],
      providers: [SwaggerService],
      bootstrap: [AppComponent]
    })
    export class AppModule { }
    
    
  5. 创建服务:创建 swagger.service.ts::

    import { Injectable } from '@angular/core';
    import { HttpClient } from '@angular/common/http';
    import { Language } from './language';
    
    @Injectable()
    export class SwaggerService {
        constructor(private _http: HttpClient){}
    
        getLanguages() {
            return this._http.get<Array<Language>>('https://languagetool.org/api/v2/languages');
        }
    }
    
    
  6. 创建数据对象类:创建语言. ts.

    export class Language {
        private _code: string;
        private _name: string;
        public get code() {
            return this._code;
        }
        public get name() {
            return this._name;
        }
        public set code(newValue: string){
            this._code = newValue;
        }
        public set name(newValue: string){
            this._name = newValue;
        }
    }
    
    
  7. 编辑组件:编辑 app.component.ts,修改为:

    import { Component, OnInit } from '@angular/core';
    import { SwaggerService } from './swagger.service';
    import { Language } from './language';
    
    @Component({
      selector: 'app-root',
      template: `
      <h1>Countries</h1>
      <ul>
      <li *ngFor="let language of _languages">
      {{language.name}}&nbsp;({{language.code}})
      </li>
      </ul>
      `,
      styles: []
    })
    export class AppComponent implements OnInit{
      _languages = new Array<Language>();
    
      constructor(private _swaggerService: SwaggerService) {}
    
      ngOnInit(){
        this._swaggerService.getLanguages().subscribe(
          res => {
           this._languages = res;
          },
          error => { console.log('an error occurred'); }
        )
      }
    }
    
    

您的应用应该在 localhost:4200 上工作,您应该会看到一个语言列表。请注意以下几点:

  • swagger.service.ts 文件创建一个具有可注入注释的服务,使其能够被注入到 app 组件中。这个服务有一个构造函数,Angular Http 模块被注入到这个构造函数中。它还包含方法getLanguages,该方法对服务器进行 HTTP 调用,服务器返回一个可观察值。请注意,get 方法使用泛型将响应类型指定为“语言”(参见 >)。
  • 文件“language.ts”定义了语言数据对象,我们将使用该对象将数据从服务传递到组件。
  • app.component.ts 文件创建一个组件。注意,swagger 服务是使用构造函数注入到这个组件中的。当组件初始化时,它调用 swagger 服务并用两种方法订阅可观察的结果:第一种方法表示成功,第二种方法表示失败。
  • 第一个方法(成功的方法)接受 HTTP 结果作为参数,并将实例变量_langages设置为返回的 JavaScript 对象数组,然后在组件中可见。

使用参数的 HTTP GET 方法:示例

我们已经讨论了GET,但是“得到”什么呢?一个GET使用参数来获取一个特定的东西(或一些东西)——如果您想从服务器上获取特定客户的信息,这非常有用。您可以通过简单地修改GET的 URI 以包含查询参数来实现,或者您可以使用嵌入到RequestOptionsArgs对象中的 Angular 搜索或参数对象来实现。

图 18-4 显示了一个使用查询参数以三种不同方式执行 HTTP GET的例子,由三个不同的按钮触发。

A458962_1_En_18_Fig4_HTML.jpg

图 18-4

GET in three different ways

这将是 http-ex200 的示例:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new http-ex200 --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd http-ex200
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”

  4. 编辑模块:编辑 app.module.ts,修改为:

    import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';
    import { FormsModule } from '@angular/forms';
    import { AppComponent } from './app.component';
    import { HttpClientModule } from '@angular/common/http';
    
    @NgModule({
      declarations: [
        AppComponent
      ],
      imports: [
        BrowserModule,
        HttpClientModule,
        FormsModule
      ],
      providers: [],
      bootstrap: [AppComponent]
    })
    export class AppModule { }
    
    
  5. 编辑组件:编辑 app.component.ts,修改为:

    import { Component } from '@angular/core';
    import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
    
    @Component({
      selector: 'app-root',
      template: `
        <input [(ngModel)]="_search" placeholder="city">
        <button (click)="doSearchConcatenatedUrl()">Search (Concatenated URL)</button>
        <button (click)="doSeachHttpParams1()">Search (Http Params1)</button>
        <button (click)="doSeachHttpParams2()">Search (Http Params2)</button>
        <p>JSON {{_result | json}}</p>
      `,
      styles: []
    })
    export class AppComponent {
      _search = 'Atlanta';
      _result = {};
    
      constructor(private _http: HttpClient){
      }
    
      doSearchConcatenatedUrl(){
        const concatenatedUrl: string =
          "https://trailapi-trailapi.p.mashape.com?q[city_cont]=" +
          encodeURIComponent(this._search);
        const mashapeKey = 'OxWYjpdztcmsheZU9AWLNQcE9g9wp1qdRkFjsneaEp2Yf68nYH';
        const httpHeaders: HttpHeaders = new HttpHeaders(
          {'Content-Type': 'application/json',
         'X-Mashape-Key': mashapeKey});
        this._http.get(concatenatedUrl, { headers: httpHeaders }).subscribe(
          res => { this._result = res; });
      }
    
      doSeachHttpParams1(){
        const url: string =
          'https://trailapi-trailapi.p.mashape.com';
        const mashapeKey = 'OxWYjpdztcmsheZU9AWLNQcE9g9wp1qdRkFjsneaEp2Yf68nYH';
        const httpHeaders = new HttpHeaders(
          {'Content-Type': 'application/json',
          'X-Mashape-Key': mashapeKey});
        const params = new HttpParams({
          fromString: 'q[city_cont]=' + this._search;
        });
        this._http.get(url, {headers: httpHeaders, params: params}).subscribe(
            res => { this._result = res; });
      }
    
      doSeachHttpParams2(){
        const url: string =
          'https://trailapi-trailapi.p.mashape.com';
        const mashapeKey = 'OxWYjpdztcmsheZU9AWLNQcE9g9wp1qdRkFjsneaEp2Yf68nYH';
        const httpHeaders = new HttpHeaders(
          {'Content-Type': 'application/json',
          'X-Mashape-Key': mashapeKey});
        const params = new HttpParams().set('q[city_cont]', this._search);
        this._http.get(url, {headers: httpHeaders, params: params}).subscribe(
            res => { this._result = res; });
      }
    }
    
    

你的应用应该工作在本地主机:4200。请注意以下几点:

  • 方法doSearchConcatenatedUrl通过将带有编码的‘q[city _ cont]’参数集的 URL 附加到编码的输入字符串来手动构建 URL 字符串。在 Http 客户端调用GET方法。
  • 方法 doSeachHttpParams1 从一个查询字符串构建一个 HttpParams 对象,类似于上面的方法'doSearchConcatenatedUrl'。在 Http 客户端调用GET方法,在第二个参数中传递HttpParams对象。注意,HttpParams 对象为我们进行编码。
  • 方法 doSeachHttpParams2 创建一个 HttpParams 对象,并将“q[city_cont]”参数设置为输入字符串。在 Http 客户机上调用GET方法,在第二个。

使用路径参数的 Http GET 方法:示例

这是一个使用路径参数执行 HTTP GET的例子。向用户显示文章列表,每一篇文章都有一个显示按钮,如图 18-5 所示。用户可以点击 Show 按钮,一个 HTTP GET将被调用,带有路径参数,以获取文章的细节,然后显示在一个弹出的模态上。

A458962_1_En_18_Fig5_HTML.jpg

图 18-5

Showing an article from a list

这将是 http-ex300 的示例:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new http-ex300 --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd http-ex300
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”

  4. 编辑模块:编辑 app.module.ts,修改为:

    import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';
    
    import { AppComponent } from './app.component';
    
    import { HttpClientModule } from '@angular/common/http';
    
    @NgModule({
      declarations: [
        AppComponent
      ],
      imports: [
        BrowserModule,
        HttpClientModule
      ],
      providers: [],
      bootstrap: [AppComponent]
    })
    export class AppModule { }
    
    
  5. 编辑组件:编辑 app.component.ts,修改为:

    import { Component, OnInit, AfterViewInit, ViewChild } from '@angular/core';
    import { HttpClient } from '@angular/common/http';
    
    @Component({
      selector: 'app-root',
      template: `
      <ul>
      <li *ngFor="let post of _posts">
        {{post.title}}&nbsp;&nbsp;<button (click)="showPost(post.id)">Show</button>
      </li>
      </ul>
      <div #modal id="myModal" class="modal">
        <div class="modal-content">
          <span class="close" (click)="closeModal()">&times;</span>
          <h3>{{this._post.title}}</h3>
          <p>{{this._post.body}}</p>
        </div>
      </div>
      `,
      styles: []
    })
    export class AppComponent implements OnInit {
      _posts = [];
      _post = {};
      @ViewChild('modal') _myModal: any;
    
      constructor(private _http: HttpClient) {
      }
    
      ngOnInit() {
        return this._http.get<any>("http://jsonplaceholder.typicode.com/posts").subscribe(
          res => {
            this._posts = res;
          }
        );
      }
    
      showPost(postId: number) {
        this._http.get<any>(`http://jsonplaceholder.typicode.com/posts/${postId}`).subscribe(
          res => {
            this._post = res;
            this._myModal.nativeElement.style.display = 'block';
          }
        )
      }
    
      closeModal() {
        this._myModal.nativeElement.style.display = 'none';
      }
    
    }
    
    
  6. 编辑样式:编辑 styles.css 并将其更改为以下内容:

    .modal {
        display: none;
        position: fixed;
        z-index: 1;
        left: 0;
        top: 0;
        width: 100%;
        height: 100%;
        overflow: auto;
        background-color: rgb(0,0,0);
        background-color: rgba(0,0,0,0.2);
    }
    
    .modal-content {
        background-color: #fefefe;
        margin: 15% auto;
        padding: 20px;
        border: 1px solid #888;
        width: 60%;
    }
    
    .close {
        color: #aaa;
        float: right;
        font-size: 28px;
        font-weight: bold;
    }
    
    .close:hover,
    .close:focus {
        color: black;
        text-decoration: none;
        cursor: pointer;
    }
    
    

注意,在 app 组件方法showPost中,我们使用模板文字将文章 ID 注入 URL 字符串。

HTTP POST 方法:示例

POST非常常用于向服务器发布数据。它通常在请求正文中发送数据。例如,对于 URL /customers/new,请求主体是 name = Mark&city = Atlanta&state = GA。

以下是 HTTP POST的一些重要方面:

  • 它不是幂等的——多次调用同一个 put 会产生与调用一次不同的效果。
  • 不能缓存。
  • 它不能保留在浏览器历史中。
  • 不能加书签。
  • 它没有长度限制。
  • 请求使用 HTTP 正文。
  • 响应作为 HTTP 主体返回。

就其本质而言,HTTP POST 不是等幂的。它有副作用—例如,通过两次提交数据来两次添加客户(双重提交)。

图 18-6 显示了一个执行 HTTP POST的例子。用户可以输入标题和正文,然后单击 Add 将它们发送到服务器。服务器向浏览器返回信息,这些信息被添加到底部的“您添加的”列表中。

A458962_1_En_18_Fig6_HTML.jpg

图 18-6

HTTP POST

这将是示例 http-ex400:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new http-ex400 --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd http-ex400
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”

  4. 编辑模块:编辑 app.module.ts,修改为:

    import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';
    import { HttpClientModule } from '@angular/common/http';
    import { FormsModule } from '@angular/forms';
    import { AppComponent } from './app.component';
    @NgModule({
      declarations: [
        AppComponent
      ],
      imports: [
        BrowserModule,
        HttpClientModule,
        FormsModule
      ],
      providers: [],
      bootstrap: [AppComponent]
    })
    export class AppModule { }
    
    
  5. 编辑组件:编辑 app.component.ts,修改为:

    import { Component, OnInit, AfterViewInit, ViewChild } from '@angular/core';
    import { HttpClient } from '@angular/common/http';
    
    @Component({
      selector: 'app-root',
      template: `
          <div>
          Title:
          <br/>
          <input type="text" [(ngModel)]="_title" size="50" />
          <br/>
          <br/>
          Body:
          <br/>
          <textarea [(ngModel)]='_body' rows="2" cols="50">
          </textarea>
          <br/>
          <button (click)="onAdd()">Add</button>
          </div>
          <p><b>You Added:</b></p>
          <p *ngIf="_added.length == 0">None</p>
          <p *ngFor="let added of _added">
            {{added.title}}
          </p>
        `,
      styles: ['div { padding: 20px; background-color: #C0C0C0 }']
    })
    export class AppComponent {
      _title: string;
      _body: string;
      _added: Array<any> = new Array<any>();
    
      constructor(private _http: HttpClient) {
      }
    
      onAdd(){
        const requestBody = {
          title: this._title || '[Unspecified]',
          body: this._body || '[Unspecified]',
        };
        this._http.post("http://jsonplaceholder.typicode.com/posts", requestBody).subscribe(
          res => {
            this._added.push(res);
          }
        )
      }
    }
    
    

请注意以下几点:

  • 在 app 组件方法onAdd中,我们创建了将在请求体中发送到服务器的对象。

  • 在 app 组件方法onAdd中,我们使用了or操作符来确保我们向服务器传递了一些有效的东西——要么是this._title(如果存在的话),要么是文本“[Unspecified]:

    title: this._title || '[Unspecified]'
    
    
  • 在 app 组件方法onAdd中,我们订阅 HTTP POST并使用箭头函数来处理返回的结果。我们将返回的结果添加到_added的数组中,因此它出现在底部。

使用路径参数的 HTTP PUT 方法

除了在 REST 服务中通常用于更新资源而不是创建资源之外,PUT方法类似于POST方法。

HTTP PUT的一些重要方面包括:

  • 这是幂等的——多次调用同一个PUT与调用一次效果相同。
  • 它没有长度限制
  • 它不可缓存。
  • 请求使用 HTTP 正文。
  • 响应作为 HTTP 头返回。

使用路径参数的 HTTP 修补方法

PATCH方法类似于PUT,除了它不是幂等的。例如,将某项资源的价值增加一定的数量会很有用。

PATCH的一些重要方面包括:

  • 它不是幂等的——多次调用同一个PUT与调用一次有不同的效果。
  • 它没有长度限制。
  • 它不可缓存。
  • 请求使用 HTTP 正文。
  • 响应作为 HTTP 头返回。

使用路径参数的 HTTP 删除方法

DELETE方法用于从服务器上删除资源。请注意以下几点:

  • 它不是幂等的——多次调用DELETE与调用一次效果相同。
  • 它不可缓存。
  • 请求使用 HTTP 正文。
  • 响应作为 HTTP 头返回。

修改服务器响应:示例

记住,Http 客户端服务调用返回可观察的对象。这意味着服务器向客户端(浏览器)返回一个异步数据流,并且您可以使用 RxJS 模块使用第十六章中讨论的操作符来处理该数据。这包括我们可以用来转换数据的map操作符。

图 18-7 显示了一个使用 RxJS map操作符(带函数)修改从服务器返回的响应的例子。在这种情况下,我们使用它将数据转换为类型化数据(在类中构造的数据)。

A458962_1_En_18_Fig7_HTML.jpg

图 18-7

Using map to modify a response

这将是 http-ex500 的示例:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new http-ex500 --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd http-ex500
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”

  4. 创建类:在 app 中创建以下 TypeScript 类post.``ts:

    export class Post {
        _title: string = "";
        _body: string = "";
    
        constructor(title: string, body: string){
            const titleNaN = title || '';
            const bodyNaN = body || '';
            this._title = titleNaN.length > 10 ? titleNaN.substring(0,9): titleNaN;
            this._body = bodyNaN.length > 20 ? bodyNaN.substring(0,19): bodyNaN;
        }
    
        get title(): string{
            return this._title;
        }
    
        get body(): string{
            return this._body;
        }
    
    }
    
    
  5. 编辑模块:编辑 app.module.ts,修改为:

    import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';
    import { AppComponent } from './app.component';
    import { HttpClientModule } from '@angular/common/http';
    
    @NgModule({
      declarations: [
        AppComponent
      ],
      imports: [
        BrowserModule,
        HttpClientModule
      ],
      providers: [],
      bootstrap: [AppComponent]
    })
    export class AppModule { }
    
    
  6. 编辑组件:编辑 app.component.ts,修改为:

    import { Component } from '@angular/core';
    import { HttpClient } from '@angular/common/http';
    import { Post } from './Post';
    import 'rxjs/Rx';
    
    @Component({
      selector: 'app-root',
      template: `
        <ul>
          <li *ngFor="let post of _posts">
            <b>{{post.title}}:</b> {{post.body}}
          </li>
        </ul>
      `,
      styles: []
    })
    export class AppComponent {
      _posts: Array<Post>;
      constructor(private _http: HttpClient) {}
      ngOnInit() {
        return this._http.get<Array<Post>>("http://jsonplaceholder.typicode.com/posts")
          .map(
            response => {
              const postsArray: Array<Post> = new Array<Post>();
              for (const responseItem of response){
                const post =
                  new Post(responseItem['title'], responseItem['body']);
                postsArray.push(post);
              }
              return postsArray;
            }
          )
          .subscribe(
            response => {
              this._posts = response;
            }
        );
      }
    }
    
    

我们创建 typescript 类Post来存储每篇文章。请注意,这个类有一个构造函数,用于修剪每篇文章的标题和正文。我们使用or技巧将“不真实”的值转换为空字符串:

const titleNaN = title || '';

我们使用下面的代码从服务器获取数据。我们使用映射将响应(一个对象数组)转换成一个Post类的类型化数组。然后我们订阅结果:

    return this._http.get("http://jsonplaceholder.typicode.com/posts")
      .map(
        response => {
          const postsArray: Array<Post> = new Array<Post>();
          for (const responseItem of response){
            const post =
              new Post(responseItem['title'], responseItem['body']);
            postsArray.push(post);
          }
          return postsArray;
        }
      )
      .subscribe(
        response => {
          this._posts = response;
        }

    );

处理服务器错误响应:示例

当您订阅 HTTP 方法调用时,您提供了一个处理结果的处理程序方法。但是,您也可以提供其他处理程序方法—一个用于处理错误,另一个用于处理完成:

.subscribe(
    function(response) { console.log("Success " + response)},
    function(error) { console.log("Error " + error)},
    function() { console.log("Completion")}
);

这是一个处理服务器错误并显示适当错误信息的例子(如图 18-8 所示)。

A458962_1_En_18_Fig8_HTML.jpg

图 18-8

Displaying an error message

这将是 http-ex600 的示例:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new http-ex600 --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd http-ex600
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”

  4. 创建服务类:在应用中创建下面的 TypeScript 类service.ts:

    import { Injectable } from '@angular/core';
    import { HttpClient } from '@angular/common/http';
    import { Observable } from 'rxjs/Observable';
    import 'rxjs/Rx';
    
    @Injectable()
    export class Service {
    
      constructor(private _http: HttpClient) {
      }
    
      getPosts() : Observable<any> {
        return this._http.get("http://jsonplaceholder.typicode.com/postss");
      };
    
    }
    
    
  5. 编辑模块:编辑 app.module.ts,修改为:

    import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';
    import { AppComponent } from './app.component';
    import { HttpClientModule } from '@angular/common/http';
    import { Service } from './Service';
    
    @NgModule({
      declarations: [
        AppComponent
      ],
      imports: [
        BrowserModule,
        HttpClientModule
      ],
      providers: [HttpClientModule, Service],
      bootstrap: [AppComponent]
    })
    export class AppModule { }
    
    
  6. 编辑组件:编辑 app.component.ts,修改为:

    import { Component } from '@angular/core';
    import { Service } from './Service';
    import 'rxjs/Rx';
    
    @Component({
      selector: 'app-root',
      template: `
        <ul>
          <li *ngFor="let post of _posts">
            <b>{{post.title}}:</b> {{post.body}}
          </li>
        </ul>
        <div *ngIf="_error">
          Error: {{_error.status}}: {{_error.statusText}}
        </div>
      `,
      styles: ['div {font-size:20px; padding: 5px; background-color: red;color: white}']
    })
    export class AppComponent {
      _posts = [];
      _error;
    
      constructor(private _service: Service) {}
    
      ngOnInit() {
        this._service.getPosts()
          .subscribe(
            response => {
              this._posts = response;
            },
            error => {
              this._error = error;
            }
          );
      }
    }
    
    

请注意以下几点:

  • 服务类service. ts使用了不正确的 URL,这将引发 404 错误。
  • 组件 app.component.ts 调用方法getPosts并订阅其结果,使用两个方法,每个方法都用一个箭头函数实现。第一个处理成功的结果,第二个处理错误。在这个例子中,我们处理错误,将实例变量_error设置为它的结果。
  • 模板中引用了_error实例变量。如果设置,它显示一条红色消息。

异步管道:示例

异步管道订阅可观察对象或承诺,并返回它发出的最新值。当发出一个新值时,异步管道标记要检查更改的组件。当组件被破坏时,异步管道会自动取消订阅,以避免潜在的内存泄漏。

图 18-9 显示了一个使用映射来转换服务器输出,然后使用管道输出的例子。

A458962_1_En_18_Fig9_HTML.jpg

图 18-9

Using a pipe to output

这将是 http-ex700 的示例:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new http-ex700 --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd http-ex700
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”

  4. 编辑模块:编辑 app.module.ts,修改为:

    import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';
    import { AppComponent } from './app.component';
    import { HttpClientModule } from '@angular/common/http';
    
    @NgModule({
      declarations: [
        AppComponent
      ],
      imports: [
        BrowserModule,
        HttpClientModule
      ],
      providers: [HttpClientModule],
      bootstrap: [AppComponent]
    })
    export class AppModule { }
    
    
  5. 编辑组件:编辑 app.component.ts,修改为:

    import { Component } from '@angular/core';
    import { Injectable } from '@angular/core';
    import { HttpClient } from '@angular/common/http';
    import 'rxjs/Rx';
    import { Observable } from 'rxjs/Observable';
    
    @Component({
      selector: 'app-root',
      template: `
        <h1>Post Title Names</h1>
        <p>{{_result|async}}</p>
      `,
      styles: []
    })
    export class AppComponent {
      _result: any;
    
      constructor(private _http: HttpClient) {}
    
      ngOnInit() {
        this._result =
          this._http.get<Array<any>>("http://jsonplaceholder.typicode.com/posts")
          .map(
            response => {
              let titles = '';
              for (const responseItem of response){
                titles += responseItem['title'];
              }
              return titles;
            }
          );
      }
    }
    
    

请注意以下几点:

  • 组件 app.component.ts 调用 HTTP GET方法返回帖子列表。它使用map操作符(和一个函数)将其转换成所有文章标题的字符串。然后,它将结果赋给_result实例变量。
  • 组件 app.component.ts 使用一个模板来显示_result实例变量的值。

摘要

这一重要章节讲述了 Http 通信的基础知识,以及如何使用 HTTP 服务来获取和发送数据到服务器。我建议您完成所有的练习,因为 Http 服务对您将来编写单页应用非常重要。请记住,您将使用的 Http 服务可能有自己的自定义头和内容类型。例如,您的服务器开发人员可能会引入一个自定义头,以便在发生错误时将错误信息从服务器返回到客户端。您可能还需要为每个 Http 服务调用添加安全令牌。您将需要知道这些 Http 服务是如何工作的,以及如何扩展它们的功能以按照您想要的方式工作。

下一章也很重要:它涵盖了表单,Angular 的另一个你会一直用到的特性。

十九、表单

没有表单,您无法在应用中输入数据。AngularJS 允许用户快速创建表单,使用NgModel指令将输入元素绑定到$scope 中的数据。您也可以在 Angular 中做同样的事情,但是 Angular 4 有一个新的表单模块,可以更容易地执行以下操作:

  • 动态创建表单
  • 用通用验证器验证输入(必需)
  • 用自定义验证器验证输入
  • 测试表格

两种书写表格的方式

您可以像以前在 AngularJS 中一样继续编写表单,但是我推荐使用新的表单模块,因为它为您做了更多的工作。表单模块提供了两种处理表单的主要方式:模板驱动的表单和反应式表单。这两种方式都适用于同一个表单模块。

模板驱动的表单

这与 Angular.JS 中的工作方式类似。我们构建 HTML 模板并添加一些指令来指定附加信息(如验证规则),Angular 负责在幕后为我们构建模型对象:底层表单、表单组和控件。

  • 优点:简单,快速入门,非常适合简单的表单,不需要知道表单模型对象是如何工作的
  • 缺点:HTML 和业务规则是耦合的,没有单元测试

反应式表单

反应式表单不同。我们自己构建模型对象(包括验证表单规则),表单绑定(并同步)到模板。我通常使用反应式表单多于模板驱动的表单。

  • 优点:更多的控制,对于更高级的表单是完美的,支持单元测试,HTML 和业务规则是分离的
  • 缺点:需要知道表单模型对象是如何工作的,需要更多的时间来开发

在撰写本书时,Angular CLI 生成的项目已经建立了对表单模块的节点依赖。您所要做的就是调整您的模块来导入表单模块。下面是 app.module.ts 文件的一个示例:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';

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

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    FormsModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

表单模型对象

本节适用于两种形式的写作:模板和反应。两者使用相同的模型对象。让我们快速浏览一下。

NgForm

存储表单的状态信息,包括以下内容:

  • 表单内所有控件的值
  • 表单中的字段组
  • 表单中的字段
  • 验证器

表单组

存储一组FormControl实例的值和有效状态:

  • 表单组中所有控件的值

表单控件

存储单个控件的值和有效状态,例如列表框:

  • 价值
  • 验证状态
  • 状态(例如,禁用)

您可以添加订户来响应表单控件值的更改:

this.form.controls['make'].valueChanges.subscribe(
        (value) => { console.log(value); }
);

您可以添加订户来响应表单控制状态的更改:

this.form.controls['make'].statusChanges.subscribe(
        (value) => { console.log(value); }
);

示例输出:

INVALID
VALID

表单阵列

这用于跟踪多个FormControlFormGroupFormArray的值和状态。这对于处理多个表单对象和跟踪整体有效性和状态很有用。

表单和 CSS

本节适用于两种编写表单的方法:模板和反应式。当您进行表单验证时,您需要在无效数据出现时突出显示它。表单模块被设计成与 CSS 一起工作,使得突出显示无效的用户输入变得非常容易。表 19-1 中列出的样式会自动添加到表单元素中——你需要做的只是添加 CSS 代码来产生所需的视觉效果。

表 19-1

Styles Added to Form Elements

| 风格 | 描述 | | :-- | :-- | | `ng-touched` | 控件失去焦点时应用的样式 | | `ng-untouched` | 如果控件尚未失去焦点,则应用样式 | | `ng-valid` | 控件通过验证时应用的样式 | | `ng-invalid` | 控件未通过验证时应用的样式 | | `ng-dirty` | 如果用户已经与控件集成,则应用样式 | | `ng-pristine` | 用户尚未与控件交互时应用的样式 |

模板表单:示例

如前所述,模板表单使用指令来创建表单模型对象。您在模板中构建输入表单和输入,并添加一些指令,表单就准备好了,可以工作了。模板表单非常适合快速构建具有简单验证的简单表单。

模板表单异步工作。因此,在视图初始化和指令处理完成之前,模型对象是不可用的。甚至不是所有的模型对象在AfterViewInit生命周期方法中都可用。

要使用 Angular 模板表单,您的应用模块需要从@angular/forms 节点模块导入表单模块:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';

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

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    FormsModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

让我们来创建一个模板表单,看看需要什么来使它工作。这是表格示例-ex100。

  1. 使用 CLI 构建应用:使用以下命令:

    ng new forms-ex100 --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd forms-ex100
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“应用工作!”

  4. 编辑模块:编辑 app.module.ts,修改为:

    import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';
    import { FormsModule } from '@angular/forms';
    
    import { AppComponent } from './app.component';
    
    @NgModule({
      declarations: [
        AppComponent
      ],
      imports: [
        BrowserModule,
        FormsModule
      ],
      providers: [],
      bootstrap: [AppComponent]
    })
    export class AppModule { }
    
    
  5. 编辑组件:编辑 app.component.ts,修改为:

    import { Component, ViewChild } from '@angular/core';
    import { NgForm, RequiredValidator } from '@angular/forms';
    
    @Component({
      selector: 'app-root',
      template: `
      <form #f novalidate>
        <p>First Name <input name="fname"/></p>
        <p>Last Name <input name="lname"/></p>
        Valid: {{ f.valid }}
        Data: {{ f.value | json }}
      </form>
      `,
      styles: []
    })
    export class AppComponent {
      @ViewChild('f') f: NgForm;
    }
    
    
  6. View app: Notice that this component just displays the input forms, as shown in Figure 19-1. It doesn’t display any further information.

    A458962_1_En_19_Fig1_HTML.jpg

    图 19-1

    Displaying input forms

  7. 编辑组件:现在我们将向表单添加一些指令和输入标签,以使表单作为模板表单工作。这些变化在下面的代码中以粗体突出显示:

    import { Component, ViewChild } from '@angular/core';
    import { NgForm, RequiredValidator } from '@angular/forms';
    
    @Component({
      selector: 'app-root',
      template: `
      <form #f="ngForm" novalidate>
        <p>First Name <input name="fname" ngModel required /></p>
        <p>Last Name <input name="lname" ngModel required /></p>
        Valid: {{ f.valid }}
        Data: {{ f.value | json }}
      </form>  `,
      styles: []
    })
    export class AppComponent {
        @ViewChild('f') f: NgForm;
    }
    
    
  8. View app: Note that this component displays the input forms and the state of the form in Figure 19-2—its validity and its data.

    A458962_1_En_19_Fig2_HTML.jpg

    图 19-2

    State of the form

这展示了使用ngFormngModel指令制作模板表单有多快,其中的form对象保存表单状态(包括数据)。还要注意 HTML 输入字段是如何使用name属性的——这是由表单指令获取的,用来标识控件及其值。

模板变量和数据绑定:示例

有时,您需要访问每个控件来访问其状态、值等。您可以使用以下语法将模板变量设置为控件的ngModel(即其FormControl对象)。您也可以使用ViewChild来访问作为变量的FormControl:

import { Component, ViewChild } from '@angular/core';
import { NgForm, FormControl, RequiredValidator } from '@angular/forms';

@Component({
  selector: 'app-root',
  template: `
  <form #f="ngForm" novalidate>
    <p>First Name <input name="fname" ngModel #fname="ngModel" required />
    </p>
    <h2>Form Template Variable</h2>
    Valid {{ fname.valid}}
    Data: {{ fname.value | json }}
    <h2>From Instance Variable</h2>
    Valid {{ fname2.valid}}
    Data: {{ fname2.value | json }}
  </form>  `,
  styles: []
})
export class AppComponent {
    @ViewChild('f') f: NgForm;
    @ViewChild('fname') fname2: FormControl;
}

您还可以使用模板变量来查询表单控件状态,如表 19-2 所示。这使得在模板中添加隐藏和显示错误消息的逻辑变得非常容易。

表 19-2

Template Variables

| 可变的 | 描述 | | :-- | :-- | | `.touched` | 用户是否在该字段中进行了任何输入?返回真或假。 | | `.valid` | 字段输入是否通过验证?返回真或假。 | | `.value` | 当前表单值。 | | `.hasError('required')` | 是否出现了指定的错误?返回真或假。 |

有时,您需要将每个控件的值双向绑定到模型,以便可以根据需要获取和设置每个控件的值。如果要设置表单控件,这很有用。更改ngModel指令以使用双向绑定,并将其链接到实例变量——在下面的例子中是_name:

<input type="text" class="form-control" name="name" placeholder="Name (last, first)" [(ngModel)]="_name" required>

让我们来创建一个模板表单并将表单控件绑定到实例变量。让我们用 bootstrap 样式构建这个表单,这样它看起来会很好。提交表单有一个根据用户输入启用或禁用的按钮,如图 19-3 所示。

A458962_1_En_19_Fig3_HTML.jpg

图 19-3

Creating a template form binding form controls to instance variables

这将是表格示例-ex200:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new forms-ex200 --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd forms-ex200
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“应用工作!”

  4. 编辑网页:编辑 index.html 文件,将其更改为:

    <!doctype html>
    <html lang="en">
    <head>
      <meta charset="utf-8">
      <title>FormsEx200</title>
      <base href="/">
    
      <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/css/bootstrap.min.css" integrity="sha384-rwoIResjU2yc3z8GV/NPeZWAv56rSmLldC3R/AZzGRnGxQQKnKkoFVhFQhNUwEyJ" crossorigin="anonymous">
      <script src="https://code.jquery.com/jquery-3.1.1.slim.min.js" integrity="sha384-A7FZj7v+d/sdmMqp/nOQwliLvUsJfDHW+k9Omg/a/EheAdgtzNs3hpfag6Ed950n" crossorigin="anonymous"></script>
      <script src="https://cdnjs.cloudflare.com/ajax/libs/tether/1.4.0/js/tether.min.js" integrity="sha384-DztdAPBWPRXSA/3eYEEUWrWCy7G5KFbe8fFjk5JAIxUYHKkDx6Qin1DkWx51bBrb" crossorigin="anonymous"></script>
      <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/js/bootstrap.min.js" integrity="sha384-vBWWzlZJ8ea9aCX4pEW3rVHjgjt7zpkNpZk+02D9phzyeVkE+jo0ieGizqPLForn" crossorigin="anonymous"></script>
    
      <meta name="viewport" content="width=device-width, initial-scale=1">
      <link rel="icon" type="image/x-icon" href="favicon.ico">
    </head>
    <body>
      <app-root></app-root>
    </body>
    </html>
    
    
  5. 编辑模块:编辑 app.module.ts 文件,将其更改为:

    import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';
    import { FormsModule } from '@angular/forms';
    
    import { AppComponent } from './app.component';
    
    @NgModule({
      declarations: [
        AppComponent
      ],
      imports: [
        BrowserModule,
        FormsModule
      ],
      providers: [],
      bootstrap: [AppComponent]
    })
    export class AppModule { }
    
    
  6. 编辑组件:编辑 app.component.ts 文件,将其更改为:

    import { Component, ViewChild } from '@angular/core';
    import { NgForm, RequiredValidator } from '@angular/forms';
    
    @Component({
      selector: 'app-root',
      template: `
      <form #appointmentForm="ngForm" novalidate (ngSubmit) = "onSubmitForm(appointmentForm)">
        <legend>Appointment</legend>
        <div class="form-group">
          <label for="name">Name</label>
          <input type="text" class="form-control" name="name" placeholder="Name (last, first)" [(ngModel)]="_name" required>
        </div>
        <div class="form-group">
          <label for="password">Password</label>
          <input type="password" class="form-control" name="password" placeholder="Password" [(ngModel)]="_password" required>
        </div>
        <div class="form-group">
          <div class="form-check">
            <div>
              <label>Appointment Time</label>
            </div>
            <label class="form-check-label">
              <input type="radio" class="form-check-input" name="time" value="12pm" [(ngModel)]="_time" required>
              12pm
            </label>
          </div>
          <div class="form-check">
            <label class="form-check-label">
              <input type="radio" class="form-check-input" name="time" value="2pm" [(ngModel)]="_time" required>
              2pm
            </label>
          </div>
          <div class="form-check">
            <label class="form-check-label">
              <input type="radio" class="form-check-input" name="time" value="4pm" [(ngModel)]="_time" required>
              4pm
            </label>
          </div>
        </div>
        <div class="form-group">
    
          <label for="exampleTextarea">Ailment</label><textarea class="form-control" name="ailment" rows="3" [(ngModel)]="_ailment" required ></textarea>
        </div>
        <button type="submit" class="btn btn-primary" [disabled]="!_appointmentForm.valid">Submit</button>
        Valid: {{ _appointmentForm.valid }}
        Data: {{ _appointmentForm.value | json }}
      </form>
      `,
      styles: ['form { padding: 20px }', '.form-group { padding-top: 20px }']
    })
    export class AppComponent {
      @ViewChild('appointmentForm') _appointmentForm: NgForm;
      _name: string = 'mark';
      _password: string = '';
      _time: string = '';
      _ailment: string = '';
    
      onSubmitForm() {
        alert("Submitting data:" + JSON.stringify(this._appointmentForm.value));
      }
    }
    
    

你的应用应该工作在本地主机:4200。注意,文件 index.html 被修改为链接到引导 CSS 和 JavaScript 文件。

文件 app.component 执行以下操作:

  • 设置一个模板变量appointmentForm的表单。表单在提交时触发方法onSubmitForm

  • 设置输入字段,并使用双向绑定和ngModel指令将每个字段的值链接到一个实例变量。

  • 模板中包含以下标记以启用或禁用提交按钮:

    <button type="submit" class="btn btn-primary" [disabled]="!_appointmentForm.valid">Submit</button>
    
    
  • 在下面显示表单有效性和值。

模板表单和 CSS:示例

让我们通过创建一个带有颜色编码的输入表单来形成验证状态。绿色表示有效输入,红色表示无效输入。还有错误信息的代码,如图 19-4 所示。

A458962_1_En_19_Fig4_HTML.jpg

图 19-4

Input form with color coding

这将是表格示例-ex300:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new forms-ex300 --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd forms-ex300
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“应用工作!”

  4. 编辑样式:编辑文件 styles.css,并将其更改为:

    input.ng-valid {
      border-left: 5px solid #42A948; /* green */
    }
    
    input.ng-invalid {
      border-left: 5px solid #a94442; /* red */
    }
    
    .error {
        color: #ff0000;
    }
    
    label {
        display: inline-block;
        width: 100px;
    }
    
    button {
        border: 1px solid black;
        margin: 20px;
    }
    
    
  5. 编辑模块:编辑文件 app.module.ts,修改为:

    import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';
    import { FormsModule } from '@angular/forms';
    
    import { AppComponent } from './app.component';
    
    @NgModule({
      declarations: [
        AppComponent
      ],
      imports: [
        BrowserModule,
        FormsModule
      ],
      providers: [],
      bootstrap: [AppComponent]
    })
    export class AppModule { }
    
    
  6. 编辑组件:编辑 app.component.ts 文件,将其更改为:

    import { Component, ViewChild } from '@angular/core';
    import { NgForm, FormControl, RequiredValidator } from '@angular/forms';
    
    @Component({
      selector: 'app-root',
      template: `
      <form #f="ngForm" novalidate>
        <p><label>First Name</label><input name="fname" ngModel #fname="ngModel" required />
        <span class="error" *ngIf="fname.touched && fname.hasError('required')">Required</span>
        </p>
        <p><label>Last Name</label><input name="lname" ngModel #lname="ngModel" required />
        <span class="error" *ngIf="lname.touched && lname.hasError('required')">Required</span>
        </p>
        <p><label>Email</label><input name="email" ngModel #email="ngModel" required email />
        <span class="error" *ngIf="email.touched && email.hasError('required')">Required</span>
        <span class="error" *ngIf="email.value && email.touched && email.hasError('email')">Invalid email</span>
        </p>
        <button (click)="onSubmit()" [disabled]="!f.valid">Submit</button>
      </form>`,
      styles: []
    })
    export class AppComponent {
        onSubmit(){
          alert('Submitted');
        }
    }
    
    

你的应用应该工作在本地主机:4200。请注意以下几点:

  • 文件 styles.css 将所需的样式应用于适当的状态——例如,当表单控件有有效数据时,将ng-valid样式设置为显示绿色指示器。
  • app.component.ts 文件包含根据表单控件状态显示错误信息的逻辑。

反应式表单:示例

您为表单构建模型对象——它们与模板表单相同——然后将它们绑定到模板中的输入控件。因此,您正在类中构建表单控件,并修改模板以链接到这些控件。这使您可以完全控制表单、表单值和表单验证。您可以直接操作模型对象(例如,更改值),绑定会立即同步生效。事实上,值和有效性的更新总是同步的,并且在您的控制之下。

要使用 Angular 模板表单,您的应用模块需要从@angular/forms 节点模块导入反应式表单模块:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';

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

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    ReactiveFormsModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

要将模板绑定到模型,您需要在组件的模板中创建 HTML form和 HTML 输入。然后在组件的类中创建一个表单模型。现在,您使用以下指令将两者绑定在一起:

  • <form [formGroup]="registerForm">:连接表单模型和模板中的form HTML。
  • <fieldset formGroupName="address">:连接表单组和模板中的fieldset HTML。
  • <input formControlName="name">:连接模型中的表单控件和模板中的form input HTML。

让我们来创建一个反应式表单,看看需要什么来使它工作。这将是表格示例-ex400:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new forms-ex400 --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd forms-ex400
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“应用工作!”

  4. 编辑样式:编辑文件 styles.css,并将其更改为:

    input.ng-valid {
      border-left: 5px solid #42A948; /* green */
    }
    
    input.ng-invalid {
      border-left: 5px solid #a94442; /* red */
    }
    
    .error {
        color: #ff0000;
    }
    
    label {
        display: inline-block;
        width: 100px;
    }
    
    button {
        border: 1px solid black;
        margin: 20px;
    }
    
    
  5. 编辑模块:编辑 app.module.ts 文件,将其更改为:

    import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';
    import { ReactiveFormsModule } from '@angular/forms';
    
    import { AppComponent } from './app.component';
    
    @NgModule({
      declarations: [
        AppComponent
      ],
      imports: [
        BrowserModule,
        ReactiveFormsModule
      ],
      providers: [],
      bootstrap: [AppComponent]
    })
    export class AppModule { }
    
    
  6. 编辑组件:编辑 app.component.ts 文件,将其更改为:

    import { Component, OnInit } from '@angular/core';
    import { FormGroup, FormControl, FormControlName, Validators } from '@angular/forms';
    
    @Component({
      selector: 'app-root',
      template: `
        <form #form [formGroup]="formGroup" (ngSubmit)="onSubmit(form)" novalidate>
          <label>Name:
            <input formControlName="name">
          </label>
          <br/>
          <label>Location:
            <input formControlName="location">
          </label>
          <br/>
          <input type="submit" value="Submit" [disabled]="!formGroup.valid">
        </form>
      `,
      styles: []
    })
    export class AppComponent implements OnInit{
    
      formGroup: FormGroup;
    
      ngOnInit(){
        this.formGroup = new FormGroup({
          name: new FormControl('', Validators.required),
          location: new FormControl('', Validators.required)
        });
      }
    
      onSubmit(form: FormGroup){
        alert('sumit');
      }
    }
    
    

你的应用应该工作在本地主机:4200。请注意以下几点:

  • 文件 styles.css 设置 css 样式。
  • app.component.ts 文件包含表单模板中的 HTML。
  • 当组件初始化时,app.component.ts 文件初始化模型,这是一个在ngInit方法中带有表单控件的表单组。
    • app.component.ts 文件将模板中的 HTML 链接到模型。它使用以下代码将 HTML form链接到formGroup:

      <form #form [formGroup]="formGroup" (ngSubmit)="onSubmit(form)" novalidate>
      
      
    • 它使用以下代码将 HTML input链接到formControl:

      <input formControlName="name">
      
      

反应式表单:表单生成器

FormBuilder类旨在帮助您用更少的代码构建表单模型。将FormBuilder注入到你的组件的类中,并使用表 19-3 中列出的方法。

表 19-3

FormBuilder Methods

| 方法 | 目的 | 争论 | 返回 | | :-- | :-- | :-- | :-- | | `group` | 创建表单组 | 配置对象,额外参数(验证器,异步验证器) | `FormGroup` | | `control` | 创建表单控件 | 当前表单状态(值/禁用状态),验证器数组,异步验证器数组 | `FormControl` | | `array` | 创建表单数组 | 配置对象(数组)、验证器、异步验证器 | `FormArray` |

在接下来的例子中,我们将开始使用FormBuilder

反应式表单:表单组嵌套示例

有时我们的表单由多种不同的元素组成。例如,如果您正在输入客户订单,信息可以按以下方式组织:

  • 名字
  • 地址
  • 命令
    • 订单项目
  • 信用卡信息

每个元素都可以包含一个或多个表单控件,所以我们需要能够管理每个元素。这就是表单组的用武之地。在这种情况下,你可以拥有如图 19-5 所示的表单组层次结构。

A458962_1_En_19_Fig5_HTML.gif

图 19-5

Hierarchy of form groups

该示例使用户能够输入并提交一个订单,其中包括客户名称、客户地址和商品列表,如图 19-6 所示。

A458962_1_En_19_Fig6_HTML.jpg

图 19-6

Entering and submitting an order

这将是表格示例-ex500:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new forms-ex500 --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd forms-ex500
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“应用工作!”

  4. 编辑模块:编辑 app.module.ts 文件,将其更改为:

    import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';
    import { ReactiveFormsModule } from '@angular/forms';
    
    import { AppComponent } from './app.component';
    
    @NgModule({
      declarations: [
        AppComponent
      ],
      imports: [
        BrowserModule,
        ReactiveFormsModule
      ],
      providers: [],
      bootstrap: [AppComponent]
    })
    export class AppModule { }
    
    
  5. 编辑组件类:编辑 app.component.ts 文件,将其更改为:

    import { Component, OnInit } from '@angular/core';
    import { FormGroup, FormArray, FormBuilder, Validators } from '@angular/forms';
    
    @Component({
      selector: 'app-root',
      templateUrl: 'app.component.html',
      styles: ['div { background-color: #f2f2f2; padding: 15px; margin: 5px }',
        'p { margin: 0px }'
      ]
    })
    
    export class AppComponent implements OnInit {
    
      public _parentForm: FormGroup;
      public _name: FormGroup;
      public _addr: FormGroup;
      public _items: FormArray;
    
      constructor(private _fb: FormBuilder){}
    
      ngOnInit() {
        this._name = this._fb.group({
            fname: ['', [Validators.required]],
            lname: ['', [Validators.required]]
          });
        this._addr = this._fb.group({
            addr1: ['', [Validators.required]],
            addr2: [''],
            city: ['', [Validators.required]],
            state: ['', [Validators.required]],
            zip: ['', [Validators.required, Validators.minLength(5), Validators.maxLength(5)]],
          });
        this._items = this._fb.array(
          [this.createItemFormGroup()]
        );
        this._parentForm = this._fb.group({
          name: this._name,
          addr: this._addr,
          items: this._items
        });
      }
    
      createItemFormGroup(){
         return this._fb.group({
                name: ['', Validators.required],
                qty: ['1', Validators.required],
                price: ['', Validators.required]
            });
      }
    
      addItem(){
        this._items.push(this.createItemFormGroup());
      }
    
      deleteItem(index){
        delete this._items[index];
      }
    
      onSubmit(form: FormGroup){
        alert('Submitted');
      }
    }
    
    
  6. 编辑组件模板:编辑文件 app.component.html,将其更改为:

    <form [formGroup]="_parentForm" novalidate (ngSubmit)="onSubmit(parentForm)">
      <div formGroupName="name">
        <b>Name</b>
        <br/>
        <label>First Name
          <input type="text" formControlName="fname">
          <small *ngIf="_name.controls.fname.touched && !_name.controls.fname.valid">Required.</small>
        </label>
        <br/>
        <label>Last Name
          <input type="text" formControlName="lname">
          <small *ngIf="_name.controls.lname.touched && !_name.controls.lname.valid">Required.</small>
        </label>
      </div>
      <br/>
      <div formGroupName="addr">
        <b>Address</b>
        <br/>
        <label class="left">Address #1
          <input type="text" formControlName="addr1">
          <small *ngIf="_addr.controls.addr1.touched && !_addr.controls.addr1.valid">Required.</small>
        </label>
        <br/>
        <label>Address #2
          <input type="text" formControlName="addr2">
        </label>
        <br/>
        <label>City
          <input type="text" formControlName="city">
          <small *ngIf="_addr.controls.city.touched && !_addr.controls.city.valid">Required.</small>
        </label>
        <br/>
        <label>State
          <select formControlName="state">
            <option>AL</option>
            <option>GA</option>
            <option>FL</option>
          </select>
          <small *ngIf="_addr.controls.state.touched && !_addr.controls.state.valid">Required.</small>
        </label>
        <br/>
        <label>Zip
          <input type="number" formControlName="zip">
          <small *ngIf="_addr.controls.zip.touched && !_addr.controls.zip.valid">Required.</small>
        </label>
      </div>
      <br/>
      <div formArrayName="items">
    
        <b>Items</b>
        <br/>
        <p [formGroupName]="i" *ngFor="let item of _items.controls;let i=index">
          <label>Name:&nbsp;<input type="text" formControlName="name" size="30">
            <small *ngIf="item.controls.name.touched && !item.controls.name.valid">Required.</small>
          </label>
          <label>Qty:&nbsp;<input type="number" formControlName="qty" min="1" max="10">
            <small *ngIf="item.controls.qty.touched && !item.controls.qty.valid">Required.</small>
          </label>
          <label>Price:&nbsp;<input type="number" formControlName="price" min="0.01" max="1000" step=".01">
            <small *ngIf="item.controls.price.touched && !item.controls.price.valid">Required.</small>
          </label>
        </p>
      </div>
      <br/>
      <div>
        <input type="button" value="Add Item" (click)="addItem()"/>
        <input type="submit" value="Submit" [disabled]="!_parentForm.valid"/>
      </div>
    </form>
    
    

你的应用应该工作在本地主机:4200。请注意以下几点:

  • 我们至少有四个固定的FormGroup对象:一个用于名称,一个用于地址,一个用于第一项,另一个用于父表单。
  • FormArray包含一个FormGroup对象,但是如果用户点击添加项目按钮,它可以包含其他FormGroup对象。
  • 整个表单的有效性仍然控制着 Submit 按钮的启用和禁用。

验证器

Angular 为我们的表单提供了一些验证器。您可以向同一个FormControl(在FormGroup中的一个项目)添加多个验证器:

  • 所需验证:

    this.form = fb.group({
                'name': ['', Validators.required],
            });
    
    
  • 最小长度验证:

    this.form = fb.group({
                'name': ['', Validators.required, Validators.minLength(4)]
            });
    
    
  • 最大长度验证:

    this.form = fb.group({
                'name': ['', Validators.required, Validators.maxLength(4)]
            });
      }
    
    

组合多个验证器

Validators类提供了compose方法,允许用户为一个控件指定多个验证器:

constructor(private fb: FormBuilder){
    this.form = fb.group({
            'name': ['', Validators.compose( [Validators.required, Validators.maxLength(6)] ) ],
        });
  }

自定义验证示例

Angular Forms 模块允许您创建一个自定义类来验证您的输入。验证方法是静态的,只有在出错时才返回验证结果。如果一切正常,这个方法返回一个空值。这个自定义类可以在指定FormBuilder中的字段时使用,也可以在组件模板中使用,以提供可视提示。

该组件不允许用户进入奔驰,如图 19-7 所示。

A458962_1_En_19_Fig7_HTML.jpg

图 19-7

Custom validation

这将是表格示例-ex600:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new forms-ex600 --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd forms-ex600
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“应用工作!”

  4. 编辑样式:编辑文件 styles.css,并将其更改为:

    input.ng-valid {
      border-left: 5px solid #42A948; /* green */
    }
    
    input.ng-invalid {
      border-left: 5px solid #a94442; /* red */
    }
    
    
  5. 编辑模块:编辑 app.module.ts 文件,将其更改为:

    import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';
    import { ReactiveFormsModule } from '@angular/forms';
    
    import { AppComponent } from './app.component';
    
    @NgModule({
      declarations: [
        AppComponent
      ],
      imports: [
        BrowserModule,
        ReactiveFormsModule
      ],
      providers: [],
      bootstrap: [AppComponent]
    })
    export class AppModule { }
    
    
  6. 编辑组件:编辑 app.component.ts 文件,将其更改为:

    import { Component, OnInit } from '@angular/core';
    import { AbstractControl, FormGroup, FormControl, FormControlName, Validators } from '@angular/forms';
    
    export function validateNotMercedes(control: AbstractControl) {
        return (control.value.toLowerCase() != 'mercedes') ?
          null :
          { validateNotMercedes: {
            valid: false
          }
        }
    }
    
    @Component({
      selector: 'app-root',
      template: `
        <form #form [formGroup]="formGroup" (ngSubmit)="onSubmit(form)" novalidate>
          <label>Make:
            <input formControlName="make">
          </label>
          <br/>
          <label>Model:
            <input formControlName="model">
          </label>
          <br/>
          <input type="submit" value="Submit" [disabled]="!formGroup.valid">
        </form>
      `,
      styles: []
    })
    export class AppComponent implements OnInit{
    
      formGroup: FormGroup;
    
      ngOnInit(){
        this.formGroup = new FormGroup({
          make: new FormControl('', [Validators.required, validateNotMercedes]),
          model: new FormControl('', Validators.required)
        });
      }
    
      onSubmit(form: FormGroup){
        alert('sumit');
      }
    }
    
    

你的应用应该工作在本地主机:4200。请注意以下几点:

  • 文件 app.component.ts 中的代码导出validateNotMercedes函数来验证 make。注意,它返回一个 null 来表示有效性——否则,它返回一个属性valid设置为 false 的对象。
  • 文件 app.component.ts 中的代码使用FormControl对象设置表单组。注意这里的make FormControl是如何将validateNotMercedes函数指定为验证器的。

摘要

你不必使用有角的表单模块,但是它们为你做了很多工作,节省了你很多时间。Angular 为您提供了两种选择:快速简单的模板表单和更高级的反应式表单。您需要了解这两者,因为它们都非常有用并且实现得很好。他们可能需要一些时间来学习,但回报是值得的。

下一章讨论管道。管道不是必不可少的,但可能很有用。

二十、管道

从安古拉吉斯时代起,管道就已经存在了。它们在转换数据时很有用,尤其是在整个应用中使用相同的转换时。管道可以很容易地将这些转换添加到组件模板中。

弯管

Angular 包括几个要添加到模板中的管道。您不需要导入它们,也不需要将它们添加为指令或任何东西——只需开始使用它们。

小写字母

Lowercase: {{ "The Quick Brown Fox Jumped Over The Lazy Dogs" | lowercase }}

生产:

Lowercase: the quick brown fox jumped over the lazy dogs

大写字母

Uppercase: {{ "The Quick Brown Fox Jumped Over The Lazy Dogs" | uppercase }}

生产:

Uppercase: THE QUICK BROWN FOX JUMPED OVER THE LAZY DOGS

货币

Currency: {{ 2012.55 | currency }}

生产:

Currency: USD2,012.55

英国英镑货币

UK Pound Currency: {{ 2012.55 | currency: 'gbp':true }}

生产:

UK Pound Currency: £2,012.55

百分比

Percentage: {{ 0.5 | percent }}

生产:

Percentage: 50%

日期

Date: {{ dt | date }}

生产:

Date: Jul 12, 2017

短期的

Short Date: {{ dt | date:shortdate }}

生产:

Short Date: Jul 12, 2017

特殊日期格式

Special Date Format: {{ dt | date:'yMMMMEEEEd' }}

生产:

Special Date Format: Wednesday, July 12, 2017

表 20-1 列出了预定义的日期格式。

表 20-1

Predefined Date Formats

| 名字 | 格式 | 示例(英语/美国) | | :-- | :-- | :-- | | `medium` | yMMMdjms | 2010 年 9 月 3 日,下午 12:05:08 | | `short` | yMdjm | 2010 年 3 月 9 日下午 12 时 05 分 | | `fullDate` | yMMMMEEEEd | 2010 年 9 月 3 日星期五 | | `longDate` | ymmmmm | 2010 年 9 月 3 日 | | `mediumDate` | yMMMd | 2010 年 9 月 3 日 | | `shortDate` | 宜昌船舶柴油机厂 | 9/3/2010 | | `mediumTime` | (同 JavaMessageService)Java 消息服务 | 下午 12 时 05 分 08 秒 | | `shortTime` | 牙买加 | 下午 12 点 05 分 |

表 20-2 显示了如何组合日期格式元素。

表 20-2

Combining Date Formats

| 名字 | 格式 | 完整的文本表单 | 文本形式简短 | 数字形式 | 数字形式 2 位数 | | :-- | :-- | :-- | :-- | :-- | :-- | | `era` | G | 俄文 | G |   |   | | `year` | y |   |   | y | 尤尼克斯 | | `month` | M | 嗯 | 嗯 | M | 梅智节拍器 | | `day` | D |   |   | d | 截止日期(Deadline Date 的缩写) | | `weekday` | E | 依依社区防屏蔽 | 东方马脑脊髓炎 |   |   | | `hour` | J |   |   | j | 姐姐 | | `12 hour` | H |   |   | h | 倍硬 | | `24 hour` | H |   |   | H | 殿下 | | `minute` | M |   |   | m | 梅智节拍器 | | `second` | S |   |   | s | 悬浮物 | | `timezone` | z / Z | z | Z |   |   |

数据

{{ {customerName: 'Mark', 'address': '2312 welton av 30333'} | json }}

生产:

{ "customerName": "Mark", "address": "2312 welton av 30333" }

前面的示例执行了以下操作:

  • 生成包含两个属性的 JavaScript 对象:客户姓名和地址
  • 将这个 JavaScript 对象传递给json管道
  • json管道输出所提供对象的 JSON 表示

弯管:示例

图 20-1 所示的组件使用各种角管显示信息。

A458962_1_En_20_Fig1_HTML.jpg

图 20-1

Showing various Angular pipes

这将是示例管道-ex100:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new pipes-ex100 --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd pipes-ex100
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“应用工作!”

  4. 编辑组件:编辑 app.component.ts 文件,将其更改为:

    import { Component } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      template: `
        <p>
          Lowercase: {{ "The Quick Brown Fox Jumped Over The Lazy Dogs" | lowercase }}
        </p>
        <p>
          Uppercase: {{ "The Quick Brown Fox Jumped Over The Lazy Dogs" | uppercase }}
        </p>
        <p>
          Currency: {{ 2012.55 | currency }}
        </p>
        <p>
          UK Pound Currency: {{ 2012.55 | currency: 'gbp':true }}
        </p>
        <p>
          Percentage: {{ 0.5 | percent }}
        </p>
        <p>
          Date: {{ dt | date }}
        </p>
        <p>
          Short Date: {{ dt | date:shortdate }}
        </p>
        <p>
          Special Date Format: {{ dt | date:'yMMMMEEEEd' }}
        </p>
      `,
      styles: []
    })
    export class AppComponent {
      dt = new Date();
    }
    
    

该应用应该工作,并显示格式化的数据。

自定义管道:示例

编写定制管道非常简单。但是,引入了一些新语法,因此需要记住一些事情:

  • 使用定制管道的组件需要将Pipe类声明为导入,并在@Component注释中指定它。
  • 管道类以@Pipe注释为前缀。它还需要导入PipePipeTransform,以及实现PipeTransform接口。

您可以使用 Angular CLI 命令ng generate pipe <pipe name>在 CLI 生成的项目中生成自定义管道。忽略<管道名> .pipe.spec.ts 文件(用于测试),但编辑<管道名> .pipe.ts 文件:

ng generate pipe reverse

installing pipe
  create src/app/reverse.pipe.spec.ts
  create src/app/reverse.pipe.ts
  update src/app/app.module.ts

您的自定义管道应该是实现PipeTransform接口的 TypeScript 类:

interface PipeTransform {
  transform(value: any, ...args: any[]): any
}

图 20-2 所示的组件允许用户反转一些文本。它还有一个可选参数—反转文本的每个字符之间的空格数。

A458962_1_En_20_Fig2_HTML.jpg

图 20-2

Reversing text with a pipe

这将是示例管道-ex200:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new pipes-ex200
    
    
  2. 开始ng serve:使用以下代码:

    cd pipes-ex200
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“应用工作!”

  4. 生成管道:使用 CLI 生成自定义管道:

    ng generate pipe reverse
    
    
  5. 编辑管道:编辑 reverse.pipe.ts 文件,并将其更改为:

    import { Pipe, PipeTransform } from '@angular/core';
    
    @Pipe({
      name: 'reverse'
    })
    export class ReversePipe implements PipeTransform {
    
      transform(value: any, args?: any): any {
        let spaces = 0;
        if (args){
          spaces = parseInt(args);
        }
        let reversed = '';
        for (let i=value.length-1;i>=0;i--){
          reversed += value.substring(i, i+1);
          reversed += Array(spaces + 1).join(' ');
        }
        return reversed;
      }
    }
    
    
  6. 编辑组件:编辑 app.component.ts 文件,将其更改为:

    import { Component } from '@angular/core';
    import { ReversePipe } from './reverse.pipe';
    @Component({
      selector: 'app-root',
      template: `
        <p>My name is {{name | reverse}}
        <p>My name is {{name | reverse:5}}
      `,
      styles: []
    })
    export class AppComponent {
      name: string = 'Michael Caine';
    }
    
    

该应用应该工作,并显示格式化的数据。请注意以下几点:

  1. ReversePipe像任何管道一样实现了PipeTransform接口。
  2. ReversePipe通过使用Array对象构造函数添加额外的空格。如果向构造函数提供单个值,它会将数组长度设置为该值。然后,join方法指定一个字符串来分隔数组的每个元素。

摘要

这简短的一章展示了管道的用处。我在以下情况下使用它们:

  • 当我需要在整个应用中以标准方式格式化数据时,例如,货币。
  • 当我想调试一些实例变量时,我有时会用一个json管道将它们添加到模板中。这使得它们的当前状态始终可见,这样我就可以看到它们是如何变化的。

我们将在下一章继续讨论,并涵盖更高级的主题:区域和变化检测。

二十一、区域和变化检测

Angular 使用一个名为 Zone.js 的 JavaScript 模块,其目的是产生一个跨异步任务持续的执行上下文。目前,浏览器 DOM 和 JavaScript 的异步活动数量有限,比如 DOM 事件、承诺和服务器调用。Zone.js 可以拦截这些活动,并在异步活动完成之前和之后让您的代码有机会采取行动。当您需要查看与该任务相关的所有信息时,尤其是发生错误时,这非常有用。

某些事情会导致变化,例如:

  • 一个 DOM 事件:例如:有人点击了某个东西。
  • 通信:示例:浏览器从服务器获取数据。
  • 定时器事件发生:例如:每 10 秒刷新一次。

当处理模型视图控制器(MVC)时,记住模型就是数据,视图显示模型中的数据。

Angular 中变更检测的目的是寻找模型中的变更,并确保视图(即 DOM)与它保持同步。变更检测可能会变得复杂,因为它需要在代码运行时确定何时需要重绘视图。

下面是更改模型的一些代码的示例。对服务器进行 HTTP 调用,并返回数据。模型中的客户列表被更新。所以现在这个变化需要 Angular 来检测,UI 需要刷新:

@Component()
class App implements OnInit{
  customers:Customer[] = [];
  constructor(private http: Http) {}
  ngOnInit() {
    this.http.get('/customers)
      .map(res => res.json())
      .subscribe(customers => this.customers = customers);
  }
}

Angular 是如何知道某些东西可能已经改变了,并且它应该寻找改变的呢?因为NgZone告诉它!

NgZone 是 Angular 的 Zone.js

NgZone类是 zone.js 框架的包装器。依赖注入器也可以通过构造函数注入传入区域。

事件循环和消息

JavaScript 有一个基于事件循环的并发模型。JavaScript 运行时包含一个消息队列,它是要处理的消息的列表。消息从队列中取出,由浏览器 UI 线程处理。所以,浏览器基本上是在一个循环中工作,拾取和处理消息来做事,如图 21-1 所示。

A458962_1_En_21_Fig1_HTML.gif

图 21-1

Event loop

浏览器 UI 线程

浏览器 UI 线程是通过运行事件循环代码、处理消息来更新用户界面的单个线程。在处理下一条消息之前,每条消息都被完全处理。只有一个线程用于更新用户界面(用户查看的文档)。如果浏览器 UI 线程过载,浏览器会向用户显示如图 21-2 所示的消息(或类似的消息)。

A458962_1_En_21_Fig2_HTML.jpg

图 21-2

Browser UI thread is overloaded

猴子补丁

使用NgZone /Zones.js,系统 JavaScript 代码被“打了猴子补丁”(当它必须这样做时),以便它挂钩到事件循环代码,查看正在处理的消息发生了什么。这使它能够提供有关区域中发生的事件或调用的代码的附加信息,例如,异步服务器调用完成。

Note

猴子补丁是程序在本地扩展或修改支持系统软件的一种方式。就 Angular 和 Zone.js 而言,Zone 将在必要时对 JavaScript 核心代码进行猴子式修补,以便提供执行信息。

NgZone发出onTurnStartonTurnEnd事件,通知观察者某事即将发生和某事已经发生。

Angular 使用NgZone来寻找需要变化检测的事件。在核心 Angular 代码中,Angular 监听NgZone onTurnDone事件。当此事件触发时,Angular 对模型执行更改检测并更新 UI。

Angular 和变化检测

正如我以前说过的,Angular 应用是由多个类似乐高积木的组件构建而成的,具有树状层次结构。您有主应用组件,然后您有子组件,等等。

图 21-3 展示了组件 UI,图 21-4 展示了组件树。

A458962_1_En_21_Fig4_HTML.gif

图 21-4

Component tree

A458962_1_En_21_Fig3_HTML.jpg

图 21-3

Component UI

每个 Angular 分量都有自己的变量变化检测器。您看不到它发生,但是 Angular 在运行时会创建变化检测器类。因此,如果你有一个组件树,那么你就有一个变化检测器树。核心 Angular code 自下而上扫描树中的变化(调用每个变化检测器)以查看发生了什么变化。

Note

可变对象可以改变。不可变对象不能。显然,当变化检测运行在没有变化的对象上时,它会更快。如果你想让你的 Angular 代码运行得更快,开始考虑使用不可变对象来处理那些不会改变的东西。

我们知道NgZone用于检测 Angular 变化。NgZone是一个对我们(以及系统 Angular 代码)有用的类,因为它允许我们在 Angular 区域内部或外部运行异步进程。

在 Angular 区域内运行方法时:

  • 他们更新了 Angular UI。
  • 他们跑得更慢。

当我们需要进行变更检测并需要不断更新 UI 时,我们在 Angular 区域内运行异步流程。为了在 Angular 区域内运行异步流程,我们在注入的NGZone对象中调用了run方法,并传入了process函数。

在 Angular 区域之外运行方法时:

  • 他们不更新 Angular UI。
  • 他们跑得更快。

当我们不需要发生变化检测并且不希望 UI 不断更新时,我们在 Angular 区域之外运行异步流程。这可能看起来没有必要,但是当需要终极性能时,应该考虑这一点。为了在 Angular 区域之外运行异步流程,我们在注入的NgZone对象中调用runOutsideAngular方法,并传入process方法。

在 Angular 区域内运行异步代码:示例

此示例基于默认的 Angular TypeScript Plunker 应用。app.ts 文件如图 21-5 所示。

A458962_1_En_21_Fig5_HTML.jpg

图 21-5

Running asynchronous code within the Angular zone

让我们看一下这个例子:

  1. 导入NgZone
  2. 使用构造函数注入来注入NgZone的一个实例。
  3. 该方法由 Count 按钮触发,该按钮使用注入的NgZone运行initiateCount方法。注意,它调用方法run在注入的 Angular 区域内运行该方法。
  4. 方法initiateCountupdateCount使用时间间隔计时器,以异步任务的形式生成控制台日志。当计数器超过 1000 时,它们更新计数器并结束计数。

当你运行这个应用并点击计数按钮时,你会看到计数器更新 1,2,3,4 …一直到 1000,然后出现警报。用户界面显示计数。这是因为计数是在 Angular 区域内的函数中执行的,用NgZone观察事件并引起变化检测。变化检测检测到count变量发生了变化,更新 UI,如图 21-6 所示。

A458962_1_En_21_Fig6_HTML.jpg

图 21-6

count variable updates the UI

在 Angular 区域外运行异步代码:示例

此示例也基于默认的 Angular TypeScript Plunker 应用。app.ts 文件如图 21-7 所示。

  1. 导入NgZone
  2. 使用构造函数注入来注入NgZone的一个实例。
  3. 此方法由计数按钮触发。它使用注入的NgZone运行initiateCount方法。注意,它调用方法runOutsideAngular在注入的 Angular 区域之外运行该方法。
  4. 方法initiateCountupdateCount使用时间间隔计时器,以异步任务的形式生成控制台日志。当计数器超过 1000 时,它们更新计数器并结束计数。

A458962_1_En_21_Fig7_HTML.jpg

图 21-7

Running asynchronous code outside the Angular zone

当您运行此应用并单击 Count 时,您看不到计数器发生变化。用户界面显示计数为 0,直到报警出现,如图 21-8 所示。这是因为计数是在 Angular 区域之外的函数中执行的,而没有NgZone观察事件并引起变化检测。注意到它有多快了吗?

A458962_1_En_21_Fig8_HTML.jpg

图 21-8

count variable not updated until alert

摘要

本章试图介绍 Angular 的一些内部工作方式。它并不打算涵盖这个主题的每一个细节——那将需要许多章节。

这一章(简要地)介绍了不变性的概念,这是你需要知道的,特别是如果你将来要做函数式编程的话。不变性是对象一旦被创建就不能被修改的概念。作为开发人员,您需要考虑尽可能使用不可变对象,因为它们有很多好处:

  • 它们简化了编码(因为移动的部分更少),你知道对象不会改变值。
  • 它们与 Angular 变化检测算法一起工作得更好。
  • 当您尽可能地限制应用中可以更改对象的方式时,您可以使代码更简单,并对正在更改的内容保持更多的控制。
  • 它们最大限度地减少了当对象变异时有时会出现的副作用。
  • 它们在多线程下工作得更好。

下一章将介绍测试你的 Angular 代码。

二十二、测试

这本书主要是关于如何开始高效地使用 Angular,但是如果没有至少介绍测试你写的代码的方法,这本书是不完整的。测试框架相当复杂,所以不要指望看完这一章就能了解它的一切。

我将介绍一些概念,然后详细介绍如何编写代码来自动测试用 Angular CLI 生成的项目。

单元测试是对应用的最小可能单元的测试,无论是手动的还是自动的。单元测试的目的是确保代码按预期执行,并且新代码不会破坏旧代码。测试驱动开发的过程是按以下顺序开发代码:

  1. 编写测试代码(测试工具)
  2. 编写应用代码以通过测试
  3. 清理和重构应用代码以通过编码标准
  4. 检查它是否仍然通过测试

这个过程应该应用于较小的代码单元,并且应该经常重复这个过程。单元测试在现代软件开发过程中是必不可少的。

软件开发使用了开发人员从中央存储库中取出最新代码并对其进行处理的过程。工作完成后(代码经过测试),开发人员签入完成的代码。持续集成是一天数次将所有开发人员代码集成(或合并)到共享代码库中的过程。尽可能频繁地集成代码突出了快速合并问题,并避免了更大的代码不兼容性。我们的目标是在尽可能短的时间内签出代码,并在有人在此期间做了太多更改之前尽快签入和集成更改。

图 22-1 显示了一个(非常通用的)开发过程工作图。它没有考虑代码分支、合并问题和其他因素。

A458962_1_En_22_Fig1_HTML.gif

图 22-1

The development process

自动化单元测试需要一些前期工作,但是从长远来看,可以节省人们的时间。自动化测试可以很快发现问题,至少应该在以下两种情况下使用它们:

  • 当用户要签入代码变更时,他们应该在本地机器上调用自动化单元测试,以确保代码按预期运行。
  • 每当开发人员签入代码更改时,构建服务器都应该调用自动化单元测试。构建服务器还应该跟踪这些测试的结果,让人们知道他们是通过了还是失败了。

集成测试发生在单元测试之后。它测试组合代码,模拟用户运行完整的应用。这是更高层次的测试——在不了解系统结构或实现的情况下测试系统的各个方面。集成测试确保应用按照用户的预期工作,并且应用的各个组成部分能够协同工作。

您的 Angular 应用由具有依赖性的组件组成。您需要开发您的单元测试,以便它们独立地测试代码单元。例如,如果您想测试一个使用服务从服务器获取数据的组件,您可能需要分别测试组件和服务。您可能需要执行以下操作:

  • 编写代码来测试组件,向它注入一个以预定方式运行的服务的模拟(虚拟)版本。模拟服务模拟服务的输出。这样,您就可以测试组件是否按照预期处理服务的输出。
  • 编写测试服务的代码,注入一个与服务器对话的通信层(后端)的模拟版本(例如,Http 服务)。模拟通信层模拟连接,这些模拟连接能够模拟来自服务器的响应。这样,您就不需要真正的服务器,并且可以测试组件是否按照预期处理来自服务器的输出。

让测试变得复杂的一点是,我们测试的许多代码都是异步的,这意味着它不会阻塞并等待代码完成。测试库(和您的测试代码)有处理异步操作的代码,这使事情变得更加复杂。有时,代码必须在一个特殊的异步区域中运行,以模拟这些操作。

因果报应

Karma 是 Angular 团队在 AngularJS 开发过程中开发的一个自动化测试运行器。Karma 可以在真正的浏览器上快速运行单元测试。

您使用 Karma 来启动一个运行一组 Jasmine 测试的服务器。Karma 打开一个 web 浏览器并自动执行测试,您可以看到它在那个浏览器中运行测试。有时它甚至会在测试后让浏览器保持打开状态。

当您构建 CLI 项目时,它会创建 karma.conf.js 文件,允许您为项目配置 karma。配置选项包括基本路径、包含/排除哪些测试文件、autowatch 文件、在哪些浏览器上进行测试、颜色、超时、测试框架(例如 Jasmine,将在下一节中介绍)、服务器主机名和端口(例如 localhost:8080)、日志记录、插件、预处理程序、报告程序、单次运行等等。

Tip

如果您想在测试完成后让浏览器保持打开状态,那么单次运行配置非常有用。如果出现故障,并且您需要通过查看浏览器的开发人员工具来了解发生了什么,这有时会很有用。

茉莉

Jasmine 是一个开源的自动化单元测试 JavaScript 框架,通常用于 Angular 和其他 JavaScript 库。

当您编写 Jasmine 测试时,您必须遵循 Jasime 的做事方式。您在. spec.ts 文件中编写描述的测试集(每个文件一个或多个),每个描述的测试集包含多个测试。每个测试对它测试的代码做一些事情,得到一个结果,然后检查结果的有效性。图 22-2 展示了茉莉的结构。

A458962_1_En_22_Fig2_HTML.gif

图 22-2

Jasmine structure

Jasmine 单元测试有两层结构:

  • 一套“描述的”测试:开发人员使用describe函数建立一套一起执行的测试。例如,连通性测试。请注意,describe方法也用于为要测试的对象提供依赖关系。在describe中声明的变量对于套件中的任何it代码块都是可用的。
  • it在“描述的”测试套件中执行测试的代码块:开发人员使用it函数建立一个测试,在那里执行代码,并在预期和实际结果之间进行比较。开发人员在测试中使用expect方法来设定结果预期。如果满足这些条件,代码就通过了测试,否则就失败了。Jasmine 使用“匹配器”来比较预期和实际结果,例如,expect(a).toEqual(12):
describe("[The class you are about to test]", () => {

  beforeEachProviders(() => {
    return [Array of dependencies];
  });
  it("test1", injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => {
    return tcb.createAsync([The class you are about to test]).then((fixture) => {

          // test code ...
      // expect a result
    });
  }));

  it("test2", injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => {
    return tcb.createAsync([The class you are about to test]).then((fixture) => {

          // test code ...
      // expect a result
    });
  }));

});

茉莉概念

表 22-1

Jasmine Concepts

| 名字 | 描述 | 代码关键字 | | :-- | :-- | :-- | | 套房 | 对应于需要测试的代码区域的一组描述的测试。每个单元测试文件通常有一套测试,例如 app.component.suite.ts。但是,在一个单元测试文件中可以有一组以上的测试。 | `describe` | | 投机 | 执行代码并根据预期检查结果的测试。一个套件中可以有多个规格。 | `it` | | 预期 | 在测试中用于检查结果。 | `expect` | | 匹配项 | 由预期使用,将预期指定为规则。 | `toBe`、`toEqual`、`toBeNull`、`toContain`、`toThrow`、`toThrowError`等等 |

表 22 是您需要学习的 Jasmine 概念以及与每个概念相关的代码关键字。看一看一个基本 Jasmine 测试的代码(在表格下面),看看它与表格中的概念是如何对应的。

describe("CalcUtils", function() {                              // suite
//Spec for sum operation
    it("2 plus 2 equals 4", function() {                        // spec
        var calc = new CalcUtils();
        expect(calc.sum(2,2))                                   // expect
                .toEqual(4);                                    // matcher
    });

    //Spec for sum operation with decimal
    it("2.5 plus 2 equals 4.5", function() {                    // spec
        var calc = new CalcUtils();
        expect(calc.sum(2.5,2))                                 // expect
                .toEqual(4.5);                                  // matcher
    });
});

Jasmine 安装和拆卸

您有一套测试(describe d),其中包含一个或多个测试(spec s)。通常情况下,spec会非常相似,会一次又一次地测试同一个物体。这可能会导致重复代码,因为在每一个spec中,您将实例化要测试的对象,测试它,然后销毁它。你可以在表 22-1 后面的代码中看到这一点。

Jasmine 提供了一个解决方案:setupteardown方法。这些函数在每个测试(spec)运行之前和之后立即被调用。这使您能够用尽可能少的代码来设置所有的测试并清理所有的测试。

看看setup如何清理我们刚刚看到的代码:

describe("CalcUtils", function() {                             // suite
    var calc;

    //This will be called before running each spec
    beforeEach(function() {                                    // setup
        var calc = new CalcUtils();
    });

    describe("calculation tests", function(){                  // suite

        //Spec for sum operation
        it("2 plus 2 equals 4", function() {                   // spec
            expect(calc.sum(2,2))                              // expect
                    .toEqual(4);                               // matcher
        });

        //Spec for sum operation with decimal
        it("2 plus 2 equals 4", function() {                   // spec
            expect(calc.sum(2.5,2))                            // expect
                    .toEqual(4.5);                             // matcher
        });

    });
});

硬币指示器 (coin-levelindicator 的缩写)命令行界面(Command Line Interface for batch scripting)

当我们使用 Angular CLI 生成我们的 Angular 项目时,它会自动(默认)为您生成与 Karma 和 Jasmine 一起工作的单元测试代码。例如,当您生成 Angular 项目时,它会生成一个名为 app.component.ts 的应用组件和一个名为 app.component.spec.ts 的单元测试文件。这个单元测试文件已经包含了对组件进行单元测试的方法。

运行单元测试

当您发出以下命令时,Angular 执行项目(当前工作目录中的项目)的编译,然后调用 Karma 来运行所有的单元测试:

ng test

这个命令包括一个文件监视器。如果您更改其中一个项目文件,它将自动重新生成项目并重新运行测试。

单元测试文件

当您使用 Angular CLI 生成 Angular 项目时,该项目会生成使用 Karma 和 Jasmine 的单元测试文件。这些单元测试文件

  • 通常以. spec.ts 结尾
  • 遵循 Jasmine 格式,有一个包含一组it测试的describe块。
  • 可以被修改,允许您添加更多的测试。
  • 可以从头开始写,Karma 会帮你捡起并运行它们。
  • 使用 Angular @angular/core/testing 模块中的许多 Angular 测试对象。

依赖注入

每个描述的测试套件都有点像一个“迷你模块”,因为它运行具有依赖性的代码,因此需要像模块一样设置它们(Angular @NgModule)。

Angular 测试对象

Angular 提供了一个包含辅助对象的模块@angular/core/testing,使得编写单元测试更加容易:

import { TestBed, async } from '@angular/core/testing';

表 22-2 列出了你最有可能在测试模块中使用的对象。

表 22-2

Angular Testing Objects

| 名字 | 类型 | 描述 | | :-- | :-- | :-- | | `TestBed` | 班级 | 使开发人员能够创建要测试的代码可以在其中运行的外壳,并提供以下内容:外壳内组件的实例化控制组件的依赖注入的方法查询组件的 DOM 元素的方法调用 Angular 变化检测的方法编译要测试的组件的方法 | | `async` | 功能 | 它采用一个无参数函数,并返回一个函数,该函数成为`beforeEach`的真参数。以免异步执行`beforeEach`(规范`setup`)中的初始化代码。 |

组件夹具

TestBed方法createComponent使您能够在测试外壳中创建组件,并返回一个ComponentFixture对象的实例。组件夹具非常有用的原因之一是它提供了对正在调试的组件的访问。

ComponentFixturedebugElement属性表示 Angular 分量及其对应的 DOM 元素。它包含以下属性,如表 22-3 所示。

表 22-3

debugElement Properties

| 财产 | 描述 | | :-- | :-- | | `componentInstance` | 对组件类的引用,如果您想访问组件中的实例变量和方法,这很有用 | | `nativeElement` | 对 DOM 中组件类对应的`html`元素的引用,如果您想访问 DOM 以查看模板是如何呈现组件的,这很有用 |

件实例

在夹具的debugElement属性中,用户可以通过componentInstance属性访问 Angular 分量。一旦你访问了debugElement,你就可以在你的组件中调用你的方法来测试它。

本土元素

同样在debugElement中,用户可以通过nativeElement属性访问 DOM 元素。nativeElement为我们提供了由 Angular 组件生成的 HTML 的root元素。这个root元素由一个HTMLElement对象表示,这是一个具有许多属性和方法的成熟对象。

HTMLElement对象不是特定于 Angular 的,但它是 web 开发中非常常用的对象。详见 https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement

当您为debugElement获取nativeElement时,这将为您的组件返回HTMLElement对象,而不是整个 DOM!

有时,开发人员会错误地认为这个元素包含组件范围之外的 HTML 元素。他们不会有空的!

表 22-4 中列出了HTMLElement的一些更有用的方法和属性。

表 22-4

HTMLElement Methods and Properties

| 名字 | 描述 | | :-- | :-- | | `innerText`(属性) | 用于返回元素内部的文本。请记住,这个元素可能包含意外的空白。 | | `innerHTML`(属性) | 返回属于组件的元素内标记的 HTML 语法。 | | `outerHTML`(属性) | 返回元素内部标记的 HTML 语法,包括其后代。 | | `querySelector`(方法) | 返回第一个元素,该元素是调用它的元素的后代,与指定的选择器组匹配。有助于在属于组件的元素中查找元素。例如,下面的代码期望定义带有 CSS 类`button-primary`按钮的按钮:`expect(element.querySelector("button.button-primary")).toBeDefined();` | | `querySelectorAll`(方法) | 返回一个所有元素的`NodeList`,这些元素是调用它的元素的子元素,并且与指定的 CSS 选择器组相匹配。有助于在属于组件的元素中查找子元素。例如,这段代码获得了元素:`let textAreas = element.querySelectorAll("text-area");`中的`text-area`元素的列表 | | `getAttribute` `([name])`(方法) | 返回元素的属性(由名称标识),例如`disabled`。下面的代码期望`approveButton`是`disabled` : `expect(approveButton.getAttribute("disabled"))` `.toBeDefined();` |

CLI 单元测试:示例

第一个例子并不激动人心,但它将展示一个示例 CLI 项目的生成,并检查生成的测试代码。这将是示例测试-ex100:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new testing-ex100 --inline-template --inline-style
    
    
  2. 导航到文件夹:使用以下命令:

    cd testing-ex100
    
    
  3. 打开文件:打开 app.component.spec.ts 并注意以下内容:

    • 在每个规范之前调用beforeEach方法。此方法配置测试模块来测试 AppComponent 组件。
    • 有三个规格(测试)。使用测试模块中的async方法异步调用每一个。
    • 第一个规范创建一个 fixture,然后从debug元素获取组件实例。它检查组件是否真实(即是否有指定的值)。
    • 第二个规范创建一个 fixture,然后从debug元素获取组件实例。它检查组件的title实例变量是否有值‘app’。
    • 第三个规范(test)创建一个 fixture,然后从 debug 元素获取组件的元素。它检查该元素是否有包含值“欢迎使用应用!”的“h1”元素
  4. Run tests: Use the following command:

    ng test
    
    

    Now let’s create a simple component (Figure 22-3) that allows you to increment a counter. Then we’ll write a unit test for it.

    A458962_1_En_22_Fig3_HTML.jpg

    图 22-3

    Incrementing a counter This will be example testing-ex200:

  5. 使用 CLI 构建应用:使用以下命令:

    ng new testing-ex200 --inline-template --inline-style
    
    
  6. 开始ng serve:使用以下代码:

    cd testing-ex200
    ng serve
    
    
  7. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”

  8. 编辑类:编辑 app.component.ts 文件,将其更改为:

    import { Component } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      template: `
        <h1>
          {{counter}}
        </h1>
        <button (click)="incrementCounter()">Increment Counter</button>
      `,
      styles: []
    })
    export class AppComponent {
      counter = 0;
    
      incrementCounter(){
        this.counter++;
      }
    }
    
    
  9. 编辑单元测试:编辑 app.component.spec.ts 文件,并将其更改为:

    import { TestBed, async } from '@angular/core/testing';
    import { AppComponent } from './app.component';
    
    describe('AppComponent', () => {
      beforeEach(async(() => {
        TestBed.configureTestingModule({
          declarations: [
            AppComponent
          ],
        }).compileComponents();
      }));
    
      it('should create the app', async(() => {
        const fixture = TestBed.createComponent(AppComponent);
        const app = fixture.debugElement.componentInstance;
        expect(app).toBeTruthy();
      }));
    
      it(`should have as title '0'`, async(() => {
        const fixture = TestBed.createComponent(AppComponent);
        const app = fixture.debugElement.componentInstance;
        expect(app.counter).toEqual(0);
      }));
    
      it(`should render '0' in a h1 tag`, async(() => {
        const fixture = TestBed.createComponent(AppComponent);
        fixture.detectChanges();
        const compiled = fixture.debugElement.nativeElement;
        expect(compiled.querySelector('h1').textContent).toContain('0');
      }));
    
      it('should increment counter ten times', async(() => {
        const fixture = TestBed.createComponent(AppComponent);
        fixture.detectChanges();
        const compiled = fixture.debugElement.nativeElement;
        for (let i=0;i<10;i++){
          compiled.querySelector('button').click();
          fixture.detectChanges();
          const nbrStr = (i + 1) + '';
          expect(compiled.querySelector('h1').textContent).toContain(nbrStr);
          }
        }));
    
    });
    
    
  10. 运行测试:使用以下命令:

```ts
ng test

```

请注意,在末尾添加了一个额外的测试,它会点击 Increment 按钮十次。还要注意,单击按钮后,在调用fixture.detectChanges方法执行变更检测之前,附加测试不会起作用。

使用假 Http 响应进行测试

介绍

在现实世界中,我们的 Angular 应用必须一直使用 HTTP 与服务器通信。当我们编写单元测试时,我们不能假设有一个 API 端点可供我们测试。所有的服务器都可能关闭。可能没有备用服务器。我们需要做的是不使用真正的服务器,模拟(伪造)我们的 Angular 应用和服务器之间的 HTTP 通信。通过这种方式,我们可以编写测试来查看我们的应用如何处理来自 HTTP 服务器的各种响应。

幸运的是,Angular 背后的 Google 工程师让我们的生活变得更加轻松,特别是现在我们有了 Angular 5 和 HttpClient 模块,它位于@angular/common/http 名称空间中。这个新的 HttpClient 模块有自己的新测试模块,称为 HttpClientTestingModule,它驻留在@angular/common/http/testing 名称空间中,可以用来为单元测试创建假的 http 响应。

如何使用 HttpClientTestingModule 创建假的 Http 响应

  1. 将 HttpClientTestingModule 导入到单元测试中。
  2. 将 HttpClient 和 HttpClientTestingModule 注入到您的测试中。
  3. 通过调用下面的方法之一来设置一个测试请求对象,告诉 HttpClientTestingModule 在测试中应该接收到多少个 http 请求(见下面)。HttpClientTestingModule 将断言它收到的请求数与其预期的相匹配。 请求数量 http clienttestingmodule 方法Unsure``match``0``expectNone``1``expectOne
  4. 您在测试请求对象上调用“flush”方法来发送回模拟结果。

使用 HttpClient 的测试服务:示例

对于第三个例子,我们将创建一个简单的组件,它使用一个服务使您能够使用 http 服务搜索踪迹(图 22-4 )。然后,我们将为服务编写一个单元测试,并测试它如何处理服务器响应。

A458962_1_En_22_Fig4_HTML.jpg

图 22-4

Component to search for trails

  1. 对使用服务的组件进行单元测试,并测试它如何处理服务器响应。

这将是示例测试-ex300:

  1. 使用 CLI 构建应用:使用以下代码:

    ng new testing-ex300 --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd testing-ex300
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”

  4. 编辑模块:编辑 app.module.ts 文件,将其更改为:

    import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';
    import { FormsModule } from '@angular/forms';
    import { HttpClientModule, HttpClient } from '@angular/common/http';
    import { AppComponent } from './app.component';
    import { Service } from './service';
    
    @NgModule({
      declarations: [
        AppComponent
      ],
      imports: [
        BrowserModule,
        FormsModule,
        HttpClientModule
      ],
      providers: [HttpClient, Service],
      bootstrap: [AppComponent]
    })
    export class AppModule { }
    
    
  5. 编辑类:编辑 app.component.ts 文件,将其更改为:

    import { Component } from '@angular/core';
    import { Service } from './service';
    import { FormsModule } from '@angular/forms';
    
    @Component({
      selector: 'app-root',
      template: `
        <h2>Trail Finder</h2>
        <input [(ngModel)]="_search" placeholder="city">
        <button (click)="doSearch()">Find Me a Trail</button>
        <div id="notFound" class="notFound" *ngIf="_searched && !_result">
          We could not find a trail here. :(
        </div>
        <div class="found" *ngIf="_searched && _result">
        <p id="name">Name: {{_result?.name}}</p>
        <p id="state">State: {{_result?.state}}</p>
        <p id="directions">Directions: {{_result?.directions}}</p>
        <p>Activities:</p>
        <ul id="activities" *ngIf="_result?.activities">
          <li *ngFor="let activity of _result.activities">
            {{activity.activity_type_name}} {{activity.description}}
          </li>
        </ul>
      `,
      styles: [`.found {
        border: 1px solid black;
        background-color: #8be591;
        color: black;
        margin: 10px;
        padding: 10px;
      }`,
      `.notFound {
        border: 1px solid black;
        background-color: #d13449;
        color: white;
        margin: 10px;
        padding: 10px;
      }`]
    })
    export class AppComponent {
      _search = 'Atlanta';
      _searched = false;
      _result = '';
    
      constructor(private _service: Service) {
      }
    
      doSearch() {
        this._service.search(this._search).subscribe(
          res => {
            this._result = res;
          },
          err => {
            console.log(err);
          },
          () => {
            this._searched = true;
          }
        );
      }
    }
    
    
  6. 添加服务类:创建文件 service.ts 并将其更改为:

    import { Injectable } from '@angular/core';
    import { HttpClient, HttpHeaders } from '@angular/common/http';
    import 'rxjs/Rx';
    
    @Injectable()
    export class Service {
    constructor(private _http: HttpClient){}
    search(search) {
      const concatenatedUrl: string =
        "https://trailapi-trailapi.p.mashape.com?q[city_cont]=" +
        encodeURIComponent(search);
      const mashapeKey = 'OxWYjpdztcmsheZU9AWLNQcE9g9wp1qdRkFjsneaEp2Yf68nYH';
      const httpHeaders: HttpHeaders = new HttpHeaders(
        {'Content-Type': 'application/json',
       'X-Mashape-Key': mashapeKey});
      return this._http
        .get<any>(concatenatedUrl, { headers: httpHeaders })
        .map(res => {
          // return the first place.
          if ((res) && (res['places']) && (res['places'].length) && (res['places'].length > 0)){
            return res['places'][0];
          }else{
            // otherwise return nothing
            return undefined;
          }
         })
         .catch(err => {
           console.log(‘error',err)
           return undefined;
         });
      }
    }
    
    
  7. 该应用现在应该可以工作了:返回到您的 web 浏览器并导航到 localhost:4200。你应该能够搜索踪迹。

  8. 添加服务单元测试:创建文件 service.spec.ts,并将其更改为:

    import { TestBed, getTestBed, async } from '@angular/core/testing';
    import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
    import { FormsModule } from '@angular/forms';
    import { AppComponent } from './app.component';
    import { Service } from './service';
    import { HttpClientModule } from '@angular/common/http/src/module';
    import 'rxjs/Rx';
    
    describe('AppComponent (data found)', () => {
      let service: Service;
      let httpMock: HttpTestingController;
    
      beforeEach(() => {
    
        TestBed.configureTestingModule({
          imports: [HttpClientTestingModule],
          providers: [Service]
        });
    
        service = TestBed.get(Service);
        httpMock = TestBed.get(HttpTestingController);
    
      });
    
      it('should return the first place if there is one', async() => {
        service.search("Atlanta").subscribe((res: any) => {
            expect(res.name).toContain('Boat Rock');
            expect(res.city).toBe('Atlanta');
            expect(res.state).toBe('Georgia');
            expect(res.country).toBe('United States');
            expect(res.directions).toContain('Interstate 20 and Fulton Industrial');
            expect(res.activities.length).toBe(1);
            });
        const req = httpMock.expectOne('https://trailapi-trailapi.p.mashape.com?q[city_cont]=Atlanta');
        const mockData =
        {
          "places":[
              {
                "city":"Atlanta",
                "state":"Georgia",
                "country":"United States",
                "name":"Boat Rock",
                "parent_id":null,
                "unique_id":5370,
    
                "directions":"From the intersection of Interstate 20 and Fulton Industrial Boulevard go south for 3.8 miles, turn left onto Bakers Ferry Road SW, go 0.5 miles, turn left on Boat Rock Road SW, go 0.4 miles, look for small gravel driveway on the right, pull into small 6 car parking lot. There is a small kiosk at the edge of the lot with a rough map of the area and a trail leading up to the boulders. The lake area is located a few hundred yards to the southeast (see drtopo map).<br /><br /><br /><br /><br /><br />1220 Boat Rock Road Mapquest Link ",
                "lat":0.0,
                "lon":0.0,
                "description":null,
                "date_created":null,
                "children":[
                ],
                "activities":[
                    {
                      "name":"Boat Rock",
                      "unique_id":"2-1012",
                      "place_id":5370,
                      "activity_type_id":2,
                      "activity_type_name":"hiking",
                      "url":"http://www.tripleblaze.com/trail.php?c=3&i=1012",
                      "attribs":{
                          "\"length\"":"\"1\""
                      },
                      "description":"For those of us who like hiking AND rock climbing! Very cool place just inside of Atlanta. We took our children here and they could climb some of the boulders. A great experience for families and it's fun getting to watch the expert climbers on the rocks!",
                      "length":1.0,
                      "activity_type":{
                          "created_at":"2012-08-15T16:12:21Z",
                          "id":2,
                          "name":"hiking",
                          "updated_at":"2012-08-15T16:12:21Z"
                      },
                      "thumbnail":"http://images.tripleblaze.com/2009/07/Myspace-Pictures-130-0.jpg",
                      "rank":null,
                      "rating":0.0
                    }
                ]
              }
          ]
        }
        req.flush(mockData); // valid response from server
        httpMock.verify();
      });
    
      it('should return undefined if there is empty response from the server', async() => {
        service.search("Atlanta").subscribe((res: any) => {
            expect(res).toBe(undefined);
            });
        const req = httpMock.expectOne('https://trailapi-trailapi.p.mashape.com?q[city_cont]=Atlanta');
        req.flush(''); // empty response from server
        httpMock.verify();
      });
    
      it('should return undefined if there is empty response object from the server', async() => {
        service.search("Atlanta").subscribe((res: any) => {
            expect(res).toBe(undefined);
            });
        const req = httpMock.expectOne('https://trailapi-trailapi.p.mashape.com?q[city_cont]=Atlanta');
        req.flush('{}'); // empty response object from server
        httpMock.verify();
      });
    });
    
    
  9. 运行测试:使用以下命令:

    ng test
    
    

请注意以下几点:

  • 在“beforeEach”(在每个“it”测试之前触发)中,我们:

    • 将我们的测试床配置为导入 HttpClientTestingModule,而不是 HttpClient。这将使我们能够模拟 Http 响应。
    • 我们获得对服务的引用。
    • 我们得到一个对 http 测试控制器的引用。
  • 在每个测试中,我们对来自服务的可观察响应的订阅设置期望,以便它可以测试返回的数据:

    service.search("Atlanta").subscribe((res: any) => {
      expect(res).toBe(undefined);
    });
    
    
  • 在每个测试中,我们调用 http 测试控制器中的方法‘expect one ’,告诉它预期一个 http 请求,它的 URI 应该是:

    const req = httpMock.expectOne('https://trailapi-trailapi.p.mashape.com?q[city_cont]=Atlanta');
    
    
  • “expectOne”方法返回一个 TestRequest 对象。在下一行,我们告诉 TestRequest“刷新”一个响应(在本例中是一个空响应):

    req.flush('');
    
    
  • 在使用“flush”方法发送回模拟响应后,我们调用“verify”方法来确保没有未完成的 Http 请求:

    httpMock.verify();
    
    

测试使用服务的组件:示例

对于第四个例子,我们将建立在前一个例子的基础上。我们将为使用服务的组件添加单元测试,使您能够搜索踪迹(图 22-4 )。

  1. 添加组件测试类:创建 app.component.spec.ts 文件,并将其更改为:

    import { TestBed, getTestBed, async } from '@angular/core/testing';
    import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
    import { FormsModule } from '@angular/forms';
    import { AppComponent } from './app.component';
    import { Service } from './service';
    import { HttpClient, HttpClientModule } from '@angular/common/http';
    import 'rxjs/Rx';
    
    describe('AppComponent (data found)', () => {
      let service: Service;
      let httpMock: HttpTestingController;
      let fixture, app, compiled;
    
      beforeEach(() => {
    
        TestBed.configureTestingModule({
          declarations: [AppComponent],
          imports: [FormsModule, HttpClientTestingModule],
          providers: [HttpClient, Service]
        }).compileComponents();
    
        service = TestBed.get(Service);
    
        httpMock = TestBed.get(HttpTestingController);
    
        fixture = TestBed.createComponent(AppComponent);
        app = fixture.debugElement.componentInstance;
        expect(app).toBeTruthy();
        fixture.detectChanges();
        compiled = fixture.debugElement.nativeElement;
        compiled.querySelector('button').click();
    
      });
    
      it('should display the first place if there is one', async() => {
    
        const req = httpMock.expectOne('https://trailapi-trailapi.p.mashape.com?q[city_cont]=Atlanta');
        const mockData =
        {
          "places":[
              {
                "city":"Atlanta",
                "state":"Georgia",
                "country":"United States",
                "name":"Boat Rock",
                "parent_id":null,
                "unique_id":5370,
                "directions":"From the intersection
    
    of Interstate 20 and Fulton Industrial Boulevard go south for 3.8 miles, turn left onto Bakers Ferry Road SW, go 0.5 miles, turn left on Boat Rock Road SW, go 0.4 miles, look for small gravel driveway on the right, pull into small 6 car parking lot. There is a small kiosk at the edge of the lot with a rough map of the area and a trail leading up to the boulders. The lake area is located a few hundred yards to the southeast (see drtopo map).<br /><br /><br /><br /><br /><br />1220 Boat Rock Road Mapquest Link ",
                "lat":0.0,
                "lon":0.0,
                "description":null,
                "date_created":null,
                "children":[
                ],
                "activities":[
                    {
                      "name":"Boat Rock",
                      "unique_id":"2-1012",
                      "place_id":5370,
                      "activity_type_id":2,
                      "activity_type_name":"hiking",
                      "url":"http://www.tripleblaze.com/trail.php?c=3&i=1012",
                      "attribs":{
                          "\"length\"":"\"1\""
                      },
                      "description":"For those of us who like hiking AND rock climbing!
    
    Very cool place just inside of Atlanta. We took our children here and they could climb some of the boulders. A great experience for families and it's fun getting to watch the expert climbers on the rocks!",
                      "length":1.0,
                      "activity_type":{
                          "created_at":"2012-08-15T16:12:21Z",
                          "id":2,
                          "name":"hiking",
                          "updated_at":"2012-08-15T16:12:21Z"
                      },
                      "thumbnail":"http://images.tripleblaze.com/2009/07/Myspace-Pictures-130-0.jpg",
                      "rank":null,
                      "rating":0.0
                    }
                ]
              }
          ]
        }
        req.flush(mockData);
        httpMock.verify();
    
        fixture.detectChanges();
    
        expect(compiled.querySelector('#notFound')).toBeNull();
        expect(compiled.querySelector('#name').textContent).
            toContain('Boat Rock');
        expect(compiled.querySelector('#state').textContent).
            toContain('Georgia');
      });
    
      it('should display a not found message
    
    if there is empty response from the server', async() => {
        const req = httpMock.expectOne('https://trailapi-trailapi.p.mashape.com?q[city_cont]=Atlanta');
        req.flush('');
        httpMock.verify();
    
        fixture.detectChanges();
    
        expect(compiled.querySelector('#notFound').textContent).
            toContain('We could not find a trail here. :(');
        expect(compiled.querySelector('#name')).toBeNull();
        expect(compiled.querySelector('#state')).toBeNull();
      });
    
      it('should display a not found message undefined if there is empty response object from the server', async() => {
        const req = httpMock.expectOne('https://trailapi-trailapi.p.mashape.com?q[city_cont]=Atlanta');
        req.flush('{}');
        httpMock.verify();
    
        fixture.detectChanges();
    
        expect(compiled.querySelector('#notFound').textContent).
            toContain('We could not find a trail here. :(');
        expect(compiled.querySelector('#name')).toBeNull();
        expect(compiled.querySelector('#state')).toBeNull();
      });
    });
    
    
  2. 运行测试:使用以下命令:

    ng test
    
    

请注意以下几点:

  • 在“beforeEach”(在每个“it”测试之前触发)中,我们:
    • 配置我们的测试平台以导入 FormsModule(组件处理输入所需的)和 HttpClientTestingModule。HttpClientTestingModule 将使我们能够模拟 Http 响应。我们还将 HttpClient 和服务设置为提供者。请注意,我们调用“compileComponents”来确保任何组件都已编译就绪。
    • 我们获得对服务的引用。
    • 我们得到一个对 http 测试控制器的引用。
    • 我们在测试平台中创建 AppComponent 的一个实例。
    • 我们检测变化,以允许 Angular 在此时执行它需要的任何变化检测。
    • 我们获得了对组件的 DOM 元素的引用。
    • 我们在 DOM 元素中获取一个对按钮的引用,然后单击它。这模拟用户点击“搜索”按钮。
  • 在每次测试中,我们:
    • 按照与上例类似的方式,对同一搜索设置不同的响应。
    • 我们检测变化,以允许 Angular 在此时执行它需要的任何变化检测。Angular 需要重新绘制 ui,以反映由于响应而导致的模型中的任何变化。不要漏掉这一行!
    • 我们检查 DOM 元素是否符合预期的结果。

摘要

你可以写一整本关于软件测试的书——事实上,没有人这么做过。这是一个复杂的话题。

显然写更多的测试更好,测试是一件好事。例如,如果你必须重构(或设计)你的代码,单元测试是非常好的。如果您重构的代码被单元测试很好地覆盖,并且您更改了您的代码,它仍然通过了测试,这将使您对重构的正确性更有信心。

编写测试代码可能是困难和复杂的,并且会花费大量的时间,所以我建议您考虑将测试集中在代码中最重要的部分:代码执行计算的地方,应用业务规则的地方,等等。您需要编写关注代码最重要部分的基本测试。之后,您可以优先测试应用的其余部分,并根据可用的时间来调整编写测试所花费的时间。

下一章将介绍视图封装和其他高级主题。

二十三、更多高级主题

本章集中介绍了几个更高级的 Angular 主题。

查看封装

还记得如何使用@Component注释的stylesstyleUrls属性将样式应用于组件吗?“封装”一词的意思是“将某物装入或好象装入胶囊中的动作。”

Angular 视图封装与 Angular 使用何种方法将这些样式(应用了stylesstyleUrls属性的样式)与组件封装在一起有关。

为什么需要视图封装?当您使用stylesstyleUrls属性来样式化一个组件时,Angular 将样式代码添加到 HTML 文档的head部分的style标签中。那很好,但是你需要注意一些事情。如果在不同的组件中有冲突的 CSS 样式规则会发生什么?如果(例如)一个组件中有.h2 {color:red}而另一个组件中有.h2 {color:green}呢?

如果你的组件使用的是影子 DOM(或者模拟的影子 DOM ),你不需要担心这些冲突的样式。您可能正在使用一个阴影 DOM(或者至少是一个模拟的阴影 DOM ),因为这是 Angular 4 默认提供给您的。

但是,您需要了解影子 DOM,因为如果您的组件没有使用影子 DOM(或模拟的影子 DOM),那么这些冲突的样式可能会让您头疼。

影子穹顶

一段时间以来,浏览器的范围一直是个问题。开发人员已经能够轻松地对 HTML 文档进行全面的全局更改,几乎不需要做什么工作。他们可以添加几行 CSS 并立即影响许多 DOM 元素。这很强大,但是会使组件的样式很容易被覆盖或意外破坏。

影子 DOM 是 web 上一个新出现的标准。影子 DOM 可以在大多数浏览器上工作(除了 Internet Explorer)。Shadow DOM 背后的思想是让开发人员可以选择用他们自己的独立 DOM 树创建组件,这些组件与其他组件分开封装,包含在主机元素中。这使得开发人员可以将样式“限定”在不会影响文档其余部分的单个组件上。

当你写一个组件时,你不需要使用影子 DOM,但是它是一个选项,你可以使用@Component注释的encapsulation选项来控制。

组件封装

@Component注释的encapsulation选项让开发人员可以控制视图封装的级别——换句话说,是否实现影子 DOM。表 23-1 显示了该选项的三种变化。

表 23-1

Encapsulation Option

| [计]选项 | 描述 | | :-- | :-- | | `ViewEncapsulation.Emulated` | 模拟阴影 DOM,Angular 的默认模式 | | `ViewEncapsulation.Native` | 原生阴影 DOM | | `ViewEncapsulation.None` | 一点影子都没有 |

查看封装。仿真:示例

让我们创建一个带有样式的示例组件,并将ViewEncapsulation指定为Emulated。这是 Angular 的默认模式。这将是高级示例-ex100:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new advanced-ex100 --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd advanced-ex100
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“应用工作!”

  4. 编辑组件:编辑 app.component.ts 文件,将其更改为:

    import { Component, ViewEncapsulation } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      template: `
        <h1>
          {{title}}
        </h1>
      `,
      styles: ['h1 { color: red }'],
      encapsulation: ViewEncapsulation.Emulated
    })
    export class AppComponent {
      title = 'app';
    }
    
    

该应用应该工作,并以红色显示单词 app。图 23-1 为该文件。

A458962_1_En_23_Fig1_HTML.jpg

图 23-1

ViewEncapsulation.Emulated

如您所见,样式被写入文档的head。Angular 还重写了我们的组件风格,为style和组件都添加了一个标识符,以将两者链接在一起,并避免与具有其他标识符的其他组件发生冲突。在这种情况下,标识符是_ngcontent-c0

查看封装。本地:示例

让我们创建一个带有样式的示例组件,并将ViewEncapsulation指定为Native。这将是高级示例-ex200:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new advanced-ex200 --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd advanced-ex200
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“应用工作!”

  4. 编辑组件:编辑 app.component.ts 文件,将其更改为:

    import { Component, ViewEncapsulation } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      template: `
        <h1>
          {{title}}
        </h1>
      `,
      styles: ['h1 { color: red }'],
      encapsulation: ViewEncapsulation.Native
    
    })
    export class AppComponent {
      title = 'app';
    }
    
    

该应用应该工作,并以红色显示单词 app。图 23-2 为文档。

A458962_1_En_23_Fig2_HTML.jpg

图 23-2

ViewEncapsulation.Native

样式不再被写到文档的head中,而是被写到组件的影子 DOM 中。要查看此输出,必须在浏览器中打开显示阴影 DOM。现在很容易看到您的样式是如何只应用于组件的,该组件驻留在主机元素app-root中。

查看封装。无:示例

现在让我们创建一个带有样式的示例组件,并将ViewEncapsulation指定为None。这将是高级示例-ex300:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new advanced-ex300 --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd advanced-ex300
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“应用工作!”

  4. 编辑组件:编辑 app.component.ts 文件,将其更改为:

    import { Component, ViewEncapsulation } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      template: `
        <h1>
          {{title}}
        </h1>
      `,
      styles: ['h1 { color: red }'],
      encapsulation: ViewEncapsulation.None
    })
    export class AppComponent {
      title = 'app';
    }
    
    

该应用应该工作,并以红色显示单词 app。图 23-3 为文档。

A458962_1_En_23_Fig3_HTML.jpg

图 23-3

ViewEncapsulation.None

该样式被写入文档的头部,并且该样式应用于整个文档,可能与来自其他组件的其他样式相冲突。小心这种模式。

Angular 为您提供了两个世界中最好的东西:将封装视为默认设置,并且能够共享样式。即使您没有将encapsulation规范添加到@Component注释中,您的特定于组件的样式也会受到保护。

如果需要在组件中共享样式,可以在@Component注释中使用styleUrls规范来指定共享的公共样式文件。

样式内容子项

还记得如何使用@Component注释的stylesstyleUrls属性将样式应用于组件吗?这些样式仅适用于组件自身模板中的 HTML。如果您从服务器获取 HTML 内容,并将这些内容动态注入到您的组件中,会发生什么呢?你是怎么设计的?

答案是使用特殊的样式标签将样式应用于组件及其子元素(例如,来自服务器的 HTML 内容)。例如,以下样式规则对组件及其子元素中的所有h3元素进行样式化:

:host /deep/ h3 { font-style: italic; }

摘要

本章介绍了视图封装的概念,并讨论了如何在 Angular 中实现它。这听起来可能不是很重要,但是你应该知道它,因为它会影响你如何编写你的 CSS 样式。

我们快到终点了。最后一章是关于不同的 Angular 资源,可以在未来进一步提高你的 Angular 技能。

二十四、资源

我希望这本书对你有用。我不是凭空写的——我依赖于许多信息来源。我想分享一些资源,可以帮助你的 Angular 发展。

Angular 官网

Angular 官方网站在 https://angular.io ,其主页如图 24-1 所示。它包含了大量的信息,并且布局合理。这应该是你进行任何 Angular 研究的起点。

A458962_1_En_24_Fig1_HTML.jpg

图 24-1

Angular website

我发现 https://angular.io/docs/ts/latest/api/ 的 API 预览页面特别有用。输入你要找的东西,它会显示搜索结果。这些搜索结果包括与搜索匹配的对象,按其包分组。这个包信息对于在类的顶部编写import非常有用。当您在搜索结果中单击某个对象时,它会向您显示有关其 API 的详细信息。

开源代码库

GitHub 位于 https://github.com ,是一个流行的基于 web 的 Git 仓库托管服务。开发人员用它来发布和管理他们的代码。GitHub 提供付费和免费账户。付费账户享有私有存储库的优势。但是免费账户很受欢迎,人们在编写开源软件项目时经常使用。GitHub 报告了超过 1200 万用户和超过 3100 万存储库,使其成为世界上最大的源代码主机。

Note

查看本书中的 https://github.com/markclow 代码示例和示例项目。

Git 是一个广泛用于软件开发的源代码管理系统。与更老、更传统的源代码管理系统不同,Git 允许开发人员以分布式方式工作,在他们的计算机上管理他们自己的本地存储库,不管有没有网络。没有“中央”存储库,只有“对等”分布式存储库。一旦开发人员完成了代码更改,他们就可以将他们的更改合并到共享存储库中。

Angular 相关的博客

表 24-1 列出了一些你可能想关注的好的 Angular 相关博客。

表 24-1

Angular–related Blogs

| 博客地址 | 描述 | | :-- | :-- | | `http://blog.thoughtram.io` | 高级 Angular 文章 | | `https://toddmotto.com` | 高级 Angular 文章 | | `http://victorsavkin.com` | Angular 的物品 | | `http://blog.jhades.org` | 大量的 JavaScript 和 Angular 文章 | | `http://johnpapa.net` | 很多文章,包括 Angular 的文章 |

角空气

Angular Air 是一个关于 Angular 的精彩视频播客:点击 www.youtube.com/channel/UCdCOpvRk1lsBk26ePGDPLpQ 查看。

摘要

我希望你喜欢这本书。如果你发现任何代码不工作,给我发电子邮件到 markclow@hotmail.com,我会修复它。如果你觉得我在这本书里漏掉了一些有价值的东西,请发邮件给我。

就这些了,伙计们!我希望这本书对您有所帮助,并且您可以从 GitHub 下载并使用代码示例(参见本章前面的内容)。当我工作的时候,我当然经常浏览那个网站。

我很幸运能享受我正在做的事情。我希望你对你的工作有同样的感觉,对你职业的热爱让你有动力继续学习。

我祝你在努力中一切顺利。永远不要气馁:做大事是困难的!

posted @   绝不原创的飞龙  阅读(9)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 推荐几款开源且免费的 .NET MAUI 组件库
· 实操Deepseek接入个人知识库
· 易语言 —— 开山篇
· 【全网最全教程】使用最强DeepSeekR1+联网的火山引擎,没有生成长度限制,DeepSeek本体
点击右上角即可分享
微信分享提示