Angular6-示例第三版-一-

52 阅读1小时+

Angular6 示例第三版(一)

原文:zh.annas-archive.org/md5/ea9e0be757a76027d750b88ab0b9bedf

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

Angular 6 已经到来,我们非常兴奋!这本书让我们有机会向你伸出援手,帮助你学习 Angular。Angular 已经变得主流,并已成为网络和移动开发的通用平台。

如果你是一名 AngularJS 开发者,那么有许多令人兴奋的内容可以学习,对于刚开始的开发者来说,有一个全新的世界可以探索。即使是经验丰富的 AngularJS 开发者,开始学习 Angular 也可能感到不知所措。会有很多术语被抛向你:例如 TypeScript、Transpiler、Shim、Observable、Immutable、Modules、Exports、Decorators、Components、Web Component 和 Shadow DOM。放松!我们正在努力拥抱现代网络,这里的新事物都是为了使我们的生活更简单。许多这些概念并非特定于 Angular 本身,而是突出了网络平台开发的发展方向。我们将尽力以清晰简洁的方式呈现这些概念,帮助每个人理解这些部分如何融入这个庞大的生态系统。通过示例学习有其优点,例如,你将立即看到概念在实际中的应用。这本书遵循其前辈的相同模式。使用自己动手做DIY)的方法,我们使用 Angular 构建了多个简单和复杂的应用程序。

这本书面向谁

Angular 可以帮助你更快、更高效、更灵活地构建跨平台应用程序。Angular 目前处于第 6 个版本,其之前的版本中进行了重大变更。这是一本独特的网络开发书籍,将帮助你掌握 Angular 并探索开发单页应用程序的强大解决方案。

这本书涵盖了什么内容

第一章,入门,介绍了 Angular 框架。我们在 Angular 中创建了一个超级简单的应用程序,突出了框架的一些核心功能。

第二章,构建我们的第一个应用程序 – 7 分钟健身,教我们如何构建我们的第一个真正的 Angular 应用程序。在这个过程中,我们将了解更多关于 Angular 的主要构建块之一——组件。我们还将介绍 Angular 的模板结构、数据绑定能力和服务。

第三章,更多 Angular – SPA 和路由,介绍了框架中的路由结构,我们在其中为7 分钟健身构建了多个页面。本章还探讨了组件间通信的多种模式。

第四章,私人教练,介绍了一种新的锻炼,我们将7 分钟健身转变为通用的私人教练应用程序。这个应用程序具有创建除原始 7 分钟健身之外的新锻炼计划的能力。本章涵盖了 Angular 的表单功能以及我们如何使用它们来构建自定义锻炼。

第五章,支持服务器数据持久性,处理从服务器保存和检索锻炼数据。当我们探索 Angular 的 http 客户端库及其如何使用 RxJS Observables 时,我们增强了个人教练的持久性功能。

第六章,深入 Angular 指令,深入探讨了 Angular 指令和组件的内部工作原理。我们构建了一系列指令来支持个人教练。

第七章测试个人教练,向您介绍了 Angular 的测试世界。您构建了一系列单元测试和端到端测试,以验证个人教练的功能。

第八章,一些实用场景,提供了一些在开发此框架上的应用可能遇到的场景的实用技巧和指导。我们涵盖了诸如身份验证和授权、性能以及最重要的案例,即从 AngularJS 迁移到 Angular 最新版本的应用。

要充分利用本书

我们将使用 TypeScript 语言构建我们的应用程序;因此,如果您有一个使 TypeScript 开发变得容易的 IDE,那就更好了。Atom、Sublime、WebStorm 和 Visual Studio(或 VS Code)都是这个目的的出色工具。

下载示例代码文件

您可以从www.packtpub.com的账户下载本书的示例代码文件。如果您在其他地方购买了此书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。

您可以通过以下步骤下载代码文件:

  1. www.packtpub.com上登录或注册。

  2. 选择“支持”标签。

  3. 点击“代码下载与勘误”。

  4. 在搜索框中输入书籍名称,并遵循屏幕上的说明。

下载文件后,请确保您使用最新版本解压缩或提取文件夹,使用以下工具:

  • Windows 版的 WinRAR/7-Zip

  • Mac 版的 Zipeg/iZip/UnRarX

  • Linux 版的 7-Zip/PeaZip

本书代码包也托管在 GitHub 上,网址为github.com/chandermani/angular6byexample。如果代码有更新,它将在现有的 GitHub 仓库中更新。

