Angular5 项目教程(六)
二十、管道
从安古拉吉斯时代起,管道就已经存在了。它们在转换数据时很有用,尤其是在整个应用中使用相同的转换时。管道可以很容易地将这些转换添加到组件模板中。
弯管
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 所示的组件使用各种角管显示信息。
图 20-1
Showing various Angular pipes
这将是示例管道-ex100:
-
使用 CLI 构建应用:使用以下命令:
ng new pipes-ex100 --inline-template --inline-style -
开始
ng serve:使用以下代码:cd pipes-ex100 ng serve -
打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“应用工作!”
-
编辑组件:编辑 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注释为前缀。它还需要导入Pipe和PipeTransform,以及实现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 所示的组件允许用户反转一些文本。它还有一个可选参数—反转文本的每个字符之间的空格数。
图 20-2
Reversing text with a pipe
这将是示例管道-ex200:
-
使用 CLI 构建应用:使用以下命令:
ng new pipes-ex200 -
开始
ng serve:使用以下代码:cd pipes-ex200 ng serve -
打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“应用工作!”
-
生成管道:使用 CLI 生成自定义管道:
ng generate pipe reverse -
编辑管道:编辑 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; } } -
编辑组件:编辑 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'; }
该应用应该工作,并显示格式化的数据。请注意以下几点:
- 类
ReversePipe像任何管道一样实现了PipeTransform接口。 - 类
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 所示。
图 21-1
Event loop
浏览器 UI 线程
浏览器 UI 线程是通过运行事件循环代码、处理消息来更新用户界面的单个线程。在处理下一条消息之前,每条消息都被完全处理。只有一个线程用于更新用户界面(用户查看的文档)。如果浏览器 UI 线程过载,浏览器会向用户显示如图 21-2 所示的消息(或类似的消息)。
图 21-2
Browser UI thread is overloaded
猴子补丁
使用NgZone /Zones.js,系统 JavaScript 代码被“打了猴子补丁”(当它必须这样做时),以便它挂钩到事件循环代码,查看正在处理的消息发生了什么。这使它能够提供有关区域中发生的事件或调用的代码的附加信息,例如,异步服务器调用完成。
Note
猴子补丁是程序在本地扩展或修改支持系统软件的一种方式。就 Angular 和 Zone.js 而言,Zone 将在必要时对 JavaScript 核心代码进行猴子式修补,以便提供执行信息。
NgZone发出onTurnStart和onTurnEnd事件,通知观察者某事即将发生和某事已经发生。
Angular 使用NgZone来寻找需要变化检测的事件。在核心 Angular 代码中,Angular 监听NgZone onTurnDone事件。当此事件触发时,Angular 对模型执行更改检测并更新 UI。
Angular 和变化检测
正如我以前说过的,Angular 应用是由多个类似乐高积木的组件构建而成的,具有树状层次结构。您有主应用组件,然后您有子组件,等等。
图 21-3 展示了组件 UI,图 21-4 展示了组件树。
图 21-4
Component tree
图 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 所示。
图 21-5
Running asynchronous code within the Angular zone
让我们看一下这个例子:
- 导入
NgZone。 - 使用构造函数注入来注入
NgZone的一个实例。 - 该方法由 Count 按钮触发,该按钮使用注入的
NgZone运行initiateCount方法。注意,它调用方法run在注入的 Angular 区域内运行该方法。 - 方法
initiateCount和updateCount使用时间间隔计时器,以异步任务的形式生成控制台日志。当计数器超过 1000 时,它们更新计数器并结束计数。
当你运行这个应用并点击计数按钮时,你会看到计数器更新 1,2,3,4 …一直到 1000,然后出现警报。用户界面显示计数。这是因为计数是在 Angular 区域内的函数中执行的,用NgZone观察事件并引起变化检测。变化检测检测到count变量发生了变化,更新 UI,如图 21-6 所示。
图 21-6
count variable updates the UI
在 Angular 区域外运行异步代码:示例
此示例也基于默认的 Angular TypeScript Plunker 应用。app.ts 文件如图 21-7 所示。
- 导入
NgZone。 - 使用构造函数注入来注入
NgZone的一个实例。 - 此方法由计数按钮触发。它使用注入的
NgZone运行initiateCount方法。注意,它调用方法runOutsideAngular在注入的 Angular 区域之外运行该方法。 - 方法
initiateCount和updateCount使用时间间隔计时器,以异步任务的形式生成控制台日志。当计数器超过 1000 时,它们更新计数器并结束计数。
图 21-7
Running asynchronous code outside the Angular zone
当您运行此应用并单击 Count 时,您看不到计数器发生变化。用户界面显示计数为 0,直到报警出现,如图 21-8 所示。这是因为计数是在 Angular 区域之外的函数中执行的,而没有NgZone观察事件并引起变化检测。注意到它有多快了吗?
图 21-8
count variable not updated until alert
摘要
本章试图介绍 Angular 的一些内部工作方式。它并不打算涵盖这个主题的每一个细节——那将需要许多章节。
这一章(简要地)介绍了不变性的概念,这是你需要知道的,特别是如果你将来要做函数式编程的话。不变性是对象一旦被创建就不能被修改的概念。作为开发人员,您需要考虑尽可能使用不可变对象,因为它们有很多好处:
- 它们简化了编码(因为移动的部分更少),你知道对象不会改变值。
- 它们与 Angular 变化检测算法一起工作得更好。
- 当您尽可能地限制应用中可以更改对象的方式时,您可以使代码更简单,并对正在更改的内容保持更多的控制。
- 它们最大限度地减少了当对象变异时有时会出现的副作用。
- 它们在多线程下工作得更好。
下一章将介绍测试你的 Angular 代码。
二十二、测试
这本书主要是关于如何开始高效地使用 Angular,但是如果没有至少介绍测试你写的代码的方法,这本书是不完整的。测试框架相当复杂,所以不要指望看完这一章就能了解它的一切。
我将介绍一些概念,然后详细介绍如何编写代码来自动测试用 Angular CLI 生成的项目。
单元测试是对应用的最小可能单元的测试,无论是手动的还是自动的。单元测试的目的是确保代码按预期执行,并且新代码不会破坏旧代码。测试驱动开发的过程是按以下顺序开发代码:
- 编写测试代码(测试工具)
- 编写应用代码以通过测试
- 清理和重构应用代码以通过编码标准
- 检查它是否仍然通过测试
这个过程应该应用于较小的代码单元,并且应该经常重复这个过程。单元测试在现代软件开发过程中是必不可少的。
软件开发使用了开发人员从中央存储库中取出最新代码并对其进行处理的过程。工作完成后(代码经过测试),开发人员签入完成的代码。持续集成是一天数次将所有开发人员代码集成(或合并)到共享代码库中的过程。尽可能频繁地集成代码突出了快速合并问题,并避免了更大的代码不兼容性。我们的目标是在尽可能短的时间内签出代码,并在有人在此期间做了太多更改之前尽快签入和集成更改。
图 22-1 显示了一个(非常通用的)开发过程工作图。它没有考虑代码分支、合并问题和其他因素。
图 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 展示了茉莉的结构。
图 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 提供了一个解决方案:setup和teardown方法。这些函数在每个测试(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对象的实例。组件夹具非常有用的原因之一是它提供了对正在调试的组件的访问。
ComponentFixture的debugElement属性表示 Angular 分量及其对应的 DOM 元素。它包含以下属性,如表 22-3 所示。
表 22-3
debugElement Properties
件实例
在夹具的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
CLI 单元测试:示例
第一个例子并不激动人心,但它将展示一个示例 CLI 项目的生成,并检查生成的测试代码。这将是示例测试-ex100:
-
使用 CLI 构建应用:使用以下命令:
ng new testing-ex100 --inline-template --inline-style -
导航到文件夹:使用以下命令:
cd testing-ex100 -
打开文件:打开 app.component.spec.ts 并注意以下内容:
- 在每个规范之前调用
beforeEach方法。此方法配置测试模块来测试 AppComponent 组件。 - 有三个规格(测试)。使用测试模块中的
async方法异步调用每一个。 - 第一个规范创建一个 fixture,然后从
debug元素获取组件实例。它检查组件是否真实(即是否有指定的值)。 - 第二个规范创建一个 fixture,然后从
debug元素获取组件实例。它检查组件的title实例变量是否有值‘app’。 - 第三个规范(test)创建一个 fixture,然后从 debug 元素获取组件的元素。它检查该元素是否有包含值“欢迎使用应用!”的“h1”元素
- 在每个规范之前调用
-
Run tests: Use the following command:
ng testNow 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.
图 22-3
Incrementing a counter This will be example testing-ex200:
-
使用 CLI 构建应用:使用以下命令:
ng new testing-ex200 --inline-template --inline-style -
开始
ng serve:使用以下代码:cd testing-ex200 ng serve -
打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”
-
编辑类:编辑 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++; } } -
编辑单元测试:编辑 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); } })); }); -
运行测试:使用以下命令:
```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 响应
- 将 HttpClientTestingModule 导入到单元测试中。
- 将 HttpClient 和 HttpClientTestingModule 注入到您的测试中。
- 通过调用下面的方法之一来设置一个测试请求对象,告诉 HttpClientTestingModule 在测试中应该接收到多少个 http 请求(见下面)。HttpClientTestingModule 将断言它收到的请求数与其预期的相匹配。 请求数量 http clienttestingmodule 方法
Unsure``match``0``expectNone``1``expectOne - 您在测试请求对象上调用“flush”方法来发送回模拟结果。
使用 HttpClient 的测试服务:示例
对于第三个例子,我们将创建一个简单的组件,它使用一个服务使您能够使用 http 服务搜索踪迹(图 22-4 )。然后,我们将为服务编写一个单元测试,并测试它如何处理服务器响应。
图 22-4
Component to search for trails
- 对使用服务的组件进行单元测试,并测试它如何处理服务器响应。
这将是示例测试-ex300:
-
使用 CLI 构建应用:使用以下代码:
ng new testing-ex300 --inline-template --inline-style -
开始
ng serve:使用以下代码:cd testing-ex300 ng serve -
打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”
-
编辑模块:编辑 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 { } -
编辑类:编辑 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; } ); } } -
添加服务类:创建文件 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; }); } } -
该应用现在应该可以工作了:返回到您的 web 浏览器并导航到 localhost:4200。你应该能够搜索踪迹。
-
添加服务单元测试:创建文件 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(); }); }); -
运行测试:使用以下命令:
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 )。
-
添加组件测试类:创建 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(); }); }); -
运行测试:使用以下命令:
ng test
请注意以下几点:
- 在“beforeEach”(在每个“it”测试之前触发)中,我们:
- 配置我们的测试平台以导入 FormsModule(组件处理输入所需的)和 HttpClientTestingModule。HttpClientTestingModule 将使我们能够模拟 Http 响应。我们还将 HttpClient 和服务设置为提供者。请注意,我们调用“compileComponents”来确保任何组件都已编译就绪。
- 我们获得对服务的引用。
- 我们得到一个对 http 测试控制器的引用。
- 我们在测试平台中创建 AppComponent 的一个实例。
- 我们检测变化,以允许 Angular 在此时执行它需要的任何变化检测。
- 我们获得了对组件的 DOM 元素的引用。
- 我们在 DOM 元素中获取一个对按钮的引用,然后单击它。这模拟用户点击“搜索”按钮。
- 在每次测试中,我们:
- 按照与上例类似的方式,对同一搜索设置不同的响应。
- 我们检测变化,以允许 Angular 在此时执行它需要的任何变化检测。Angular 需要重新绘制 ui,以反映由于响应而导致的模型中的任何变化。不要漏掉这一行!
- 我们检查 DOM 元素是否符合预期的结果。
摘要
你可以写一整本关于软件测试的书——事实上,没有人这么做过。这是一个复杂的话题。
显然写更多的测试更好,测试是一件好事。例如,如果你必须重构(或设计)你的代码,单元测试是非常好的。如果您重构的代码被单元测试很好地覆盖,并且您更改了您的代码,它仍然通过了测试,这将使您对重构的正确性更有信心。
编写测试代码可能是困难和复杂的,并且会花费大量的时间,所以我建议您考虑将测试集中在代码中最重要的部分:代码执行计算的地方,应用业务规则的地方,等等。您需要编写关注代码最重要部分的基本测试。之后,您可以优先测试应用的其余部分,并根据可用的时间来调整编写测试所花费的时间。
下一章将介绍视图封装和其他高级主题。
二十三、更多高级主题
本章集中介绍了几个更高级的 Angular 主题。
查看封装
还记得如何使用@Component注释的styles或styleUrls属性将样式应用于组件吗?“封装”一词的意思是“将某物装入或好象装入胶囊中的动作。”
Angular 视图封装与 Angular 使用何种方法将这些样式(应用了styles或styleUrls属性的样式)与组件封装在一起有关。
为什么需要视图封装?当您使用styles或styleUrls属性来样式化一个组件时,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:
-
使用 CLI 构建应用:使用以下命令:
ng new advanced-ex100 --inline-template --inline-style -
开始
ng serve:使用以下代码:cd advanced-ex100 ng serve -
打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“应用工作!”
-
编辑组件:编辑 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 为该文件。
图 23-1
ViewEncapsulation.Emulated
如您所见,样式被写入文档的head。Angular 还重写了我们的组件风格,为style和组件都添加了一个标识符,以将两者链接在一起,并避免与具有其他标识符的其他组件发生冲突。在这种情况下,标识符是_ngcontent-c0。
查看封装。本地:示例
让我们创建一个带有样式的示例组件,并将ViewEncapsulation指定为Native。这将是高级示例-ex200:
-
使用 CLI 构建应用:使用以下命令:
ng new advanced-ex200 --inline-template --inline-style -
开始
ng serve:使用以下代码:cd advanced-ex200 ng serve -
打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“应用工作!”
-
编辑组件:编辑 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 为文档。
图 23-2
ViewEncapsulation.Native
样式不再被写到文档的head中,而是被写到组件的影子 DOM 中。要查看此输出,必须在浏览器中打开显示阴影 DOM。现在很容易看到您的样式是如何只应用于组件的,该组件驻留在主机元素app-root中。
查看封装。无:示例
现在让我们创建一个带有样式的示例组件,并将ViewEncapsulation指定为None。这将是高级示例-ex300:
-
使用 CLI 构建应用:使用以下命令:
ng new advanced-ex300 --inline-template --inline-style -
开始
ng serve:使用以下代码:cd advanced-ex300 ng serve -
打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“应用工作!”
-
编辑组件:编辑 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 为文档。
图 23-3
ViewEncapsulation.None
该样式被写入文档的头部,并且该样式应用于整个文档,可能与来自其他组件的其他样式相冲突。小心这种模式。
Angular 为您提供了两个世界中最好的东西:将封装视为默认设置,并且能够共享样式。即使您没有将encapsulation规范添加到@Component注释中,您的特定于组件的样式也会受到保护。
如果需要在组件中共享样式,可以在@Component注释中使用styleUrls规范来指定共享的公共样式文件。
样式内容子项
还记得如何使用@Component注释的styles或styleUrls属性将样式应用于组件吗?这些样式仅适用于组件自身模板中的 HTML。如果您从服务器获取 HTML 内容,并将这些内容动态注入到您的组件中,会发生什么呢?你是怎么设计的?
答案是使用特殊的样式标签将样式应用于组件及其子元素(例如,来自服务器的 HTML 内容)。例如,以下样式规则对组件及其子元素中的所有h3元素进行样式化:
:host /deep/ h3 { font-style: italic; }
摘要
本章介绍了视图封装的概念,并讨论了如何在 Angular 中实现它。这听起来可能不是很重要,但是你应该知道它,因为它会影响你如何编写你的 CSS 样式。
我们快到终点了。最后一章是关于不同的 Angular 资源,可以在未来进一步提高你的 Angular 技能。
二十四、资源
我希望这本书对你有用。我不是凭空写的——我依赖于许多信息来源。我想分享一些资源,可以帮助你的 Angular 发展。
Angular 官网
Angular 官方网站在 https://angular.io ,其主页如图 24-1 所示。它包含了大量的信息,并且布局合理。这应该是你进行任何 Angular 研究的起点。
图 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 下载并使用代码示例(参见本章前面的内容)。当我工作的时候,我当然经常浏览那个网站。
我很幸运能享受我正在做的事情。我希望你对你的工作有同样的感觉,对你职业的热爱让你有动力继续学习。
我祝你在努力中一切顺利。永远不要气馁:做大事是困难的!