Angular 企业就绪的 Web 应用(二)
原文:
zh.annas-archive.org/md5/eaf56b09bedec2a30920ca225cb1149e译者:飞龙
第四章:自动化测试、持续集成和发布到生产环境
发布它,否则它从未发生过!在第三章中,您在创建基本 Angular 应用时创建了一个可以检索当前天气数据的应用程序。您在这个过程中创造了一定的价值;然而,如果您不将您的应用放到网络上,您最终创造的价值为零。将您的作品发布出去的这种动机在许多行业中都很普遍。然而,将作品交付给他人或公开接受审查可能会令人恐惧。在软件工程中,交付任何东西都是困难的;将东西交付到生产环境则更加困难。本章将帮助您实现持续集成(CI)管道。持续集成管道将帮助您实现频繁、可靠、高质量和灵活的发布。
只有在我们有一套可以快速验证我们代码正确性的自动化测试时,才能实现频繁和可靠的发布。我们在上一章创建的应用程序有失败的单元测试和端到端(e2e)测试。我们需要修复这些单元测试,然后通过利用 GitHub 工作流和 CircleCI 确保它们不再中断。然后我们将介绍如何将您的 Angular 应用交付到网络上。在第九章使用 Docker 进行 DevOps中,我们将介绍持续交付(CD)管道,它也可以自动化您的交付。
查看我的 2018 年演讲,发布它,否则它从未发生过:Docker、Heroku 和 CircleCI 的力量,链接为bit.ly/ship-it-or-it-never-happened。
本章涵盖以下内容:
-
使用测试替身进行单元测试
-
使用 Jasmine 进行 Angular 单元测试
-
Angular 端到端测试
-
GitHub 工作流
-
生产就绪
-
使用 CircleCI 进行持续集成
-
使用 Vercel Now 在网络上部署应用
本书样本代码的最新版本可在以下链接的 GitHub 仓库中找到。该仓库包含代码的最终和完整状态。您可以通过查看projects文件夹下的章节末尾代码快照来验证本章的进度。
对于第四章:
-
在根文件夹中执行
npm install以安装依赖项 -
本章的代码示例位于以下子文件夹中:
projects/ch4 -
要运行本章的 Angular 应用,请执行以下命令:
npx ng serve ch4 -
要运行本章的 Angular 单元测试,请执行以下命令:
npx ng test ch4 --watch=false -
要运行本章的 Angular 端到端测试,请执行以下命令:
npx ng e2e ch4 -
要构建本章的生产就绪 Angular 应用,请执行以下命令:
npx ng build ch4 --prod注意,仓库根目录下的
dist/ch4文件夹将包含编译结果。
注意,书中或 GitHub 上的源代码可能并不总是与 Angular CLI 生成的代码相匹配。由于生态系统不断演变,书中代码与 GitHub 上代码之间的实现也可能存在细微差异。随着时间的推移,示例代码发生变化是自然的。此外,在 GitHub 上,您可能会找到更正、支持库新版本的修复,或者为读者观察而并排实现多种技术的示例。您只需实现书中推荐的理想解决方案即可。如果您发现错误或有疑问,请为所有读者创建 GitHub 上的问题或提交拉取请求。
你可以在附录 C,保持 Angular 和工具常青中了解更多关于更新 Angular 的内容。您可以从static.packt-cdn.com/downloads/9781838648800_Appendix_C_Keeping_Angular_and_Tools_Evergreen.pdf或expertlysimple.io/stay-evergreen在线找到这个附录。
在本章中,您需要在 CircleCI 和 Vercel Now 上注册账户。但在我们可以部署我们的应用程序之前,我们需要确保我们已自动部署测试来确保我们的应用程序的质量随时间保持。首先,我们将深入探讨单元测试基础,让您熟悉测试驱动开发的益处,并介绍如 FIRST 和 SOLID 等原则。
单元测试
单元测试对于确保您的应用程序的行为不会无意中随时间改变至关重要。单元测试将使您和您的团队能够继续对应用程序进行更改,而不会引入先前已验证的功能的变化。开发者编写单元测试,其中每个测试的范围仅限于测试待测试函数(FUT)或待测试类(CUT)中的代码。Angular 组件和服务都是类;然而,也鼓励您开发可重用的函数。单元测试应该数量充足、自动化且快速。您应该在原始代码旁边编写单元测试。如果它们与实现分离,即使是一两天,您也可能开始忘记代码的细节。正因为如此,您可能会忘记为潜在的边缘情况编写测试。
单元测试应遵循FIRST 原则:
-
Fast
-
Isolated
-
Repeatable
-
Self-verifying
-
Timely
单元测试应该快速,只需毫秒即可运行,这样我们就可以在几分钟内运行数千个测试。为了实现快速测试,单元测试应该是隔离的。它不应该与数据库通信、通过网络发送请求或与 DOM 交互。隔离的测试将是可重复的,因此每次测试运行都会返回相同的结果。可预测性意味着我们可以断言测试的正确性,而不依赖于任何外部环境,这使得我们的测试可以自我验证。如前所述,你应该及时编写单元测试;否则,你会失去编写单元测试的好处。
如果你的测试只关注单个 FUT/CUT,那么你就可以坚持 FIRST 原则。但其他类、服务或我们必须传递给 FUT/CUT 的参数怎么办?单元测试可以通过利用测试双倍来隔离 FUT/CUT 的行为。测试双倍允许我们控制外部依赖,因此,而不是向你的组件注入HttpService,你可以注入一个假的或模拟的HttpService。使用测试双倍,我们可以控制外部依赖的影响,并创建快速且可重复的测试。
多少测试才算足够?你应该至少有与生产代码一样多的测试代码。如果没有,那么你离编写足够的测试还差得远。
单元测试并不是你可以创建的唯一类型的测试,但它们是你应该创建最多的一种。让我们考虑你可以创建的三个主要测试类别:单元、集成和 UI。
正如我们所说的,单元测试一次只关注一个 FUT/CUT。集成测试测试各种组件的集成,以便它们可以包括数据库调用、网络请求以及与 DOM 的交互。由于它们的性质,集成测试运行速度较慢,并且需要频繁维护。运行时间和维护的增加意味着随着时间的推移,集成测试比单元测试更昂贵。UI 测试模拟用户使用应用程序,填写字段、点击按钮并观察预期的结果。
你可能会想象这些测试是最慢且最脆弱的测试类型。应用程序的 UI 经常变化,使用 UI 测试创建可重复的测试非常困难。
我们可以利用集成和 UI 测试的混合来创建验收测试。验收测试是为了自动化我们交付的功能的业务验收而编写的。Angular 的端到端测试是一种创建验收测试的方法。
我们可以用迈克·科恩的测试金字塔可视化三种主要自动化测试类别的优缺点,如下所示:
图 4.1:迈克·科恩的测试金字塔
测试金字塔有效地总结了在考虑速度和成本的情况下,我们应该为我们的应用程序创建的每种类型的测试的相对数量。
在实现方面,单元测试由三个部分组成:
-
安排 – 设置
-
行动 - 运行你想要测试的东西
-
断言 - 验证结果
在安排步骤中,我们设置测试双、预期结果和任何其他必需的依赖项。在行动步骤中,我们执行我们正在测试的代码行。最后,在断言阶段,我们验证行动步骤的结果是否与安排步骤中定义的预期结果相匹配。我们将在下一节中看到安排、行动和断言在实际中的工作方式。
让我们来看看在 Angular 中单元测试意味着什么。
Angular 单元测试
Angular 中单元测试的定义与我们之前定义的单元测试的严格定义略有不同。Angular CLI 使用 Jasmine 框架为我们自动生成单元测试。然而,这些所谓的单元测试包括 DOM 交互,因为它们渲染组件的视图。
从 第一章,Angular 及其概念简介 中考虑 Angular 组件的架构:
图 4.2:组件的解剖结构
由于 Angular 使用绑定,组件类及其模板是不可分割的,实际上代表了一个单元。我们仍然可以通过测试单个函数来编写纯单元测试,但除此之外,组件及其模板被认为是测试的最小单元。
随着应用程序的增长,您可能会发现 Angular 单元测试运行缓慢,因为它们渲染视图并解析依赖关系树。有各种方法可以解决这个问题,包括测试运行的并行化、选择不使用 Angular TestBed 以及更积极地使用测试双。
如您所注意到的,我们可以将服务注入到组件中或在我们的模板中使用其他组件。我们将利用 Jasmine 提供的测试双机制来隔离我们的组件,使其不受此类外部依赖的影响。
让我们来看看 Jasmine 是什么。
Jasmine
Jasmine 是一个用于浏览器和 Node.js 测试的行为驱动测试框架。Jasmine 还支持 Ruby 和 Python。Jasmine 是一个包含电池的框架。它支持基本的单元测试需求,如测试固定装置、断言、模拟、间谍和报告器。
Jasmine 测试文件的命名约定是在文件名后使用spec.ts,例如fileUnderTest.spec.ts。Jasmine 测试组织在describe块中,这些块可以按层级分组,以反映文件、类或属于单个函数的多个测试的结构。单个测试用例或规格用it块表示。以下示例显示了一个名为converters.ts的文件导出一个将摄氏度转换为华氏度的函数:
**Sample Jasmine Test**
describe('Converters', () => {
describe('convertCtoF', () => {
it('should convert 0c to 32f', () => {
...
})
})
})
规格以这种方式组织,当它们执行时,它们读起来像一句话。在这种情况下,结果将是 Converters convertCtoF 应将 0c 转换为 32f。
想了解更多关于 Jasmine 的信息,请访问 jasmine.github.io.
接下来,让我们探讨 Jasmine 和大多数其他测试框架的主要功能类别——固定装置和匹配器——这些功能帮助你使用行动、安排和断言结构编写连贯的单元测试。
固定装置
如前所述,单元测试有三个部分:安排、行动和断言。单元测试的安排部分可能是重复的,因为多个测试案例通常需要相同的设置。Jasmine 提供固定装置来帮助减少你的代码中的重复。
以下是四个固定装置:
-
beforeAll()– 在describe中的所有规格之前运行 -
afterAll()– 在每个测试固定装置之后运行所有describe中的规格 -
beforeEach()– 在describe中的每个规格之前运行 -
afterEach()– 在describe中的每个规格之后运行
固定装置在指定其describe块的作用域内,在某个特定或一组特定规格之前和之后执行。
匹配器
在单元测试的断言部分,我们需要让 Jasmine 知道一个规格是通过了还是失败了。我们可以通过编写一个断言来实现这一点。有两种断言类型:
-
fail('message')– 这会明确地使一个规格失败 -
expect()– 给定一个匹配器,动态断言预期的结果是否与实际结果匹配
expect断言需要匹配器来确定测试的结果。expect和匹配器的组合旨在读起来像一句话。以下是一些你可能使用的常见匹配器:
**Jasmine Matchers**
expect(expected).toBe(actual)
.toEqual(actual)
.toBeDefined()
.toBeFalsy()
.toThrow(exception)
.nothing()
关于 Jasmine 匹配器的完整范围,请参阅jasmine.github.io/api/edge/matchers.html。
存在着具有更丰富功能的其他库,例如 Jest、Mocha 或 testdouble.js。然而,当开始使用像 Angular 这样的新框架时,保持你的工具集最小化是很重要的。坚持默认设置是一个好主意。
此外,Jasmine 还提供了间谍(spies),通过spyOn函数支持存根(stubbing)和模拟(mocking)。我们将在本章的后面更详细地介绍这些测试替身。
自动生成单元测试的解剖结构
默认情况下,Angular 配置为可以使用 Jasmine 编写单元测试。Karma 是测试运行器,它可以持续监控代码的变化,并自动重新运行你的单元测试。
Angular 的默认配置利用了TestBed,这是一个特定于 Angular 的组件,它简化了模块的提供、依赖注入、模拟、触发 Angular 生命周期事件(如ngOnInit)以及执行模板逻辑。
如前所述,当你利用TestBed时,在术语的最严格定义中,不可能将这些测试称为单元测试。这是因为,默认情况下,TestBed注入了你的依赖项的实际实例。这意味着当你执行测试时,你也在执行服务或其他组件中的代码,而你应该只测试当前正在测试的服务或组件中的代码。我们利用测试替身来帮助我们编写隔离和可重复的单元测试。
在第三章,创建一个基本的 Angular 应用中,Angular CLI 在你创建新的组件和服务时创建了单元测试文件,例如current-weather.component.spec.ts和weather.service.spec.ts。请查看以下 spec 文件,并观察should create测试。框架断言任何CurrentWeatherComponent类型的组件不应为 null 或 undefined,而应该是真值:
**src/app/current-weather/current-weather.component.spec.ts**
describe('CurrentWeatherComponent', () => {
let component: CurrentWeatherComponent
let fixture: ComponentFixture<CurrentWeatherComponent>
beforeEach(
async(() => {
TestBed.configureTestingModule({
declarations: [CurrentWeatherComponent],
}).compileComponents()
})
)
beforeEach(() => {
fixture = TestBed.createComponent(CurrentWeatherComponent)
component = fixture.componentInstance
fixture.detectChanges()
})
it('should create', () => {
expect(component).toBeTruthy()
})
})
WeatherService spec 包含一个类似的测试。然而,你会注意到每种类型的测试设置略有不同:
**src/app/weather/weather.service.spec.ts**
describe('WeatherService', () => {
let service: WeatherService
beforeEach(() => {
TestBed.configureTestingModule({})
service = TestBed.inject(WeatherService);
})
it('should be created', () => {
expect(service).toBeTruthy()
})
)
})
在WeatherService spec 的beforeEach函数中,CUT 被注入到TestBed中。另一方面,CurrentWeatherComponent spec 有两个beforeEach函数。第一个beforeEach函数异步声明和编译组件的依赖模块,而第二个beforeEach函数创建测试固定装置并开始监听组件的变化,一旦编译完成,就准备好运行测试。
接下来,让我们执行我们的单元测试,看看有多少通过或失败。
单元测试执行
Angular CLI 使用 Jasmine 单元测试库来定义单元测试,并使用 Karma 测试运行器来执行它们。最好的是,这些测试工具已经配置好可以直接运行。你可以使用以下命令执行单元测试:
$ npm test
测试是由 Karma 测试运行器在一个新的 Chrome 浏览器窗口中运行的。Karma 的主要好处是它带来了类似于 Angular CLI 在开发应用程序时使用 webpack 实现的实时重新加载功能。
在执行npm test命令的初始执行后,你很可能会遇到编译错误,因为我们实现应用程序代码时没有更新相应的单元测试代码。
在开发过程中,遇到许多错误是正常的。所以,不要沮丧!参见附录 A,调试 Angular,了解如何使用 Chrome/Edge Dev Tools 和 VS Code 来调试错误。
让我们看看如何解决这些错误。
编译错误
在开发应用程序代码时,更新你的单元测试代码是很重要的。不这样做通常会导致编译错误。
记住,当你构建 Angular 应用时,测试代码不会被构建。你必须执行npm test来构建和运行你的测试代码。
当你执行测试时,你应该会看到一个类似于以下错误消息:
ERROR in src/app/app.component.spec.ts:21:16 - error TS2339:
Property 'title' does not exist on type 'AppComponent'.
21 expect(app.title).toEqual('local-weather-app')
我们需要纠正的第一个测试位于app.component.spec.ts中,名为'should have as title "local-weather-app"'。我们在上一章中从AppComponent中删除了title属性,因为我们没有使用它。所以,我们不再需要这个单元测试。
-
删除
should have as title 'local-weather-app'单元测试。如前所述,Jasmine 结合了
describe和it函数中提供的文本。因此,这个测试被称为'AppComponent should have as title 'local-weather-app''。这是一个方便的约定,可以快速定位测试。当你编写新的测试时,维护你规格的可读描述取决于你。第二个要修复的测试位于
AppComponent下,名称为should render title。我们现在将“LocalCast Weather”作为标题渲染,所以让我们更改测试。 -
更新
should render title测试,如下所示:**src/app/app.component.spec.ts** it('should render title', () => { ... expect(compiled.querySelector('h1').textContent) .toContain('LocalCast Weather') }) -
提交你的代码更改。
我们已经修复了单元测试中的逻辑问题。它们现在应该可以无编译错误地执行。然而,你应该预期它们都会失败,因为我们还没有配置 Angular 的TestBed。
测试结果
你应该在终端上观察到最后一条信息是TOTAL: 2 FAILED, 2 SUCCESS。这是正常的,因为我们根本没有关注这些测试,所以让我们修复它们。
图 4.3:Karma Runner 显示 Jasmine 单元测试结果
将 Karma Runner 窗口与 VS Code 并排打开,这样你可以立即看到你更改的结果。
现在我们来配置 TestBed。
配置 TestBed
TestBed 有三个主要功能,可以帮助你创建可单元测试的组件:
-
声明 – 构建组件类及其模板逻辑,以方便测试
-
提供者 – 提供没有模板逻辑和需要注入的依赖项的组件类
-
导入 – 导入支持模块以能够渲染模板逻辑或其他平台功能
TestBed 不是在 Angular 中编写单元测试的强制要求,这是一个在angular.io/guide/testing中很好地介绍的话题。我的同事和本书的审稿人 Brendon Caulkins 为第十二章,配方 – 主/详细,数据表和 NgRx代码示例贡献了一个无床的 spec 文件,名为current-weather.component.nobed.spec.ts。他引用了在运行测试时性能显著提高,导入更少,维护更少,但需要更高水平的关注和专业知识来实现测试。如果你在一个大型项目中,你应该认真考虑跳过 TestBed。
你可以在 GitHub 上找到示例代码,链接为github.com/duluca/local-weather-app/tree/master/projects/ch12。
让我们逐一介绍这些功能,同时修复手头的测试,以确保它们可以成功运行。
声明
声明使我们能够提供渲染待测试组件所需的所有组件。通常,你只需声明待测试的组件。因此,app.component.spec.ts声明了AppComponent,而current-weather.component.spec.ts声明了CurrentWeatherComponent等等。
注意,我们在AppComponent的模板中使用了<app-current-weather>;然而,这并不意味着我们还需要在app.component.spec.ts中声明CurrentWeatherComponent。Angular 的旧版本TestBed要求将子组件作为父组件单元测试的一部分进行声明,这导致了创建单元测试时的显著开销。在声明中包含多个组件会产生副作用,需要注入所有已声明组件的所有依赖项,而不仅仅是待测试组件的依赖项。这意味着将无关的依赖项添加到我们的“单元”测试中,使它们变成了集成测试。
在这种情况下,CurrentWeatherComponent是AppComponent的硬编码依赖项。可以通过两种方式进一步解耦这两个组件:一种方式是使用ng-container动态注入组件,另一种方式是利用 Angular Router 和router-outlet。router-outlet策略是我们构建大多数多屏 Angular 应用的方式,我将在后面的章节中介绍。使用ng-container正确解耦组件的任务留给读者作为练习。
你可以尝试在app.component.spec.ts中声明CurrentWeatherComponent:
**src/app/app.component.spec.ts**
...
TestBed.configureTestingModule({
declarations: [AppComponent, CurrentWeatherComponent],
}).compileComponents()
...
注意,这样做会在AppComponent测试中引入与HttpClient相关的错误,尽管AppComponent本身没有导入WeatherService。实际上,CurrentWeatherComponent导入了WeatherService,而WeatherService本身又导入了HttpClient。你可以看到依赖项的复杂性是如何迅速失控的。Angular 单元测试配置为不需要声明子组件,但请注意,单元测试框架正在抛出一个关于未知元素的警告:
WARN: ''app-current-weather' is not a known element
在编程中,警告几乎和错误一样严重。不解决警告注定会在将来造成麻烦。当我们后面讨论模拟时,我们将介绍如何正确解决这个问题。
在继续之前,请确保撤销你的更改。
目前,你不需要为父组件测试声明子组件,这使得最初通过单元测试变得更容易。在某些情况下,你必须声明依赖组件,例如当你创建自定义控件并需要测试你的控件是否在组件的上下文中正常工作时。创建自定义控件的例子包括在第十一章的配方 - 可重用性、路由和缓存中。
在下一节中,我们将探讨提供者,它们帮助我们注入依赖项的真实和模拟实现,这样我们就可以避免测试像WeatherService这样的依赖项,而只测试“单元”。
提供者
提供者允许我们在不使用模板逻辑或注入到待测试组件中的服务的情况下提供组件。你会注意到我们的CurrentWeatherComponent测试没有通过,出现了一个错误,抱怨缺少HttpClient的提供者:
CurrentWeatherComponent > should create
NullInjectorError: R3InjectorError(DynamicTestModule)[WeatherService -> HttpClient -> HttpClient]:
NullInjectorError: No provider for HttpClient!
这是因为注入到 CurrentWeatherComponent 中的 WeatherService 需要一个 HttpClient 的提供者。然而,CurrentWeatherComponent 并不知道 HttpClient。它只知道 WeatherService。你可能猜到我们可能并不是严格地进行单元测试,而是实际上在进行集成测试,你会是对的。
然而,让我们继续并将在 current-weather.component.spec.ts 中添加 WeatherService 的提供者。在 current-weather.component.spec.ts 中的声明中提供 WeatherService,如下所示:
**src/app/current-weather/current-weather.component.spec.ts**
...
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [...],
providers: [WeatherService],
})
...
})
...
在这个例子中,我们提供了 WeatherService 的实际实现,但这并没有解决当前的问题。WeatherService 的实现仍然依赖于 HttpClient,错误仍然存在。
在继续之前,请确保撤销您的更改。
提供者允许我们提供依赖项的替代实现,如该依赖项的伪造或模拟。
如果我们定义一个名为 FakeWeatherService 的 WeatherService 伪造实现,我们可以通过以下 useClass 方式提供伪造而不是实际实现:
providers: [{ provide: WeatherService, useClass: FakeWeatherService }]
伪造实现将打破对 HttpClient 的依赖,并解决我们的问题。我将在下一节关于测试替身的部分中介绍如何实现伪造。
或者,如果我们为 WeatherService 创建一个名为 mockWeatherService 的模拟,我们可以通过以下方式提供模拟的 useValue:
providers: [{ provide: WeatherService, useValue: mockWeatherService }]
使用模拟,我们甚至不需要实现伪造类并确保我们只测试正在测试的组件。下一节关于测试替身的部分将详细介绍模拟。
现在我们已经很好地理解了提供者能为我们做什么以及不能做什么,让我们看看导入如何完善 TestBed。
导入
导入有助于引入代码,这些代码可以促进视图或其他依赖项的渲染到测试中。目前,测试仍然失败,因为 WeatherService 本身依赖于 HttpClient,因此我们需要提供 HttpClient。如果我们这样做,那么我们的单元测试将尝试通过 HTTP 进行调用。我们不希望我们的测试依赖于其他服务,因为这违反了本章前面提到的 FIRST 原则。因此,我们不应该提供实际的 HttpClient。
Angular 为 HttpClient 提供了一个名为 HttpClientTestingModule 的测试替身。为了利用它,您必须导入它,这将自动为您提供测试替身。
为 current-weather.component.spec.ts 导入 HttpClientTestingModule:
**src/app/current-weather/current-weather.component.spec.ts**
import { HttpClientTestingModule } from '@angular/common/http/testing'
...
describe(' CurrentWeatherComponent', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
...
})
...
})
...
与 HttpClientTestingModule 类似,还有一个 RouterTestingModule 和 NoopAnimationsModule,它们是真实服务的模拟版本,因此单元测试可以仅关注测试您编写的组件或服务代码。在后面的章节中,我们还将介绍如何编写您自己的模拟。
呼吸!现在,所有您的单元测试都应该通过。如您所见,CurrentWeatherComponent 测试不是我们的单元测试,因为它们正在使用实际的 WeatherService,而 WeatherService 本身依赖于 HttpClient。
现在,让我们看看测试替身如何帮助我们编写符合 FIRST 原则的单元测试。
测试替身
应该只对 CUT(Cut,即代码单元测试中的“代码单元”)中的代码进行测试。在 CurrentWeatherComponent 的情况下,我们需要确保服务代码不被执行。因此,你应该永远不要提供服务的实际实现。
我们需要了解两种测试替身类型:
-
模拟
-
模拟、存根或间谍
通常,对模拟进行推理更容易,所以我们将从这里开始。一旦你对单元测试感到舒适,并且你的现有测试集处于正常工作状态,我强烈建议切换到仅使用模拟,这将使你的测试更加健壮、高效和易于维护。
模拟
模拟是一个现有类的替代、简化实现。它就像一个模拟服务,其中不进行任何实际的 HTTP 调用,但你的服务返回预制的响应。在单元测试期间,模拟被实例化并像真实类一样使用。在前一节中,我们使用了 HttpClientTestingModule,这是一个模拟的 HttpClient。我们的自定义服务是 WeatherService,因此我们必须提供我们的测试替身实现。
我们通过创建服务的模拟来创建测试替身。由于 WeatherService 的模拟在测试多个组件时使用,你的实现应该在一个单独的文件中。为了保持代码库的可维护性和可发现性,每个文件一个类是一个很好的经验法则。将类保存在单独的文件中可以防止你犯一些编程错误,比如在两个类之间错误地创建或共享全局状态或独立函数,从而在过程中保持代码解耦。
我们还需要确保实际实现和测试替身的 API 随时间不会不同步。我们可以通过为服务创建一个接口来实现这一点。
-
将
IWeatherService添加到weather.service.ts中,如下所示:**src/app/weather/weather.service.ts** export interface IWeatherService { getCurrentWeather( city: string, country: string ): Observable<ICurrentWeather> } -
更新
WeatherService以实现新的接口:**src/app/weather/weather.service.ts** export class WeatherService implements IWeatherService -
创建一个新的文件
weather/weather.service.fake.ts -
在
weather.service.fake.ts中实现一个基本的模拟,如下所示:**src/app/weather/weather.service.fake.ts** import { Observable, of } from 'rxjs' import { IWeatherService } from './weather.service' import { ICurrentWeather } from '../interfaces' export const fakeWeather: ICurrentWeather = { city: 'Bethesda', country: 'US', date: 1485789600, image: '', temperature: 280.32, description: 'light intensity drizzle', } export class WeatherServiceFake implements IWeatherService { public getCurrentWeather( city: string, country: string): Observable<ICurrentWeather> { return of(fakeWeather) } }我们正在利用现有的
ICurrentWeather接口,我们的模拟数据已经正确地塑造了它,但我们必须将其转换为Observable。这可以通过使用of来轻松实现,它根据提供的参数创建一个可观察的序列。现在,你已经准备好为
AppComponent和CurrentWeatherComponent提供模拟了。 -
更新
current-weather.component.spec.ts中的提供者,以使用WeatherServiceFake,这样就会使用模拟而不是实际的服务:**src/app/current-weather/current-weather.component.spec.ts** ... beforeEach( async(() => { TestBed.configureTestingModule({ ... providers: [{ provide: WeatherService, useClass: WeatherServiceFake }], ...注意,这个替代实现是在一个名为
current-weather.component.fake.spec的不同文件中提供的,它是 GitHub 上projects/ch4子文件夹的一部分。 -
从导入中删除
HttpClientTestingModule,因为它不再需要随着你的服务和组件变得越来越复杂,很容易提供一个不完整或不充分的测试替身。你可能会看到诸如
NetworkError: Failed to execute 'send' on 'XMLHttpRequest'、Can't resolve all parameters或[object ErrorEvent] thrown这样的错误。在后者的情况下,点击 Karma 中的 调试 按钮以发现视图错误详情,这可能看起来像 超时 - jasmine 指定的时间超出了异步回调。单元测试设计为以毫秒为单位运行,所以实际上触碰到默认的 5 秒超时是不可能的。问题几乎总是与测试设置或配置有关。 -
验证所有测试是否通过
使用假对象,我们能够在一定程度上减少测试复杂性并提高隔离性。我们可以通过模拟、存根和间谍做得更好。
模拟、存根和间谍
模拟、存根或间谍不包含任何实现。模拟在单元测试文件中配置,以对特定的函数调用响应一系列响应,这些响应可以根据测试轻松地变化。
在 声明 部分中较早的时候,我们讨论了在 app.component.spec.ts 中声明 CurrentWeatherComponent 以解决未知元素警告的必要性。如果我们声明真实的 CurrentWeatherComponent,那么 AppComponent 的测试配置就会变得过于复杂,因为我们需要解决子组件的依赖关系树,包括 WeatherService 和 HttpClient。此外,仅仅为了提供假天气数据就创建一个整个假服务是过度设计,并且不是一个灵活的解决方案。如果我们想根据不同的输入测试不同的服务响应怎么办?我们就必须开始在假服务中引入逻辑,然后不知不觉中,你就在处理 WeatherService 的两个独立实现。
创建一个假对象的替代方案是创建一个空对象,它冒充真实对象但没有任何实现。这些对象被称为模拟。我们将在下面利用两种不同的技术来创建模拟组件和模拟服务。
模拟组件
如果我们在 app.component.spec.ts 中提供一个 CurrentWeatherComponent,我们可以解决未知元素的警告,并且不需要担心 CurrentWeatherComponent 所依赖的所有组件和服务。
如果你手动实现它,一个模拟组件看起来像这样:
@Component({
selector: 'app-current-weather',
template: ``,
})
class MockCurrentWeatherComponent {}
然而,这可能会很快变得繁琐,这就是为什么我发布了一个单元测试辅助库,名为 angular-unit-test-helper,以使模拟组件更容易。使用这个库,你只需用这个函数调用替换声明中的组件:
createComponentMock('CurrentWeatherComponent')
让我们更新 app.component.spec.ts 以使用模拟组件:
-
执行
npm i -D angular-unit-test-helper -
使用模拟组件更新
AppComponent:**src/app/app.component.spec.ts** import { createComponentMock } from 'angular-unit-test-helper' TestBed.configureTestingModule({ declarations: [ ..., createComponentMock('CurrentWeatherComponent') ], ... }) -
完全删除
providers属性 -
清理未使用的导入
注意,单元测试文件保持简洁,警告已解决。angular-unit-test-helper 推断 CurrentWeatherComponent 代表一个 HTML 标签,如 <app-current-weather>,并在浏览器的窗口对象中提供它。然后 createComponentMock 函数通过分配选择器 'app-current-weather' 和一个空模板来正确装饰空的 CurrentWeatherComponent 类。然后 TestBed 能够解析 <app-current-weather> 为这个模拟组件。createComponentMock 还允许你根据需要提供自定义选择器或假模板。这是一个可扩展的解决方案,减少了超过一半的导入,并遵循 FIRST 原则。
模拟的概念扩展到我们可以定义的所有类型的对象,包括 Angular 服务。通过模拟服务,我们不必担心可能注入到该服务中的任何依赖项。
让我们看看如何模拟一个服务。
模拟服务
让我们为 CurrentWeatherComponent 编写两个新的单元测试,以展示模拟服务而不是实现其假值的优点。模拟允许我们创建一个空对象,并给我们提供只提供可能需要的测试函数的选项。然后我们可以根据每个测试来模拟这些函数的返回值或监视它们以查看我们的代码是否调用了它们。监视特别有用,如果相关的函数没有返回值。我们需要在我们的规范安排部分设置我们的间谍。
-
让我们从创建一个
WeatherService间谍对象开始,使用jasmine.createSpyObj,如下所示:**src/app/current-weather/current-weather.component.spec.ts** import { ComponentFixture, TestBed, async } from '@angular/core/testing' import { injectSpy } from 'angular-unit-test-helper' import { WeatherService } from '../weather/weather.service' import { CurrentWeatherComponent } from './current-weather.component' describe('CurrentWeatherComponent', () => { ... let weatherServiceMock: jasmine.SpyObj<WeatherService> beforeEach(async(() => { const weatherServiceSpy = jasmine.createSpyObj( 'WeatherService', ['getCurrentWeather'] ) TestBed.configureTestingModule({ ... }) }) -
使用
useValue将weatherServiceSpy作为WeatherService的值。 -
最后,从
TestBed获取注入的实例并将其分配给weatherServiceMock,使用angular-unit-test-helper中的injectSpy方法,如下所示:**src/app/current-weather/current-weather.component.spec.ts** beforeEach(async(() => { ... TestBed.configureTestingModule({ ..., providers: [{ provide: WeatherService, useValue: weatherServiceSpy }] }).compileComponents() weatherServiceMock = injectSpy(WeatherService) }
注意,injectSpy 是 TestBed.inject(WeatherService) 的简写,作为任何。
在前面的例子中,我们有一个模拟的 WeatherService 版本,其中声明它有一个名为 getCurrentWeather 的函数。然而,请注意,你现在得到了一个错误:
TypeError: Cannot read property 'subscribe' of undefined
这是因为 getCurrentWeather 不会返回一个可观察对象。使用 weatherServiceMock,我们可以监视 getCurrentWeather 是否被调用,也可以根据测试来模拟其返回值。
为了操纵 getCurrentWeather 的返回值,我们需要更新 should create 测试以反映安排、执行和断言结构。为此,我们需要将 fixture.detectChanges() 从第二个 beforeEach 中移除,这样我们就可以控制其执行顺序,使其在安排部分之后执行。
**src/app****/current-weather/current-weather.component.spec.ts**
import { of } from 'rxjs'
...
beforeEach(() => {
fixture = TestBed.createComponent(CurrentWeatherComponent)
component = fixture.componentInstance
})
it('should create', () => {
// Arrange
weatherServiceMock.getCurrentWeather.and.returnValue(of())
// Act
fixture.detectChanges() // triggers ngOnInit
// Assert
expect(component).toBeTruthy()
})
在安排部分,我们配置getCurrentWeather应使用RxJS\of函数返回一个空的 Observable。在行为部分,我们触发 TestBed 的detectChanges函数,这会触发生命周期事件,如ngOnInit。由于我们正在测试的代码位于ngOnInit中,这是正确执行的操作。最后,在断言部分,我们确认组件已成功创建。
在接下来的测试中,我们可以验证getCurrentWeather函数确实被调用了一次:
**src/app/current-weather/current-weather.component.spec.ts**
it('should get currentWeather from weatherService', () => {
// Arrange
weatherServiceMock.getCurrentWeather.and.returnValue(of())
// Act
fixture.detectChanges() // triggers ngOnInit()
// Assert
expect(weatherServiceMock.getCurrentWeather)
.toHaveBeenCalledTimes(1)
})
最后,我们可以测试返回的值是否正确分配在组件类中,并且也正确渲染在模板上:
**src/app/current-weather/current-weather.component.spec.ts**
import { By } from '@angular/platform-browser'
import { fakeWeather } from '../weather/weather.service.fake'
...
it('should eagerly load currentWeather in Bethesda from weatherService', () => {
// Arrange
weatherServiceMock.getCurrentWeather
.and.returnValue(of(fakeWeather))
// Act
fixture.detectChanges() // triggers ngOnInit()
// Assert
expect(component.current).toBeDefined()
expect(component.current.city).toEqual('Bethesda')
expect(component.current.temperature).toEqual(280.32)
// Assert on DOM
const debugEl = fixture.debugElement
const titleEl: HTMLElement = debugEl.query(By.css('span'))
.nativeElement
expect(titleEl.textContent).toContain('Bethesda')
})
在前面的例子中,您可以看到我们提供了一个名为fakeWeather的模拟对象,其中城市名称为 Bethesda。然后我们能够断言当前属性具有正确的city,并且具有class=mat-title的<div>元素包含文本 Bethesda。
您现在应该有七个通过测试:
TOTAL: 7 SUCCESS
通过使用模拟(mocks)、存根(stubs)和间谍(spies),我们可以快速测试外部依赖项可以返回和不能返回的多种可能性,并且我们可以通过观察 DOM 来验证组件或服务类中驻留的代码的断言。
要了解更多关于模拟、存根和间谍的信息,请参阅jasmine.github.io。此外,我发现 Dave Ceddia 的 Jasmine 2 Spy Cheat Sheet 非常有用,位于daveceddia.com/jasmine-2-spy-cheat-sheet。
通常,您的单元测试应该最多断言一两个事情。为了达到足够的单元测试覆盖率,您应该专注于测试包含业务逻辑的函数的正确性:通常在您看到if或switch语句的地方。
要编写可单元测试的代码,请确保遵循 SOLID 原则中的单一责任原则和开放/封闭原则。
查看我的同事 Brendan Sawyer 创建的ng-tester库,位于www.npmjs.com/package/ng-tester。它为您的 Angular 组件创建具有angular-unit-test-helper的规范文件,以帮助进行模拟。此外,该库展示了如何模拟依赖项并在不使用TestBed的情况下创建测试。
您可以通过命令npm install -D ng-tester安装库,并使用命令npx ng generate ng-tester:unit创建单元测试。
除了单元测试之外,Angular CLI 还会为您的应用程序生成和配置端到端测试。接下来,让我们了解端到端测试。
Angular 端到端测试
虽然单元测试侧重于隔离 CUT,但 e2e 测试是关于集成测试。Angular CLI 利用 Protractor 和 WebDriver,以便您可以从用户与浏览器中交互的角度编写 自动化验收测试(AAT)。作为一个经验法则,您应该始终编写比 AATs 多一个数量级的单元测试,因为您的应用程序经常变化,因此与单元测试相比,AATs 的脆弱性和维护成本要高得多。
如果“web driver”这个词听起来很熟悉,那是因为它是经典 Selenium WebDriver 的一种演变。在 2017 年 3 月 30 日,WebDriver 被提议作为 W3C 的官方网络标准。您可以在 www.w3.org/TR/webdriver 上了解更多信息。如果您熟悉 Selenium,您应该会感到很自在,因为许多模式和做法几乎相同。
CLI 为初始的 AppComponent 提供端到端测试,具体取决于您应用程序的复杂性和功能集。您需要遵循提供的模式来更好地组织您的测试。在 e2e 文件夹下,每个组件都会生成两个文件:
**e2e/src/app.e2e-spec.ts**
import { browser, logging } from 'protractor'
import { AppPage } from './app.po'
describe('workspace-project App', () => {
let page: AppPage
beforeEach(() => {
page = new AppPage()
})
it('should display welcome message', () => {
page.navigateTo()
expect(page.getTitleText())
.toEqual('local-weather-app app is running!')
})
afterEach(async () => {
// Assert that there are no errors emitted from the browser
const logs = await browser
.manage()
.logs()
.get(logging.Type.BROWSER)
expect(logs).not.toContain(
jasmine.objectContaining({
level: logging.Level.SEVERE,
} as logging.Entry)
)
})
})
app.e2e-spec.ts 使用 Jasmine 编写并实现了验收测试。规范依赖于页面对象(po)文件,该文件定义在 spec 文件旁边:
**e2e/src/app.po.ts**
import { browser, by, element } from 'protractor'
export class AppPage {
navigateTo(): Promise<unknown> {
return browser.get(browser.baseUrl) as Promise<unknown>
}
getTitleText(): Promise<string> {
return element(by.css('app-root div h1'))
.getText() as Promise<string>
}
}
考虑以下图表,它以视觉方式表示 e2e 测试架构:
图 4.4:e2e 测试的架构
'should display welcome message' 测试的目标是验证 app.component.html 是否显示了正确的文本。页面对象文件 app.po.ts 封装了 WebDriver 实现,使用 getTitleText 函数检索消息。最后,测试在 app.e2e-spec.ts 文件中以 Jasmine 测试的形式编写。AATs 是最脆弱的测试类型。在 HTML 和规范文件之间有一个页面对象层,这使得测试易于维护且易于阅读。通过在这一级别分离关注点,您可以将 AATs 的脆弱性限制在一个位置。通过利用类继承,您可以构建一个健壮的页面对象集合,随着时间的推移更容易维护。
e2e 测试执行
您可以使用以下命令在终端中执行 e2e 测试;确保 npm test 进程没有运行:
$ npm run e2e
注意,测试执行与单元测试不同。虽然您可以使用 Karma 配置监视器来持续执行单元测试,但由于 e2e 测试的用户驱动和有状态性质,尝试对 e2e 测试进行类似的配置并不是一个好的做法。运行一次测试并停止测试工具确保每次运行都有一个干净的状态。
e2e 页面对象和规范
执行 e2e 测试后,您应该看到类似于以下错误消息:
**************************************************
* Failures *
**************************************************
1) web-app App should display welcome message
- Failed: No element found using locator: By(css selector, app-root .content span)
Executed 1 of 1 spec (1 FAILED) in 0.659 sec.
测试失败是因为我们在 app.component.html 中的 HTML 结构以及页面对象的 getTitleText 方法中进行了重大修改,导致该方法不再正确。
-
首先,通过纠正
getTitleText以获取正确的文本:e2e/src/app.po.ts getTitleText(): Promise<string> { return element(by.css('app-root div h1')). getText() as Promise<string> }注意,错误信息现在说:
- Expected 'LocalCast Weather' to equal 'local-weather-app app is running!'. -
更新
spec以期望正确的标题如下:e2e/src/app.e2e-spec.ts it('should display welcome message', () => { page.navigateTo() expect(page.getTitleText()).toEqual('LocalCast Weather') }) -
重新运行测试;现在它们应该通过了:
Jasmine started web-app App √ should display welcome message Executed 1 of 1 spec SUCCESS in 0.676 sec. -
提交您的代码更改。
我们的单元测试和端到端测试现在正在工作。
对于自动化验收测试,还有更强大的工具,如 cypress.io 和 github.com/bigtestjs。考虑使用这些工具而不是 Angular 的 e2e 测试。
您可以在 第七章,创建以路由为第一线的业务应用 中找到 LemonMart 项目的 Cypress 示例实现,该章节位于 github.com/duluca/lemon-mart。
执行 npm run cypress:run 以查看 Cypress 的实际效果。Cypress 可以记录和重放测试运行,以便轻松调试;它是您下一个企业项目的强大工具。
从现在开始,确保您的测试保持正常工作状态。
接下来,我们需要为生产部署准备我们的应用程序,这意味着以生产模式构建应用程序并设置适当的环境变量。
生产准备就绪
当您运行 npm start 时,Angular 以调试模式构建,这可以加快构建时间,启用断点调试和实时重新加载。这也意味着一个小型应用捆绑包的大小会膨胀到超过 7 MB。在慢速 3G 连接上,7 MB 的捆绑包大小会导致超过两分钟的加载时间,而我们的应用只需几秒钟即可加载。此外,在调试模式下,我们使用的是为本地开发而设计的环境变量。然而,在生产环境中,我们需要使用不同的设置,以便我们的应用程序可以在托管环境中正确运行。
让我们先实现一个 npm 脚本来帮助我们以生产模式构建。
构建生产版本
Angular 随带一个强大的构建工具,可以通过从调试构建中删除冗余、未使用和不高效代码以及预编译代码部分来优化捆绑包的大小,以便浏览器可以更快地解释它。因此,7 MB 的捆绑包可以缩小到 700 KB,即使在慢速 3G 连接上也能在 7 秒内加载完成。
默认情况下,ng build 命令以调试模式构建您的代码。通过向其中添加 --prod 选项,我们可以启用 prod 模式。
-
在
package.json中添加一个名为build:prod的新脚本,如下所示:**package.json** "scripts": { ... "build:prod": "ng build --prod" } -
通过执行以下命令测试脚本:
$ npm run build:prod
这是高效交付 Angular 应用的关键配置。
在启用生产模式之前,不要发布 Angular 应用程序。
接下来,让我们设置生产环境的环境变量。
设置环境变量
在 第三章,创建基本的 Angular 应用 中,我们使用存储在 src/environment/environment.ts 文件中的环境变量配置了 OpenWeatherMap API 的 URL。我们需要更新我们的变量以用于生产,因为我们的 Angular 应用程序所在的环境正在发生变化。在本地或测试环境中工作的设置不一定适用于托管环境。
将以下更改应用到 environment.prod.ts 文件中:
-
将
production设置为true -
如有必要,提供生产
appId变量 -
将
baseUrl更新为https:**src/environments/environment.prod.ts** export const environment = { production: true, appId: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', baseUrl: 'https://', }
将 production 设置为 true 允许应用程序代码检查应用程序的模式以调整其行为。此外,我们将 baseUrl 从 HTTP 更改为 HTTPS,因为我们的应用程序是通过 HTTPS 托管的。浏览器不允许提供混合内容,这会削弱 HTTPS 提供的整体安全优势。如果我们不切换到 HTTPS,那么我们对 OpenWeatherMap API 的调用将失败。
你可以在 developers.google.com/web/fundamentals/security/prevent-mixed-content/what-is-mixed-content 上了解更多关于混合内容的信息。
接下来,让我们设置 CI 以确保在将应用程序部署到生产之前,我们的测试总是通过。
持续集成
在将代码推送到生产之前,你应该启用 CI。这个基本设置有助于确保即使我们更改代码,我们的应用程序代码也能继续工作,因为它通过自动化执行我们的单元测试来实现。
CircleCI
CircleCI 为初学者和专业人士 alike 提供了免费层和优秀的文档,使其易于上手。如果你有独特的企业需求,CircleCI 可以在企业内部署,位于企业防火墙之后,或作为云中的私有部署。
CircleCI 为免费设置的虚拟配置预置了构建环境,但它也可以使用 Docker 容器运行构建,使其成为一个可以扩展到用户技能和需求解决方案,如在第九章 使用 Docker 的 DevOps 中所述:
-
在
circleci.com/创建 CircleCI 账户。 -
使用 GitHub 注册:
图 4.5:CircleCI 注册页面
-
添加一个新项目:
图 4.6:CircleCI 项目页面
在下一屏,你可以选择 Linux 或 macOS 构建环境。macOS 构建环境适合构建 iOS 或 macOS 应用。然而,这些环境没有免费层;只有具有 1x 并行性的 Linux 实例是免费的。
-
搜索
local-weather-app并点击 Set Up Project。 -
选择 Linux。
-
将 Language 设置为 Node,它提供了一个样本
.yml文件。本节使用 local-weather-app 仓库。本节的
config.yml文件命名为.circleci/config.ch4.yml。您还可以在 CircleCI 上找到本章中执行 yml 文件的拉取请求:github.com/duluca/local-weatherapp/pull/52使用branch build_ch4。请注意,此分支在config.yml和Dockerfile中进行了修改配置,以使用 local-weather-app 中的projects/ch4代码。 -
在您的源代码中,创建一个名为
.circleci的文件夹,并添加一个名为config.yml的文件:**.circleci/config.yml** version: 2.1 jobs: build: docker: - image: circleci/node:lts-browsers working_directory: ~/repo steps: - checkout - restore_cache: keys: - v1-dependencies-{{ checksum "package-lock.json" }} - run: npm ci # force update the webdriver - run: cd ./node_modules/protractor && npm i webdrivermanager@latest # because we use "npm ci" to install NPM dependencies # we cache "~/.npm" folder - save_cache: key: v1-dependencies-{{ checksum "package-lock.json" }} paths: - ~/.npm - run: npm run style - run: npm run lint - run: npm run build:prod - run: npm run test:coverage -- --watch=false - run: npm run e2e - run: name: Tar & Gzip compiled app command: tar zcf dist.tar.gz dist/local-weather-app - store_artifacts: path: dist.tar.gz workflows: version: 2 build-and-test: jobs: - build -
将您的更改同步到 Github。
-
在 CircleCI 上,点击 开始构建 以注册您的项目。
如果一切顺利,您应该有一个通过,绿色 的构建。如果不顺利,您会看到一个失败的,红色 的构建。以下截图显示了一个失败的构建,#97,以及随后的成功构建,#98:
图 4.7:CircleCI 上的绿色构建
现在您有了绿色构建,可以利用 CircleCI 在每次代码推送时强制执行您的自动化管道的执行。GitHub 流允许我们控制代码如何流入我们的仓库。
GitHub 流
我们开发软件的主要原因是为了提供价值。在自动化软件交付方式的过程中,我们正在创建一个价值交付流。交付有缺陷的软件很容易;然而,为了可靠地提供价值,每次对代码库的更改都应该通过一系列的检查和平衡流程。
通过控制门,我们可以强制执行标准,使我们的质量控制流程对每个团队成员都是可重复的,并且能够隔离更改。如果出现问题或工作不符合您的标准,您可以轻松地丢弃提议的更改并重新开始。
GitHub 流程是定义价值交付流和实施控制门的关键部分。正如 GitHub 所说,"GitHub 流是一个轻量级的基于分支的工作流程,支持定期部署的团队和项目。"
GitHub 流程包括 6 个步骤,如下所示,来自 GitHub 的以下图形:
图 4.8:GitHub 流图
-
分支 – 总是在新分支中添加用于修复错误或新功能的代码
-
提交 – 对您的分支进行多次提交
-
创建拉取请求 – 向团队成员发出您的工作准备就绪的信号,并在拉取请求中查看 CI 结果
-
讨论和审查 – 请求对您的代码更改进行审查,处理一般性或行级评论,并进行必要的修改
-
部署 – 可选地在测试服务器或生产环境中测试您的代码,并具有回滚到主分支的能力
-
合并 – 将您的更改应用到主分支
使用 GitHub 流,您可以确保只有高质量的代码最终进入主分支。坚实的基础为其他团队成员在开始他们的更改时设定了成功的基础。为了强制执行 GitHub 流,您需要限制对主分支的推送访问。
让我们为 master 分支启用分支保护:
-
导航到您项目的 GitHub 设置标签页
-
从左侧导航面板中选择分支
-
点击添加规则按钮
-
按照以下图像配置您的规则!
图 4.9:GitHub 分支保护规则
-
保存您的更改后,您应该在 分支 页面上看到您的新规则,如下所示:
图 4.10:GitHub 分支
您不再能够直接向 master 分支提交代码。要提交代码,您首先需要从 master 创建一个分支,将更改提交到新分支,然后准备好后,使用新分支创建拉取请求。如果您不熟悉 git 命令,可以使用 GitHub Desktop 来协助您进行这些操作。请参阅 GitHub Desktop 中的实用 分支 菜单:
图 4.11:GitHub Desktop 分支菜单
创建拉取请求后,您现在可以观察对您的分支运行的检查。现在我们已经配置了 CircleCI,如果一切顺利,您应该能够合并拉取请求,如下所示:
图 4.12:GitHub.com 状态检查通过
当检查失败时,您必须修复任何问题后才能合并新代码。此外,如果团队成员在您正在工作分支的同时合并到 master,您可能会遇到合并冲突。在这种情况下,您可以使用 GitHub Desktop 的 从 master 更新 功能来使您的分支与最新的 master 分支保持同步。
观察以下图像中失败的拉取请求的状态:
图 4.13:GitHub.com 状态检查失败
注意,我还有一个额外的检查,DeepScan,它会对我的代码库运行额外的测试。您可以在 deepscan.io 上注册您的仓库。在 第九章,使用 Docker 的 DevOps 中,我演示了如何使用 Coveralls 强制执行单元测试代码覆盖率。
更多信息,请参阅 guides.github.com/introduction/flow。
现在我们已经确保了我们的自动化检查正在执行,我们可以合理地确信我们不会将损坏的应用程序推送到生产环境。接下来,让我们学习如何将我们的应用程序部署到云端。
部署到云端
如果从编码的角度来看,将东西部署到生产环境很困难,那么从基础设施的角度来看,正确地做到这一点则非常复杂。在 第十三章,AWS 上的高可用云基础设施 中,我介绍了如何为您的应用程序配置世界级的 AWS 弹性容器服务(ECS)基础设施,但这在您需要快速展示一个想法或不需要高度可配置的解决方案时不会有所帮助。这时就出现了 Vercel Now。
Vercel Now
Vercel Now,vercel.com,是一个多云服务,它允许您直接从命令行实时全球部署应用程序。Vercel Now 支持静态文件、Node.js、PHP、Go 应用程序,以及您愿意为其编写自定义构建器的任何自定义软件堆栈,这使得与它一起工作变得相当简单。目前处于版本 2 的 Vercel Now 提供了一个免费层,您可以使用它来非常快速地部署 Angular 应用程序的dist文件夹。在第九章,使用 Docker 的 DevOps中,我展示了您如何部署 Angular 应用程序的容器化版本。
请参考第二章,设置您的开发环境,以获取安装 Vercel Now 的说明。
使用now工具,我们已准备好将我们的应用程序部署到网络上。
发布静态文件
在构建 Angular 项目后,构建输出位于dist文件夹中。这个文件夹中的文件被认为是静态文件;所有网络服务器需要做的就是将这些文件未修改地发送到客户端浏览器,然后浏览器动态执行您的代码。
这意味着任何网络服务器都能够提供您的 Angular 项目。然而,now使这一过程变得极其简单且免费。
让我们开始使用now的静态文件托管功能部署您的 Angular 应用程序。
-
在
package.json中添加两个新的脚本,如下所示:**package.json** ... "scripts": { ... "prenow:publish": "npm run build:prod", "now:publish": "now --platform-version 2 dist/local-weather-app" }要从
github.com/duluca/local-weather-app部署第四章特定的代码,您需要执行now --platform-version 2 dist/ch4。接受 CLI 提示的默认选项。在我的情况下,应用程序部署到了ch4-dun.now.sh/。 -
执行
npm run now:publish。 -
接受 CLI 提示的默认选项。
在终端窗口中,注意 Angular 项目首先构建然后上传到
now:$ npm run now:publish > localcast-weather@9.0.0 prenow:publish C:\dev\local-weather-app > npm run build:prod > localcast-weather@9.0.0 build:prod C:\dev\local-weather-app > ng build --prod Generating ES5 bundles for differential loading... ES5 bundle generation complete. chunk {2} polyfills-es2015.ca64e4516afbb1b890d5.js (polyfills) 35.6 kB [initial] [rendered] chunk {3} polyfills-es5.1d087d4db6b105875851.js (polyfills-es5) 128 kB [initial] [rendered] chunk {1} main-es2015.941dc398feac35a1a67d.js (main) 485 kB [initial] [rendered] chunk {1} main-es5.941dc398feac35a1a67d.js (main) 577 kB [initial] [rendered]chunk {0} runtime-es2015.0811dcefd377500b5b1a.js (runtime) 1.45 kB [entry] [rendered] chunk {0} runtime-es5.0811dcefd377500b5b1a.js (runtime) 1.45 kB [entry] [rendered] chunk {4} styles.1938720bb6985e81892f.css (styles) 62 kB [initial] [rendered]Date: 2020-03-24T00:14:52.939Z - Hash: 4d78a666345c6761dc95 - Time: 14719ms > localcast-weather@9.0.0 now:publish C:\dev\local-weather-app > now --platform-version 2 --prod dist/local-weather-app > UPDATE AVAILABLE Run `npm i now@latest` to install Now CLI 17.1.1 > Changelog: https://github.com/zeit/now/releases/tag/now@17.1.1 Now CLI 17.0.4 ? Set up and deploy "C:\dev\local-weather-app\dist\local-weather-app"? [Y/n] y ? Which scope do you want to deploy to? Doguhan Uluca ? Found project "duluca/local-weather-app". Link to it? [Y/n] y  Linked to duluca/local-weather-app (created .now and added it to .gitigre)  Inspect: https://zeit.co/duluca/local-weather-app/jy2k1szdi [2s]  Production: https://local-weather-app.duluca.now.sh [copied to clipboard] [4s] -
按屏幕上显示的 URL 查看,您的应用程序已成功部署,在我的情况下,
local-weather-app.duluca.now.sh。注意关于缺少
now.json文件的警告。当我们运行命令时,我们使用选项--platform-version 2指定我们的平台版本为 2,因此配置文件不是必需的。然而,如果您希望自定义部署的任何方面,例如使用自定义域名、选择地理位置或使用扩展选项,您应该配置此文件。有关如何充分利用now的更多信息,请参阅vercel.com/docs。
如果您的部署成功,您应该看到您的应用程序显示了美国贝塞斯达的当前天气:
图 4.14:成功部署
完成了!恭喜,您的 Angular 应用程序已上线!
摘要
在本章中,你了解了单元测试的重要性,并掌握了 Angular 单元和端到端测试的配置和设置。你学习了如何配置 Angular 的 TestBed 以及如何使用测试替身编写单元测试。你为生产部署配置了你的 Angular 应用。通过使用 CI 管道和 GitHub 流创建价值交付流,你确保了应用程序的质量。最后,你成功地将一个网络应用程序部署到云端。
现在,你知道了构建一个可靠、有弹性且容器化的生产就绪 Angular 应用程序需要哪些条件,它允许灵活的部署策略。在下一章中,我们将介绍如何将 Angular Material 添加到你的项目中,让你的本地天气预报应用看起来很棒。在这个过程中,你将了解用户控件或 UI 组件库可能对你的应用程序产生的负面影响,包括基本 Material 组件;Angular Flex Layout;无障碍性;排版;主题;以及如何更新 Angular Material。
进一步阅读
-
通过敏捷成功:使用 Scrum 的软件开发,迈克·科恩,2009 年。
-
测试金字塔,马丁·福勒,2012 年,
martinfowler.com/bliki/TestPyramid.html. -
Jasmine 2 间谍备忘单,戴夫·塞迪亚,2015 年,
daveceddia.com/jasmine-2-spy-cheat-sheet. -
实用的测试金字塔,汉姆·沃克,2018 年,
martinfowler.com/articles/practical-test-pyramid.html. -
SOLID 原则,维基百科,2019 年,
en.wikipedia.org/wiki/SOLID.
问题
尽可能地回答以下问题,以确保你已理解本章的关键概念,无需使用 Google。你需要帮助回答这些问题吗?请参阅 附录 D,自我评估答案,在线位于 static.packt-cdn.com/downloads/9781838648800_Appendix_D_Self-Assessment_Answers.pdf 或访问 expertlysimple.io/angular-self-assessment。
-
测试金字塔是什么?
-
固定装置和匹配器是什么?
-
模拟、间谍和存根之间有什么区别?
-
在生产模式下构建 Angular 的好处是什么?
-
GitHub 流是如何工作的?
-
为什么我们应该保护主分支?
第五章:使用 Material 提供高质量 UX
在第四章,“自动化测试、CI 和发布到生产”中,我们提到了交付高质量应用程序的需求。目前,应用程序的外观和感觉非常糟糕,只适合 20 世纪 90 年代末创建的网站。用户或客户对你产品或工作的第一印象非常重要,因此我们必须能够创建一个外观出色且在移动和桌面浏览器上提供出色用户体验的应用程序。
作为全栈开发者,专注于应用程序的打磨是件困难的事情。当应用程序的功能集迅速增长时,这种情况往往会变得更糟。编写支持视图的出色模块化代码固然有趣,但随后又匆忙地使用 CSS 技巧和内联样式来改善应用程序的外观和感觉,这并不愉快。
在与 Angular 紧密协调下开发的 Angular Material 非常出色。如果你学会了如何有效地利用 Angular Material,你创建的功能将从一开始就看起来和运行得很好,无论你是在开发小型还是大型应用程序。
Angular Material 将使你成为一个更有效的 Web 开发者,因为它提供了一系列你可以利用的用户控件,你不必担心浏览器兼容性。作为额外的奖励,编写自定义 CSS 将变得罕见。
虽然本章介绍了如何创建一个吸引人的用户界面(UI),并利用 Angular Material 实现开箱即用的用户体验(UX),但了解不应该做什么也同样重要。有一个名为 User Interface 的网站,展示了 UI/UX 的糟糕实践,网址为userinyerface.com。
在本章中,你将学习以下内容:
-
区分 Angular Material 作为 UI/UX 库的特点
-
如何配置 Angular Material
-
使用 Angular Flex Layout 进行响应式设计
-
使用 Angular Material 提升 UX
-
通过命令行界面(CLI)工具强制执行可访问性合规性
-
构建交互式原型
书籍的样本代码的最新版本可在以下 GitHub 仓库链接中找到。该仓库包含代码的最终和完成状态。你可以在本章结束时通过查找projects文件夹下的代码章节快照来验证你的进度。
对于第五章:
-
在根目录下执行
npm install以安装依赖项 -
本章的代码示例位于以下子文件夹中:
projects/ch5 -
要运行本章的 Angular 应用程序,请执行:
npx ng serve ch5 -
要运行本章的 Angular 单元测试,请执行:
npx ng test ch5 --watch=false -
要运行本章的 Angular e2e 测试,请执行:
npx ng e2e ch5 -
要构建本章的生产就绪型 Angular 应用程序,请执行:
npx ng build ch5 --prod
注意,存储库根目录下的dist/ch5文件夹将包含编译结果。
请注意,书中或 GitHub 上的源代码可能并不总是与 Angular CLI 生成的代码相匹配。由于生态系统不断演变,书中代码与 GitHub 上代码之间的实现也可能存在细微差异。随着时间的推移,示例代码发生变化是自然的。在 GitHub 上,您可能会找到更正、修复以支持库的新版本,或者观察多种技术并行的实现。您只需实现书中推荐的理想解决方案即可。如果您发现错误或有问题,请创建一个 issue 或提交一个 pull request 到 GitHub,以供所有读者受益。
让我们先了解是什么让 Angular Material 成为 UI/UX 库的一个优秀选择。
Angular Material
Angular Material 项目的目标是提供一组有用且具有标准设置的高质量 UI 组件。该库实现了谷歌的 Material Design 规范,该规范在谷歌的移动应用、网络属性和 Android 操作系统中无处不在。Material Design 具有特定的数字和方盒式外观和感觉,但它不仅仅是一个像 Bootstrap 一样的 CSS 库。考虑这里使用 Bootstrap 编写的登录体验:
图 5.1:Bootstrap 登录体验
注意,输入字段及其标签位于单独的行上,复选框是一个小目标,错误信息以临时吐司通知的形式显示,而提交按钮则静静地坐在角落里。现在,考虑以下 Angular Material 示例:
图 5.2:Angular Material 登录体验
输入字段及其标签最初是合并的,以紧凑的形态因素吸引用户的注意力。复选框触感友好,提交按钮默认拉伸以占用可用空间,从而提供更响应式的用户体验。一旦用户点击一个字段,标签就会折叠到输入字段的左上角,如下所示:
图 5.3:Angular Material 动画和错误
此外,验证错误信息以行内形式显示,与标签的颜色变化相结合,保持用户的注意力在输入字段上。
Material Design 帮助您设计具有自己品牌和样式的模块化 UI,同时定义动画,使用户在使用您的应用程序时拥有更好的用户体验。人类大脑无意识地跟踪对象及其位置。任何有助于过渡或对人类输入产生的变化做出反应的动画都能减少用户的认知负荷,因此使用户能够专注于处理内容,而不是试图弄清楚您特定应用程序的怪癖。
模块化 UI 设计和流畅运动的结合创造了一个出色的用户体验。看看 Angular Material 如何实现一个简单的按钮:
图 5.4:Angular Material 按钮动画
在前面的屏幕截图中,请注意按钮上的点击动画是从用户实际点击的位置开始的。虽然这种效果很微妙,但它创造了一种连续的运动,从而对用户的行为产生适当的屏幕反应。当按钮在移动设备上使用时,这种效果更为明显,从而带来更自然的人机交互。大多数用户无法明确表达出什么使得直观的用户体验真正直观,而这些微妙但至关重要的设计经验和设计中的提示,可以帮助你在为用户设计这种体验方面取得巨大进步。
Angular Material 还旨在成为 Angular 高质量 UI 组件的参考实现。如果您打算开发自己的自定义控件,Angular Material 的源代码应该是您首要的资源。术语“高质量”经常被使用,而且真正重要的是要量化它的含义。Angular Material 团队在他们的网站上表达得很好:
我们所说的“高质量”是什么意思?
国际化和可访问性,确保所有用户都能使用它们。直观的 API 不会让开发者感到困惑,并且能够在各种用例中按预期工作,而不会出现错误。行为经过单元和集成测试的充分测试。在 Material 设计规范范围内可定制。性能成本降至最低。代码整洁且文档完善,可作为 Angular 开发者的示例。支持浏览器和屏幕阅读器。
Angular Material 支持所有主流浏览器的最新两个版本:Chrome(包括 Android)、Firefox、Safari(包括 iOS)和 IE11/Edge。
构建网络应用程序,尤其是那些也兼容移动设备的,是非常困难的。有很多细微之处你必须注意。Angular Material 抽象掉了这些细微之处,包括支持所有主流浏览器,这样你就可以专注于创建你的应用程序。Angular Material 不是一种时尚,也不应该被轻视。如果使用得当,你可以大大提高你的生产力和工作的感知质量。
并非总是可以在项目中使用 Angular Material。我建议使用 PrimeNG,可在 www.primefaces.org/primeng 找到,或者使用 Clarity,可在 vmware.github.io/clarity 找到,作为可以满足您大部分甚至所有用户控件需求的组件工具包。这里要避免的一件事是从不同的来源拉取数十个用户控件,最终得到一个包含数百个怪癖和错误的混乱库,这些怪癖和错误需要学习、维护或绕过。
在使用 UI 组件时,最显著的挑战之一是它们可以添加到您的应用程序包大小中的大量内容。接下来,让我们看看如何使用一致的组件库来帮助保持您应用程序的性能处于最佳状态,并为您的应用程序配置 Angular Material。
Angular Material 设置和性能
默认情况下,Angular Material 配置为优化最终交付物的包大小。在 Angular JS 和 Angular Material 1.x 中,整个依赖库都会被加载。然而,现在使用 Angular Material,我们能够指定我们打算使用的组件,从而实现显著的性能提升。
在以下表格中,您可以看到典型 Angular 1.x + Angular Material 1.x 应用程序与 Angular 6 + Material 6 应用程序在高速低延迟光纤连接下的性能特性改进:
| 光纤网络 | Angular 6 + Material 6 | Angular 1.5 + Material 1.1.5 | % 差异 |
|---|---|---|---|
| 首次绘制(DOMContentLoaded)* | 0.61 秒 | 1.69 秒** | 大约 2.8 倍更快 |
| JS 包大小* | 113 KB | 1,425 KB | 12.6 倍更小 |
为了公平比较,结果中未包含图像或其他媒体内容
平均值:较差的基础设施导致渲染时间从 0.9 秒到 2.5 秒不等
在高速低延迟的理想条件下,Angular 6 + Material 6 应用程序在 1 秒内加载完成。然而,当我们切换到更常见的普通速度和高延迟的快速 3G 移动网络时,差异变得更加明显,如下表所示:
| 快速 3G 移动网络 | Angular 6 + Material 6 | Angular 1.5 + Material 1.1.5 | % 差异 |
|---|---|---|---|
| 首次绘制* | 1.94 秒 | 11.02 秒 | 5.7 倍更快 |
| JS 包大小* | 113 KB | 1,425 KB | 12.6 倍更小 |
为了公平比较,结果中未包含图像或其他媒体内容
尽管应用程序的大小差异保持一致,但您可以看到,由移动网络引入的额外延迟导致传统 Angular 应用程序的运行速度急剧下降,达到了不可接受的水平。
将所有组件添加到 Material 中会导致大约~1.3 MB 的额外负载需要发送给用户。正如您从之前的比较中看到的,这必须不惜一切代价避免。为了提供尽可能小的应用程序,这在移动和销售相关场景中至关重要,因为每次加载时间增加 100 毫秒都会影响用户保留率,您可以单独加载和包含模块。Webpack 的 tree-shaking 过程将模块分割成不同的文件,从而减少初始下载大小。
作为现实世界的例子,当您完成 LocalCast Weather 应用程序的最终版本构建后,您的应用程序包大小将约为 800 KB,在快速 3G 连接上使用 Angular 9 + Material 9 时,首次绘制时间仅为 2 秒多。一个功能齐全的多页面应用程序,利用懒加载,仅加载大约~300 KB 的依赖项,同时保持首次绘制时间低于 2 秒。
注意,示例应用程序包含可以裁剪掉的示例代码,这使得应用程序的体积更小。这证明了 Angular 生态系统如何提供丰富和优化的用户体验。
接下来,让我们设置 Angular Material。
安装 Angular Material
你有几种方法可以为你的 Angular 应用配置 Angular Material:
-
使用 Angular CLI 自动安装
-
使用 npm 手动安装
让我们开始这个任务,并使用 Angular Material 提升天气应用的 UX。将 提升应用的 UX 任务移动到我们 GitHub 项目的 进行中 状态。在这里,你可以看到我的看板状态:
图 5.5:GitHub 项目看板
自动安装
从 Angular 6 开始,你可以自动将 Angular Material 添加到你的项目中,从而在过程中节省大量时间:
-
执行
add命令,如下所示:$ npx ng add @angular/material -
选择名为
indigo-pink的预构建主题 -
当你得到提示 “
Set up global Angular Material typography styles?” 时,输入 “no” -
当你得到提示 “
Set up browser animations for Angular Material?” 时,输入 “yes” -
输出应类似于以下示例:
Installing packages for tooling via npm. Installed packages for tooling via npm. ? Choose a prebuilt theme name, or "custom" for a custom theme: Indigo/Pink [ Preview: https://material.angular.io?theme=indigo-pink ] ? Set up global Angular Material typography styles? No ? Set up browser animations for Angular Material? Yes UPDATE package.json (1348 bytes) √ Packages installed successfully. UPDATE src/app/app.module.ts (423 bytes) UPDATE angular.json (3740 bytes) UPDATE src/index.html (487 bytes) UPDATE src/styles.css (181 bytes)注意,
index.html文件已被修改,以添加图标库和默认字体,如下所示:**src/index.html** <head> ... <link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> </head>angular.json文件已被更新以设置默认主题:**angular.json** ... "styles": [ "./node_modules/@angular/material/prebuilt-themes/indigo-pink.css", "src/styles.css" ], ...styles.css已被更新为默认的全局 CSS 样式:**src/styles.css** html, body { height: 100%; } body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }还请注意,
app.module.ts已被更新以导入BrowserAnimationsModule,如下所示:**src/app/app.module.ts** import { BrowserAnimationsModule } from '@angular/platform- browser/animations'; @NgModule({ declarations: [ AppComponent ], imports: [ ... BrowserAnimationsModule ], -
启动你的应用并确保它正常工作:
$ npm start
到此为止,你已经完成了。你的应用应该已经配置了 Angular Material。你现在可以跳转到 导入模块 部分,看看如何以稳健的方式导入 Material 模块。
我强烈建议快速浏览所有手动安装和配置步骤。知道的越多越好!
仍然重要的是要理解构成 Angular Material 的所有各种组件,或者你可能不喜欢自动化的东西;在接下来的章节中,我们将介绍手动安装和配置步骤。
手动安装
我们将首先安装所有必需的库。截至 Angular 5,Angular Material 的主版本应与你的 Angular 安装版本相匹配,并且从 Angular 6 开始,版本应同步:
-
在终端中执行
npm install @angular/material @angular/cdk -
观察到
package.json版本:**package.json** "dependencies": { "@angular/cdk": "9.0.0", "@angular/material": "9.0.0", ...
在这种情况下,所有库都有相同的主版本和次版本。如果你的主版本和次版本不匹配,你可以重新运行 npm install 命令来安装特定版本,或者选择通过将包的服务器版本附加到 install 命令来升级你的 Angular 版本:
$ npm install @angular/material@9.0.0 @angular/cdk@9.0.0
如果你在一个类似 Bash 的 shell 中工作,你可以通过使用方括号语法来节省一些输入,避免重复命令的部分,形式为 npm install @angular/{material,cdk}@9.0.0。
如果你需要更新你的 Angular 版本,请参考附录 C 中的更新 Angular部分,保持 Angular 和工具始终如一。您可以从static.packt-cdn.com/downloads/9781838648800_Appendix_C_Keeping_Angular_and_Tools_Evergreen.pdf或expertlysimple.io/stay-evergreen在线找到此附录。
理解 Material 的组件
让我们看看我们到底在安装什么:
-
@angular/material是官方的 Material 库。 -
@angular/cdk是一个同级依赖项,除非你打算构建自己的组件,否则你不会直接使用它。 -
@angular/animations为某些 Material 模块启用了一些动画。可以省略以保持应用大小最小。你可以使用NoopAnimationsModule来禁用需要此依赖项的模块中的动画。结果,你将失去 Angular Material 的一些 UX 优势。
手动配置 Angular Material
现在依赖项已安装,让我们在 Angular 应用中配置 Angular Material。请注意,如果你使用ng add @angular/material安装 Angular Material,其中一些工作将为你完成。
导入模块
我们将首先创建一个单独的模块文件来存放所有的 Material 模块导入:
-
在终端中执行以下命令以生成
material.module.ts:$ npx ng g m material --flat -m app注意使用
--flat标志,它表示不应为material.module.ts创建额外的目录。另外,注意-m是--module的别名,这样我们的新模块就会自动导入到app.module.ts中。 -
观察新创建的文件
material.module.ts并移除CommonModule:**src/app/material.module.ts** import { NgModule } from '@angular/core' @NgModule({ imports: [], declarations: [], }) export class MaterialModule {} -
确保模块已被导入到
app.module.ts中:**src/app/app.module.ts** import { MaterialModule } from './material.module' ... @NgModule({ ... imports: [..., MaterialModule], } -
添加动画和手势支持(如果尚未自动添加):
**src/app/app.module.ts** import { BrowserAnimationsModule } from '@angular/platform-browser/animations' @NgModule({ ... imports: [..., MaterialModule, BrowserAnimationsModule], } -
修改
material.module.ts以导入和导出MatButton、MatToolbar和MatIcon的基本组件:**src/app/material.module.ts** import { NgModule } from '@angular/core' import { MatButtonModule } from '@angular/material/button' import { MatIconModule } from '@angular/material/icon' import { MatToolbarModule } from '@angular/material/toolbar' @NgModule({ imports: [ MatButtonModule, MatToolbarModule, MatIconModule ], exports: [ MatButtonModule, MatToolbarModule, MatIconModule ], }) export class MaterialModule {}imports和exports数组有时会变得很长且重复。如果你在其中一个数组中遗漏了一个元素,你可能会花费数小时来追踪错误。考虑实现一个单一的数组作为常量,你可以将其分配给imports和exports属性以获得更可靠的配置。感谢 Brendon Caulkins 提供的提示。 -
优化你的代码,将你的模块存储在数组中,并重复使用它来导入和导出:
**src/app/material.module.ts** ... const modules = [MatButtonModule, MatToolbarModule, MatIconModule] @NgModule({ declarations: [], imports: modules, exports: modules, }) export class MaterialModule {}
现在 Material 已被导入到应用中;现在让我们配置一个主题并向我们的应用添加必要的 CSS。
导入主题
为了使用 Material 组件,需要一个基本主题。我们在安装 Angular Material 时已经选择了一个默认主题。我们可以在angular.json中定义或更改默认主题:
**angular.json**
...
"styles": [
{
"input":
"node_modules/@angular/material/prebuilt-themes/indigo-pink.css"
},
"src/styles.css"
],
...
从这里选择一个新的选项:
-
deeppurple-amber.css -
indigo-pink.css -
pink-bluegrey.css -
purple-green.css
更新angular.json以使用新的 Material 主题。
你也可以创建自己的主题,这在本章的自定义主题部分有介绍。更多信息,请访问material.angular.io/guide/theming。
注意,在styles.css中实现的任何 CSS 都将全局可用。话虽如此,请不要在此文件中包含特定视图的 CSS。每个组件都有自己的 CSS 文件用于此目的。
添加 Material 图标字体
你可以通过将 Material Icon 网络字体添加到你的应用程序中,来获取一个很好的默认图标集。这个库的大小为 48 KB,是一个非常轻量级的库。
为了支持图标,请在index.html中导入字体:
**src/index.html**
<head>
...
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head>
在material.io/resources/icons上发现和搜索图标。
为了获取更丰富的图标集,请访问MaterialDesignIcons.com。这个图标集包含了 Material 图标的基础集合,以及一个丰富的第三方图标集,其中包含来自社交媒体网站的有用图像,涵盖了众多丰富的操作,覆盖了广泛的领域。这个字体大小为 118 KB。
我们为 Angular Material 配置了 UI/UX 库。我们还需要一个布局库,以便在页面上放置组件时使生活更加轻松。
接下来,让我们了解不同的布局技术,从 Bootstrap 到 Flexbox CSS,以及为什么 Angular Flex Layout 是管理布局的绝佳工具。在为我们的应用程序配置 Angular Flex Layout 之后,我们将准备好在我们的应用程序中实现 Material UI 组件。
Angular Flex Layout
在你能够有效使用 Material 之前,你必须了解其布局引擎。如果你已经从事了一段时间的 Web 开发,你可能已经遇到过 Bootstrap 的 12 列布局系统。我发现这非常令人烦恼,因为它在我的大脑中遇到了一个数学障碍,我的大脑习惯于将事物分成 100%的部分。Bootstrap 还要求严格遵循div列和行的层次结构,这必须从你的顶级 HTML 精确管理到最底层。这可能会使开发体验非常令人沮丧。
在下面的屏幕截图中,你可以看到 Bootstrap 的 12 列方案看起来如何:
图 5.6:Bootstrap 的 12 列布局方案
Bootstrap 的定制网格布局系统在当时是革命性的,但随后 CSS3 Flexbox 出现了。结合媒体查询,这两种技术允许创建响应式 UI。然而,有效地利用这些技术是非常费力的。截至 Angular v4.1,Angular 团队引入了其 Flex Layout 系统,它只需简单设置即可工作。
GitHub 上的 Angular Flex Layout 文档很好地解释了以下内容:
Angular Flex Layout 使用 FlexBox CSS 和 mediaQuery 提供了一个复杂的布局 API。此模块为 Angular(v4.1 及以上)开发者提供了使用自定义 Layout API、mediaQuery 可观察对象和注入 DOM flexbox-2016 CSS 样式的组件布局功能。
Angular 的出色实现使得使用 Flexbox 变得非常容易。正如文档进一步解释的那样:
布局引擎智能地自动化了将适当的 FlexBox CSS 应用于浏览器视图层次结构的过程。这种自动化还解决了使用传统的、仅 CSS 的手动应用 FlexBox CSS 时遇到的大多数复杂性和解决方案。
该库功能强大,可以适应您能想象到的任何类型的网格布局,包括与您期望的所有 CSS 功能集成,例如calc()函数。在下一幅插图,您可以看到如何使用 CSS Flexbox 描述列:
图 5.7:Angular Flex Layout 方案
好消息是,Angular Flex Layout 与 Angular Material 没有任何耦合,可以独立使用。这是一个非常重要的解耦,解决了使用 AngularJS 与 Material v1 时的一个主要痛点,即 Material 的版本更新经常会引起布局中的错误。
更多详情,请查看github.com/angular/flex-layout/wiki。
您会注意到@angular/flex-layout安装时带有 beta 标签。这个状态已经持续了很长时间。由于库无法覆盖回 Internet Explorer 11 的所有边缘情况,这阻止了它退出 beta。然而,在持续更新的浏览器中,我发现该库的行为是可靠和一致的。此外,CSS Grid 有取代 CSS Flexbox 的趋势,因此,该库使用的底层技术可能会改变。我的愿望是,这个库作为布局引擎下方的抽象层。
响应式布局
所有您设计和构建的用户界面(UI)都应该是以移动端优先的 UI。这不仅仅是为了服务于手机浏览器,还包括笔记本电脑用户可能在一侧并排使用您的应用程序的情况。正确实现以移动端优先的设计有很多细微之处。
以下是Mozilla Holy Grail 布局,它展示了“动态改变不同屏幕分辨率的布局”的能力,同时优化移动设备上的显示内容。
您可以在mzl.la/2vvxj25了解更多关于 Flexbox 基本概念的信息。
这表示了 UI 在大屏幕上的外观:
图 5.8:大屏幕上的 Mozilla Holy Grail 布局
如下所示,相同的布局在小屏幕上表示:
图 5.9:小屏幕上的 Mozilla Holy Grail 布局
Mozilla 的参考实现需要 85 行代码来完成这种响应式 UI。Angular Flex Layout 只需一半的代码就能完成同样的任务。
安装 Angular Flex Layout
让我们安装并将 Angular Flex Layout 添加到我们的项目中:
-
在终端中执行
npm i @angular/flex-layout要解决依赖错误,执行
npm i @angular/flex-layout@next或npm i @angular/flex-layout --force,如 附录 C 中所述,保持 Angular 和工具始终如一。您可以从static.packt-cdn.com/downloads/9781838648800_Appendix_C_Keeping_Angular_and_Tools_Evergreen.pdf或expertlysimple.io/stay-evergreen在线找到此附录。 -
更新
app.module.ts,如下所示:**src/app.module.ts** import { FlexLayoutModule } from '@angular/flex-layout' imports: [..., **FlexLayoutModule** ],
安装 Flex Layout 后,让我们来了解一下库的基本工作原理。
布局基础
Bootstrap 和 CSS Flexbox 与 Angular Flex Layout 不同。如果你学习 Angular Flex Layout,你会发现你将使用更少的布局代码,因为 Angular Material 大多数时候会自动做正确的事情,但一旦你意识到一旦离开 Angular Flex Layout 的保护壳,你需要编写更多的代码才能使事物工作,你可能会感到失望。然而,你的技能仍然可以迁移,因为概念大多相同。
让我们在以下章节中回顾 Flex Layout API。
如果你是 CSS 或 Flexbox 的初学者,一些使用的缩写可能没有意义。我建议你尝试在文档中提供的实时演示应用程序中实验,以更好地了解库在更直观的层面的功能。更多信息及访问实时演示的链接请访问 github.com/angular/flex-layout/wiki/Declarative-API-Overview。
Flex Layout API 用于 DOM 容器
这些指令可以用于 DOM 容器,如 <div> 或 <span>,以操纵它们的布局方向、对齐或元素之间的间隙。
考虑以下示例:
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="15px">...</div>
div 被布局为行,因此多个 div 将会堆叠在一起,而不是列布局,它们会并排渲染。
div 在其父容器内水平左对齐且垂直居中。
div 与其周围元素之间有 15-px 的间隙。
考虑以下图表来在空间上映射 fxLayout 术语:
图 5.10:Angular Flex Layout 术语的空间映射
选项的完整列表如下表所示:
| HTML API | 允许的值 | |||||||
|---|---|---|---|---|---|---|---|---|
fxLayout | <direction> | <direction> <wrap>使用:row | column | row-reverse | column-reverse | |||||||
fxLayoutAlign | <main-axis> <cross-axis> main-axis: start | center | end | space-around | space-between cross-axis: start | center | end | stretch |
fxLayoutGap | % | px | vw | vh |
DOM 元素的 Flex 布局 API
这些指令影响 DOM 元素在其容器内的行为。
考虑以下示例:
<div fxLayout="column">
<input fxFlex />
</div>
input 元素会扩展以填充父 div 提供的所有可用空间。如果 fxFlex 被设置为 fxFlex="50%",它将只填充可用空间的一半。在这种情况下,可以使用 fxFlexAlign 属性在 div 内部左对齐、右对齐或居中对齐元素。
以下表格展示了完整的选项列表:
| HTML API | 允许的值 | |||||
|---|---|---|---|---|---|---|
fxFlex | "" | px | % | vw | vh | <grow> <shrink> <basis> |
fxFlexOrder | int | |||||
fxFlexOffset | % | px | vw | vh | ||
fxFlexAlign | start | baseline | center | end | ||
fxFlexFill | none |
任何元素的 Flex 布局 API
以下指令可以应用于任何 HTML 元素,以显示、隐藏或更改这些元素的样式和外观。
考虑以下示例:
<div fxShow fxHide.lt-sm></div>
默认设置为 true 的 fxShow 将显示 div 元素。除非 lt-sm 条件变为 true,这发生在浏览器窗口缩小到 small 的阈值以下。Small 被定义为 468 像素的像素值。因此,如果浏览器窗口的宽度缩小到 467 像素或更少,fxHide 将隐藏 div 元素。
以下表格展示了完整的选项列表:
| HTML API | 允许的值 | |||
|---|---|---|---|---|
fxHide | TRUE | FALSE | 0 | "" |
fxShow | TRUE | FALSE | 0 | "" |
ngClass | @extends ngClass core | |||
ngStyle | @extends ngStyle core |
本节涵盖了静态布局的基础知识。您可以在github.com/angular/flex-layout/wiki/Declarative-API-Overview了解更多关于静态 API 的信息。我们将在第十一章“食谱 - 可重用性、路由和缓存”中介绍响应式 API。您可以在github.com/angular/flex-layout/wiki/Responsive-API了解更多关于响应式 API 的信息。
现在我们已经配置了布局引擎,并且你对它的工作原理有了基本的了解,我们可以开始构建我们应用的屏幕。
使用 Material 组件
现在我们已经安装了所有各种依赖项,我们可以开始修改我们的 Angular 应用以添加 Material 组件。我们将添加一个工具栏和一个 Material Design 卡元素,并介绍可访问性和排版问题,同时涵盖基本的布局技术。
Angular Material 规划图
自从 Angular 6 和脚图引入以来,像 Material 这样的库可以提供自己的代码生成器。在出版时,Angular Material 随带三个基本的生成器,用于创建具有侧导航、仪表板布局或数据表的 Angular 组件。你可以在 material.angular.io/guide/schematics 上了解更多关于生成器脚图的信息。
例如,你可以通过执行以下命令创建一个侧导航布局:
$ ng generate @angular/material:material-nav --name=side-nav
CREATE src/app/side-nav/side-nav.component.css (110 bytes) CREATE src/app/side-nav/side-nav.component.html (945 bytes) CREATE src/app/side-nav/side-nav.component.spec.ts (619 bytes) CREATE src/app/side-nav/side-nav.component.ts (489 bytes) UPDATE src/app/app.module.ts (882 bytes)
此命令更新 app.module.ts,直接将该文件中的 Material 模块导入,破坏了之前建议的 material.module.ts 模式。此外,一个新的 SideNavComponent 作为单独的组件添加到应用中,但如第八章 设计身份验证和授权 中的 侧导航 节所述,这种导航体验需要在应用的根目录中实现。
简而言之,Angular Material Schematics 使得向你的 Angular 应用添加各种 Material 模块和组件变得更加简单;然而,正如提供的,这些脚图并不适合创建一个灵活、可扩展且结构良好的代码库,这正是本书追求的目标。
目前,我建议将这些脚图用于快速原型设计或实验目的。
现在,让我们开始手动向我们的 LocalCast Weather 应用添加一些组件。
使用 Material 工具栏修改登录页面
在我们开始对 app.component.ts 进行进一步修改之前,让我们将组件切换到使用内联模板和内联样式,这样我们就不必在文件之间来回切换,因为这是一个相对简单的组件:
-
将
app.component.ts更新为使用内联模板。将app.component.html的内容剪切并粘贴到app.component.ts中,并移除如下所示的styleUrls属性:**src/app/app.component.ts** import { Component } from '@angular/core' @Component({ selector: 'app-root', **template: `** <div style="text-align:center"> <h1> LocalCast Weather </h1> <div>Your city, your forecast, right now!</div> <h2>Current Weather</h2> <app-current-weather></app-current-weather> </div> **`,** }) export class AppComponent {} -
删除文件
app.component.html和app.component.css。 -
让我们从实现一个全局工具栏开始改进我们的应用。观察
app.component.ts中的h1标签:**src/app/app.component.ts** <h1> LocalCast Weather </h1> -
更新
h1标签为mat-toolbar:**src/app/app.component.ts** **<mat-toolbar>** <span>LocalCast Weather</span> **</mat-toolbar>** -
将
mat-toolbar更新为更具吸引力的颜色:**src/app/app.component.ts** <mat-toolbar **color="primary"**>
注意,如果你的应用未能按照前面章节 导入模块 中所述导入 MatToolbarModule,则应用将无法编译。
注意,Material 会添加以下全局样式:
**src/styles.css**
body {
margin: 0;
}
0 外边距提供了原生应用的感觉,其中工具栏触及浏览器的边缘。这在大屏幕和小屏幕格式上都很好用。当你将汉堡菜单或帮助按钮等可点击元素放置在工具栏的左侧或右侧时,你会避免用户点击空白区域的可能性。这就是为什么 Material 按钮实际上比视觉上表示的点击区域更大。这为制作无烦恼的用户体验做出了很大的贡献。
类似地,如果你正在构建一个信息密集型的应用程序,请注意,你的内容将延伸到应用程序的边缘,这使得内容更难以阅读,这不是一个理想的结果。在这些情况下,你应该在内容区域包裹一个div,并使用 CSS 应用适当的边距,如下所示:
**example**
.content-margin {
margin-left: 8px;
margin-right: 8px;
}
在下一个屏幕截图中,你可以看到边缘到边缘的工具栏,并应用了主要颜色:
图 5.11:改进后的工具栏的 LocalCast 天气
现在我们已经配置了工具栏,让我们继续制作天气信息的容器。
材料卡片
材料卡片是一个很好的容器,可以表示当前的天气信息。卡片元素周围有一个阴影,将内容与其周围环境区分开来:
-
在
material.module中导入MatCardModule:**src/app/material.module.ts** import { MatCardModule } from '@angular/material/card' ... const modules = […, MatCardModule] -
在
AppComponent的模板中,将<app-current-weather>包围在<mat-card>中:**src/app/app.component.ts** ... template: ` ... <div style="text-align:center"> <mat-toolbar color="primary"> <span>LocalCast Weather</span> </mat-toolbar> <div>Your city, your forecast, right now!</div> **<mat-card>** <h2>Current Weather</h2> <app-current-weather></app-current-weather> **</mat-card>** </div> ... `, ... -
观察屏幕底部附近的几乎无法区分的卡片元素及其阴影:
图 5.12:LocalCast 天气与难以区分的卡片元素
为了更好地布局屏幕,我们需要切换到 Flex 布局引擎。我们将从移除组件模板中的训练轮开始。
-
从最外层的
<div>元素中移除style="text-align:center"。 -
使用以下 HTML 将
<mat-card>包围起来,其中<mat-card>的内容替换了代码中间的省略号:要在页面上居中一个元素,我们需要创建一个行,为居中元素分配一个宽度,并在两侧创建两个额外的列,这些列可以伸缩以填充空余空间,如下所示:
**src/app/app.component.ts** ... <div fxLayout="row"> <div fxFlex></div> <div fxFlex="300px"> **...** </div> <div fxFlex></div> </div> ... -
注意到
mat-card元素被正确地居中,如下所示:
图 5.13:LocalCast 天气与居中的卡片
通过阅读卡片文档并查看 Material 文档站点material.angular.io/components/card/overview上的示例,你会注意到mat-card提供了容纳标题和内容的元素。我们将在接下来的部分中实现这一点。
在material.angular.io上,你可以通过点击括号图标查看任何示例的源代码,或者通过点击箭头图标在StackBlitz.io上启动一个工作示例。
卡片标题和内容
现在,让我们使用mat-card-header和mat-card-content来实现mat-card的标题和内容元素,如下所示:
**src/app/app.component.ts**
...
<mat-toolbar color="primary">
<span>LocalCast Weather</span>
</mat-toolbar>
<div>Your city, your forecast, right now!</div>
<div fxLayout="row">
<div fxFlex></div>
<div fxFlex="300px">
<mat-card>
**<mat-card-header>**
**<mat-card-title>Current Weather</mat-card-title>**
**</mat-card-header>**
**<mat-card-content>**
**<app-current-weather></app-current-weather>**
**</mat-card-content>**
</mat-card>
</div>
<div fxFlex></div>
</div>
...
所有 Material 元素都原生支持 Flex 布局引擎。这允许我们优化我们的 HTML,将<div fxFlex="300px">与<mat-card>合并并简化代码:
**src/app/app.component.ts**
...
<div fxLayout="row">
<div fxFlex></div>
**<mat-card fxFlex="300px">**
...
</mat-card>
<div fxFlex></div>
</div>
...
这对复杂 UI 的可维护性有巨大的积极影响。
不要忘记:使用 Material,少即是多。
在我们应用mat-card-header和mat-card-content之后,你可以看到这个结果:
图 5.14:LocalCast 天气卡片,带有标题和内容
注意,卡片内的字体现在与材料的 Roboto 字体相匹配。然而,当前天气不再像以前那样引人注目。如果你在mat-card-title内的h2标签中添加回来,当前天气在视觉上看起来会更大;然而,字体不会与你的应用程序的其他部分匹配。要解决这个问题,你必须了解材料的排印功能。
材料排印
材料文档恰如其分地表述如下:
排印是排列文本以使其在显示时易于阅读、可读且吸引人的方式。
材料提供了一种不同的排印级别,它具有不同的font-size、line-height和font-weight特性,你可以将这些特性应用于任何 HTML 元素,而不仅仅是那些开箱即用的组件。
下表列出了你可以用来应用材料排印的 CSS 类:
考虑以下示例:
<div class="mat-display-4">Hello, Material world!</div>
display-4排印通过在div前添加"mat-"来应用于div。
查看下表以获取完整的排印样式列表:
| 类名 | 用途 |
|---|---|
display-4, display-3, display-2, 和 display-1 | 大型一次性标题,通常位于页面顶部(例如,英雄标题) |
h1, headline | 与<h1>标签对应的章节标题 |
h2, title | 与<h2>标签对应的章节标题 |
h3, subheading-2 | 与<h3>标签对应的章节标题 |
h4, subheading-1 | 与<h4>标签对应的章节标题 |
body-1 | 基础正文文本 |
body-2 | 更粗的正文文本 |
Caption | 较小的正文和提示文本 |
Button | 按钮 |
你可以在material.angular.io/guide/typography了解更多关于材料排印的信息。
应用排印
应用排印的方式有多种。一种方式是利用mat-typography类并使用相应的 HTML 标签,例如<h2>:
**example**
<mat-card-header class="mat-typography">
<mat-card-title><h2>Current Weather</h2></mat-card-title>
</mat-card-header>
另一种方式是将特定的排印直接应用于一个元素上,例如class="mat-title":
**example**
<mat-card-title>
<div class="mat-title">Current Weather</div>
</mat-card-title>
注意,class="mat-title"可以应用于div、span或具有相同结果的h2。
作为一条经验法则,通常实施更具体和本地化的选项会更好,这里就是第二种实现。
在接下来的章节中实现材料排印时,我们需要确保卡片标题在屏幕上的其他元素中脱颖而出。在这种情况下,我更喜欢mat-headline排印的外观来实现这一目标,因此你的实现应该看起来像:
**src/app/app.component.ts**
<mat-card-title>
<div **class="mat-headline"**>Current Weather</div>
</mat-card-title>
接下来,让我们看看如何对屏幕上的其他元素进行对齐。
弹性布局对齐
我们可以使用fxLayoutAlign来居中对齐应用程序的标语,并使用mat-caption排印给它一种低调的外观:
-
使用
fxLayoutAlign居中对齐包含标语标签的div:**src/app/app.component.ts** **<div fxLayoutAlign="center">** <div> Your city, your forecast, right now! </div> **</div>** -
将
mat-caption排印应用于标语:**src/app/app.component.ts** <div **class="mat-caption"**> Your city, your forecast, right now! </div> -
观察以下结果,如图所示:
图 5.15:带有居中标语标签的 LocalCast 天气
接下来,我们需要对齐和样式化元素以匹配设计。
Flex 布局
为了使 UI 看起来像设计,还需要做更多的工作。观察以下当前天气卡的以下设计:
图 5.16:当前天气的 Lo-fi 设计
为了设计布局,我们将利用 Angular Flex。
你将编辑 current-weather.component.html,它使用 <div> 和 <span> 标签来建立分别位于单独行或同一行的元素。随着切换到 Angular Flex,我们需要将所有元素切换到 <div>,并使用 fxLayout 指定行和列。
实施布局脚手架
我们需要首先实现初步的脚手架。考虑模板的当前状态:
**src/app/current-weather/current-weather.component.html**
**1** <div *ngIf="!current">
**2** no data
**3** </div>
**4** <div *ngIf="current">
**5** <div>
**6** <span>{{current.city}}, {{current.country}}</span>
**7** <span>{{current.date | date:'fullDate'}}</span>
**8** </div>
**9** <div>
**10** <img [src]='current.image'>
**11** <span>{{current.temperature | number:'1.0-0'}}˚F</span>
**12** </div>
**13** <div>
**14** {{current.description}}
**15** </div>
**16** </div>
让我们一步一步地通过文件并更新它。首先,让我们进行结构更改以支持 Flex 布局:
-
在第 6、7 和 11 行,将
<span>元素更新为<div>元素。 -
在第 10 行,将
<img>元素包裹在一个<div>元素中。 -
在第 5 和 9 行,向具有多个子元素的父
<div>元素添加fxLayout="row"属性。
接下来,将 fxFlex 属性应用到 div 元素上,以确定元素应占用多少水平空间:
-
在第 6 行,城市和国家列应占据屏幕的大约 ²⁄³,因此向
<div>元素添加fxFlex="66%"。 -
在第 7 行,向
<div>元素添加fxFlex以确保它填满剩余的水平空间。 -
在第 10 行,向包围
<img>元素的新<div>元素添加fxFlex="66%"。 -
在第 11 行,向
<div>元素添加fxFlex。
模板的最终状态应如下所示:
**src/app/current-weather/current-weather.component.html**
**5** <div fxLayout="row">
**6** **<div fxFlex="66%">**{{current.city}}, ...**</div>**
**7** **<div fxFlex>**{{current.date | date:'fullDate'}}**</div>**
**8** </div>
**9** <div **fxLayout="row"**>
**10** **<div fxFlex="66%">**<img [src]='current.image'>**</div>**
**11** **<div fxFlex>**{{current.temperature | number:'1.0-0'}}˚F**</div>**
**12** </div>
**13** <div>
**14** {{current.description}}
**15** </div>
你可以在添加 Angular Flex 属性时更加详细;然而,你写的代码越多,你需要维护的也就越多,这会使未来的更改更加困难。例如,在第 13 行,<div> 元素不需要 fxLayout="row",因为 <div> 隐式地得到一个新行。同样,在第 7 和 11 行,右侧列不需要显式的 fxFlex 属性,因为左侧元素会自动压缩它。但是,我们将保留那些 fxFlex 属性。
从网格放置的角度来看,所有元素现在都位于正确的 单元格 中,如图所示:
图 5.17:带有布局脚手架的 LocalCast 天气
实现响应式设计后,接下来让我们处理主要元素的对齐。
使用 CSS 对齐元素
现在,我们需要对齐和样式化每个单元格以匹配设计。为此,我们依赖于 CSS 而不是 fxLayoutAlign。日期和温度需要右对齐,而描述需要居中:
-
要使日期和温度右对齐,请在
current-weather.component.css中创建一个新的 CSS 类.right:**src/app/current-weather/current-weather.component.css** .right { text-align: right } -
在第 7 和 11 行的
<div>元素上添加class="right"。 -
以与本章早期中心对齐应用标语相同的方式,将描述的
<div>元素居中。使用具有fxLayoutAlign="center"属性的周围div。 -
观察到元素对齐正确,如下所示:
图 5.18:LocalCast 天气正确对齐
在对主要元素进行对齐后,让我们为每个元素应用第一层样式以匹配设计。
单独设置元素样式
完成元素样式通常是前端开发中最耗时的部分。我建议先进行多次迭代,以最小的努力实现设计的一个足够接近的版本,然后让你的客户或团队决定是否值得投入额外资源来进一步润色设计:
-
添加一个新的 CSS 属性:
**src/app/current-weather/current-weather.component.css** .no-margin { margin-bottom: 0 } -
对于城市名称,添加
class="mat-title no-margin"。 -
对于日期,将
class="right"修改为添加"mat-h3 no-margin"。 -
将日期的显示格式从
'fullDate'更改为'EEEE MMM d'以匹配设计。 -
将
<img>修改为添加style="zoom: 175%"。 -
对于温度,将
class="right"修改为添加"mat-display-3 no-margin"。 -
对于描述,添加
class="mat-caption"。这是模板的最终状态:
**src/app/current-weather/current-weather.component.html** <div *ngIf="!current"> no data </div> <div *ngIf="current"> <div fxLayout="row"> <div fxFlex="66%" class="mat-title no-margin"> {{current.city}}, {{current.country}} </div> <div fxFlex class="right mat-h3 no-margin"> {{current.date | date:'EEEE MMM d'}} </div> </div> <div fxLayout="row"> <div fxFlex="66%"> <img style="zoom: 175%" [src]='current.image'> </div> <div fxFlex class="right mat-display-3 no-margin"> {{current.temperature | number:'1.0-0'}}˚F </div> </div> <div fxLayoutAlign="center"> <div class="mat-caption"> {{current.description}} </div> </div> </div> -
观察到代码的样式输出发生了变化,如图所示:
图 5.19:LocalCast 天气样式
我们已经完成了设计的第一层样式添加。接下来,让我们微调元素之间的间距和对齐。
微调样式
标语可以从一些顶部和底部边距中受益。这是我们可能会在整个应用中使用的常见 CSS,所以让我们将其放入 styles.css:
-
在全局
styles.css中实现vertical-margin:**src/styles.css** .vertical-margin { margin-top: 16px; margin-bottom: 16px; } -
在
app.component.ts中,为应用的标语应用vertical-margin:**src/app/app.component.ts** <div class="mat-caption **vertical-margin**"> Your city, your forecast, right now! </div> -
在
current-weather.component.html中,图片和温度没有居中,所以将这些元素的外围div添加fxLayoutAlign="center center":**src/app/current-weather/current-weather.component.html** <div fxLayout="row" **fxLayoutAlign="center center**"> ... </div> -
观察你应用最终布局,它应该看起来像这样:
图 5.20:LocalCast 天气最终布局
最后,让我们通过紧缩我们的设计来增加一些视觉亮点,比如修复日期和月份之间缺失的换行符,并添加一些锦上添花的特性。
调整以匹配设计
这是你可能会花费大量时间的地方。如果我们遵循 80-20 原则,像素级的调整通常会是最后的 20%,而这需要 80% 的时间来完成。让我们比较前一个图中的实现和以下图中的原始设计,以及弥合差距需要做什么:
图 5.21:LocalCast 天气原始设计
日期需要进一步定制。在我们的实现中缺少数字序数th;为了完成这个任务,我们需要引入第三方库,如moment,或者实现我们自己的解决方案并将其绑定到模板上的日期旁边:
-
在
CurrentWeatherComponent中实现一个getOrdinal函数:**src/app/current-weather/current-weather.component.ts** export class CurrentWeatherComponent implements OnInit { ... getOrdinal(date: number) { const n = new Date(date).getDate() return n > 0 ? ['th', 'st', 'nd', 'rd'][(n > 3 && n < 21) || n % 10 > 3 ? 0 : n % 10] : '' } ... } -
在模板中,更新
current.date以向其添加一个序数:**src/app/current-weather/current-weather.component.html** <div fxFlex class="right mat-h3 no-margin"> {{current.date | date:'EEEE MMM d'}}**{{getOrdinal(current.date)}}** </div>注意,
getOrdinal的实现归结为一个复杂的一行代码,可读性差,且难以维护。如果这些函数对你的业务逻辑至关重要,应该对它们进行大量的单元测试。接下来,让我们修复星期和月份之间缺失的换行符。在某些日子,比如 3 月 23 日(星期一),星期一和三月将位于第一行,而 23 日(星期一)则单独位于第二行。然而,在 3 月 24 日(星期二),这个问题并不存在,三月和 24 日(星期二)都位于同一行。在发布时,Angular 不支持日期模板中的新行断开;理想情况下,我们应该能够指定日期格式为“EEEE\nMMM d”,以确保行断开始终一致。然而,我们可以对问题进行一些低效的代码处理,并强制执行我们想要的操作。
-
将当前日期分成两部分,并用换行标签
<br>分隔它们,然后从外部的div中移除类:**src/app/current-weather/current-weather.component.html** <div fxFlex class="mat-h3 no-margin"> {{current.date | date:'EEEE'}}<br> {{current.date | date:'MMM d'}}{{getOrdinal(current.date)}} </div>不要为了布局目的使用
<br>。在这个有限的例子中,这是可以接受的,因为我们正在将内容拆分在div或p标签内。现在,让我们添加一些视觉亮点,当显示温度单位时。为了实现这一点,温度实现需要使用
<span>元素将数字与单位分开,并用<p>元素包围,这样就可以在单位上应用上标样式,例如<span class="unit">``˚``F</span>,其中unit是一个 CSS 类,使其内容看起来像上标元素。 -
实现一个
unitCSS 类:**src/app/current-weather/current-weather.component.css** .unit { vertical-align: super; } -
将图像的弹性设置为 55%,将温度和单位用
p标签包裹,并在p标签上应用mat-display-3。然后,在温度单位周围实现一个span,并用p标签应用unit和mat-display-1类:**src/app/current-weather/current-weather.component.html** <div fxFlex=**"55%"**> <img style="zoom: 175%" [src]='current.image'> </div> <div fxFlex class="right no-margin"> **<p class="mat-display-3">** {{current.temperature | number:'1.0-0'}} **<span class="mat-display-1 unit">**˚F**</span>** **</p>** </div>
你通常需要通过调整前一行上的fxFlex值来实验预测图像应该有多少空间。如果它占用了太多空间,温度就会溢出到下一行。你的设置还可以受到浏览器窗口大小的影响。60%似乎效果不错,但当我编写这个示例时,当前天气是 55˚F,所以出于完全诗意的理由,我决定选择 55%。在这里查看我们应用的精炼版本:
图 5.22:调整后的 LocalCast 天气
和往常一样,您可以进一步调整边距和填充来进一步自定义设计。然而,任何与库的偏差都可能在后续的维护中产生后果。除非您真正围绕显示天气数据来构建业务,否则您应该将任何进一步的优化推迟到项目末尾,如果时间允许,并且如果经验是任何指导,您可能不会进行这种优化。
使用两个负边距下边距的技巧,您可以获得一个相当接近原始设计的样式,但在这里我不会包括这些技巧,将其作为读者在 GitHub 仓库中发现的练习。这样的技巧有时是必要的恶,但通常它们指向设计和实现现实之间的脱节。调整部分之前的解决方案是最佳点,在那里 Angular Material 最为繁荣。超出这一点,您可能是在浪费时间。我提前浪费了我的时间,以下是我的结果:
图 5.23:调整和技巧后的 LocalCast 天气
现在我们已经完成了布局和设计,让我们来看看如何使用 Angular Material 创建一个自定义主题。
自定义主题
正如我们之前讨论的,Material 随带一些默认主题,包括 deeppurple-amber、indigo-pink、pink-blue-grey 和 purple-green。然而,您的公司或产品可能有它自己的配色方案。为此,您可以创建一个自定义主题来改变您应用程序的外观。
为了创建一个新的主题,您必须实现一个新的 SCSS 文件:
-
从
angular.json中移除您默认主题的所有定义。 -
重新运行命令
npx ng add @angular/material。 -
这次选择
Custom作为主题。 -
运行命令后,请确保您的
index.html和styles.css文件没有被修改。如果被修改了,请撤销更改。 -
这将在
src目录下创建一个名为custom-theme.scss的新文件。将其重命名为localcast-theme.scss,如下所示:**src/localcast-theme.scss** // Custom Theming for Angular Material // For more information: https://material.angular.io/guide/theming @import '~@angular/material/theming'; // Plus imports for other components in your app. // Include the common styles for Angular Material. // We include this here so that you only have to // load a single css file for Angular Material in your app. // Be sure that you only ever include this mixin once! @include mat-core(); // Define the palettes for your theme using // the Material Design palettes available in palette.scss // (imported above). For each palette, you can optionally // specify a default, lighter, and darker hue. // Available color palettes: https://material.io/design/color/ $local-weather-app-primary: mat-palette($mat-indigo); $local-weather-app-accent: mat-palette( $mat-pink, A200, A100, A400 ); // The warn palette is optional (defaults to red). $local-weather-app-warn: mat-palette($mat-red); // Create the theme object (a Sass map containing // all of the palettes). $local-weather-app-theme: mat-light-theme( $local-weather-app-primary, $local-weather-app-accent, $local-weather-app-warn ); // Custom Theming for Angular Material // For more information: https://material.angular.io/guide/theming @import '~@angular/material/theming'; // Plus imports for other components in your app. // Include the common styles for Angular Material. // We include this here so that you only have to // load a single css file for Angular Material in your app. // Be sure that you only ever include this mixin once! @include mat-core(); // Define the palettes for your theme using // the Material Design palettes available in palette.scss // (imported above). For each palette, you can optionally // specify a default, lighter, and darker hue. // Available color palettes: https://material.io/design/color/ $local-weather-app-primary: mat-palette($mat-indigo); $local-weather-app-accent: mat-palette($mat-pink, A200, A100, A400); // The warn palette is optional (defaults to red). $local-weather-app-warn: mat-palette($mat-red); // Create the theme object (a Sass map containing // all of the palettes). $local-weather-app-theme: mat-light-theme( $local-weather-app-primary, $local-weather-app-accent, $local-weather-app-warn ); // Include theme styles for core and each component used in // your app. Alternatively, you can import and @include the // theme mixins for each component that you are using. @include angular-material-theme($local-weather-app-theme);您可以在
material.angular.io/guide/theming找到 Material 主题指南,获取更详细的信息。注意,
mat-core()应该只包含在您的应用程序中一次;否则,您将在应用程序中引入不必要的和重复的 CSS 负载。mat-core()包含将自定义颜色注入 Material 所必需的 SCSS 函数,例如mat-palette、mat-light-theme和mat-dark-theme。至少,我们必须定义一个新的主色调和一个强调色。然而,定义新的颜色并不是一个简单的过程。Material 需要通过
mat-palette定义调色板,这需要一个复杂的颜色对象来初始化,不能简单地通过如#BFB900这样的十六进制值来覆盖。要选择您的颜色,您可以使用位于
material.io/resources/color的 Material Design 颜色工具。以下是工具的截图:图 5.24:Material.io 色彩工具
-
使用Material 调色板选择主色和辅助色:
-
我的主色选择是带有色调值
500的红色。 -
我的次要选择是带有色调值
A400的靛蓝色。
-
-
通过浏览页面左侧的六个预建屏幕,观察您的选择如何应用于 Material Design 应用。
-
评估您选择的可访问性影响,如图所示:
图 5.25:Material.io 色彩工具的可访问性选项卡
工具警告我们,当在主色上使用白色文本时,我们的选择会导致文本难以辨认。您应该注意避免在主色上显示白色文本,或者更改您的选择。
如果您想创建自己的调色板,那么
mat-palette的界面看起来是这样的:mat-palette($base-palette, $default: 500, $lighter: 100, $darker: 700) -
使用默认色调定义主色和辅助色的
mat-palette对象:**src/localcast-theme.scss** $local-weather-app-primary: mat-palette($mat-red, 500); $local-weather-app-accent: mat-palette($mat-indigo, A400);
即使您的主题在 SCSS 中,您也可以继续在应用程序的其他部分使用 CSS。Angular CLI 支持编译 SCSS 和 CSS。如果您想更改默认行为,您可以通过将angular.json文件中的defaults.styleExt属性从 CSS 更改为 SCSS 来完全切换到 SCSS。
您还可以选择删除styles.css并将其内容合并到localcast-theme.scss中,或者通过简单地将其重命名为styles.scss将styles.css转换为 SASS 文件。如果您这样做,别忘了更新angular.json。
恭喜!您的应用程序现在应该具有您自己的商标色彩方案:
图 5.26:使用自定义主题的 LocalCast 天气
将您的代码推送到 GitHub 并检查您的 CircleCI 流水线。
使用 Material 进行单元测试
一旦您提交代码,您会注意到由于测试失败,您的流水线现在失败了。为了保持您的单元测试运行,您需要将MaterialModule导入到任何使用 Angular Material 的组件的spec文件中:
***.component.spec.ts**
...
beforeEach(async(() => {
TestBed.configureTestingModule({
...
imports: [..., MaterialModule],
}).compileComponents()
})
)
您还需要更新任何搜索特定 HTML 元素的测试,包括端到端测试。
例如,由于应用程序的标题 LocalCast Weather 不再位于h1标签中,您必须更新spec文件以在span元素中查找它:
**src/app/app.component.spec.ts**
expect(compiled.querySelector('**span**').textContent).toContain('LocalCast Weather')
另一个例子是在CurrentWeather组件中,city周围的元素不再是span,因此您可以使用mat-title CSS 类:
**src/app/current-weather/current-weather.component.spec.ts**
import { By } from '@angular/platform-browser'
// Assert on DOM
const debugEl = fixture.debugElement
const titleEl: HTMLElement =
debugEl.query(By.css('**.mat-title**')).nativeElement
expect(titleEl.textContent).toContain('Bethesda')
类似地,在端到端测试中,您需要更新您的页面对象函数以从正确的位置检索文本:
**src/e2e/app.po.ts**
getParagraphText() {
return element(by.css('app-root mat-toolbar span'))
.getText() as Promise<string>
}
一旦您的测试通过,再次将您的代码推送到 GitHub。当您的 CircleCI 流水线成功时,使用 Vercel Now 发布您的应用程序。记住,如果您不发布它,它就从未发生过!
我们现在可以将 UX 任务移动到完成列:
图 5.27:GitHub 项目看板板状态
在 第七章,创建一个以路由器为第一线的业务应用 中,你将了解更多高级的工具,以便进一步自定义你的 Material 主题的外观和感觉,从而创建一个真正独特的体验,适合你所代表的品牌。
可访问性
了解你的应用中可能存在的潜在可访问性问题非常重要。你可以通过访问 A11Y 项目网站 a11yproject.com 来熟悉可访问性方面的考虑。Material 本身也提供了额外的工具来帮助你提高可访问性;你可以在 material.angular.io/cdk/a11y/overview 上了解更多信息。
利用这样的 Material 功能可能感觉不必要;然而,在设计你的应用时,你必须考虑响应性、样式、间距和可访问性方面的考虑。Material 团队投入了大量努力,以确保你的代码在大多数情况下都能正确运行,并且能够为最大可能的用户群体提供高质量的 UX。这可能包括视觉障碍者或以键盘为主的用户,他们必须依赖专门的软件或键盘功能,如标签,来导航你的应用。利用 Material 元素为这些用户提供关键的元数据,使他们能够导航你的应用。
Material 声称支持以下屏幕阅读软件:
-
在 Windows 上的 Internet Explorer/Firefox/Chrome 中使用 NVDA 和 JAWS
-
在 iOS 上的 Safari 和 macOS X 上的 Safari/Chrome 中使用 VoiceOver
-
在 Android 上的 Chrome 中使用 TalkBack
除了 Material 之外,你可能需要或希望支持特定的可访问性标准,如基于美国的第 508 条或 W3C 定义的 Web 内容可访问性指南(WCAG)。声称对这样的标准提供官方支持需要昂贵的认证和合格的测试人员来确保合规性。
考虑 pa11y,这是一个自动化可访问性测试的命令行工具。由于它是一个 CLI 工具,你可以轻松地将它集成到你的 CI 管道中。能够在开发周期的早期自动捕捉到可访问性问题,可以显著降低在应用中实现可访问性功能的成本。
A11y 是 accessibility 的缩写,因为在单词 accessibility 中,字母 a 和 y 之间有 11 个字符。你可以在 a11yproject.com/ 上了解更多关于为什么支持可访问性很重要。
你可以在 pa11y.org/ 上了解更多关于 pa11y 的信息。接下来,让我们在我们的项目中配置 pa11y CLI 工具。
配置自动 pa11y 测试
pa11y 是一个可以从命令行执行的自动化可访问性工具,你可以用它来检查你的 Web 应用是否符合各种可访问性规则集,如第 508 条或 WCAG 2 AAA。你可以配置 pa11y 在你的项目本地或 CI 服务器上运行。在两种情况下,你必须对已部署的应用版本进行测试。
让我们从为本地运行配置 pa11y 开始:
-
使用以下命令安装 pa11y 和 pa11y-ci 包:
npm i -D pa11y pa11y-ci http-server -
添加
npm脚本以执行 pa11y 进行本地运行,检查第五百零八部分合规性问题:**package.json** ... "scripts": { ... "test:a11y": "pa11y --standard Section508 http://localhost:5000" } -
通过执行
npm start确保应用正在运行。 -
在一个新的终端窗口中,执行
npm run test:a11y。输出应该如下所示:Welcome to Pa11y > Running Pa11y on URL http://localhost:5000 Results for URL: http://localhost:5000/ • Error: Img element missing an alt attribute. Use the alt attribute to specify a short text alternative. ├── Section508.A.Img.MissingAlt ├── html > body > app-root > div:nth-child(4) > mat-card > mat-card-content > app-current-weather > div > div:nth-child(2) > div:nth-child(1) > img └── <img _ngcontent-pbr-c132="" style="margin-bottom:32px; zoom:175%" src=""> 1 Errors注意,我们有一个错误。错误信息表明,在
app-current-weather下,我们在mat-card-content内部显示的图片缺少一个alt属性。观察以下导致错误的代码行:**src/app/current-weather/current-weather.component.html** ... <img style="zoom: 175%" [src]="current.image" />上述代码指的是我们从 OpenWeatherMap API 获取的图片。一个视觉障碍用户,依赖屏幕阅读器,如果没有
alt属性,将无法确定图片的用途。由于这是一个动态图片,一个静态的alt属性,如当前天气图标,将不利于我们的用户。然而,将当前天气描述值绑定为属性是合适的。我们可以像下面这样修复可访问性问题:**src/app/current-weather/current-weather.component.html** ... <img style="zoom: 175%" [src]="current.image" [alt]="current.description" /> -
重新运行 pa11y 以确认问题已修复。
现在,依赖屏幕阅读器的用户可以快速了解页面上的图片反映了当前天气。在这种情况下,我们已经在页面上有了描述。这是一个非常重要的问题,需要修复,因为避免在我们的页面上出现整个用户群体都无法解读的神秘元素至关重要。
现在,让我们为我们的 CI 管道配置 pa11y。
-
在项目的根目录下创建一个
.pa11yci配置文件:**.pa11yci** { "default": { "timeout": 1000, "page": { "viewport": { "width": 320, "height": 480 } } }, "urls": [ "https://localcast-weather.duluca.now.sh/" ] } -
添加
npm脚本以执行 pa11y 进行本地运行,检查第五百零八部分合规性问题:**package.json** ... "scripts": { ... "test:a11y:ci": "pa11y-ci" }
现在,我们可以将命令npm run test:a11y:ci添加到.circleci/config.yml中。然而,如您所注意到的,我们将对已经部署的应用版本进行测试。为了克服这个挑战,您必须创建一个替代的now:publish命令,将我们的分支部署到不同的 URL,更新.pa11yci以检查新的 URL,并在您的管道中执行部署。由于这里涉及的所有操作都是 CLI 命令,您可以按顺序执行它们。我将这个作为练习留给用户来完成。
CircleCI 的更多高级用法在第九章,使用 Docker 的 DevOps中有所介绍。接下来,我们将讲解如何构建一个交互式原型,以便在开发早期发现 UI/UX 问题,从而降低开发成本。
构建交互式原型
外观很重要。无论您是在开发团队中工作还是作为自由职业者,您的同事、老板或客户都会更加重视一个精心准备好的演示。在第三章,创建基本的 Angular 应用中,我提到了全栈开发者面临的时间和信息管理挑战。我们必须选择一个可以以最少的劳动量实现最佳结果的工具。这通常意味着走付费工具的道路,但 UI/UX 设计工具很少是免费或便宜的。
一个原型工具将帮助你创建一个更好、更专业的应用原型。无论你选择什么工具,它也应该支持你选择的 UI 框架,在这种情况下,是 Material。
如果一张图片值一千个字,那么你应用的交互式原型就值一千行代码。一个交互式原型将帮助你在你写第一行代码之前检验想法,并节省你大量的代码编写。
MockFlow WireframePro
我选择了 MockFlow WireframePro,可在mockflow.com获取,作为一个易于使用、功能强大的在线工具,它支持 Material Design UI 元素,并允许你创建多个页面,然后可以将它们链接在一起,以创建一个工作应用的错觉。
最重要的是,在发布时,MockFlow 允许永久免费使用一个完整功能集和能力的项目。这将给你一个真正检验工具有用性的机会,而不受人工限制或总是比你预期的更快过去的试用期。
Balsamiq(可在balsamiq.com获取)是一个更知名的线框工具;然而,它不提供任何免费使用。如果你在寻找一个没有月费的工具,我强烈推荐 Balsamiq 的桌面应用程序 Mockups,它有一个一次性购买成本。
构建原型
我们首先添加一个新任务来创建一个交互式原型,并在任务结束时,我将把所有工件附加到这个任务上,以便它们存储在 GitHub 上,所有团队成员都可以访问,并且也可以从维基页面上链接,以实现持久的文档。
让我们把这项新任务拖到进行中列,并查看我们从 Waffle.io 的看板状态:
图 5.28:当前看板板状态
WireframePro 作为一个拖放式设计界面非常直观,所以我不将深入探讨工具的工作原理,但我将突出一些技巧:
-
创建你的项目
-
选择一个组件包,无论是手绘 UI还是Material Design
-
按照以下截图所示,将每个屏幕作为新页面添加:
图 5.29:MockFlow.com WireFrame Pro
我建议坚持使用手绘 UI 的外观和感觉,因为这为你的观众设定了正确的期望。如果你在与客户的第一次会面中展示一个非常高质量的样稿,你的第一次演示将会是保守的。你最多只能满足期望,最坏的情况是令观众感到失望。
主屏幕
这是刚刚创建的主屏幕的新原型:
图 5.30:LocalCast 天气主屏幕线框
你会注意到一些差异,例如应用工具栏与浏览器栏合并,以及重复元素的故意模糊。我做出了这些选择,以减少我需要在每个屏幕上花费的设计时间。我只是简单地使用了水平和垂直线对象来创建网格。
搜索结果
搜索屏幕同样故意保持模糊,以避免维护任何详细的信息。令人惊讶的是,你的观众更有可能关注你的测试数据,而不是关注设计元素。
通过模糊处理,我们故意将观众的注意力集中在重要的事情上。以下是搜索屏幕的原型图:
图 5.31:LocalCast 天气搜索屏幕线框图
设置面板
设置面板是一个独立的屏幕,它将主页面的元素复制过来,并应用了 85%的不透明度以创建类似模型的体验。设置面板本身只是一个带有黑色边框和纯白色背景的矩形。
看看下面的原型图:
图 5.32:LocalCast 天气设置线框图
添加交互性
能够点击原型并了解导航工作流程是一种不可或缺的工具,可以让你在早期获得用户反馈。这将为你和你的客户节省大量的挫败感、时间和金钱。
要将元素链接在一起,请按照以下步骤操作:
-
选择一个可点击的元素,例如主页面的齿轮图标
-
在链接子标题下,点击选择页面
-
在弹出窗口中,选择设置
-
点击创建链接,如图所示:
图 5.33:在 Wireframe Pro 中添加链接
现在,当你点击齿轮图标时,工具将显示设置页面,这将创建侧边栏实际上在同一页面上显示的效果。要返回主页,你可以将齿轮图标和侧边栏外的部分链接回该页面,以便用户可以来回导航。
导出功能原型
一旦你的原型完成,你可以将其导出为各种格式:
-
在项目菜单下,选择导出线框按钮,如图所示
图 5.34:Wireframe Pro 的导出线框菜单选项
-
现在选择你的文件格式,如下所示
图 5.35:Wireframe Pro 中的文件格式
我更喜欢 HTML 格式,因为它具有灵活性;然而,你的工作流程和需求可能会有所不同。
-
如果你选择了 HTML,你将下载一个包含所有资产的 ZIP 压缩包。
-
解压包并使用浏览器导航到它;你应该会得到一个交互式的线框图版本,如图所示
图 5.36:Wireframe Pro 中的交互式线框图
交互元素在截图中被突出显示为黄色(打印为浅灰色),并由前一个截图中的粗箭头指出。你可以通过屏幕左下角的 Reveal Links 选项启用或禁用此行为。
现在将所有资产添加到 GitHub 问题的评论中,包括 ZIP 压缩包,我们就完成了。
你还可以使用 第四章 中讨论的 Vercel Now 发布你的原型 HTML 项目。
摘要
在本章中,你学习了 Angular Material 是什么,如何使用 Angular Flex Layout 引擎,UI 库对性能的影响,以及如何将特定的 Angular Material 组件应用到你的应用程序中。你意识到了过度优化的 UI 设计中 CSS 微调的陷阱,以及如何为你的应用程序添加自定义主题。
我们还介绍了如何提高你应用程序的可访问性,并在实现之前验证你的设计。
在下一章中,我们将更新天气应用程序以响应用户输入的响应式表单,并保持我们的组件解耦,同时使用 BehaviorSubject 在它们之间启用数据交换。在下一章之后,我们将完成天气应用程序,并将我们的重点转移到构建更大的业务线应用程序。
查看 附录 C,保持 Angular 和工具常青,了解如何升级 Angular Material。你可以从 static.packt-cdn.com/downloads/9781838648800_Appendix_C_Keeping_Angular_and_Tools_Evergreen.pdf 或 expertlysimple.io/stay-evergreen 在线找到此附录。
进一步阅读
-
《疯狂简单:推动苹果成功的执着,肯·塞格尔》,2013
-
《Material Design,谷歌》,2020,位于
material.io -
Pa11y,Team Pa11y,2020,位于
pa11y.org
练习
通过实现替代的 Now 部署在你的 CI 流程中实施 pa11y,以便你可以测试你分支中的更改。
问题
尽可能好地回答以下问题,以确保你在没有使用 Google 的情况下理解了本章的关键概念。你需要帮助回答这些问题吗?请参阅 附录 D,自我评估答案,在线位于 static.packt-cdn.com/downloads/9781838648800_Appendix_D_Self-Assessment_Answers.pdf 或访问 expertlysimple.io/angular-self-assessment。
-
使用 Angular Material 的好处是什么?
-
Angular Flex Layout 依赖于哪种底层 CSS 技术?
-
为什么测试可访问性很重要?
-
为什么你应该构建交互式原型?