我们还有其他来自我们丰富的图书和视频目录的代码包可供选择,网址为**github.com/PacktPublishing/**。查看它们吧!

使用的约定

本书使用了多种文本约定。

CodeInText: 表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“我们可以看到路由器如何将app.routes.ts中的路由与workout-builder.routes.ts中的默认路由组合在一起”。

代码块如下设置:

"styles": [
   "node_modules/bootstrap/dist/css/bootstrap.min.css",
   "src/styles.css"
],

当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:

const routes: Routes = [
    ...
    { path: 'builder', loadChildren: './workout-builder/workout-builder.module#WorkoutBuilderModule'},
    { path: '**', redirectTo: '/start' }
];

任何命令行输入或输出都应如下编写:

ng new guessthenumber --inlineTemplate

粗体:表示新术语、重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词在文本中如下所示。以下是一个示例:“在Sources标签页中打开Developer工具”。

警告或重要注意事项如下所示。

小技巧和技巧如下所示。

联系我们

我们始终欢迎读者的反馈。

一般反馈:请发送电子邮件至feedback@packtpub.com,并在邮件主题中提及书籍标题。如果您对本书的任何方面有疑问,请发送电子邮件至questions@packtpub.com

勘误:尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们将不胜感激,如果您能向我们报告此错误。请访问www.packtpub.com/submit-erra…,选择您的书籍,点击“勘误提交表单”链接,并输入详细信息。

盗版:如果您在互联网上以任何形式发现我们作品的非法副本,如果您能提供位置地址或网站名称,我们将不胜感激。请通过copyright@packtpub.com与我们联系,并提供材料的链接。

如果您有兴趣成为作者:如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com

评论

请留下评论。一旦您阅读并使用了这本书,为何不在购买该书的网站上留下评论呢?潜在读者可以查看并使用您的客观意见来做出购买决定,Packt 公司可以了解您对我们产品的看法,我们的作者也可以看到他们对书籍的反馈。谢谢!

如需了解 Packt 的更多信息,请访问packtpub.com

第一章:入门

在 JavaScript 中开发应用程序始终是一个挑战。由于其可塑性和缺乏类型检查,构建一个相当大的 JavaScript 应用程序是困难的。此外,我们使用 JavaScript 进行各种过程,如**用户界面(UI)**操作、客户端-服务器交互以及业务处理/验证。结果,我们得到了难以维护和测试的意大利面代码。

类似于 jQuery 这样的库在处理各种浏览器怪癖和提供可以减少代码行数的结构方面做得很好。然而,这些库缺乏任何结构性的指导,这在我们代码库/增长时能帮助我们。

近年来,JavaScript 框架已经出现以管理这种复杂性。许多这些框架,包括 Angular 的早期版本,使用一种称为**模型-视图-控制器(MVC)**的设计模式来将应用程序的元素分割成更易于管理的部分。这些框架的成功以及它们在开发者社区中的流行确立了使用这种模式的价值。

Web 开发,然而,一直在不断演变,自从 2009 年 Angular 首次推出以来已经发生了很大变化。诸如 Web 组件、JavaScript 的新版本(ES2015)和 TypeScript 等技术都已经出现。综合来看,它们为我们提供了构建一个新、前瞻性框架的机会。而随着这个新框架的出现,也带来了一种新的设计模式——组件模式。

本章致力于理解组件模式,以及如何在构建一个简单的 Angular 应用时将其付诸实践。

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

  • Angular 基础知识:我们将简要介绍用于构建 Angular 应用的组件模式。

  • 构建我们的第一个 Angular 应用:我们将使用 Angular 构建一个小游戏——“猜数字!”

  • 一些 Angular 构造的介绍:我们将回顾 Angular 中使用的一些构造,例如插值、表达式和数据绑定语法。

  • 变更检测:我们将讨论在 Angular 应用中如何管理变更检测。

  • 工具和资源:最后,我们将提供一些在 Angular 开发和调试过程中会很有用的资源和工具。

Angular 基础知识

让我们从了解 Angular 如何实现组件模式开始。

组件模式

Angular 应用使用组件模式。你可能没有听说过这种模式,但它无处不在。它不仅用于软件开发,还用于制造、建筑和其他领域。简单来说,它涉及将较小的、离散的构建块组合成更大的成品。例如,电池是汽车的一个组件。

在软件开发中,组件是逻辑单元,可以组合成更大的应用程序。组件通常具有内部逻辑和属性,这些逻辑和属性被屏蔽或隐藏在更大的应用程序中。然后,更大的应用程序通过称为接口的特定网关消耗这些构建块,这些接口仅公开使用组件所需的内容。这样,只要接口没有改变,就可以修改组件的内部逻辑,而不会影响更大的应用程序。

回到我们的电池例子,汽车通过一系列连接器消耗电池。然而,如果电池耗尽,只要新电池有相同的连接器,就可以完全更换。这意味着汽车制造商不必担心电池的内部结构,这简化了汽车制造的过程。更重要的是,车主不必每次电池耗尽时都更换他们的汽车。

为了扩展这个类比,电池制造商可以将它们推广到各种不同的车辆上,例如 ATV、船只或雪地摩托车。因此,组件模式使它们能够实现更大的规模经济。

在 Web 应用程序中使用组件模式

随着 Web 应用程序变得越来越复杂,能够用更小、更离散的组件构建它们的必要性变得更加迫切。组件允许以防止应用程序变成一团乱麻代码的方式构建应用程序。相反,基于组件的设计允许我们独立于其他部分对应用程序的特定部分进行推理,然后我们可以通过约定的连接点将应用程序缝合成一个完整的成品。

此外,维护成本较低,因为每个组件的内部逻辑可以单独管理,而不会影响应用程序的其他部分。并且使用自描述组件组装应用程序使得在更高层次上理解应用程序变得更加容易。

为什么以前在 Angular 中没有使用组件?

如果这个想法如此合理,为什么组件模式没有被早期版本的 Angular 采用?答案是,Angular 最初发布时存在的技术并没有完全支持在 Web 应用程序中实现这种模式。

然而,Angular 的早期版本在实现更智能的 Web 应用程序设计和组织方向上迈出了实质性的步伐。例如,它们实现了 MVC 模式,将应用程序分为模型、视图和控制(你将在我们将在 Angular 中构建的组件中看到 MVC 模式的持续使用)。

使用 MVC 模式,模型是数据,视图是一个网页(或移动应用屏幕,甚至 Flash 页面),控制器从模型中填充视图的数据。通过这种方式,实现了关注点的分离。遵循这个模式并智能地使用指令,可以使你非常接近组件。

因此,Angular 的早期版本允许以更逻辑的方式设计和构建应用程序。然而,这种方法受到所使用的技术并非真正隔离的限制。相反,它们最终都被渲染,没有任何真正的分离,与其他屏幕上的元素混合在一起。

什么新特性使得 Angular 能够使用组件模式?

相比之下,Angular 的最新版本拥抱了最近兴起的科技,这使得更全面地实现组件模式成为可能。这些技术包括 Web Components、ES2015(JavaScript 的新版本)和 TypeScript。让我们讨论一下这些技术各自为这个可能带来的贡献。

Web Components

Web Components 是一个总称,实际上涵盖了四个针对网络浏览器的正在兴起的标准化:

  • 自定义元素

  • 阴影 DOM

  • 模板

  • HTML 导入

更多关于 Web Components 的信息可以在www.webcomponents.org/introduction找到

现在我们将详细讨论这些内容:

  • 自定义元素允许创建除标准 HTML 标签(如<div><p>)之外的新类型的 DOM 元素。你将在本书的各个部分看到这些自定义元素的使用。例如,我们在本章构建的应用程序将有一个名为<app-root>的根元素,但你也可以给它任何你喜欢的名字。单个组件也将使用自定义元素。例如,在接下来的章节中,我们将构建一个更复杂的应用程序,将屏幕分解成组件。页面的头部将使用自定义元素<abe-header>来显示其内容(前缀abe仅属于我们的应用程序,有助于避免与原生 HTML 元素或其他应用程序中的自定义元素发生命名冲突)。添加自定义标签的能力提供了一个屏幕上的位置,可以用于绑定组件。简而言之,这是将组件从页面其余部分分离出来并使其真正自包含的第一步。

  • 阴影 DOM在页面上提供了一个隐藏区域,用于脚本、CSS 和 HTML。这个隐藏区域内的标记和样式不会影响页面的其余部分,同样重要的是,它们也不会受到页面其他部分标记和样式的影响。我们的组件可以使用这个隐藏区域来渲染其显示。因此,这是使我们的组件自包含的第二步。

  • 模板是 HTML 片段,最初不会在网页中渲染,但可以在运行时通过 JavaScript 激活。许多 JavaScript 框架已经支持某种形式的模板。Web Components 标准化了这种模板,并在浏览器中直接提供支持。模板可以用来使我们的组件使用的 Shadow DOM 中的 HTML 和 CSS 动态化。因此,这是制作我们组件的第三步。

  • 构成 Web Components 的最后一个标准是HTML 导入。它们提供了一种在单个包中加载资源(如 HTML、CSS 和 JavaScript)的方法。Angular 不使用 HTML 导入。相反,它依赖于 JavaScript 模块加载,我们将在本章稍后讨论这一点。

Angular 和 Web Components

Web Components 在当前的网络浏览器中并未得到完全支持。因此,Angular 组件并不是严格意义上的 Web Components。可能更准确的说法是,Angular 组件实现了 Web Components 背后的设计原则。它们还使得构建能在当今浏览器中运行的组件成为可能。

在撰写本文时,Angular 支持 Chrome、Firefox、Safari 和 Edge 等 evergreen 浏览器,以及 IE 9 及以上版本。它还支持 Android 和 iOS。要查看 Angular 支持的浏览器列表,请访问angular.io/guide/browser-support.

因此,在本书的剩余部分,我们将专注于构建 Angular 组件而不是 Web Components。尽管有这种区别,Angular 组件与 Web Components 非常接近,甚至可以与它们交互。随着浏览器开始更全面地支持 Web Components,Angular 组件与 Web Components 之间的差异将开始消失。所以,如果你想要开始采用未来的 Web Component 标准,Angular 为你提供了这样的机会。

Angular 中的语言支持

你可以使用 ES5(所有当前浏览器支持的 JavaScript 版本)开发组件,但 Angular 通过添加对最新语言(如 ES2015 和 TypeScript)中找到的关键特性的支持,增强了开发组件的能力。

ES2015

ES2015 是 JavaScript 的新版本;它在 2015 年 6 月获得批准。它为语言添加了许多改进,我们将在本书的整个过程中看到这些改进,但在此阶段最吸引我们注意的两个是以下内容:

  • 模块加载

在 JavaScript 中之前并不存在。现在它们存在后,使用它们的关键优势是它们提供了一个简单、清晰的语法,我们可以用它来创建方便的容器来存放组件中的代码。正如你将在本书中的应用程序开发中找到的那样。类还为我们的组件提供了一个方便的简写名称,这使得通过诸如依赖注入之类的手段将它们拼接在一起变得更加容易。

为了明确,JavaScript 类并没有引入完全新的东西。Mozilla 开发者网络MDN)将它们描述为在 JavaScript 现有的基于原型的继承之上的主要语法糖。更多信息请访问developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes

我们将在本书的示例中探索类。如果你没有使用过面向对象的语言,你可能对类不太熟悉,因此我们将随着本章的示例逐步介绍它们。

ES2015 还引入了一种新的模块加载方法。模块提供了一种封装 JavaScript 文件的方式。当它们被封装时,它们不会污染全局命名空间,并且可以以受控的方式与其他模块交互。

一旦我们定义了我们的模块,我们需要一种方法将它们加载到我们的应用程序中以执行。模块加载允许我们从构成 Angular 和我们创建或使用的其他组件的模块中选择我们需要的部分。

目前,存在一系列方法和库来支持 JavaScript 中的模块加载。ES2015 为加载模块添加了一种新的、一致的语法,作为语言的一部分。这种语法简单明了,涉及在模块前加上 export 关键字(或使用默认导出),然后在应用程序的其他地方使用 import 来消费它们。

ES 2015 模块加载使我们能够将组件组合成有用的包或功能,这些包或功能可以在我们的应用程序中导入或导出。实际上,模块是 Angular 本身的核心。我们将看到模块在 Angular 本身以及本书中构建的应用程序中都被广泛使用。

重要的是要理解,虽然 Angular 使用与 ES2015 模块加载语法相似的语法,但 Angular 模块(我们将在本章稍后讨论)并不等同于 JavaScript 模块。有关这些差异的更多详细信息,请参阅 Angular 文档angular.io/guide/architecture#ngmodules-vs-javascript-modules

由于 ES2015 并非今天所有浏览器都完全支持,因此我们需要将 ES2015 转换为 ES5,以便在我们的应用程序中使用类和模块加载等功能。我们通过一个称为转译的过程来完成这项工作。

转译类似于编译,但与编译将我们的代码转换为机器语言不同,转译将一种源代码转换为另一种源代码。在这种情况下,它将 ES2015 转换为 ES5。有几个称为转译器的工具使我们能够做到这一点。常见的转译器包括 Traceur 和 Babel。TypeScript(我们将在下一节讨论)也是一个转译器,我们将使用它来本书中的示例。

一旦 ES2015 被转换为 ES5,我们就可以使用像SystemJS这样的模块加载器来加载我们的模块。SystemJS遵循 ES2015 的模块加载语法,并使我们能够在今天的浏览器中执行模块加载。或者,我们可以使用像webpack这样的模块打包器来加载和组合我们的模块。对于本书中的项目,我们将使用webpack来加载、打包和部署我们的应用程序中的模块。

自从 ES2015 发布以来,ECMAScript(JavaScript 的官方名称)新版本的发布周期已经变为每年一次——因此我们现在有了 ES2016 和 ES2017,很快我们还将有 ES2018。由于本书中强调的功能最初是在 ES2015 中引入的,因此我们将引用 ES2015 而不是任何更新的版本。然而,更新的版本与本书中强调的语言功能完全兼容。

TypeScript

TypeScript 是由微软创建的,它是 JavaScript 的超集,这意味着它包含了 ES2015 的功能(如类和模块加载)并添加了以下内容:

  • 类型

  • 装饰器

类型允许我们在类中标记变量、属性和参数,以指示它们是数字、字符串、布尔值或各种结构,如数组和对象。这使得我们能够在设计时执行类型检查,以确保在我们的应用程序中使用正确的类型。

装饰器是我们可以使用@符号和函数添加到我们的类中的简单注释。它们为我们的类的使用提供了指令(称为元数据)。在 Angular 的情况下,装饰器允许我们将我们的类标识为 Angular 组件。装饰器还使我们能够指定一个自定义元素来绑定我们的组件,并识别一个模板,该模板为我们的组件添加 HTML 视图。随着我们通过本书的学习,我们将详细介绍装饰器的使用。

装饰器不是 ES2015 的一部分,但它是将它们包含在 JavaScript 语言未来版本中的一个提案的一部分。它们作为微软和谷歌合作的一部分被添加到 TypeScript 中。如前所述,TypeScript 编译成 ES5,因此我们能够在不完全支持 ES2015 或装饰器提议标准的浏览器中使用类型和装饰器。

如前所述,使用 ES2015 或 TypeScript 与 Angular 一起使用并非必需。然而,我们认为,随着我们通过本书中的示例进行,你会看到使用它们的优点。

将所有内容整合在一起

通过遵循 Web 组件标准并添加对 ES2015 和 TypeScript 的支持,Angular 使我们能够创建实现组件设计模式的 Web 应用程序。这些组件有助于实现通过自描述和自包含的构建块构建大型应用程序的愿景。

我们希望你在本书的示例中看到,Angular 使组件能够以简单和声明性的方式构建,这使得开发者更容易实现它们。随着我们通过本书中的示例进行,我们将突出显示这些技术被使用的位置。

Angular 模块

组件是 Angular 应用程序的基本构建块。但我们是如何将这些构建块组织成完整的应用程序的呢?Angular 模块为这个问题提供了答案。它们使我们能够将我们的组件组合成可重用的功能组,这些组可以在整个应用程序中导出和导入。例如,在一个更复杂的应用程序中,我们可能希望有用于认证、通用工具和外部服务调用的模块。同时,模块使我们能够以允许按需加载的方式对应用程序中的功能进行分组。这被称为懒加载,我们将在第四章“构建个人教练”中介绍这个主题。

每个 Angular 应用程序将有一个或多个包含其组件的模块。Angular 引入了NgModule作为方便指定构成模块的组件的方式。每个 Angular 应用程序都必须至少有一个这样的模块——根模块。

Angular 本身是作为模块构建的,我们将它们导入到我们的应用程序中。所以,当你构建 Angular 应用程序时,你将看到模块的广泛应用。

构建 Angular 应用程序的基本步骤

总结一下:在基本层面上,你会发现,要在 Angular 中开发应用程序,你需要做以下事情:

  1. 创建组件

  2. 将它们打包成模块

  3. 引导你的应用程序

通过看到 Angular 和组件设计模式在实际中的应用来理解 Angular 和组件设计模式是最佳方式。因此,我们将使用 Angular 构建我们的第一个 Hello World 应用程序。这个应用程序将帮助你熟悉 Angular 框架,并看到组件设计模式在实际中的应用。

让我们开始做吧。

传统的 Hello Angular 应用程序 - 猜数字!

作为我们的第一个练习,我们希望保持简单,但仍然展示框架的能力。因此,我们将构建一个非常简单的游戏,名为猜数字!。游戏的目标是用尽可能少的尝试次数猜出一个随机生成的计算机数字。

这就是这个游戏的外观:

图片

让我们现在构建“猜数字”!

构建“猜数字”!

在构建用户界面时,通常的做法是从上到下构建。首先设计 UI,然后根据需要插入数据和行为。采用这种方法,应用程序的 UI、数据和行为方面都紧密耦合,这并不是一个理想的情况!

基于组件的设计,我们的工作方式有所不同。我们首先查看 UI 和预期的行为,然后将其封装到一个我们称之为组件的构建块中。然后,这个组件被托管在我们的页面上。在组件内部,我们将 UI 分为视图,将行为分为类,并包含支持行为的适当属性和方法。如果您不熟悉类,请不要担心。随着我们通过示例的进展,我们将详细讨论它们是什么。

好的,那么让我们确定我们应用程序需要的 UI 和行为。

设计我们的第一个组件

为了确定我们的组件需要包含什么,我们将首先详细说明我们希望应用程序支持的功能:

  • 生成随机数字(original

  • 为用户提供猜测值的输入(guess

  • 跟踪已经做出的猜测数量(noOfTries

  • 根据用户的输入(偏差)提供提示以改善他们的猜测

  • 如果用户猜对了数字,显示成功消息(偏差

现在我们有了我们的功能,我们可以确定我们需要向用户显示什么,以及我们需要跟踪哪些数据。对于前面的功能集,括号中的元素表示将支持这些功能的属性,并将需要包含在我们的组件中。

设计组件是一个非常关键的过程。如果做得正确,我们可以逻辑地组织我们的应用程序,使其易于理解和维护。

在构建任何应用程序时,我们强烈建议您首先考虑您想要提供的功能,然后是支持该功能的数据和行为。最后,考虑如何为它构建用户界面。这是一个无论您使用什么库或框架来构建应用程序都是良好的实践。

开发我们的第一个组件

现在我们已经为我们的第一个组件设计了方案,我们将开始使用Angular 命令行界面Angular CLI)来开发它。Angular CLI 使我们能够通过一系列控制台命令开始构建 Angular 应用程序并将它们部署出去。我们将在未来的章节中更详细地介绍Angular CLI。目前,我们将安装它并使用它来生成一个基本的应用程序,作为我们第一个组件的起点。

要使用 Angular CLI,您必须首先安装 Node.jsnpmNode 的包管理器)。Node 可跨平台使用,您可以从 nodejs.org 下载它。安装 Node 也会安装 npm。对于本书,我们使用 Node.js 版本 8.9.4 和 npm 版本 5.6.0。您可以在 docs.npmjs.com/getting-started/installing-node 找到有关安装 Node 和将 npm 更新到最新版本的更多信息。

一旦安装了 Nodenpm,打开命令提示符并输入以下内容:

npm install -g @angular/cli

这将安装我们将用于启动应用程序的 Angular CLI。现在,从您的本地机器上的一个目录中,输入以下命令:

ng new guessthenumber --inlineTemplate
cd guessthenumber
ng serve

第一个命令将使用 Angular CLI 在您的本地机器上创建一个新的 Angular 项目(--inlineTemplate 标志在组件内部创建模板,这对于我们本章要展示的内容非常合适)。第二个命令将您导航到 Angular CLI 为您的新项目创建的目录。第三个命令启动应用程序,您可以在 http://localhost:4200/ 上查看。如果您这样做,您应该在浏览器中看到一个标准的默认 Angular 页面。

安装 Bootstrap

在我们构建应用程序的具体细节之前,还有一步。让我们添加 Bootstrap 库来增强应用程序的外观和感觉。首先,通过在启动应用程序的终端中输入 Ctrl + C 来停止应用程序,并在被询问是否要终止批处理作业时输入 Y。然后从 guessthenumber 目录,输入以下命令:

npm install bootstrap --save

这将安装 Bootstrap 的最新版本(在撰写本文时为版本 4.0.0)。您可能会看到一些关于未满足依赖项的警告消息。您可以忽略它们。

接下来配置您的项目以包含 Bootstrap 样式表:

  1. guessthenumber 目录中找到并打开文件 angular.json

  2. 在该文件中找到 projects 属性,它包含我们新项目的设置

  3. 然后找到 architect.build.options 中的 styles 属性,您会看到它包含一个数组,该数组包含 styles.css,这是新项目的默认样式表。

  4. bootstrap.min.css 样式表的路径添加到该数组中:

"styles": [
   "node_modules/bootstrap/dist/css/bootstrap.min.css",
   "src/styles.css"
],

使用 Angular CLI 包含 Bootstrap 的说明可以在 github.com/angular/angular-cli/wiki/stories-include-bootstrap 找到。

我们目前有什么?

如果你查看guessthenumber目录,你会看到Angular CLI已经创建,你会看到大量的文件。一开始这可能会让你感到不知所措,但重要的是要理解Angular CLI只通过几个命令行语句就为我们生成了所有这些文件。这样,它使得开始使用 Angular 应用程序变得更加顺畅和容易。它从过程中移除了繁琐的工作,使我们能够以最小的努力构建和提供我们的应用程序。在本章中,我们将专注于我们为了创建应用程序需要接触的几个文件。

如果你正在使用 Internet Explorer 运行应用程序,你需要查看一个文件——polyfill.ts。这个文件添加了运行应用程序在 Internet Explorer 中所需的其他文件。你需要取消注释该文件中的几个部分以添加这些必要的文件。有关如何操作的说明包含在该文件本身中。

在转向构建我们应用程序的具体细节之前,让我们先看看一个关键的文件,这个文件将用于使我们的应用程序启动和运行。

下载示例代码

本书中的代码可在 GitHub 上找到,网址为github.com/chandermani/angular6byexample。它组织成检查点,允许你跟随我们的步骤逐步构建本书中的示例项目。本章要下载的分支是 GitHub 的分支:checkpoint1.1。在guessthenumber文件夹中查找我们在这里覆盖的代码。如果你不使用 Git,请从以下 GitHub 位置下载 Checkpoint 1.1 的快照(ZIP 文件):github.com/chandermani/angular6byexample/tree/checkpoint1.1。在设置快照时,请参考guessthenumber文件夹中的readme.md文件。

主文件 - index.html

导航到guessthenumber目录中的src文件夹并打开index.html。你会看到以下内容:

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Guessthenumber</title>
  <base href="/">

  <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>

index.html是我们应用程序的主文件。当应用程序首次运行时,浏览器将启动它,并托管我们的应用程序组件。如果你有任何接触过 Web 开发,这个文件中的大部分 HTML 代码应该看起来很熟悉。它有标准的htmlheadbody标签,以及几个可选标签,其中一个是一个 viewport 的 meta 标签,它配置了应用程序在移动设备上的显示方式,另一个是链接到 Angular favicon 图像的链接,该图像将在加载应用程序的浏览器标签中显示。

自定义元素

然而,页面上还有一个可能对你来说不那么熟悉的标签:

<app-root></app-root> 

这个标签是一个自定义元素。它指示 Angular 将我们构建的组件注入到何处。

猜数字游戏! 以及本书中的所有其他应用程序都已针对 Angular 6 最终版本进行了测试。

组件文件

现在,让我们转向构建我们应用程序的具体细节。鉴于之前对组件模式的讨论,你不会对这样做需要构建一个组件感到惊讶。在这种情况下,我们的应用程序足够简单,我们只需要一个组件(在本书的后面部分,你将看到在构建更复杂的应用程序时使用多个组件)。Angular CLI 已经为我们生成了一个组件文件。当然,该文件不包含我们应用程序的任何特定细节,因此我们需要对其进行修改。为此,导航到 app 目录下的 src 文件夹并打开 app.component.ts

导入语句

在页面顶部,你会找到以下行:

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

这是一个导入语句。它告诉我们将在组件中加载和使用哪些模块。在这种情况下,我们正在选择从 Angular 加载的一个模块:Component。Angular 有许多其他模块,但我们只加载所需的模块。

你会注意到,我们导入的位置并没有被标识为应用程序中的路径或目录。相反,它被标识为 @angular/core。Angular 已经被划分为以 @angular 为前缀的桶模块。

这些桶组合了逻辑上相关的几个模块。在这种情况下,我们表示我们想要导入 core 桶模块,它反过来又引入了 Component 模块。

Angular 文档将桶描述为**:**一种将多个 ES2015 模块的导出汇总到一个方便的 ES2015 模块中的方法。桶本身是一个 ES2015 模块文件,它重新导出其他 ES2015 模块的选定导出。

更多关于桶(barrel)的信息,请参阅 angular.io/guide/glossary#barrel

装饰器

接下来,将开始于 @Component 的代码块替换为以下内容:

@Component({
 selector: 'app-root',
 template: `
  <div class="container">
      <h2>Guess the Number !</h2>
        <div class="card bg-light mb-3">
           <div class="card-body">
              <p class="card-text">Guess the computer generated random number between 1 
                                                                          and 1000.</p>
           </div>
        </div>
       <div>
         <label>Your Guess: </label>
         <input type="number" [value]="guess" (input)="guess = $event.target.value" />
         <button (click)="verifyGuess()" class="btn btn-primary btn-sm">Verify</button>
         <button (click)="initializeGame()" class="btn btn-warning btn-
                                                               sm">Restart</button>
       </div>
      <div>
         <p *ngIf="deviation<0" class="alert alert-warning">Your guess is higher.</p>
         <p *ngIf="deviation>0" class="alert alert-warning">Your guess is lower.</p>
         <p *ngIf="deviation===0" class="alert alert-success">Yes! That's it.</p>
      </div>
      <p class="text-info">No of guesses :
        <span class="badge">{{noOfTries}}</span>
      </p>
  </div> 
  `
})

这是我们的组件装饰器,它直接放置在类定义之上,我们将在稍后讨论。@ 符号用于标识装饰器。@Component 装饰器有一个名为 selector 的属性,你可能会惊讶地看到它被设置为我们的 HTML 页面中的 <app-root> 标签。这个设置告诉 Angular 将此组件注入到 HTML 页面中的该标签。

装饰器还有一个名为 template 的属性,这个属性用于标识我们组件的 HTML 标记。注意这里使用了反引号(由 ES2015 引入)来在多行中渲染模板字符串。或者,我们也可以设置一个 templateUrl 属性,它将指向一个单独的文件。

定义类

现在,将开始于 export class AppComponent 的代码块替换为以下内容:

export class AppComponent {
  deviation: number;
  noOfTries: number;
  original: number;
  guess: number;

  constructor() {
      this.initializeGame();
  }
  initializeGame() {
      this.noOfTries = 0;
      this.original = Math.floor((Math.random() * 1000) + 1);
      this.guess = null;
      this.deviation = null;
  }
  verifyGuess() {
      this.deviation = this.original - this.guess;
      this.noOfTries = this.noOfTries + 1;
  }
}

如果你一直在使用 ES5 开发,这是所有当前浏览器都支持的 JavaScript 版本,你可能不熟悉这里类的使用。因此,我们将花几分钟时间来解释一下类由什么组成(对于那些使用面向对象编程语言(如 C# 或 Java)进行开发的你们来说,这应该是一个熟悉的地方)。

类文件包含我们将用于运行组件的代码。在顶部,我们给类一个名字,它是 AppComponent。然后,在括号内,我们有四行声明了我们类的属性。这些类似于 ES5 变量,我们将使用它们来保存我们需要运行应用程序的值(你会注意到这些是我们设计组件时确定的四个值)。

这些属性与标准 JavaScript 变量不同的地方在于,每个属性名称后面跟着 : 和一个数字。这些设置了属性的类型。在这种情况下,我们表明这四个属性将被设置为数字类型,这意味着我们期望所有这些属性的值都是数字。为我们的属性指定类型的能力是由 TypeScript 提供的,而在标准 JavaScript 中不可用。

随着我们向下移动,我们将看到三个带有名称的脚本块,后面跟着括号,然后是包含多行脚本的括号。这些是我们类的函数,它们包含我们的组件将支持的运算。它们与标准的 JavaScript 函数非常相似。

这些方法中的第一个是 constructor(),这是一个特殊的方法,当我们的组件实例首次创建时将会运行。在我们的例子中,当类被创建时,构造函数只做了一件事;它调用我们类中的另一个方法,称为 initializeGame()

initializeGame() 方法使用赋值运算符 = 设置类中四个属性的起始值。我们将这些值设置为 nullzero,除了 original,我们使用随机数生成器来创建要猜测的数字。

该类还包含一个名为 verifyGuess() 的方法,该方法更新 deviationnoOfTries 属性。这个方法不是在组件类内部被调用的;相反,它将从视图中被调用,正如我们稍后更仔细地检查视图时将会看到的。你也会注意到,我们的方法通过在它们前面添加 this 来引用同一类中的属性。

模块文件

如我们之前提到的,每个 Angular 组件都必须包含在一个 Angular 模块中。这意味着我们至少必须在应用程序的根目录中添加至少一个 Angular 模块文件。我们称这个为根模块。对于像“猜数字!”这样的简单应用程序,根模块可能就是我们所需要的唯一模块。然而,随着 Angular 应用程序规模的增加,通常有多个按功能划分的 Angular 模块文件是有意义的。随着我们在本书后面的章节中构建更复杂的应用程序,我们将讨论这种情况。

让我们继续查看我们的 Angular 模块文件。同样,Angular CLI 已经为我们创建了此文件。在src文件夹中的app目录下打开app.module.ts,你会看到以下内容:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

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

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

前两个语句导入了BrowserModuleNgModule。注意,虽然NgModule是从@angular/core导入的,但BrowserModule是从一个不同的模块导入的:@angular/platform-browser。这里重要的是,导入不是来自@angular/core,而是来自一个专门针对基于浏览器的应用程序的独立模块。这是一个提醒,Angular 可以支持除浏览器以外的设备,因此需要将BrowserModule放入一个单独的模块中。

此文件中的另一个导入是我们自己的组件AppComponent。如果你回到那个组件,你会注意到在类定义前添加了export,这意味着我们正在使用我们自己的应用程序中的模块加载。

接下来,我们定义一个新的组件AppModule。在类本身中除了几个导入和一个装饰器@ngModule之外,没有其他内容。我们可以使用这个装饰器来配置我们的应用程序模块。第一个属性是声明,通过这个属性我们提供了一个数组,其中包含了将在我们的应用程序中使用的组件。在这种情况下,我们只有一个组件:AppComponent

接下来,我们添加了导入,其中包含BrowserModule。正如其名所示,这个模块将提供在浏览器中运行我们的应用程序所需的功能。下一个属性是providers。这个属性用于注册提供者(如服务和其他对象),这些提供者将通过依赖注入在整个应用程序中可用。在我们构建的简单应用程序中,我们没有必要使用提供者,因此这个属性是空的。我们将在第三章“更多 Angular – SPA,路由”中详细讨论提供者和依赖注入。

最后,我们设置了bootstrap属性。这表示当我们的应用程序启动时将首先加载的第一个组件。再次强调,这是AppComponent

在此配置就绪后,我们现在可以引导我们的组件了。

引导启动

AppComponent的类定义作为组件的蓝图,但其中的脚本在创建组件实例之前不会运行。为了运行我们的应用,我们需要在应用中添加一些代码来启动组件。完成这个过程需要我们添加启动组件的代码。

src文件夹中,寻找一个名为main.ts的文件。打开它,您将看到以下代码:

import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { AppModule } from './app/app.module';
import { environment } from './environments/environment';

if (environment.production) {
  enableProdMode();
}

platformBrowserDynamic().bootstrapModule(AppModule)
  .catch(err => console.log(err));

如您所见,我们正在从@angular/core导入enableProdMode,以及从@angular/platform-browser-dynamic导入platformBrowserDynamic模块。就像在appModule文件中导入BrowseModule一样,这个后者的导入专门用于基于浏览器的应用。接下来,我们添加了对我们的AppModule和一个位于应用environments目录中的名为environment的文件的导入。

在接下来的代码行中,我们检查environment文件中的常量environment是否将其production属性设置为true,如果是,则调用enableProdMode(),正如其名称所暗示的,这将启用生产模式。environment.production的默认设置是false,这对于我们的目的来说很合适,因为我们不是在生产模式下运行应用。

如果您打开environments.ts,您将看到一些注释,这些注释提供了在构建过程中覆盖此文件设置的指导。我们不会在第二章“构建我们的第一个应用 – 7 分钟健身”中涵盖 Angular 的构建过程;因此,我们在这里不会涉及该材料。

最后,我们使用platformBrowserDynamic().boostrapModule方法并传入我们的AppModule作为参数。然后,bootstrapModule方法创建了一个新的AppModule组件实例,它反过来初始化我们的AppComponent,这是我们标记为启动组件的。它是通过调用我们组件的构造函数方法来做到这一点的。

我们已经启动并运行了!

好吧,应用已经完成,准备进行测试!再次从guessthenumber目录中输入以下命令:

    ng serve

应用应该会出现在您的浏览器上。

如果你在运行应用时遇到困难,可以在 GitHub 上查看可用的工作版本,链接为github.com/chandermani/angular6byexample。如果你不使用 Git,可以从以下 GitHub 位置下载 Checkpoint 1.1 的快照(ZIP 文件):github.com/chandermani/angular6byexample/tree/checkpoint1.1。在首次设置快照时,请参考guessthenumber文件夹中的readme.md文件。

如果我们浏览一下组件文件和模板,我们应该会对我们所取得的成就感到非常震撼。我们在应用运行时并没有编写任何代码来更新 UI。尽管如此,一切运行得都非常完美。

深入挖掘

要理解这个应用程序在 Angular 环境中的工作方式,我们需要更深入地了解我们的组件。虽然组件中的类定义非常简单直接,但我们需要更仔细地查看模板中的 HTML,以了解 Angular 在这里是如何工作的。它看起来像标准的 HTML,但有一些新的符号,如 [ ]( ){{}}

在 Angular 的世界里,这些符号的含义如下:

  • {{}} 是插值符号

  • [ ] 表示属性绑定

  • ( ) 表示事件绑定

显然,这些符号有一些附加的行为,并且似乎将视图 HTML 和组件代码连接起来。让我们尝试理解这些符号实际上做什么。

插值

请查看 app.component.ts 模板中的此 HTML 片段:

<p class="text-info">No of guesses :  
  <span class="badge">{{noOfTries}}</span> 
</p> 

术语 noOfTries 被夹在两个插值符号之间。插值通过将插值标记的内容替换为插值符号内表达式的值(noOfTries)来实现。在这种情况下,noOfTries 是一个组件属性的名称。因此,组件属性的值将显示为插值标签内的内容。

插值使用以下语法声明:{{expression}}。这个表达式看起来类似于 JavaScript 表达式,但总是在组件的上下文中进行评估。请注意,我们没有做任何事情来传递属性的值到视图中。相反,插值标签直接从组件中读取属性的值,而不需要任何额外的代码。

跟踪尝试次数的变化

插值的一个有趣方面是,对组件属性的更改会自动与视图同步。运行应用程序并做出一些猜测;每次猜测后,noOfTries 的值都会改变,视图内容也会相应改变:

在我们需要查看模型状态的场景中,插值是一个出色的调试工具。有了插值,我们不必在代码中设置断点,只需知道组件属性的值。由于插值可以接受表达式,我们可以传递组件的方法调用或属性并查看其值。

表达式

在继续之前,我们需要花几分钟时间来理解 Angular 中的模板表达式是什么。

Angular 中的模板表达式不过是用于在它们使用的模板上下文中评估的纯 JavaScript 代码片段。但正如angular.io/docs/ts/latest/guide/template-syntax.html#template-expressions文档所明确指出的,有一些差异:

  • 赋值是被禁止的

  • new 运算符是被禁止的

  • 按位运算符 |& 不受支持

  • 增量和减量运算符 ++-- 不受支持

  • 模板表达式运算符,如 |?.,添加了新的含义

在基于组件的设计讨论中,您可能不会对文档也明确了一些事情感到惊讶;模板表达式不能:

  • 引用全局命名空间中的任何内容

  • 引用窗口或文档

  • 调用 console.log

相反,这些表达式被限制在表达式上下文中,这通常是支持特定模板的组件实例。

然而,这些限制并不会阻止我们用表达式做一些巧妙的事情。正如我们可以在以下示例中看到,这些都是有效的表达式:

// outputs the value of a component property 
{{property}} 

// adds two values 
{{ 7 + 9 }} 

//outputs the result of boolean comparison. Ternary operator 
{{property1 >=0?'positive': 'negative'}} 

//call a component's testMethod and outputs the return value 
{{testMethod()}} 

在了解了表达式之后,我们强烈建议您保持表达式简单,这样可以使 HTML 更易于阅读。*ngIf="formHasErrors()" 表达式总是比 *ng-if="name==null || email==null || emailformatInValid(email) || age < 18" 更好。因此,当表达式开始变得复杂时,请将其移动到组件中的方法中。

安全导航操作符

在我们继续之前,还有一个其他表达式我们应该讨论一下:Angular 安全导航操作符 (?.)。这个操作符提供了一个方便的方式来检查长属性路径中的空值,如下所示:

{{customer?.firstName }} 

如果安全导航操作符发现空值(这里指客户),它会停止处理路径,但允许应用程序继续运行。如果没有它,应用程序在到达第一个空值(这里指客户名称)之后将会崩溃,视图将不会显示。安全导航操作符在您异步加载数据且数据可能无法立即提供给视图的情况下特别有用。安全导航操作符将防止应用程序崩溃,并在数据可用时加载数据。

数据绑定

学习插值和表达式很容易。现在让我们看看另一个框架结构,即我们示例应用中使用的数据绑定。我们将在接下来的章节中更详细地介绍数据绑定。在此阶段,我们只是简要地讨论我们正在构建的示例应用中使用的绑定。

属性绑定

如果我们查看 app.component.ts 中的模板,我们会看到几个使用方括号 [ ] 的地方。这些都是属性绑定

让我们看看我们创建的第一个绑定:

<input type="number" [value]="guess" (input)="guess = $event.target.value" />

这种绑定通过将组件类中 guess 属性的值与视图中的输入字段的 value 相链接来实现。绑定是动态的;因此,当 guess 属性的值发生变化时,输入字段的 value 也会同步到相同的值;我们不需要编写任何代码来做这件事。

在一开始,当我们初始化游戏时,这个属性在组件类的初始化方法中被设置为 null,所以我们不会在输入字段中看到任何内容。然而,随着游戏的进行,这个数字将随着猜测值的变化而更新。

事件绑定

再次查看 app.component.ts 中的模板,我们发现有几个地方出现了括号 ( )。这些是 事件绑定

让我们看看我们为这些事件绑定中的第一个创建的 HTML 代码行。它应该很熟悉,因为事件绑定是在我们最初查看属性绑定时相同的标签上:input 标签:

<input type="number" [value]="guess" (input)="guess = $event.target.value" />

在这种情况下,输入元素的 input 事件绑定到一个表达式上。该表达式将我们的组件类中的 guess 属性设置为 $event.target.value,这是用户输入的值。在幕后,当我们使用这种语法时,Angular 为我们绑定的事件设置了一个事件处理器。在这种情况下,每当用户在 input 字段中输入一个数字时,处理器就会更新我们的组件类中的 guess 属性。

在我们的代码中还有其他几个地方出现了 ( ) 括号:

<button (click)="verifyGuess()" class="btn btn-primary btn-sm">Verify</button>
<button (click)="initializeGame()" class="btn btn-warning    btn-sm">Restart</button>

这两个事件绑定将屏幕上按钮的 click 事件绑定到我们的组件中的方法。因此在这种情况下,在幕后,Angular 设置了直接绑定到我们的组件中方法的处理器。当点击验证按钮时,会调用 verifyGuess 方法,而当点击重启按钮时,会调用 initializeGame 方法。

在你通读这本书的示例时,你会看到很多将属性绑定中的 [] 标签与事件绑定中的 () 标签结合的地方。事实上,这种配对如此常见,以至于我们稍后将会看到,Angular 已经想出了一个简写语法来将这些标签合并为一个。

结构指令

接下来,我们将检查一些看起来类似于数据绑定但结合了之前我们没有见过的 Angular 功能的东西:结构指令

<div>
  <p *ngIf="deviation<0" class="alert alert-warning"> Your guess is higher.</p> 
  <p *ngIf="deviation>0" class="alert alert-warning"> Your guess is lower.</p>
  <p *ngIf="deviation===0" class="alert alert-success"> Yes! That's it.</p> 
</div> 

<p> 标签内的 *ngIfNgIf 结构指令。结构指令允许我们操作 DOM 元素的结构。NgIf 指令根据分配给它的表达式的结果来删除或添加 DOM 元素。

前面的 ngIf 中的星号 * 是 Angular 在底层扩展为 ng-template 的简化语法,而 ng-template 是 Angular 对我们之前讨论过的 Web Components 模板的实现。我们将在下一章中学习更多关于这种语法和结构指令的内容。

在这种情况下,我们使用 NgIf 与一个简单的表达式,类似于我们看到的插值表达式。该表达式根据猜测的值及其与正确数字的关系(更高、更低或相等)解析为 truefalse。然后它将该结果分配给 NgIf,这将根据结果为 true 时添加 DOM 元素,为 false 时删除它。

重新审视我们的应用

现在我们已经更仔细地研究了构成我们视图的内容,让我们再次看看我们的应用程序在运行时的样子。当我们运行应用程序时,Angular 绑定会在浏览器渲染我们视图中的原始 HTML 后启动。然后,框架编译这个视图模板,在这个过程中设置必要的绑定。接下来,它在我们组件类和视图模板之间进行必要的同步,以生成最终的渲染输出。以下截图展示了在为我们应用程序完成数据绑定后视图模板所发生的转换:

图片

我们可以通过从输入框下方的段落中移除三个*ngIf指令及其分配的表达式,并在浏览器中刷新应用程序,来查看应用程序未转换的视图模板(如前一个截图左侧所示)。

Angular 与其他模板框架不同,其组件与其视图之间的绑定是动态的。对组件属性的更改会更新视图。Angular 永远不会重新生成 HTML;它只是在 HTML 的相关部分上工作,并仅更新需要根据组件属性更改而更改的 HTML 元素。这种数据绑定能力使 Angular 成为一个卓越的视图模板引擎。

查看我们的代码如何处理更新

如果我们回顾一下我们类的代码,我们会看到类中的属性和方法并不直接引用视图。相反,方法只是对类中的属性进行操作。因此,我们类的代码更易于阅读,因此更易于维护(当然,可测试):

图片

到目前为止,我们已经讨论了 Angular 如何根据组件属性的更改来更新视图。这是一个重要的概念,因为它可以让我们避免无数小时的调试和挫败感。下一节将专门介绍变更检测以及这些更新是如何管理的。

维护状态

首先,让我们看看如何在我们的 Angular 应用程序中维护状态。由于 Angular 应用程序是动态的而不是静态的,我们需要了解确保这些动态值随着应用程序数据的更新而保持更新的机制。例如,在我们的应用程序中,屏幕上的猜测次数是如何更新的?应用程序是如何根据用户输入决定显示关于猜测是否正确的正确消息的?

组件作为状态的容器

由于我们之前一直在强调 Angular 使用组件设计模式,因此你可能不会对知道应用程序状态的基本容器是组件本身而感到惊讶。这意味着当我们有一个组件实例时,组件中的所有属性及其值都可用于组件中引用的模板实例。在实践层面,这意味着我们可以在模板中直接使用这些值,而不需要编写任何管道代码来连接它们。

例如,在示例应用中,为了确定要显示的消息,我们可以在模板表达式中直接使用deviation。Angular 会扫描我们的组件以找到具有该名称的属性并使用其值。对于noOfTries也是如此;Angular 将在我们的组件内查找该属性的值,然后将其用于在模板的插值中设置其值。我们不需要编写任何其他代码:

 <div>
    <p *ngIf="deviation<0" class="alert alert-warning"> Your guess is higher.</p>
    <p *ngIf="deviation>0" class="alert alert-warning"> Your guess is lower.</p>
    <p *ngIf="deviation===0" class="alert alert-success"&gt; Yes! That's it.</p></div> 
    <p class="text-info">No of guesses : 
      <span class="badge">{{noOfTries}}</span> 
    </p> 
</div>

变更检测

那么,Angular 是如何在运行时跟踪我们组件中的变更的呢?到目前为止,这似乎都是通过魔法完成的。我们只是设置了组件属性和方法,然后通过插值以及属性和事件绑定将它们绑定到视图上。Angular 完成其余的工作!

但这当然不是魔法发生的,为了有效地使用 Angular,你需要了解它是如何随着这些值的变化而更新它们的。这被称为变更检测,Angular 在执行这一操作时与之前的方法非常不同。

如果你使用浏览器中的调试工具遍历应用程序,你会看到变更检测是如何工作的。在这里,我们使用 Chrome 的开发者工具并监视noOfTries属性。如果你在verifyGuess()方法的末尾设置断点,你会看到当你输入一个猜测时,noOfTries属性会立即在到达断点时更新,如下所示:

图片 2

一旦你越过断点,屏幕上的显示将更新为正确的猜测次数,如下面的截图所示:

图片 1

在引擎盖下真正发生的事情是,Angular 正在响应应用程序中的事件,并使用变更检测器,这些检测器遍历每个组件,以确定是否有任何影响视图的变更发生。在这种情况下,事件是一个按钮点击。按钮点击生成的事件在组件上调用verifyGuess()方法,以更新noOfTries属性。

该事件触发了变更检测周期,该周期识别出在视图中使用的noOfTries属性已更改。因此,Angular 使用该属性的新值更新绑定到noOfTries的视图中的元素。

如您所见,这是一个多步骤的过程,其中 Angular 首先根据事件更新组件和域对象,然后运行变更检测,最后重新渲染已更改的视图元素。而且,它会在每个浏览器事件(以及其他异步事件,如 XHR 请求和定时器)上执行此操作。Angular 的变更检测是响应式的且单向的。

这种方法允许 Angular 只通过一次遍历变更检测图。这被称为 单向数据绑定,并且极大地提高了 Angular 的性能。

我们将在第八章“一些实用场景”中深入探讨 Angular 的变更检测。要了解 Angular 团队对这一过程的描述,请访问 vsavkin.com/two-phases-of-angular-2-applications-fda2517604be#.fabhc0ynb

工具

工具使我们的生活变得简单,我们将分享一些工具,这些工具将帮助你在 Angular 开发的不同方面,从代码编写到调试:

  • Visual Studio Code: 这是 Microsoft 开发的新 IDE (code.visualstudio.com/)。它为 Angular 和 TypeScript 提供了出色的 IntelliSense 和代码补全支持。Visual Studio 2017 (www.visualstudio.com/) 也包括对 Angular 和 TypeScript 的支持。

  • IDE 扩展: 市面上许多流行的 IDE 都有插件/扩展,使 Angular 开发变得容易。例如:

  • Angular 语言服务:这是一款令人兴奋的新开发者工具,它为放置在组件装饰器或外部 HTML 文件中的 Angular 模板提供自动补全、错误检查和 F12 导航。您可以在 angular.io/guide/language-service 找到安装说明和有关该服务的更多信息。

  • 浏览器开发者控制台:所有当前浏览器在 JavaScript 调试方面都有出色的功能。由于我们使用 JavaScript,我们可以设置断点、添加监视,并执行所有其他使用 JavaScript 可能执行的操作。请记住,许多代码错误仅通过查看浏览器的控制台窗口就可以检测到。

  • Augury (augury.angular.io/):这是一个用于调试 Angular 应用程序的 Chrome Dev Tools 扩展。

  • 组件供应商也开始提供对 Angular 的支持。例如,Telerik 发布了 Kendo UI for Angular:www.telerik.com/kendo-angular-ui/.

资源

Angular 是一个新的框架,但已经有一个充满活力的社区开始围绕它形成。与这本书一起,还有博客、文章、支持论坛以及大量的帮助。以下是一些有用的突出资源:

就这样!这一章已经完成,现在是时候总结您所学的内容了。

摘要

旅程已经开始,我们已经达到了第一个里程碑。尽管这一章名为“入门”,但我们已经涵盖了您为了理解整体概念所需了解的大量概念。您的学习来自于我们构建并剖析整章的“猜数字”应用程序。

您学习了 Angular 如何使用 Web Components 的兴起标准和 JavaScript 和 TypeScript 的最新版本来实现组件设计模式。我们还回顾了 Angular 中使用的一些结构,例如插值、表达式和数据绑定语法。最后,我们探讨了变更检测以及一些有助于您开始 Angular 开发的实用工具和资源。

基础工作已经完成,现在我们准备在 Angular 框架上进行一些严肃的应用程序开发。在下一章中,我们将开始处理一个更复杂的练习,并接触许多新的 Angular 构造。

第二章:构建我们的第一个应用 – 7 分钟健身

我希望第一章足够吸引人,让你想要了解更多关于 Angular 的知识——相信我,我们只是触及了表面!这个框架有很多东西可以提供,与 TypeScript 一起,它致力于使前端开发更加有组织,因此更容易管理。

按照本书的主题,我们将使用 Angular 构建一个新的应用,在这个过程中,我们将更加熟悉这个框架。这个应用也将帮助我们探索 Angular 的一些新功能。

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

  • 7 分钟健身问题描述:我们详细说明了本章中构建的应用的功能。

  • 代码组织:对于我们的第一个真实应用,我们将尝试解释如何组织代码,特别是 Angular 代码。

  • 设计模型:我们应用的一个构建块是其模型。我们根据应用的需求设计应用模型。

  • 理解数据绑定基础设施:在构建 7 分钟健身 视图时,我们将查看框架的数据绑定功能,包括 属性属性样式事件 绑定。

  • 探索 Angular 平台指令:我们将涵盖的一些指令包括 ngForngIfngClassngStylengSwitch

  • 使用输入属性进行跨组件通信:当我们构建嵌套组件时,我们学习如何使用输入属性将数据从父组件传递到其子组件。

  • 使用事件进行跨组件通信:Angular 组件可以订阅和触发事件。我们了解了 Angular 中的事件绑定支持。

  • Angular 管道:Angular 管道提供了一种格式化视图内容的方式。我们探讨了几个标准的 Angular 管道,并构建了自己的管道以支持秒到 hh:mm:ss 的转换。

让我们开始吧!我们首先要做的是定义我们的 7 分钟健身 应用。

什么是 7 分钟健身?

我们希望所有阅读这本书的人都身体健康。因此,这本书应该具有双重目的;它不仅应该刺激你的大脑,还应该敦促你关注你的身体健康。还有什么比构建一个针对身体健康的应用更好的方式呢!

7 分钟健身 是一个锻炼/健身应用,要求我们在七分钟的时间范围内快速连续完成一组 12 个练习。由于其短小精悍的长度和巨大的益处,7 分钟健身 已经变得相当受欢迎。我们无法证实或反驳这些说法,但进行任何形式的剧烈体育活动都比什么都不做要好。如果你想了解更多关于这个健身的信息,那么请查看 well.blogs.nytimes.com/2013/05/09/the-scientific-7-minute-workout/

该应用的技术细节包括执行一组 12 个练习,每个练习分配 30 秒。在开始下一个练习之前,会有一个简短的休息期。对于我们要构建的应用,我们将休息 10 秒。因此,总时长略超过七分钟。

在本章结束时,我们将拥有一个准备好的7 分钟健身应用,其外观可能如下所示:

7 分钟健身应用

下载代码库

该应用的代码可以从专门为此书建立的 GitHub 网站(github.com/chandermani/angular6byexample)下载。由于我们是逐步构建应用,我们创建了多个检查点,这些检查点对应于GitHub 分支,如checkpoint2.1checkpoint2.2等。在叙述过程中,我们将突出显示这些分支以供参考。这些分支将包含到那个时间点为止在应用上所做的所有工作。

7 分钟健身的代码位于名为trainer的仓库文件夹中。

那么,让我们开始吧!

设置构建环境

记住,我们正在构建一个现代平台,而浏览器对此仍缺乏支持。因此,直接在 HTML 中引用脚本文件是不可能的(虽然常见,但这是一个过时的方法,我们本应避免)。浏览器不理解TypeScript;这意味着必须有一个将用 TypeScript 编写的代码转换为标准JavaScript (ES5)的过程。因此,为任何 Angular 应用设置构建环境变得至关重要。而且,由于 Angular 日益流行,我们从不缺乏选择。

如果你是一名从事 Web 栈前端开发的开发者,你无法避免Node.js。这是最广泛使用的 Web/JavaScript 开发平台。因此,无需猜测,大多数 Angular 构建解决方案都由 Node 支持。例如GruntGulpJSPMwebpack是任何构建系统的最常见构建模块。

由于我们也是在 Node.js 平台上进行开发,因此在开始之前请先安装 Node.js。

对于本书和这个示例应用,我们推荐Angular CLI(bit.ly/ng6be-angular-cli)。这是一个命令行工具,它具有构建系统和脚手架工具,极大地简化了 Angular 的开发工作流程。它很受欢迎,易于设置,易于管理,并支持现代构建系统应该拥有的几乎所有功能。关于它的更多信息将在后面介绍。

就像任何成熟的框架一样,Angular CLI 并不是网络上的唯一选择。社区创建的一些值得注意的起始网站和构建设置如下:

起始网站位置
angular2-webpack-starterbit.ly/ng2webpack
angular-seedgithub.com/mgechev/angular-seed

让我们从安装 Angular CLI 开始。在命令行中,输入以下内容:

npm i -g @angular/cli

安装完成后,Angular CLI 将在我们的执行环境中添加一个新的命令 ng。要从命令行创建一个新的 Angular 项目,请运行以下命令:

ng new PROJECT-NAME

这将生成一个包含大量文件、一个模板化的 Angular 应用和一个预配置的构建系统的文件夹结构。要从命令行运行应用程序,请执行以下操作:

ng serve --open

你可以看到一个基本的 Angular 应用正在运行!

对于我们的 7 分钟健身 应用,我们不会从头开始,而是从一个基于 ng new 生成的项目结构稍作修改的版本开始。以下是一些步骤:

好奇默认项目包含什么吗?运行 ng new 项目名称。查看生成的内容结构和 Angular CLI 文档,以了解默认设置中包含的内容。

  1. bit.ly/ngbe-base 下载此应用的基版,并将其解压到您的机器上的某个位置。如果您熟悉 Git 的工作方式,您可以直接克隆存储库并切换到 base 分支:
git checkout base

这段代码是我们应用的开端。

  1. 使用命令行导航到 trainer 文件夹,并执行命令 npm install 以安装我们应用的 包依赖

在 Node.js 世界中,是用于应用或支持应用构建过程的第三方库(例如,我们的应用中使用 Angular)。npm 是一个用于从远程仓库拉取这些包的命令行工具。

  1. 一旦 npm 从 npm 存储库中拉取了应用依赖,我们就可以构建和运行应用了。从命令行输入以下命令:
    ng serve --open

这会编译并运行应用。如果构建过程顺利,默认的浏览器窗口/标签页将打开一个基本的网页(http://localhost:4200/)。我们现在可以开始使用 Angular 开发我们的应用了!

但在我们这样做之前,了解一些关于 Angular CLI 和我们对默认项目模板所做的自定义将很有趣。

Angular CLI

Angular CLI 的创建目的是为了标准化和简化 Angular 应用的开发和部署工作流程。正如文档所建议的:

"Angular CLI 使得创建一个即开即用的应用变得简单。它已经遵循我们的最佳实践!"

它包含:

  • 基于 webpack 的构建系统

  • 一个用于生成所有标准 Angular 艺术品的 脚手架工具,包括模块、指令、组件和管道

  • 遵循 Angular 风格指南 (bit.ly/ngbe-styleguide),确保我们使用社区驱动的标准来处理各种规模的项目

你可能从未听说过风格指南这个术语,或者可能不理解它的意义。在任何技术中,风格指南都是一系列指导原则,帮助我们组织编写易于开发、维护和扩展的代码。要理解和欣赏 Angular 自己的风格指南,对框架本身有一定的熟悉度是可取的,我们已经开始了这段旅程。

  • 一个目标式代码检查器;Angular CLI 集成了codelyzer(bit.ly/ngbe-codelyzer),这是一个静态代码分析工具,它将我们的 Angular 代码与一系列规则进行验证,以确保我们编写的代码遵循 Angular 风格指南中规定的标准。

  • 预配置的单元测试端到端测试e2e)框架

以及更多!

想象一下,如果我们必须手动完成所有这些工作!陡峭的学习曲线会很快让我们感到不知所措。幸运的是,我们不必处理它,Angular CLI 会为我们处理。

Angular CLI 的构建设置基于 webpack,但它不暴露底层的 webpack 配置;这是故意的。Angular 团队希望保护开发者免受 webpack 的复杂性和内部工作原理的影响。Angular CLI 的最终目标是消除任何入门级障碍,并使设置和运行 Angular 代码变得简单。

这并不意味着 Angular CLI 不可配置。有一个配置文件angular.json),我们可以用它来修改构建设置。我们在这里不会介绍这个。请检查 7 分钟健身计划的配置文件,并在此处阅读文档:bit.ly/ng6be-angular-cli-config

我们对默认生成的项目模板所做的调整包括:

  • style.css文件中引用了 Bootstrap CSS。

  • 升级了一些 npm 库的版本。

  • 将生成的代码的前缀配置更改为使用abe(代表 Angular By Example),而不是app。这个更改意味着所有我们的组件和指令选择器都将使用abe作为前缀,而不是app。检查app.component.ts;选择器是abe-root而不是app-root

在讨论 Angular CLI 和构建时,我们在继续之前应该了解一些事情。

我们编写的 TypeScript 代码会发生什么?

代码转译

浏览器,众所周知,只支持 JavaScript,它们不理解 TypeScript。因此,我们需要一个机制将我们的 TypeScript 代码转换为纯 JavaScript(ES5是我们的最佳选择)。TypeScript 编译器负责这项工作。编译器将 TypeScript 代码转换为 JavaScript。这个过程通常被称为转译,由于 TypeScript 编译器执行这个操作,因此被称为转译器

JavaScript 语言在过去的几年中不断发展,每个新版本都为语言添加了新的功能/能力。最新的版本 ES2015 继承了 ES5,是语言的一次重大更新。尽管于 2015 年 6 月发布,但一些较旧的浏览器仍然不支持 ES2015 版本的 JavaScript,这使得其采用成为一项挑战。

当将 TypeScript 代码转换为 JavaScript 代码时,我们可以指定要使用的 JavaScript 版本。如前所述,ES5 是我们的最佳选择,但如果我们打算只与最新的浏览器合作,可以选择 ES2015。对于 7 分钟健身应用,我们将代码转换为 ES5 格式。我们在 tsconfig.json 中设置了此 TypeScript 编译器配置(请参阅 target 属性)。

有趣的是,转换可以在构建/编译时和运行时发生:

  • 构建时转换:构建过程中的转换将脚本文件(在我们的例子中是 TypeScript .ts 文件)编译成纯 JavaScript。Angular CLI 执行构建时转换。

  • 运行时转换:这发生在浏览器运行时。我们直接引用 TypeScript 文件(在我们的例子中是 .ts 文件),并且 TypeScript 编译器,在浏览器中预先加载,会即时编译这些脚本文件。这种设置仅适用于小型示例/代码片段,因为加载转换器和即时转换代码会带来额外的性能开销。

转换过程不仅限于 TypeScript。所有针对 Web 的语言,如 CoffeeScriptES2015(是的,JavaScript 本身!)或任何浏览器本身无法理解的任何其他语言,都需要转换。大多数语言都有转换器,其中最著名的是(除了 TypeScript 之外)tracuerbabel

Angular CLI 构建系统负责设置 TypeScript 编译器,并设置文件监视器,每次我们更改 TypeScript 文件时都会重新编译代码。

如果你刚接触 TypeScript,请记住 TypeScript 不依赖于 Angular;实际上,Angular 是基于 TypeScript 构建的。我强烈建议你查看 TypeScript 的官方文档(www.typescriptlang.org/)并在 Angular 的范畴之外学习这门语言。

让我们回到我们正在构建的应用程序,并开始探索代码设置。

代码组织

Angular CLI 的优势在于它规定了适用于所有规模应用程序的代码组织结构。以下是当前代码组织结构的样子:

图片

  • trainer 是应用程序的根文件夹。

  • trainer 文件夹内的文件是配置文件和一些标准文件,它们是每个标准 Node 应用程序的一部分。

  • e2e 文件夹将包含应用程序的端到端测试。

  • src 是所有开发发生的主要文件夹。所有应用程序的工件都放入 src

  • src 文件夹内的 assets 文件夹托管静态内容(例如图片、CSS、音频文件等)。

  • app 文件夹包含应用程序的源代码。

  • environments 文件夹用于为不同的部署环境(如 dev、qa、production)设置配置。

为了在 app 文件夹内组织 Angular 代码,我们借鉴了 Angular 团队发布的 Angular 风格指南(bit.ly/ng6be-style-guide)。

功能文件夹

风格指南建议使用功能文件夹来组织代码。使用功能文件夹,将链接到单个功能的文件放在一起。如果一个功能增长,我们将它进一步拆分为子功能,并将代码放入子文件夹中。将 app 文件夹视为我们的第一个功能文件夹!随着应用程序的增长,app 将添加子功能以更好地组织代码。

直接进入构建应用程序。我们的第一个重点领域,应用程序的模型!

7 分钟健身模型

设计该应用程序的模型需要我们首先详细说明 7 分钟健身 应用程序的功能方面,然后推导出一个满足这些要求的模型。根据之前定义的问题陈述,一些明显的要求如下:

  • 能够开始锻炼。

  • 提供关于当前练习及其进度的视觉提示。这包括以下内容:

    • 提供当前练习的视觉表示

    • 提供如何进行特定练习的逐步说明

    • 当前练习剩余时间

  • 当锻炼结束时通知用户。

我们将添加到该应用程序的一些其他有价值的功能如下:

  • 暂停当前锻炼的能力。

  • 提供关于接下来要进行的练习的信息。

  • 提供音频提示,以便用户可以在不经常查看屏幕的情况下进行锻炼。这包括:

    • 计时器点击声音

    • 关于下一项练习的详细信息

    • 信号表示练习即将开始

  • 显示正在进行中的练习的相关视频,并能够播放它们。

如我们所见,该应用的核心主题是锻炼练习。在这里,一个锻炼是一组按照特定顺序进行、持续特定时间的练习。因此,让我们继续定义我们的锻炼和练习模型。

根据刚刚提到的要求,我们将需要以下关于一项练习的详细信息:

  • 名称。这应该是唯一的。

  • 标题。这会显示给用户。

  • 练习的描述。

  • 如何执行练习的说明。

  • 练习的图片。

  • 练习音频片段的名称。

  • 相关视频。

使用 TypeScript,我们可以为我们的模型定义类。

Exercise 类如下所示:

export class Exercise { 
  constructor( 
    public name: string,
    public title: string,
    public description: string, 
    public image: string,
    public nameSound?: string,
    public procedure?: string,
    public videos?: Array<string>) { }
} 

TypeScript 小贴士

使用 publicprivate 声明构造函数参数是一种创建和初始化类成员的快捷方式。nameSoundprocedurevideos 后的 ? 后缀表示这些是可选参数。

对于锻炼,我们需要跟踪以下属性:

  • 名称。这应该是唯一的。

  • 标题。这会显示给用户。

  • 组成锻炼的练习。

  • 每个练习的时长。

  • 两次练习之间的休息时间。

用于跟踪锻炼进度的模型类 (WorkoutPlan) 如下所示:

export class WorkoutPlan { 
  constructor( 
    public name: string, 
    public title: string, 
    public restBetweenExercise: number, 
 public exercises: ExercisePlan[], 
    public description?: string) { } 

  totalWorkoutDuration(): number { ... } 
} 

totalWorkoutDuration 函数返回锻炼的总时长(以秒为单位)。

WorkoutPlan 在前面的定义中引用了另一个类,ExercisePlan。它跟踪锻炼和锻炼中的持续时间,一旦我们查看 ExercisePlan 的定义,这一点就非常明显:

export class ExercisePlan { 
  constructor( 
    public exercise: Exercise, 
    public duration: number) { } 
} 

让我为您节省一些输入,并告诉您在哪里获取模型类,但在那之前,我们需要决定在哪里添加它们。我们已经准备好进行第一个功能。

第一个功能模块

7 分钟锻炼 的主要功能是执行预定义的练习集。因此,我们现在将创建一个功能模块,稍后会将功能实现添加到该模块中。我们称此模块为 workout-runner。让我们使用 Angular CLI 的脚手架功能初始化功能。

从命令行导航到 trainer/src/app 文件夹并运行以下命令:

ng generate module workout-runner --module app.module.ts

跟踪控制台日志以了解生成的文件。该命令本质上:

  • 在新的 workout-runner 文件夹内创建一个新的 Angular WorkoutRunnerModule 模块

  • 将新创建的模块导入主应用程序模块 app (app.module.ts)

我们现在有一个新的 功能模块

给每个功能创建自己的模块。

注意 Angular CLI 在构建 Angular 实体时遵循的约定。从前面的示例中,通过命令行提供的模块名称是 workout-runner。虽然生成的文件夹和文件名使用相同的名称,但生成的模块的类名是 WorkoutRunnerModule(Pascal 大写并带有 Module 后缀)。

打开新生成的模块定义文件 (workout-runner.module.ts) 并查看生成的内容。WorkoutRunnerModule 导入 CommonModule,这是一个包含常见 Angular 指令(如 ngIfngFor)的模块,允许我们在 WorkoutRunnerModule 中定义的任何组件/指令中使用这些常见指令。

模块是 Angular 组织代码的方式。我们将在稍后讨论 Angular 模块。

model.ts 文件从 bit.ly/ng6be-2-1-model-ts 复制到 workout-runner 文件夹。不久我们将看到这些模型类是如何被利用的。

由于我们从一个预配置的 Angular 应用程序开始,我们只需要了解应用程序是如何启动的。

应用程序启动

第一章,入门,对应用程序启动过程进行了良好的介绍。7 分钟锻炼 的应用程序启动过程保持不变;查看 src 文件夹。有一个 main.ts 文件通过调用以下内容启动应用程序:

platformBrowserDynamic().bootstrapModule(AppModule)
    .catch(err => console.log(err));

重量级的工作由 Angular CLI 完成,它编译应用程序,将脚本和 CSS 引用包含到index.html中,并运行应用程序。我们不需要进行任何配置。这些配置是默认 Angular CLI 配置的一部分(.angular-cli.json)。

我们已经创建了一个新模块,并将一些模型类添加到module文件夹中。在我们进一步实施功能之前,让我们先谈谈Angular 模块

探索 Angular 模块

随着 7 分钟健身 应用程序的增长,我们向其中添加了新的组件/指令/管道/其他工件,这就需要对这些项目进行组织。每个项目都需要成为 Angular 模块的一部分。

一种天真方法是在我们应用程序的根模块(AppModule)中声明一切,就像我们对WorkoutRunnerComponent所做的那样,但这违背了 Angular 模块的全部目的。

为了了解为什么单模块方法永远不会是一个好主意,让我们来探索 Angular 模块。

理解 Angular 模块

在 Angular 中,模块是将代码组织成属于一起并作为一个统一单元工作的块的方式。模块是 Angular 对代码分组和组织的方式。

一个 Angular 模块主要定义:

  • 它拥有的组件/指令/管道

  • 它公开的组件/指令/管道,供其他模块消费

  • 它所依赖的其他模块

  • 模块想要在应用程序范围内提供的服务

任何相当规模的 Angular 应用都将有模块相互链接:一些模块消费来自其他模块的工件,一些向其他模块提供工件,还有一些模块两者都做。

作为一项标准实践,模块隔离是基于功能的。人们将应用分为功能或子功能(对于大型功能),并为每个功能创建模块。甚至框架也遵循此指南,因为所有框架构造都分布在模块中:

  • 存在CommonModule,它聚合了在所有基于浏览器的 Angular 应用中使用的标准框架构造。

  • 如果我们想使用 Angular 路由框架,则存在RouterModule

  • 如果我们的应用需要通过 HTTP 与服务器通信,则存在HtppModule

Angular 模块是通过将@NgModule装饰器应用于 TypeScript 类来创建的,这是我们已经在第一章 入门 中学到的。装饰器定义暴露了足够的元数据,使 Angular 能够加载模块引用的所有内容。

装饰器有多个属性,允许我们定义:

  • 外部依赖(使用imports)。

  • 模块工件(使用declarations)。

  • 模块导出(使用exports)。

  • 在模块内部定义并需要全局注册的服务(使用providers)。

  • 主要应用程序视图,称为根组件,它托管所有其他应用程序视图。只有根模块应该使用bootstrap属性设置此属性。

此图突出了模块的内部结构以及它们是如何相互链接的:

在 Angular 的上下文中定义的模块(使用@NgModule装饰器)与我们通过 TypeScript 文件中的import语句导入的模块不同。通过import语句导入的模块是JavaScript 模块,可以是遵循CommonJSAMDES2015规范的任何格式,而 Angular 模块是 Angular 用于隔离和组织其工件的结构。除非讨论的上下文是特定的 JavaScript 模块,否则任何关于模块的提及都指的是 Angular 模块。我们可以在这里了解更多信息:bit.ly/ng2be6-module-vs-ngmodule

我们希望从所有这些讨论中可以清楚地看出:除非你正在构建一些基础的东西,否则创建单个应用程序范围的模块不是正确使用 Angular 模块的方式。

是时候深入到行动的核心了;让我们构建我们的第一个组件。

我们的第一个组件 - WorkoutRunnerComponent

WorkoutRunnerComponent是我们7 分钟锻炼应用的核心部分,它将包含执行锻炼的逻辑。

WorkoutRunnerComponent实现中,我们将做以下事情:

  1. 开始锻炼

  2. 展示正在进行的锻炼过程并显示进度指示器

  3. 练习时间结束后,显示下一个练习

  4. 重复此过程,直到所有练习结束

我们准备好创建(或搭建)我们的组件。

从命令行导航到src/app文件夹并执行以下ng命令:

ng generate component workout-runner -is

生成器在workout-runner文件夹中生成一些文件(三个),并更新WorkoutRunnerModule中的模块声明,以包括新创建的WorkoutRunnerComponent

-is标志用于停止为组件生成单独的 CSS 文件。由于我们使用全局样式,我们不需要组件特定的样式。

记住要从src/app文件夹运行此命令,而不是从src/app/workout-runner文件夹运行。如果我们从src/app/workout-runner运行前面的命令,Angular CLI 将为workout-runner组件定义创建一个新的子文件夹。

前面的ng generate命令为组件生成以下三个文件:

  • <component-name>.component.html:这是组件的视图 HTML。

  • <component-name>.component.spec.ts:用于单元测试的测试规范文件。我们将用完整的一章来介绍 Angular 应用程序的单元测试。

  • <component-name>.component.ts:包含组件实现的主体组件文件。

再次,我们将鼓励您查看生成的代码,以了解生成了什么。Angular CLI 组件生成器为我们节省了一些按键,一旦生成,样板代码可以按需演变。

在第一章“入门”中,我们提到了组件装饰器(@Component),这里应用的装饰器也没有什么不同。虽然我们只看到了四个装饰器元数据属性(例如templateUrl),但组件装饰器还支持一些其他有用的属性。查看 Angular 文档中的组件部分,了解更多关于这些属性及其应用的信息。在接下来的章节中,我们将利用一些除了每个组件上使用的标准属性之外的其他元数据属性。

一个细心的读者可能会注意到生成的selector属性值有一个前缀abe;这是故意的。由于我们正在扩展 HTML领域特定语言DSL)以包含一个新元素,前缀abe帮助我们区分我们开发的 HTML 扩展。因此,我们不是在 HTML 中使用<workout-runner></workout-runner>,而是使用<abe-workout-runner></abe-workout-runner>。前缀值已在angular.json中配置,请参阅prefix属性。

总是为你的组件选择器添加一个前缀。

我们现在有了WorkoutRunnerComponent的模板;让我们开始添加实现,首先添加模型引用。

workout-runner.component.ts中,导入所有锻炼模型:

import {WorkoutPlan, ExercisePlan, Exercise} from '../model';

接下来,我们需要设置锻炼数据。让我们通过在生成的ngOnInit函数和相关类属性中添加一些代码到WorkoutRunnerComponent类中来实现这一点:

workoutPlan: WorkoutPlan; 
restExercise: ExercisePlan; 
ngOnInit() { 
   this.workoutPlan = this.buildWorkout(); 
   this.restExercise = new ExercisePlan( 
     new Exercise('rest', 'Relax!', 'Relax a bit', 'rest.png'),  
     this.workoutPlan.restBetweenExercise);   
} 

ngOnInit是一个 Angular 在组件初始化时调用的特殊函数。我们很快就会讨论ngOnInit

WorkoutRunnerComponent上的buildWorkout设置完整的锻炼,正如我们很快就会定义的。我们还初始化了一个restExercise变量来跟踪休息时间(注意restExerciseExercisePlan类型的一个对象)。

buildWorkout函数是一个较长的函数,所以最好从 Git 分支 checkpoint2.1 中可用的锻炼运行器的实现中复制实现(bit.ly/ng6be-2-1-workout-runner-component-ts)。buildWorkout代码如下:

buildWorkout(): WorkoutPlan { 
let workout = new WorkoutPlan('7MinWorkout',  
"7 Minute Workout", 10, []); 
   workout.exercises.push( 
      new ExercisePlan( 
        new Exercise( 
          'jumpingJacks', 
          'Jumping Jacks', 
          'A jumping jack or star jump, also called side-straddle hop
           is a physical jumping exercise.', 
          'JumpingJacks.png', 
          'jumpingjacks.wav', 
          `Assume an erect position, with feet together and 
           arms at your side. ...`, 
          ['dmYwZH_BNd0', 'BABOdJ-2Z6o', 'c4DAnQ6DtF8']), 
        30)); 
   // (TRUNCATED) Other 11 workout exercise data. 
   return workout; 
} 

这段代码构建了WorkoutPlan对象,并将锻炼数据推入exercises数组(ExercisePlan对象数组),返回新构建的锻炼。

初始化已完成;现在,是时候实际实现开始锻炼了。在WorkoutRunnerComponent实现中添加一个start函数,如下所示:

start() { 
   this.workoutTimeRemaining =  
   this.workoutPlan.totalWorkoutDuration(); 
   this.currentExerciseIndex = 0;  
   this.startExercise(this.workoutPlan.exercises[this.currentExerciseIndex]); 
} 

然后在函数顶部声明在函数中使用的新的变量,以及其他变量声明:

workoutTimeRemaining: number; 
currentExerciseIndex: number; 

workoutTimeRemaining变量跟踪锻炼剩余的总时间,currentExerciseIndex跟踪当前执行的锻炼索引。对startExercise的调用实际上启动了一个锻炼。这就是startExercise代码的样子:

startExercise(exercisePlan: ExercisePlan) { 
    this.currentExercise = exercisePlan; 
    this.exerciseRunningDuration = 0; 
    const intervalId = setInterval(() => { 
      if (this.exerciseRunningDuration >=  this.currentExercise.duration) { 
          clearInterval(intervalId);  
      } 
      else { this.exerciseRunningDuration++; } 
    }, 1000); 
} 

我们首先初始化currentExerciseexerciseRunningDurationcurrentExercise变量跟踪正在进行的锻炼,exerciseRunningDuration跟踪其时长。这两个变量也需要在顶部声明:

currentExercise: ExercisePlan; 
exerciseRunningDuration: number; 

我们使用延迟为一秒(1,000 毫秒)的setInterval JavaScript 函数来使进度。在setInterval回调函数内部,exerciseRunningDuration随着每一秒的过去而递增。嵌套的clearInterval调用一旦锻炼时长结束就停止计时器。

TypeScript 箭头函数

传递给setInterval的回调参数(()=>{...})是一个 lambda 函数(或 ES 2015 中的箭头函数)。Lambda 函数是匿名函数的简写形式,具有额外的优势。你可以在bit.ly/ng2be-ts-arrow-functions了解更多关于它们的信息。

组件的第一版几乎完成,但当前它有一个静态视图(UI),因此我们无法验证其实现。我们可以通过添加一个基本的视图定义来快速纠正这种情况。打开workout-runner.component.ts,注释掉templateUrl属性,并添加一个内联模板属性(template),并将其设置为以下内容:

template: `<pre>Current Exercise: {{currentExercise | json}}</pre>
<pre>Time Left: {{currentExercise.duration - exerciseRunningDuration}}</pre>`,

用反引号(` `)括起来的字符串是 ES2015 的新增功能。也称为模板字面量,这种字符串字面量可以是多行的,并允许在内部嵌入表达式(不要与 Angular 表达式混淆)。查看 MDN 文章bit.ly/template-literals以获取更多详细信息。

内联视图与外部视图模板(使用templateUrl指定的external template file)用于详细视图。Angular CLI 默认生成外部模板引用,但我们可以通过将--inline-template标志传递给ng组件生成命令(如--inline-template true)来影响这种行为。

前面的模板 HTML 将渲染原始的ExercisePlan对象和剩余的锻炼时间。在第一个插值表达式内部有一个有趣的表达式:currentExercise | jsoncurrentExercise属性在WorkoutRunnerComponent中定义,但关于|符号以及其后的内容(json)是什么?在 Angular 世界中,它被称为管道。管道的唯一目的是转换/格式化模板数据。

这里的json管道用于 JSON 数据格式化。你将在本章后面了解更多关于管道的内容,但为了对json管道的作用有一个大致的了解,我们可以移除json管道以及|符号,并渲染模板;我们将在下一部分做这件事。

为了渲染新的WorkoutRunnerComponent实现,必须将其添加到根组件的视图中。修改src/components/app/app.component.html,将h3标签替换为以下代码:

<div class="container body-content app-container">
      <abe-workout-runner></abe-workout-runner>
</div>

虽然实现看起来已经完整,但还缺少一个关键部分。代码中没有任何地方真正开始锻炼。锻炼应该在页面加载后立即开始。

组件生命周期钩子将帮助我们解决问题!

组件生命周期钩子

Angular 组件的生命周期是充满事件的。组件被创建,在其生命周期中改变状态,最终被销毁。Angular 提供了一些生命周期钩子/函数,当发生此类事件时,框架会调用(在组件上)。考虑以下示例:

  • 当组件初始化时,Angular 会调用ngOnInit

  • 当组件的数据绑定属性发生变化时,Angular 会调用ngOnChanges

  • 当组件被销毁时,Angular 会调用ngOnDestroy

作为开发者,我们可以利用这些关键时刻,并在相应的组件中执行一些自定义逻辑。

我们将要利用的钩子是ngOnInitngOnInit函数在组件的数据绑定属性首次初始化时触发,但在视图初始化开始之前。

虽然ngOnInit和类构造函数看起来很相似,但它们有不同的用途。构造函数是一种语言特性,用于初始化类成员。另一方面,ngOnInit用于在组件准备好后执行一些初始化操作。避免将构造函数用于除成员初始化之外的其他任何用途。

ngOnInit函数更新为WorkoutRunnerComponent类,并调用开始锻炼:

ngOnInit() { 
    ...
    this.start(); 
} 

Angular CLI 作为组件脚手架的一部分已经生成了ngOnInit的签名。ngOnInit函数在OnInit接口上声明,该接口是 Angular 核心框架的一部分。我们可以通过查看WorkoutRunnerComponent的导入部分来确认这一点:

import {Component,OnInit} from '@angular/core'; 
... 
export class WorkoutRunnerComponent implements OnInit {

组件支持许多其他生命周期钩子,包括ngOnDestroyngOnChangesngAfterViewInit,但在这里我们不会深入探讨它们。有关其他此类钩子的更多信息,请参阅开发者指南(angular.io/guide/lifecycle-hooks)。

实现接口(如前例中的OnInit)是可选的。只要函数名匹配,这些生命周期钩子就会正常工作。我们仍然建议您使用接口来清楚地传达意图。

是时候运行我们的应用了!打开命令行,导航到trainer文件夹,并输入以下行:

ng serve --open

代码编译通过了,但没有渲染 UI。是什么出了问题?让我们查看浏览器控制台中的错误。

打开浏览器的开发者工具(常见快捷键F12),查看控制台标签页以查找错误。存在模板解析错误。Angular 无法定位到abe-workout-runner组件。让我们做一些合理性检查来验证我们的设置:

  • WorkoutRunnerComponent实现完成 - 检查

  • WorkoutRunnerModule中声明的组件 - 检查

  • 已将 WorkoutRunnerModule 导入到 AppModule 中 - 检查

尽管如此,AppComponent 模板找不到 WorkoutRunnerComponent。这是否是因为 WorkoutRunnerComponentAppComponent 在不同的模块中?确实如此!这是问题所在!虽然 WorkoutRunnerModule 已被导入到 AppModule 中,但 WorkoutRunnerModule 仍然没有导出新的 WorkoutRunnerComponent,这将允许 AppComponent 使用它。

记住,将组件/指令/管道添加到模块的 声明 部分会使它们在模块内部可用。只有在我们导出组件/指令/管道之后,它才能在模块之间使用。

让我们通过更新 WorkoutRunnerModule 声明的导出数组来导出 WorkoutRunnerComponent

declarations: [WorkoutRunnerComponent],
exports:[WorkoutRunnerComponent]

这次,我们应该看到以下输出:

图片

如果你想在其他模块中使用在 Angular 模块内部定义的工件,请始终导出这些工件。

模型数据每秒都会更新!现在你将理解为什么插值 ({{ }}) 是一个很好的调试工具。

这也是一个尝试不带 json 管道渲染 currentExercise 的好时机,看看会渲染什么。

我们还没有完成!在页面上等待足够长的时间,我们会意识到计时器在 30 秒后停止。应用程序没有加载下一个锻炼数据。是时候修复它了!

更新 setInterval 函数内部的代码:

if (this.exerciseRunningDuration >=  this.currentExercise.duration) { 
   clearInterval(intervalId); 
 const next: ExercisePlan = this.getNextExercise(); if (next) { if (next !== this.restExercise) { this.currentExerciseIndex++; } this.startExercise(next);}
 else { console.log('Workout complete!'); } 
} 

使用 if (this.exerciseRunningDuration >= this.currentExercise.duration) 条件语句,一旦当前锻炼的时间耗尽,就切换到下一个锻炼。我们使用 getNextExercise 获取下一个锻炼,并再次调用 startExercise 以重复此过程。如果 getNextExercise 调用没有返回任何锻炼,则认为锻炼已完成。

在锻炼切换期间,只有当下一个锻炼不是休息锻炼时,我们才增加 currentExerciseIndex。记住,原始锻炼计划中没有休息锻炼。为了保持一致性,我们创建了一个休息锻炼,现在在休息锻炼和锻炼计划中的标准锻炼之间切换。因此,当下一个锻炼是休息时,currentExerciseIndex 不会改变。

让我们快速添加 getNextExercise 函数。将函数添加到 WorkoutRunnerComponent 类中:

getNextExercise(): ExercisePlan { 
    let nextExercise: ExercisePlan = null; 
    if (this.currentExercise === this.restExercise) { 
      nextExercise = this.workoutPlan.exercises[this.currentExerciseIndex + 1]; 
    } 
    else if (this.currentExerciseIndex < this.workoutPlan.exercises.length - 1) { 
      nextExercise = this.restExercise; 
    } 
    return nextExercise; 
} 

getNextExercise 函数返回需要执行的下个锻炼。

注意,getNextExercise 返回的对象是一个 ExercisePlan 对象,它内部包含锻炼的详细信息以及锻炼的持续时间。

实现相当直观。如果当前锻炼是休息,则从 workoutPlan.exercises 数组中获取下一个锻炼(基于 currentExerciseIndex);否则,如果不在最后一个锻炼上(else if 条件检查),下一个锻炼是休息。

这样,我们就准备好测试我们的实现了。练习应该在每 10 秒或 30 秒后翻转。太棒了!

当前的构建设置会在保存脚本文件时自动编译对脚本文件所做的任何更改;它也会在这些更改后刷新浏览器。但以防万一 UI 没有更新或事情没有按预期工作,请刷新浏览器窗口。如果你在运行代码时遇到问题,请查看 Git 分支checkpoint2.1以获取我们迄今为止所做工作的一个工作版本。或者如果你不使用 Git,可以从bit.ly/ng6be-checkpoint2-1下载 Checkpoint 2.1 的快照(一个 ZIP 文件)。在第一次设置快照时,请参考trainer文件夹中的README.md文件。

目前我们对组件的工作已经足够,让我们来构建视图。

构建 7 分钟健身视图

在定义模型和实现组件时,大部分艰苦的工作已经完成。现在,我们只需要使用 Angular 超棒的数据绑定功能来“皮肤”HTML。这将简单、甜蜜且优雅!

对于7 分钟健身视图,我们需要显示练习名称、练习图像、进度指示器和剩余时间。将workout-runner.component.html文件的本地内容替换为 Git 分支checkpoint2.2中的文件内容(或从bit.ly/ng6be-2-2-workout-runner-component-html下载)。视图 HTML 看起来如下:

<div class="row">
  <div id="exercise-pane" class="col-sm">
    <h1 class="text-center">{{currentExercise.exercise.title}}</h1>
    <div class="image-container row">
      <img class="img-fluid col-sm" [src]="'/assets/images/' +  
                                      currentExercise.exercise.image" />
    </div>
    <div class="progress time-progress row">
      <div class="progress-bar col-sm" 
            role="progressbar" 
            [attr.aria-valuenow]="exerciseRunningDuration" 
            aria-valuemin="0" 
            [attr.aria-valuemax]="currentExercise.duration"
            [ngStyle]="{'width':(exerciseRunningDuration/currentExercise.duration) * 
                                                                100 + '%'}">
      </div>
    </div>
    <h1>Time Remaining: {{currentExercise.duration-exerciseRunningDuration}}</h1>
  </div>
</div>

WorkoutRunnerComponent目前使用内联模板;相反,我们需要恢复使用外部模板。更新workout-runner.component.ts文件,删除template属性,然后取消注释我们之前注释掉的templateUrl

在我们理解视图中的 Angular 组件之前,让我们再次运行应用程序。保存workout-runner.component.html中的更改,如果一切顺利,我们将看到健身应用程序的全貌:

图片

基本应用现在已经上线并运行。练习图像和标题显示出来,进度指示器显示进度,当练习时间结束时发生练习转换。这感觉真是太棒了!

如果你运行代码时遇到问题,请查看 Git 分支checkpoint2.2以获取我们迄今为止所做工作的一个工作版本。你还可以从这个 GitHub 位置下载checkpoint2.2的快照(一个 ZIP 文件):bit.ly/ng6be-checkpoint-2-2。在第一次设置快照时,请参考trainer文件夹中的README.md文件。

观察视图 HTML,除了一些 Bootstrap 样式外,还有一些需要我们注意的有趣的 Angular 组件。在我们详细研究这些视图结构之前,让我们分解这些元素并提供一个快速总结:

  • <h1 ...>{{currentExercise.exercise.title}}</h1>: 使用 插值

  • <img ... [src]="'/assets/images/' + currentExercise.exercise.image" .../>: 使用 属性绑定 将图像的 src 属性绑定到组件模型属性 currentExercise.exercise.image

  • <div ... [attr.aria-valuenow]="exerciseRunningDuration" ... >: 使用 属性绑定div 上的 aria 属性绑定到 exerciseRunningDuration

  • <div ... [ngStyle]="{'width':(exerciseRunningDuration/currentExercise.duration) * 100 + '%'}">: 使用 指令 ngStyle 将进度条 div 上的 style 属性绑定到一个评估练习进度的表达式

呼吸!这里涉及了很多绑定。让我们更深入地了解绑定基础设施。

Angular 绑定基础设施

今天,大多数现代 JavaScript 框架都提供了强大的模型-视图绑定支持,Angular 也不例外。任何绑定基础设施的主要目标是减少开发者需要编写的用于保持模型和视图同步的样板代码。一个健壮的绑定基础设施总是声明性和简洁的。

Angular 绑定基础设施允许我们将模板(原始)HTML 转换为与模型数据绑定的实时视图。根据使用的绑定构造,数据可以双向流动和同步:从模型到视图和从视图到模型。

组件的模型与其视图之间的链接是通过 @Component 装饰器的 templatetemplateUrl 属性建立的。除了 script 标签外,几乎任何 HTML 片段都可以作为 Angular 绑定基础设施的模板。

要使这种绑定魔法生效,Angular 需要获取视图模板,编译它,将其链接到模型数据,并在无需任何自定义同步代码的情况下保持与模型更新的同步。

根据数据流方向,这些绑定可以分为三种类型:

  • 从模型到视图的单向绑定:在模型到视图绑定中,模型的变化与视图保持同步。插值、属性、属性、类和样式绑定属于此类。

  • 从视图到模型的单向绑定:在这个类别中,视图变化流向模型。事件绑定属于此类。

  • 双向/双向绑定:正如其名所示,双向绑定保持视图和模型同步。用于双向绑定的有一个特殊的绑定构造 ngModel,以及一些标准的 HTML 数据输入元素,如 inputselect 支持双向绑定。

让我们了解如何利用 Angular 的绑定能力来支持视图模板化。Angular 提供了以下绑定构造:

  • 插值

  • 属性绑定

  • 属性绑定

  • 类绑定

  • 样式绑定

  • 事件绑定

我们已经在第一章“入门”中提到了许多绑定功能,所以在这里我们力求减少重复,并建立在上一章获得的知识之上。

现在是学习所有这些绑定结构的好时机。插值是第一个。

插值

插值非常简单。插值符号({{ }})内的表达式(通常称为模板表达式)在模型(或组件类成员)的上下文中被评估,评估结果(字符串)被嵌入到 HTML 中。这是一个方便的框架结构,用于显示组件的数据/属性。我们在第一章“入门”中一直看到这些,也在我们刚刚添加的视图中看到。我们使用插值来渲染练习标题和剩余练习时间:

<h1>{{currentExercise.exercise.title}}</h1>
... 
<h1>Time Remaining: {{currentExercise.duration?-exerciseRunningDuration}}</h1> 

记住,插值同步模型变化与视图。插值是从模型到视图绑定的一种方式。

Angular 中的视图绑定始终在组件的作用域上下文中进行评估。

实际上,插值是属性绑定的一种特殊情况,它允许我们将任何 HTML 元素/组件属性绑定到模型。我们将很快讨论如何使用属性绑定语法编写插值。将插值视为属性绑定的语法糖。

属性绑定

如在第一章“入门”中讨论的那样,属性绑定使我们能够将原生 HTML/组件属性绑定到组件模型并保持它们同步(从模型到视图)。让我们从不同的角度来探讨属性绑定。

看看 7 分钟健身组件视图(workout-runner.component.html)的这段视图摘录:

<img class="img-responsive" [src]="'/static/images/' + currentExercise.exercise.image" /> 

看起来我们正在将imgsrc属性设置为在运行时评估的表达式。但我们真的是绑定到属性吗?或者这是一个属性?属性和属性是否不同?

在 Angular 领域,虽然前面的语法看起来像是在设置 HTML 元素的属性,但实际上它正在进行属性绑定。此外,由于我们很多人没有意识到 HTML 元素的属性和其属性之间的区别,这个声明非常令人困惑。因此,在我们查看属性绑定的工作方式之前,让我们先尝试理解元素属性和其属性之间的区别。

属性与属性的区别

选取任何 DOM 元素 API,你都会发现属性、属性、函数和事件。虽然事件和函数是自解释的,但理解属性和属性之间的区别却很困难。在日常使用中,我们经常互换使用这些词,这也没有太大帮助。以以下这段代码为例:

<input type="text" value="Awesome Angular"> 

当浏览器为这个输入文本框创建 DOM 元素(确切地说是 HTMLInputElement)时,它使用 input 上的 value 属性来设置 inputvalue 属性的初始状态为 Awesome Angular

在此初始化之后,对 inputvalue 属性的任何更改都不会反映在 value 属性上;属性始终是 Awesome Angular(除非再次明确设置)。这可以通过查询 input 状态来确认。

假设我们将 input 数据更改为 Angular rocks! 并查询 input 元素状态:

input.value // value property 

value 属性始终返回当前输入内容,即 Angular rocks!。而此 DOM API 函数:

input.getAttribute('value')  // value attribute 

返回 value 属性,并且始终是最初设置的 Awesome Angular

元素属性的主要作用是在创建相应的 DOM 对象时初始化元素的状态。

还有许多其他细微差别增加了这种混淆。以下是一些例子:

  • 属性和属性同步在属性之间并不一致。正如我们在前面的例子中所看到的,对 input 上的 value 属性的更改不会影响 value 属性,但这并不是所有属性值对的普遍情况。图像元素的 src 属性是这种情况的一个主要例子;属性或属性值的更改始终保持同步。

  • 令人惊讶的是,属性和属性之间的映射也不是一对一的。有许多属性没有任何后置属性(例如 innerHTML),也有属性在 DOM 上没有定义相应的属性(例如 colspan)。

  • 属性和属性映射也增加了这种混淆,因为它们并不遵循一个一致的模式。Angular 开发者指南中有一个很好的例子,我们将在这里逐字复制:

disabled 属性是另一个独特的例子。按钮的 disabled 属性默认为 false,因此按钮是启用的。当我们添加禁用属性时,其存在本身就会将按钮的 disabled 属性初始化为 true,从而使按钮被禁用。添加和删除禁用属性会启用和禁用按钮。属性值无关紧要,这就是为什么我们不能通过编写 <button disabled="false">仍被禁用</button> 来启用按钮。

这次讨论的目的是确保我们理解 DOM 元素的属性和属性之间的区别。这个新的思维模型将帮助我们继续探索框架的属性和属性绑定功能。让我们回到我们对属性绑定的讨论。

属性绑定继续...

现在我们已经了解了属性和属性之间的区别,让我们再次看看绑定示例:

<img class="img-responsive" [src]="'/static/images/' + currentExercise.exercise.image" /> 

使用 [propertName] 方括号语法将 img.src 属性绑定到 Angular 表达式。

属性绑定的通用语法如下:

[target]="sourceExpression"; 

在属性绑定的案例中,target 是 DOM 元素或组件上的一个属性。使用属性绑定,我们可以直接绑定到元素 DOM 上的任何属性。img 元素上的 src 属性就是我们使用的;这种绑定适用于任何 HTML 元素及其上的所有属性。

表达式目标也可以是一个事件,正如我们将在探索事件绑定时很快看到的。

绑定源和目标理解 Angular 绑定中的源和目标之间的区别很重要。出现在 [] 内部的属性是一个目标,有时被称为绑定目标。目标是数据的消费者,始终指向组件/元素上的一个属性。表达式构成了为目标提供数据的源。

在运行时,表达式在组件的/元素的属性上下文中被评估(在前面的案例中是 WorkoutRunnerComponent.currentExercise.exercise.image 属性)。

总是要记得在目标周围添加方括号 []。如果我们不这样做,Angular 会将表达式视为一个字符串常量,并且目标简单地被分配了字符串值。

属性绑定、事件绑定和属性绑定都不使用插值符号。以下是不合法的:[src]="{{'/static/images/' + currentExercise.exercise.image}}"

如果你曾经使用过 AngularJS,属性绑定与事件绑定一起使用可以让 Angular 摆脱许多指令,例如 ng-disableng-srcng-key*ng-mouse* 以及一些其他指令。

从数据绑定的角度来看,Angular 以与原生元素相同的方式处理组件。因此,属性绑定也适用于组件属性!组件可以定义输入输出属性,这些属性可以绑定到视图,例如:

<workout-runner [exerciseRestDuration]="restDuration"></workout-runner> 
exerciseRestDuration property on the WorkoutRunnerComponent class to the restDuration property defined on the container component (parent), allowing us to pass the rest duration as a parameter to the WorkoutRunnerComponent. As we enhance our app and develop new components, you will learn how to define custom properties and events on a component.

我们可以使用 bind- 语法启用属性绑定,这是属性绑定的规范形式。这意味着 [src]="'/assets/images/' + currentExercise.exercise.image" 等同于以下:bind-src="img/' + currentExercise.exercise.image"

属性绑定,就像插值一样,是单向的,从组件/元素源到视图。模型数据的变化与视图保持同步。

我们刚刚创建的模板视图中只有一个属性绑定(在 [src] 上)。其他带有方括号的绑定不是属性绑定。我们将在稍后介绍它们。

插值是属性绑定的语法糖

我们通过将插值描述为属性绑定的语法糖来结束关于插值的章节。目的是强调两者可以互换使用。插值语法比属性绑定更简洁,因此非常有用。这就是 Angular 解释插值的方式:

<h3>Main heading - {{heading}}</h3> 
<h3 [text-content]="' Main heading - '+ heading"></h3>

Angular 将第一个语句中的插值转换为 textContent 属性绑定(第二个语句)。

插值可以在你想象不到的更多地方使用。以下示例对比了使用插值和属性绑定执行相同绑定的例子:

<img [src]="'/assets/images/' + currentExercise.exercise.image" />
<img src="img/{{currentExercise.exercise.image}}" />      // interpolation on attribute

<span [text-content]="helpText"></span>
<span>{{helpText}}</span>

虽然属性绑定(和插值)使我们能够轻松地将任何表达式绑定到目标属性,但我们应该小心使用的表达式。Angular 的变更检测系统将在应用的整个生命周期中多次评估你的表达式绑定,只要我们的组件是活跃的。因此,在将表达式绑定到属性目标时,请记住以下两个准则。

快速表达式评估

属性绑定表达式应该快速评估。慢速表达式评估会杀死你的应用性能。这种情况发生在执行 CPU 密集型工作的函数是表达式的一部分时。考虑这个绑定:

<div>{{doLotsOfWork()}}</div> 

Angular 每次执行变更检测运行时都会评估前面的doLotsOfWork()表达式。这些变更检测运行比我们想象的要频繁,并且基于一些内部启发式算法,因此我们使用的表达式必须快速评估。

无副作用绑定表达式

如果在绑定表达式中使用函数,则该函数应该是无副作用的。考虑另一个绑定:

<div [innerHTML]="getContent()"></div> 

以及其底层函数,getContent

getContent() { 
  var content=buildContent(); 
  this.timesContentRequested +=1; 
  return content; 
} 

getContent调用通过每次调用时更新timesContentRequested属性来改变组件的状态。如果这个属性在如下视图中使用:

<div>{{timesContentRequested}}</div> 

Angular 会抛出如下错误:

Expression '{{getContent()}}' in AppComponent@0:4' has changed after it was checked. Previous value: '1'. Current value: '2'

Angular 框架以两种模式运行,开发模式和产品模式。如果我们启用了应用程序的产品模式,前面的错误就不会显示。查看框架文档bit.ly/enableProdMode以获取更多详细信息。

核心问题是你在属性绑定中使用的表达式应该是无副作用的。

现在我们来看一个有趣的东西,[ngStyle],它看起来像属性绑定,但实际上不是。在[]中指定的目标不是一个组件/元素属性(div没有ngStyle属性),而是一个指令。

需要介绍两个新概念,目标选择指令

Angular 指令

作为一款框架,Angular 试图增强 HTML DSL(即领域特定语言):

  • 在 HTML 中使用自定义标签如<abe-workout-runner></abe-workout-runner>(不是标准 HTML 结构的一部分)来引用组件。这突出了第一个扩展点。

  • 使用[]()进行属性和事件绑定定义了第二个。

  • 然后是指令,这是第三个扩展点,它们进一步分为属性指令结构指令,以及组件(组件也是指令!)。

虽然组件自带视图,但属性指令旨在增强现有元素/组件的外观和/或行为。

结构指令没有自己的视图;它们改变它们所应用元素的 DOM 布局。我们将在本章后面部分用完整章节来理解这些结构指令。

workout-runner视图中使用的ngStyle指令实际上是一个属性指令:

<div class="progress-bar" role="progressbar"  
 [ngStyle] = "{'width':(exerciseRunningDuration/currentExercise.duration) * 100 + '%'}"></div>  

ngStyle指令没有自己的视图;相反,它允许我们通过绑定表达式在 HTML 元素上设置多个样式(在这种情况下是width)。我们将在本书的后面部分介绍许多框架属性指令。

指令命名法

指令是一个总称,用于指代组件指令(也称为组件)、属性指令和结构指令。在本书中,当我们使用“指令”一词时,我们将根据上下文指代属性指令或结构指令。组件指令始终被称为组件。

在对 Angular 具有基本指令类型理解的基础上,我们可以理解绑定目标选择的过程。

绑定目标选择

[]中指定的目标不仅限于组件/元素属性。虽然属性名是一个常见的目标,但 Angular 模板引擎实际上会进行启发式搜索以决定目标类型。Angular 首先搜索具有匹配选择器的已注册已知指令(属性或结构),然后再寻找与目标表达式匹配的属性。考虑以下视图片段:

<div [ngStyle]='expression'></div> 

目标搜索从框架查看所有具有匹配选择器(ngStyle)的内部和自定义指令开始。由于 Angular 已经有一个NgStyle指令,因此它成为目标(指令类名为NgStyle,而选择器为ngStyle)。如果 Angular 没有内置的NgStyle指令,绑定引擎就会在底层组件上查找名为ngStyle的属性。

如果没有匹配目标表达式的内容,则会抛出未知指令错误*.*

这样就完成了我们对目标选择的讨论。下一节将关于属性绑定。

属性绑定

在 Angular 中,属性绑定存在的唯一原因是存在一些 HTML 属性没有对应的 DOM 属性。colspanaria属性就是一些没有对应属性的属性的例子。我们视图中的进度条 div 使用了属性绑定。

如果属性指令还在让你感到困惑,我无法责怪你,这可能会变得有点复杂。从根本上说,它们是不同的。属性指令(如[ngStyle])会改变 DOM 元素的外观或行为,正如其名称所暗示的那样,它们是指令。任何 HTML 元素上都没有名为ngStyle的属性或属性。另一方面,属性绑定完全是关于绑定到没有 DOM 属性支持的 HTML 属性。

7 分钟锻炼 在两个地方使用了属性绑定,[attr.aria-valuenow][attr.aria-valuemax]。我们可能会问一个问题:我们可以使用标准插值语法来设置属性吗?不,这不起作用!让我们试试:打开 workout-runner.component.html 并将两个被突出显示的 aria 属性 attr.aria-valuenowattr.aria-valuemax 包围在 [] 中替换为以下代码:

<div class="progress-bar" role="progressbar"  
    aria-valuenow = "{{exerciseRunningDuration}}"  
    aria-valuemin="0"  
    aria-valuemax= "{{currentExercise.duration}}"  ...> </div> 

保存视图,如果应用没有运行,请运行它。此错误将在浏览器控制台中弹出:

Can't bind to 'ariaValuenow' since it isn't a known native property in WorkoutRunnerComponent ... 

Angular 正在尝试在不存在 ariaValuenow 属性的 div 中查找一个名为 ariaValuenow 的属性!记住,插值实际上是属性绑定。

我们希望这能说明问题:要绑定到 HTML 属性,请使用属性绑定。

Angular 默认绑定到属性,而不是绑定到属性。

为了支持属性绑定,Angular 在 [] 内使用前缀表示法 attr。属性绑定看起来如下所示:

[attr.attribute-name]="expression" 

恢复原始的 aria 设置以使属性绑定生效:

<div ... [attr.aria-valuenow]="exerciseRunningDuration" 
    [attr.aria-valuemax]="currentExercise.duration" ...> 

记住,除非显式附加了 attr. 前缀,否则属性绑定不会生效。

尽管我们在我们的锻炼视图中没有使用样式和基于类的绑定,但这些是一些可能派上用场的绑定能力。因此,它们值得探索。

样式和类绑定

我们使用 类绑定 根据组件状态设置和移除特定的类,如下所示:

[class.class-name]="expression" 

expressiontrue 时添加 class-name,当它为 false 时移除它。一个简单的例子可以如下所示:

<div [class.highlight]="isPreferred">Jim</div> // Toggles the highlight class 

使用样式绑定根据组件状态设置内联样式:

[style.style-name]="expression";

虽然我们已经使用了 ngStyle 指令来处理锻炼视图,但我们也可以轻松地使用样式绑定,因为我们只处理一个样式。使用样式绑定,相同的 ngStyle 表达式将变为以下内容:

[style.width.%]="(exerciseRunningDuration/currentExercise.duration) * 100" 

width 是一个样式,因为它也接受单位,所以我们扩展我们的目标表达式以包括 % 符号。

记住,style.class. 是设置单个类或样式的便捷绑定。为了获得更多灵活性,有相应的属性指令:ngClassngStyle

在本章的早期部分,我们正式介绍了指令及其分类。其中一种指令类型,属性指令(再次提醒,不要与我们在上一节中介绍的属性绑定混淆)将是下一节关注的焦点。

属性指令

属性指令是 HTML 扩展,可以改变组件/元素的外观、感觉或行为。如 Angular 指令部分所述,这些指令不定义自己的视图。

除了 ngStylengClass 指令之外,还有一些属性指令是核心框架的一部分。ngValuengModelngSelectOptionsngControlngFormControl 是 Angular 提供的一些属性指令。

由于 7 分钟健身法 使用了 ngStyle 指令,因此深入了解这个指令及其紧密相关的 ngClass 是明智的。

虽然下一节是关于学习如何使用 ngClassngStyle 属性指令,但我们直到第六章 Angular 指令深入 才学习如何创建自己的属性指令。

使用 ngClass 和 ngStyle 样式化 HTML

Angular 有两个优秀的指令,允许我们动态地为任何元素设置样式并切换 CSS 类。对于 Bootstrap 进度条,我们使用 ngStyle 指令动态设置元素的样式,width,随着练习的进行:

<div class="progress-bar" role="progressbar" ... 
    [ngStyle]="{'width':(exerciseRunningDuration/currentExercise.duration) * 100 + '%'}"> </div> 

ngStyle 允许我们一次性将一个或多个样式绑定到组件的属性上。它接受一个对象作为参数。对象上的每个属性名是样式名,值是绑定到该属性的 Angular 表达式,如下例所示:

<div [ngStyle]= "{ 
'width':componentWidth,  
'height':componentHeight,  
'font-size': 'larger',  
'font-weight': ifRequired ? 'bold': 'normal' }"></div> 

样式不仅可以绑定到组件属性(componentWidthcomponentHeight),还可以设置为常量值('larger')。表达式解析器还允许使用三元运算符(?:);查看 isRequired

如果在 HTML 中样式变得难以管理,我们也有选择在组件中编写一个函数,该函数返回对象哈希,并将其设置为表达式:

<div [ngStyle]= "getStyles()"></div> 

此外,组件上的 getStyles 看起来如下:

getStyles () { 
    return { 
      'width':componentWidth, 
      ... 
    } 
} 

ngClass 的工作原理也类似,只不过它用于切换一个或多个类。例如,查看以下代码:

<div [ngClass]= "{'required':inputRequired, 'email':whenEmail}"></div> 

inputRequiredtrue 时应用 required 类,当它评估为 false 时移除。

指令(自定义或平台)就像任何其他 Angular 艺术品一样,始终属于一个模块。要跨模块使用它们,模块需要被导入。想知道 ngStyle 在哪里定义吗?ngStyle 是核心框架模块 CommonModule 的一部分,并在 workout-runner.module.ts 模块定义中导入。CommonModule 定义了多个有用的指令,这些指令在 Angular 中被广泛使用。

好吧!这就涵盖了我们需要了解的关于我们新开发视图的所有内容。

如前所述,如果您在运行代码时遇到问题,请查看 Git 分支 checkpoint2.2。如果不使用 Git,请从 bit.ly/ng2be-checkpoint2-2 下载 checkpoint2.2 的快照(ZIP 文件)。在首次设置快照时,请参考 trainer 文件夹中的 README.md 文件。

是时候添加一些增强功能并学习更多关于框架的知识了!

了解更多关于练习的信息

对于第一次做这个练习的人来说,详细说明每个练习涉及的步骤会很好。我们还可以为每个练习添加一些 YouTube 视频的引用,以帮助用户更好地理解练习。

我们将在左侧面板添加练习描述和说明,并将其称为描述面板。我们还将添加 YouTube 视频的引用,这是视频播放面板。为了使事情更加模块化并学习一些新概念,我们将为每个描述面板和视频面板创建独立的组件。

此模型的已有数据。Exercise类中的descriptionprocedure属性(见model.ts)提供了关于练习的必要细节。videos数组包含一些相关的 YouTube 视频 ID,这些视频将被用于获取这些视频。

添加描述和视频面板

一个 Angular 应用不过是一个组件的层次结构,类似于树结构。到目前为止,7 Minute Workout有两个组件,根组件AppComponent及其子组件WorkoutRunnerComponent,与 HTML 组件布局一致,现在看起来如下所示:

<abe-root>
    ...
    <abe-workout-runner>...</abe-workout-runner>
</abe-root>

运行应用并查看源代码以验证此层次结构。随着我们在应用程序中添加更多组件以实现新功能,此组件树会增长并分支。

我们将在WorkoutRunnerComponent中添加两个子组件,每个子组件分别支持练习描述和练习视频。虽然我们可以在WorkoutRunnerComponent视图中直接添加一些 HTML,但我们在这里希望学习更多关于跨组件通信的知识。让我们从添加左侧的描述面板开始,并了解组件如何接受输入。

带有输入的组件

导航到workour-runner文件夹,生成一个模板练习描述组件:

ng generate component exercise-description -is

在生成的exercise-description.component.ts文件中,添加突出显示的代码:

import { Component, OnInit, Input } from '@angular/core';
...
export class ExerciseDescriptionComponent { 
 @Input() description: string; 
  @Input() steps: string; } 

@Input装饰器表示组件属性可用于数据绑定。在我们深入研究@Input装饰器之前,让我们完成视图并将其与WorkoutRunnerComponent集成。

从 Git 分支checkpoint2.3中的workout-runner/exercise-description文件夹(GitHub 位置:bit.ly/ng6be-2-3-exercise-description-component-html)复制练习描述的视图定义,exercise-description.component.html。查看突出显示的练习描述 HTML:

<div class="card-body">
    <div class="card-text">{{description}}</div>
</div> 
...  
<div class="card-text">
    {{steps}}
</div> 

前面的插值引用了ExerciseDescriptionComponent的输入属性:descriptionsteps

组件定义已完成。现在,我们只需在WorkoutRunnerComponent中引用ExerciseDescriptionComponent,并为ExerciseDescriptionComponent视图提供descriptionsteps的值,以便正确渲染。

打开workout-runner.component.html,并更新以下代码中突出显示的 HTML 片段。在exercise-pane div 之前添加一个新的 div,名为description-panel,并对exercise-pane div 进行调整一些样式,如下所示:

<div class="row">
    <div id="description-panel" class="col-sm-3">
 <abe-exercise-description 
            [description]="currentExercise.exercise.description"
 [steps]="currentExercise.exercise.procedure"></abe-exercise-description>
 </div>
   <div id="exercise-pane" class="col-sm-6">  
   ... 

如果应用程序正在运行,描述面板应显示在左侧,并显示相关的练习详情。

WorkoutRunnerComponent能够使用ExerciseDescriptionComponent是因为它已经在WorkoutRunnerModule上声明过(参见workout-runner.module.ts声明属性)。Angular CLI 组件生成器为我们完成了这项工作。

回顾前一个视图中的abe-exercise-description声明。我们以与本章早期使用 HTML 元素属性相同的方式引用descriptionsteps属性(<img [src]='expression' ...>)。简单、直观且非常优雅!

Angular 数据绑定基础设施确保每当WorkoutRunnerComponent上的currentExercise.exercise.descriptioncurrentExercise.exercise.procedure属性发生变化时,ExerciseDescriptionComponentdescriptionsteps上的绑定属性也会更新。

@Input装饰器可以接受一个属性别名作为参数,这意味着以下内容:考虑一个属性声明,例如:@Input("myAwesomeProperty") myProperty:string。它可以在视图中如下引用:<my-component [myAwesomeProperty]="expression"....>

Angular 绑定基础设施的力量允许我们通过将其附加@Input装饰器(以及@Output)来使用任何组件属性作为可绑定属性。我们不仅限于基本数据类型,如stringnumberboolean;还可以是复杂对象,我们将在添加视频播放器时看到这一点:

@Input装饰器也可以应用于复杂对象。

workout-runner目录下生成一个新的组件用于视频播放器:

ng generate component video-player -is

通过从video-player.component.tsvideo-player.component.html(位于 Git 分支checkpoint2.3中的trainer/src/components/workout-runner/video-player文件夹,GitHub 位置:bit.ly/ng6be-2-3-video-player)复制实现来更新生成的模板代码。

让我们看看视频播放器的实现。打开video-player.component.ts并查看VideoPlayerComponent类:

export class VideoPlayerComponent implements OnInit, OnChanges { 
  private youtubeUrlPrefix = '//www.youtube.com/embed/'; 

  @Input() videos: Array<string>; 
  safeVideoUrls: Array<SafeResourceUrl>; 

  constructor(private sanitizer: DomSanitizationService) { } 

  ngOnChanges() { 
    this.safeVideoUrls = this.videos ? 
        this.videos 
            .map(v => this.sanitizer.bypassSecurityTrustResourceUrl(this.youtubeUrlPrefix + v)) 
    : this.videos; 
  } 
} 

此处的videos输入属性接受一个字符串数组(YouTube 视频代码)。虽然我们将videos数组作为输入,但我们不会直接在视频播放器视图中使用此数组;相反,我们将输入数组转换成一个新的safeVideoUrls数组并绑定它。这可以通过查看视图实现来确认:

<div *ngFor="let video of safeVideoUrls"> 
   <iframe width="198" height="132" [src]="video" frameborder="0" allowfullscreen></iframe> 
</div> 

视图还使用了一个新的 Angular 指令ngFor来绑定到safeVideoUrls数组。ngFor指令属于一类称为结构指令的指令。该指令的职责是接受一个 HTML 片段,并根据绑定集合中的元素数量重新生成它。

如果你对于 ngFor 指令如何与 safeVideoUrls 一起工作以及为什么我们需要生成 safeVideoUrls 而不是使用 videos 输入数组感到困惑,请稍等,我们很快就会解答这些问题。但首先,让我们完成 VideoPlayerComponentWorkoutRunnerComponent 的集成,以查看最终结果。

通过在 exercise-pane div 后添加组件声明来更新 WorkoutRunnerComponent 视图:

<div id="video-panel" class="col-sm-3">
    <abe-video-player [videos]="currentExercise.exercise.videos"></abe-video-player>
</div> 

VideoPlayerComponentvideos 属性绑定到练习的视频集合。

启动/刷新应用程序,视频缩略图应该显示在右侧。

如果你在运行代码时遇到问题,请查看 Git 分支 checkpoint2.3 以获取我们迄今为止所做工作的有效版本。您还可以从 bit.ly/ng6be-checkpoint-2-3 下载 checkpoint2.3 的快照(ZIP 文件)。在首次设置快照时,请参考 trainer 文件夹中的 README.md 文件。

现在,是时候回顾 VideoPlayerComponent 的实现部分了。我们特别需要理解:

  • ngFor 指令是如何工作的

  • 为什么需要将输入 videos 数组转换为 safeVideoUrls

  • Angular 组件生命周期事件 OnChanges(在视频播放器中使用)的重要性

首先,是时候正式介绍 ngFor 以及它所属的指令类别:结构化指令类别了。

结构化指令

指令的第三种分类,结构化指令,通过操作组件/元素来操纵它们的布局。

Angular 文档以简洁的方式描述了结构化指令:

"与组件指令定义和控制视图,或者像属性指令一样修改元素的外观和行为不同,结构化指令通过添加和删除整个元素子树来操纵布局。"

由于我们已经讨论了组件指令(如 workout-runnerexercise-description)和属性指令(如 ngClassngStyle),我们可以很好地将它们的行为与结构化指令进行对比。

ngFor 指令属于此类。我们可以通过 * 前缀轻松识别此类指令。除了 ngFor 之外,Angular 还提供了一些其他结构化指令,例如 ngIfngSwitch

非常有用的 NgForOf

每种模板语言都有允许模板引擎生成 HTML(通过重复)的构造。Angular 有 NgForOfNgForOf 指令是一个非常实用的指令,用于将 HTML 片段重复 n 次以上。让我们再次看看我们在视频播放器中是如何使用 NgForOf 的:

<div *ngFor="let video of safeVideoUrls"> 
   <iframe width="198" height="132" [src]="video" frameborder="0" allowfullscreen></iframe> 
</div>

NgForOf 的指令选择器为 {selector: '[ngFor][ngForOf]'},因此我们可以在视图模板中使用 ngForngForOf。我们有时也把此指令称为 ngFor

前面的代码会为每个练习视频(使用 safeVideoUrls 数组)重复 div 片段。let video of safeVideoUrls 字符串表达式被解释如下:取 safeVideoUrls 数组中的每个视频并将其分配给模板输入变量 video

现在,这个输入变量可以在 ngFor 模板 HTML 中被引用,就像我们设置 src 属性绑定时做的那样。

有趣的是,分配给 ngFor 指令的字符串不是一个典型的 Angular 表达式。相反,它是一个 微语法——一种微语言,Angular 引擎可以解析。

你可以在 Angular 的开发者指南中了解更多关于微语法的知识:bit.ly/ng6be-micro-syntax

这种微语法公开了一些迭代上下文属性,我们可以将它们分配给模板输入变量并在 ngFor HTML 块中使用它们。

其中一个例子是 indexindex 在每次迭代中从 0 增加到数组的长度,类似于任何编程语言中的 for 循环。以下示例显示了如何捕获它:

<div *ngFor="let video of videos; let i=index"> 
     <div>This is video - {{i}}</div> 
</div> 

除了 index 之外,还有一些其他的迭代上下文变量;这些包括 firstlastevenodd。这些上下文数据允许我们做一些巧妙的事情。考虑以下示例:

<div *ngFor="let video of videos; let i=index; let f=first"> 
     <div [class.special]="f">This is video - {{i}}</div> 
</div> 

它将一个 特殊 类应用到第一个视频 div 上。

NgForOf 指令可以应用于 HTML 元素以及我们的自定义组件。这是 NgForOf 的有效用法:

<user-profile *ngFor="let userDetail of users" [user]= "userDetail"></user-profile>

总是要记得在 ngFor(以及其他结构化指令)之前添加一个星号(*)。* 有其重要性。

结构化指令中的星号(*)

* 前缀是一种更简洁的结构化指令表示格式。例如,视频播放器使用 ngFor 的用法。ngFor 模板:

<div *ngFor="let video of safeVideoUrls"> 
   <iframe width="198" height="132" [src]="video" frameborder="0" allowfullscreen></iframe> 
</div>

实际上扩展为以下内容:

<ng-template ngFor let-video [ngForOf]="safeVideoUrls">  
    <div>
        <iframe width="198" height="132"  [src]="video" ...></iframe>  
    </div> 
</ng-template>  

ng-template 标签是一个 Angular 元素,它声明了 ngFor,一个模板输入变量(video),以及一个属性(ngForOf),该属性指向 safeVideoUrls 数组。前两个声明都是 ngFor 的有效用法。

不确定你是否这样,但我更喜欢 ngFor 的简洁格式!

NgForOf 性能

由于 NgForOf 根据集合元素生成 HTML,因此它因导致性能问题而臭名昭著。但我们不能责怪这个指令。它做了它应该做的事情:迭代并生成元素!如果底层集合很大,UI 渲染可能会受到性能影响,尤其是如果集合变化过于频繁。对变化集合不断销毁和创建元素的成本可能会迅速变得难以承受。

NgForOf 的性能调整之一允许我们改变 ngForOf 在创建和销毁 DOM 元素(当底层集合元素被添加或删除时)时的行为。

想象一个场景,我们经常从服务器获取一个对象数组,并使用 NgForOf 将其绑定到视图中。NgForOf 的默认行为是在每次刷新列表时重新生成 DOM(因为 Angular 进行标准的对象相等性检查)。然而,作为开发者,我们可能非常清楚变化不大。可能添加了一些新对象,删除了一些,也许还有一些被修改了。但 Angular 只是重新生成了完整的 DOM。

为了缓解这种情况,Angular 允许我们指定一个自定义的跟踪函数,这样 Angular 就知道当比较的两个对象相等时。看看下面的函数:

trackByUserId(index: number, hero: User) { return user.id; } 

可以使用这样的函数在 NgForOf 模板中告诉 Angular 根据其 id 属性比较 user 对象,而不是进行引用相等性检查。

这就是我们如何在 NgForOf 模板中使用前面提到的函数:

<div *ngFor="let user of users; trackBy: trackByUserId">{{user.name}}</div> 

NgForOf 现在将避免为已经渲染的具有 ID 的用户重新创建 DOM。

记住,如果用户的绑定属性已更改,Angular 可能仍然会更新现有的 DOM 元素。

关于 ngFor 指令就讲到这里;让我们继续前进。

我们仍然需要理解 safeVideoUrlsOnChange 生命周期事件在 VideoPlayerComponent 实现中的作用。让我们先解决前者,并理解 safeVideoUrls 的必要性。

Angular 安全性

通过尝试 videos 数组,我们可以最容易地理解为什么我们需要绑定到 safeVideoUrls 而不是 videos 输入属性。用以下内容替换现有的 ngFor 片段 HTML:

<div *ngFor="let video of videos"> 
    <iframe width="198" height="132"  
        [src]="'//www.youtube.com/embed/' + video"  frameborder="0" allowfullscreen></iframe> 
</div>

然后看看浏览器的控制台日志(可能需要刷新页面)。框架抛出了一大堆错误,例如:

Error: 在资源 URL 上下文中使用了不安全的值(请参阅 http://g.co/ng/security#xss)

不用猜测正在发生什么!Angular 正在尝试保护我们的应用程序免受 跨站脚本攻击(XSS)。

这种攻击使攻击者能够将恶意代码注入我们的网页中。一旦注入,恶意代码可以读取当前站点上下文中的数据。这使得它能够窃取机密数据,并冒充已登录的用户,从而获得对受保护资源的访问权限。

Angular 已经被设计用来通过清理任何注入到 Angular 视图中的外部代码/脚本来阻止这些攻击。记住,内容可以通过多种机制注入到视图中,包括属性/属性/样式绑定或插值。

考虑一个通过组件模型将 HTML 标记绑定到 HTML 元素的 innerHTML 属性(属性绑定)的例子:

this.htmlContent = '<span>HTML content.</span>'    // Component

<div [innerHTML]="htmlContent"> <!-- View -->

当 HTML 内容被发出时,任何不安全的内容(如 script)如果存在,将被移除。

但关于 Iframes 呢?在我们前面的例子中,Angular 也阻止了对 Iframe 的 src 属性进行属性绑定。这是对使用 Iframe 在我们自己的网站上嵌入第三方内容的警告。Angular 也阻止了这种行为。

总的来说,该框架定义了四个关于内容清理的安全上下文。这些包括:

  1. HTML 内容清理,当使用 innerHTML 属性绑定 HTML 内容时

  2. 样式清理,当将 CSS 绑定到 style 属性时

  3. URL 清理,当使用 anchorimg 等标签时

  4. 资源清理,当使用 Iframesscript 标签时;在这种情况下,内容无法清理,因此默认情况下会被阻止

Angular 正尽力让我们远离危险。但有时,我们知道内容是安全的,因此想要绕过默认的清理行为。

信任安全内容

为了让 Angular 知道正在绑定的内容是安全的,我们使用 DomSanitizer 并根据刚才描述的安全上下文调用适当的方法。可用的函数如下:

  • bypassSecurityTrustHtml

  • bypassSecurityTrustScript

  • bypassSecurityTrustStyle

  • bypassSecurityTrustUrl

  • bypassSecurityTrustResourceUrl

在我们的视频播放器实现中,我们使用 bypassSecurityTrustResourceUrl;它将视频 URL 转换为可信的 SafeResourceUrl 对象:

this.videos.map(v => this.sanitizer.bypassSecurityTrustResourceUrl(this.youtubeUrlPrefix + v)) 

map 方法将视频数组转换为 SafeResourceUrl 对象集合,并将其分配给 safeVideoUrls

列出的每个方法都接受一个字符串参数。这是我们希望 Angular 知道是安全的内容。返回的对象可以是 SafeStyleSafeHtmlSafeScriptSafeUrlSafeResourceUrl 中的任何一个,然后可以将其绑定到视图中。

有关这个主题的全面介绍可以在框架安全指南中找到,该指南可在 bit.ly/ng6be-security 获取。强烈推荐阅读!

最后一个问题是要回答为什么在 OnChanges Angular 生命周期事件中这样做?

OnChange 生命周期事件

OnChanges 生命周期事件在组件的输入(s)更改时触发。对于 VideoPlayerComponent 来说,当加载新的练习时,videos 数组输入属性会更改。我们使用这个生命周期事件来重新创建 safeVideoUrls 数组并将其重新绑定到视图中。简单!

视频面板实现现已完成。让我们添加一些更多的小增强,并在 Angular 中进一步探索它。

使用 innerHTML 绑定格式化练习步骤

当前应用程序的一个痛点是练习步骤的格式化。这些步骤读起来有点困难。

步骤应该有换行符 (<br>) 或格式化为 HTML list,以便易于阅读。这似乎是一个简单的任务,我们可以直接更改绑定到步骤插值的绑定数据,或者编写一个管道,使用行分隔约定(.)添加一些 HTML 格式化。为了快速验证,让我们通过在 workout-runner.component.ts 中的第一个练习步骤后添加一个换行符(<br>)来更新它:

`Assume an erect position, with feet together and arms at your side. <br> 
 Slightly bend your knees, and propel yourself a few inches into the air. <br> 
 While in air, bring your legs out to the side about shoulder width or slightly wider. <br> 
 ... 

当锻炼重新开始时,看看第一个锻炼步骤。输出与我们的预期不符,如下所示:

换行标签在浏览器中被直接渲染。Angular 没有将插值作为 HTML 渲染;相反,它转义了 HTML 字符,我们知道为什么,安全!

如何修复它?很简单!将插值替换为属性绑定,将步骤数据绑定到元素的innerHTML属性(在exercise-description.html中),然后你就完成了!

<div class="card-text" [innerHTML]="steps"> 

刷新锻炼页面以确认。

防止跨站脚本(XSS)安全问题

通过使用innerHTML,我们指示 Angular 不要转义 HTML,但 Angular 仍然会像前面安全部分中描述的那样对输入 HTML 进行清理。它会移除如<script>标签和其他 JavaScript,以防止 XSS 攻击。如果你想在 HTML 中动态注入样式/脚本,请使用DomSanitizer来绕过这个清理检查。

是时候进行另一个增强了!现在是学习 Angular 管道的时候了。

使用管道显示剩余锻炼时长

如果我们能在锻炼过程中告诉用户剩余时间而不是仅仅显示正在进行的锻炼时长,那就太好了。我们可以在锻炼面板中添加一个倒计时计时器来显示剩余的总时间。

我们在这里将要采取的方法是定义一个名为workoutTimeRemaining的组件属性。这个属性将在锻炼开始时初始化为总时间,并且每过一秒就会减少,直到达到零。由于workoutTimeRemaining是一个数值,但我们想以hh:mm:ss格式显示计时器,我们需要在秒数据和时间格式之间进行转换。Angular 管道是实现此功能的一个很好的选择。

Angular 管道

管道的主要目的是格式化视图中显示的数据。管道允许我们将这种内容转换逻辑(格式化)封装为一个可重用的元素。框架本身提供了多个预定义的管道,例如datecurrencylowercaseuppercaseslice等。

这就是我们如何在视图中使用管道。

{{expression | pipeName:inputParam1}} 

表达式后面跟着管道符号(|),然后是管道名称,然后是一个可选的参数(inputParam1),参数之间用冒号(:)分隔。如果管道需要多个输入,它们可以一个接一个地放置,用冒号分隔,例如内置的slice管道,它可以切割数组或字符串:

{{fullName | slice:0:20}} //renders first 20 characters  

传递给管道的参数可以是一个常量或组件属性,这意味着我们可以使用管道参数的模板表达式。请看以下示例:

{{fullName | slice:0:truncateAt}} //renders based on value truncateAt 

这里有一些使用date管道的示例,如 Angular date文档中所述。假设dateObj初始化为2015 年 6 月 15 日,时间为21:43:11,地区为en-US

{{ dateObj | date }}               // output is 'Jun 15, 2015        ' 
{{ dateObj | date:'medium' }}      // output is 'Jun 15, 2015, 9:43:11 PM' 
{{ dateObj | date:'shortTime' }}   // output is '9:43 PM            ' 
{{ dateObj | date:'mmss' }}        // output is '43:11'     

最常用的管道如下:

  • 日期(date):正如我们刚才看到的,日期过滤器用于以特定方式格式化日期。这个过滤器支持相当多的格式,并且也是区域感知的。要了解日期管道支持的其他格式,请查看框架文档,网址为bit.ly/ng2-date

  • 大写(uppercase)小写(lowercase):这两个管道,正如其名所示,会改变字符串输入的大小写。

  • 小数(decimal)百分比(percent)decimalpercent管道用于根据当前浏览器区域设置格式化小数和百分比值。

  • 货币(currency):这用于根据当前浏览器区域设置将数值格式化为货币:

 {{14.22|currency:"USD" }} <!-Renders USD 14.22 --> 
    {{14.22|currency:"USD":'symbol'}}  <!-Renders $14.22 -->
  • JSON:这是一个方便的管道,用于调试,可以将任何输入转换为字符串,使用JSON.stringify。我们在本章开头就很好地使用了它来渲染WorkoutPlan对象(参见 Checkpoint 2.1 代码)。

  • 切片(slice):这个管道允许我们将列表或字符串值分割成更小的、经过裁剪的列表/字符串。我们已经在前面的代码中看到了一个例子。

我们不会详细介绍前面的管道。从开发角度来看,只要我们知道有哪些管道以及它们有什么用途,我们就可以始终参考平台文档以获取确切的使用说明。

管道链

管道的强大功能之一是它们可以被链式调用,其中一个管道的输出可以作为另一个管道的输入。考虑以下示例:

{{fullName | slice:0:20 | uppercase}} 

第一个管道将fullName的前 20 个字符切片,第二个管道将它们转换为大写。

现在我们已经看到了管道是什么以及如何使用它们,为什么不实现一个用于7 分钟锻炼应用的管道:一个秒到时间管道?

实现自定义管道 - SecondsToTimePipe

SecondsToTimePipe,正如其名所示,应该将数值转换为hh:mm:ss格式。

workout-runner文件夹中创建一个名为shared的文件夹,并从共享文件夹中调用以下 CLI 命令以生成管道模板:

ng generate pipe seconds-to-time

shared文件夹已经创建,用于添加在workout-runner模块中可以使用的公共组件/指令/管道。这是我们遵循的将共享代码组织在不同级别的约定。在未来,我们可以在应用模块级别创建一个共享文件夹,其中包含全局共享的工件。实际上,如果需要将时间到秒管道用于其他应用模块,也可以将其移动到应用模块中。

将以下transform函数实现复制到seconds-to-time.pipe.ts文件中(定义也可以从 GitHub 网站上的 Git 分支checkpoint.2.4下载:bit.ly/nng6be-2-4-seconds-to-time-pipe-ts):

export class SecondsToTimePipe implements PipeTransform { 
  transform(value: number): any { 
    if (!isNaN(value)) { 
      const hours = Math.floor(value / 3600);
      const minutes = Math.floor((value - (hours * 3600)) / 60);
      const seconds = value - (hours * 3600) - (minutes * 60);

      return ('0' + hours).substr(-2) + ':'
        + ('0' + minutes).substr(-2) + ':'
        + ('0' + seconds).substr(-2);
    } 
    return; 
  } 
} 

在 Angular 管道中,实现逻辑放入 transform 函数中。作为 PipeTransform 接口的一部分定义,前面的 transform 函数将输入的秒数值转换成 hh:mm:ss 字符串。transform 函数的第一个参数是管道输入。后续的参数(如果提供),是传递给管道的参数,使用冒号分隔符(pipe:argument1:arugment2..)从视图中传递。

对于 SecondsToTimePipe,虽然 Angular CLI 生成了一个样板参数(args?:any),但我们没有使用任何管道参数,因为实现不需要它。

管道实现相当简单,因为我们把秒数转换成小时、分钟和秒。然后,我们将结果连接成一个字符串值并返回该值。在每个 hoursminutesseconds 变量左侧添加 0 是为了格式化值,如果计算出的小时、分钟或秒的值小于 10,则前面会有一个前导 0。

我们刚刚创建的管道只是一个标准的 TypeScript 类。是管道装饰器(@Pipe)指示 Angular 将此类视为管道:

@Pipe({ 
  name: 'secondsToTime' 
}) 

管道定义已完成,但要使用管道在 WorkoutRunnerComponent 中,必须在 WorkoutRunnerModule. 中声明管道。Angular CLI 已经为我们完成了这部分工作,作为样板生成的一部分(参见 workout-runner.module.ts 中的 declaration 部分)。

现在我们只需要在视图中添加管道。通过添加高亮片段更新 workout-runner.component.html

<div class="exercise-pane" class="col-sm-6"> 
    <h4 class="text-center">Workout Remaining - {{workoutTimeRemaining | secondsToTime}}</h4>
    <h1 class="text-center">{{currentExercise.exercise.title}}</h1> 

惊讶的是,实现仍然没有完成!我们还有一个步骤。我们有一个管道定义,并且我们在视图中引用了它,但 workoutTimeRemaining 需要随着每一秒的过去更新,以便 SecondsToTimePipe 能够有效。

我们已经在 start 函数中初始化了 WorkoutRunnerComponentworkoutTimeRemaining 属性,设置为总训练时间:

start() { 
    this.workoutTimeRemaining = this.workoutPlan.totalWorkoutDuration(); 
    ... 
} 

现在的问题是:如何随着每一秒的过去更新 workoutTimeRemaining 变量?记住,我们已经有了一个 setInterval 设置来更新 exerciseRunningDuration。虽然我们可以为 workoutTimeRemaining 写另一个 setInterval 实现,但如果一个 setInterval 设置可以同时处理这两个要求会更好。

WorkoutRunnerComponent 添加一个名为 startExerciseTimeTracking 的函数;它看起来如下:

startExerciseTimeTracking() {
    this.exerciseTrackingInterval = window.setInterval(() => {
      if (this.exerciseRunningDuration >= this.currentExercise.duration) {
        clearInterval(this.exerciseTrackingInterval);
        const next: ExercisePlan = this.getNextExercise();
        if (next) {
          if (next !== this.restExercise) {
            this.currentExerciseIndex++;
          }
          this.startExercise(next);
        }
        else {
          console.log('Workout complete!');
        }
        return;
      }
      ++this.exerciseRunningDuration;
      --this.workoutTimeRemaining;
    }, 1000);
  }  

如您所见,该函数的主要目的是跟踪运动进度并在运动完成后翻转运动。然而,它也跟踪 workoutTimeRemaining(它会递减这个计数器)。第一个 if 条件设置只是确保一旦所有运动完成,就清除计时器。内部 if 条件用于保持 currentExerciseIndex 与正在进行的运动同步。

这个函数使用一个名为exerciseTrackingInterval的数字实例变量。将其添加到类声明部分。我们稍后将要使用这个变量来实现练习暂停功能。

startExercise中移除完整的setInterval设置,并用对this.startExerciseTimeTracking();的调用替换它。我们已经准备好测试我们的实现。如果需要,刷新浏览器并验证实现:

下一个部分将介绍另一个内置的 Angular 指令ngIf以及一些小的增强。

使用 ngIf 添加下一个练习指示器

在练习之间的短暂休息期间,如果用户能被告知下一个练习的内容,那将是一件很棒的事情。这将帮助他们为下一个练习做好准备。所以,让我们添加这个功能。

要实现这个功能,我们可以简单地从workoutPlan.exercises数组中输出下一个练习的标题。我们在Time Remaining倒计时部分旁边显示标题。

将锻炼区域(class="exercise-pane")改为包含高亮内容,并移除现有的Time Remaining h1

<div class="exercise-pane"> 
<!-- Exiting html --> 
   <div class="progress time-progress"> 
       <!-- Exiting html --> 
   </div> 
 <div class="row">
 <h4 class="col-sm-6 text-left">Time Remaining:
 <strong>{{currentExercise.duration-exerciseRunningDuration}}</strong>
 </h4>
 <h4 class="col-sm-6 text-right" *ngIf="currentExercise.exercise.name=='rest'">Next up:
 <strong>{{workoutPlan.exercises[currentExerciseIndex + 1].exercise.title}}</strong>
 </h4>
 </div>
</div> 

我们将现有的Time Remaining h1包装起来,并添加另一个h3标签来显示新div中的下一个练习,同时进行一些样式更新。此外,第二个h3中有一个新的指令ngIf*前缀表示它属于与ngFor相同的指令集:结构指令。让我们简单谈谈ngIf

ngIf指令用于根据提供给它的表达式返回truefalse来添加或删除 DOM 中的特定部分。当表达式评估为true时,DOM 元素被添加,否则被销毁。将ngIf声明从前面的视图中隔离出来:

ngIf="currentExercise.details.name=='rest'" 

指令表达式检查我们是否目前处于休息阶段,并根据此显示或隐藏链接的h3

同样在这个h3中,有一个插值表达式显示了来自workoutPlan.exercises数组的练习名称。

这里有一个注意事项:ngIf会添加和销毁 DOM 元素,因此它与我们用来显示和隐藏元素的可见性构造不同。虽然styledisplay:none的最终结果与ngIf相同,但机制完全不同:

<div [style.display]="isAdmin" ? 'block' : 'none'">Welcome Admin</div> 

与此行:

<div *ngIf="isAdmin" ? 'block' : 'none'">Welcome Admin</div> 

使用 ngIf,每当表达式从 false 变为 true 时,内容将进行完整的重新初始化。递归地,从父元素到子元素创建新元素/组件,并设置数据绑定,当表达式从 true 变为 false 时,发生相反的操作:所有这些都被销毁。因此,如果 ngIf 包裹了大量的内容,并且附加的表达式经常改变,那么使用 ngIf 有时可能成为一个昂贵的操作。但除此之外,将视图包裹在 ngIf 中比使用基于 CSS/样式的显示或隐藏更高效,因为当 ngIf 表达式评估为 false 时,既不会创建 DOM 也不会设置数据绑定表达式。

新版本的 Angular 也支持分支结构。这允许我们在视图 HTML 中实现 if then else 流程。以下示例直接来自 ngIf 的平台文档:

<div *ngIf="show; else elseBlock">Text to show</div>
<ng-template #elseBlock>Alternate text while primary text is hidden</ng-template>

else 绑定指向一个带有模板变量 #elseBlockng-template

这里还有一个属于这个系列的指令:ngSwitch。当在父 HTML 中定义时,它可以根据 ngSwitch 表达式交换子 HTML 元素。考虑以下示例:

<div id="parent" [ngSwitch] ="userType"> 
<div *ngSwitchCase="'admin'">I am the Admin!</div> 
<div *ngSwitchCase="'powerUser'">I am the Power User!</div> 
<div *ngSwitchDefault>I am a normal user!</div> 
</div> 

我们将 userType 表达式绑定到 ngSwitch。根据 userType 的值(adminpowerUser 或任何其他 userType),将渲染一个内部 div 元素。ngSwitchDefault 指令是一个通配符匹配/后备匹配,当 userType 既不是 admin 也不是 powerUser 时,它将被渲染。

如果你还没有意识到,请注意,这里有三个指令在这里协同工作,以实现类似 switch-case 的行为:

  • ngSwitch

  • ngSwitchCase

  • ngSwitchDefault

回到我们的下一个练习实现,我们准备验证实现,启动应用程序,并等待休息期。在休息阶段应该提到下一个练习,如下所示:

应用程序正在成形。如果你已经使用过该应用程序,并且与它一起进行了一些身体锻炼,你将非常怀念暂停锻炼功能。锻炼直到结束才会停止。我们需要修复这种行为。

暂停练习

要暂停锻炼,我们需要停止计时器。我们还需要在视图中添加一个按钮,允许我们暂停和恢复锻炼。我们计划通过在页面中心的锻炼区域绘制按钮覆盖层来实现这一点。点击时,它将在暂停和运行之间切换锻炼状态。我们还将添加键盘支持,使用键绑定 pP 来暂停和恢复锻炼。让我们更新组件。

更新 WorkoutRunnerComponent 类,添加这三个函数,并为 workoutPaused 变量添加声明:

workoutPaused: boolean; 
...
pause() { 
    clearInterval(this.exerciseTrackingInterval); 
    this.workoutPaused = true; 
} 

resume() { 
    this.startExerciseTimeTracking(); 
    this.workoutPaused = false; 
} 

pauseResumeToggle() { 
    if (this.workoutPaused) { this.resume();    } 
    else {      this.pause();    } 
} 

暂停的实现很简单。我们首先通过调用 clearInterval(this.exerciseTrackingInterval); 来取消现有的 setInterval 设置。在恢复时,我们再次调用 startExerciseTimeTracking,这再次从我们离开的地方开始跟踪时间。

现在我们只需要为视图调用 pauseResumeToggle 函数。将以下内容添加到 workout-runner.html

<div id="exercise-pane" class="col-sm-6"> 
 <div id="pause-overlay" (click)="pauseResumeToggle()"><span class="pause absolute-center" 
            [ngClass]="{'ion-md-pause' : !workoutPaused, 'ion-md-play' : workoutPaused}">
        </span> </div> 
    <div class="row workout-content"> 

div 上的 click 事件处理器切换锻炼的运行状态,并使用 ngClass 指令在 ion-md-pauseion-md-play 之间切换 - 这是标准的 Angular 东西。现在缺少的是在按 P 键时暂停和恢复的能力。

一种方法是在 div 上应用一个 keyup 事件处理器:

 <div id="pause-overlay" (keyup)= "onKeyPressed($event)"> 

但这种方法有一些不足之处:

  • div 元素没有焦点概念,因此我们还需要在 div 上添加 tabIndex 属性来使其工作

  • 即使如此,它也只有在至少点击过 div 一次的情况下才会工作

有一种更好的方法来实现这一点;将事件处理器附加到全局 window 事件 keyup。这就是事件绑定应该在 div 上应用的方式:

<div id="pause-overlay" (window:keyup)= "onKeyPressed($event)">

注意在 keyup 事件之前的特殊 window: 前缀。我们可以使用这种语法将事件附加到任何全局对象,例如 document。Angular 绑定基础设施的一个方便且非常强大的功能!需要在 WorkoutRunnerComponent 中添加 onKeyPressed 事件处理器。将此函数添加到类中:

onKeyPressed(event: KeyboardEvent) {
    if (event.which === 80 || event.which === 112) {
      this.pauseResumeToggle();
    }
  }

$event 对象是 Angular 提供的标准 DOM 事件对象,用于操作。由于这是一个键盘事件,所以专门的类是 KeyboardEventwhich 属性与 pP 的 ASCII 值相匹配。刷新页面后,你应该会在鼠标悬停在锻炼图像上时看到播放/暂停图标,如下所示:

当我们谈论到 事件绑定 时,这是一个探索 Angular 事件绑定基础设施的好机会

Angular 事件绑定基础设施

Angular 事件绑定允许组件通过事件与其父组件通信。

如果我们回顾一下应用实现,到目前为止我们所遇到的是属性/属性绑定。这些绑定允许组件/元素从外部世界获取输入。数据流入组件。

事件绑定是属性绑定的逆过程。它们允许组件/元素通知外部世界任何状态变化。

正如我们在暂停/恢复实现中所看到的,事件绑定使用圆括号 (()) 来指定目标事件:

<div id="pause-overlay" (click)="pauseResumeToggle()"> 

这会将一个 click 事件处理器附加到 div 上,当点击 div 时调用表达式 pauseResumeToggle()

与属性一样,事件也有一个规范形式。而不是使用圆括号,可以使用 on- 前缀:on-click="pauseResumeToggle()"

Angular 支持所有类型的事件。与键盘输入、鼠标移动、按钮点击和触摸相关的事件。该框架甚至允许我们为我们创建的组件定义自己的事件,例如:

<workout-runner (paused)= "stopAudio()"></workout-runner> 

我们将在下一章中介绍自定义组件事件,其中我们将为7 分钟健身操添加音频支持。

预期事件会有副作用;换句话说,事件处理器可能会改变组件的状态,这反过来又可能触发一系列反应,其中多个组件对状态变化做出反应并改变它们自己的状态。这与属性绑定表达式不同,它应该是无副作用的。即使在我们的实现中,点击div元素也会切换练习运行状态。

事件冒泡

当 Angular 将事件处理器附加到标准 HTML 元素事件时,事件传播的方式与标准 DOM 事件传播的方式相同。这也被称为事件冒泡。子元素上的事件会向上传播,因此父元素上也可以进行事件绑定,如下所示:

<div id="parent " (click)="doWork($event)"> Try 
  <div id="child ">me!</div> 
</div> 

点击任何一个 div 都会在父div上调用doWork函数。此外,$event.target包含触发事件的div的引用。

在 Angular 组件上创建的自定义事件不支持事件冒泡。

如果分配给目标的表达式评估为falsey值(例如voidfalse),则事件冒泡会停止。因此,为了继续传播,表达式应该评估为true

<div id="parent" (click)="doWork($event) || true"> 

在这里,$event对象也值得特别注意。

绑定$event 对象的事件

当目标事件被触发时,Angular 会提供一个$event对象。这个$event包含了发生事件的详细信息。

这里需要注意的是,$event对象的结构是根据事件类型决定的。对于 HTML 元素,它是一个 DOM 事件对象(developer.mozilla.org/en-US/docs/Web/Events),它可能根据实际事件而变化。

但如果是自定义组件事件,$event对象中传递的内容由组件实现决定。我们将在下一章再次讨论这个问题。

我们现在已经涵盖了 Angular 的大部分数据绑定功能,除了双向绑定。在我们结束本章之前,有必要对双向绑定构造进行简要介绍。

使用 ngModel 的双向绑定

双向绑定帮助我们保持模型和视图的一致性。模型的变化会更新视图,视图的变化会更新模型。双向绑定适用的明显领域是表单输入。让我们看看一个简单的例子:

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

这里的ngModel指令在inputvalue属性和底层组件上的workout.name属性之间设置了一个双向绑定。用户在前面input中输入的任何内容都会与workout.name同步,而workout.name的任何更改都会反映在前面input上。

有趣的是,我们也可以不使用ngModel指令达到相同的效果,通过结合属性和事件绑定语法。考虑下一个示例;它的工作方式与之前的input相同:

<input [value]="workout.name"  
    (input)="workout.name=$event.target.value" > 

value属性上设置了一个属性绑定,在input事件上设置了一个事件绑定,使得双向同步得以实现。

我们将在第四章个人教练中更详细地介绍双向绑定,在那里我们将构建自己的自定义训练。

我们创建了一个图表,总结了迄今为止讨论的所有绑定的数据流模式。这是一个方便的图表,可以帮助你记住每个绑定构造以及数据如何流动:

现在我们有一个功能齐全的7 分钟训练应用,还有一些额外的功能,希望你在创建应用的过程中玩得开心。现在是时候结束本章并总结所学内容了。

如果你在运行代码时遇到问题,请查看 Git 分支checkpoint2.4以获取我们迄今为止所做的工作的版本。您还可以从以下 GitHub 位置下载checkpoint2.4(ZIP 文件):bit.ly/ng6be-checkpoint-2-4。在首次设置快照时,请参考trainer文件夹中的README.md文件。

摘要

我们以创建一个比第一章中创建的示例更复杂的 Angular 应用为目标开始本章。7 分钟训练应用符合要求,你在构建这个应用的过程中也学到了很多关于 Angular 框架的知识。

我们首先定义了7 分钟训练应用的功能规范。然后,我们集中精力定义应用代码结构。

为了构建应用,我们首先定义了应用的模型。一旦模型就位,我们就通过构建一个Angular 组件来开始实际的实现。Angular 组件不过是装饰了框架特定装饰器@Component的类。

我们还了解了Angular 模块以及 Angular 如何使用它们来组织代码元素。

一旦我们创建了一个功能齐全的组件,我们就为该应用创建了一个支持视图。我们还探索了框架的数据绑定功能,包括属性属性样式事件绑定。此外,我们还强调了插值是属性绑定的一种特殊情况。

组件是一类特殊的指令,它们附加了一个视图。我们提到了指令是什么,以及特殊类指令,包括属性结构化指令

我们学习了如何使用输入属性进行跨组件通信。我们组合的两个子组件(ExerciseDescriptionComponentVideoPlayerComponent)通过输入属性从父组件WorkoutRunnerComponent获取输入。

接着,我们介绍了 Angular 的另一个核心结构,管道。我们看到了如何使用日期管道以及如何创建我们自己的管道。

在本章中,我们讨论了多个 Angular 指令,包括以下内容:

  • ngClass/ngStyle:用于使用 Angular 绑定能力应用多个样式和类

  • ngFor:用于使用循环结构生成动态 HTML 内容

  • ngIf:用于条件性地创建/销毁 DOM 元素

  • ngSwitch:用于使用 switch-case 结构创建/销毁 DOM 元素

我们现在有一个基本的7 分钟健身应用。为了提供更好的用户体验,我们还对其添加了一些小改进,但我们仍然缺少一些使我们的应用更易用的功能。从框架的角度来看,我们有意忽略了某些核心/高级概念,例如变更检测依赖注入组件 路由和数据流模式,这些内容我们计划在下一章中介绍。