Angular6 面向企业级的 Web 开发(二)
原文:
zh.annas-archive.org/md5/87CFF2637ACB075A16B30B5AA7A68992译者:飞龙
第四章:与 Angular 更新保持同步
在不同版本的几十种不同浏览器的数十种组合上提供安全、快速和一致的 Web 体验并不是一件容易的事。Angular 的存在就是为了实现这一点;然而,互联网是一个不断发展的竞争技术和供应商的领域。Angular 团队已经承诺定期更新平台,但是要靠您来跟上 Angular 的补丁、次要版本和主要版本的发布。
Angular 是一个旨在最大程度减少从一个版本升级到另一个版本的工作量的平台,提供了有用的工具和指南,最重要的是确定性的发布节奏和关于废弃功能的充分沟通,这允许进行适当的规划以保持最新。
您必须以一种深思熟虑和计划的方式保持与 Angular 的最新版本同步。这样的策略将最大程度地提高您使用 Angular 这样的平台所获得的好处,将错误和浏览器之间的不一致体验降至最低。在极端情况下,您有选择:要么保留数百名测试人员来测试您的 Web 应用程序在所有主要浏览器及其最新版本上的兼容性问题,要么保持您的 Angular 版本(或您选择的框架)保持最新。请记住,最终,确保您交付的产品质量是由您来决定的。
现在可以随意跳过本章,当 Angular 的一个次要或主要版本发布时再回来阅读,或者继续阅读以了解潜在的升级过程可能是什么样子。
在本章中,我们将讨论以下主题:
-
更新节点
-
更新
npm和全局包 -
更新 Angular
-
解决安全漏洞
-
更新您的 Web 服务器
Web 框架的简要历史
首先,重要的是考虑为什么我们首先要使用 Angular 或 React 等框架?在 Angular 之前,有 AngularJS 和 Backbone,它们都严重依赖于普遍存在的 jQuery 之前的框架。在 jQuery 存在的早期,即 2006 年,它对 Web 开发人员的目的是非常明显的——创建一个一致的 API 表面来实现 DOM 操作。浏览器供应商应该实现各种 Web 技术,如 HTML、JavaScript/EcmaScript 和 CSS,这是由万维网联盟(W3C)标准化的。当时,绝大多数互联网用户依赖的唯一浏览器是 Internet Explorer,它作为推动专有技术和 API 以保持其作为首选浏览器的优势的工具。首先是 Mozilla 的 Firefox,然后是 Google 的 Chrome 浏览器成功地获得了重要市场份额。然而,新浏览器版本开始以惊人的速度发布,竞争利益和不同的实现草案和已批准标准的版本和名称的质量差异造成了开发人员无法在 Web 上提供一致的体验。因此,您可以使用 jQuery 而不是反复编写代码来检查浏览器版本,这样您就可以轻松地隐藏供应商特定实现的所有复杂性,通过优雅地填补空白来弥补缺失的功能。
在 jQuery 中创建丰富的用户体验仍然很繁琐,Backbone 和 AngularJS 等框架使构建具有本地感和速度的 Web 应用程序更具成本效益。然而,浏览器不断变化,jQuery 和早期设计决策的意想不到的影响随之而来,随着标准的不断发展,导致了在 Angular 和 React 中构建 Web 应用程序的两种新的不同方法。从 AngularJS 过渡到 Angular 对整个社区来说都是一个令人不适的经历,包括 Angular 开发团队,但这必须是一个重大发布,以创建一个可以不断发展的平台。现在,新的 Angular 平台致力于保持最新状态,定期发布增量版本,以避免过去的错误。
更新 Node
即使您不将 Node.js 用作 Web 服务器,您也已经在使用它通过 npm 安装您的依赖项,并通过基于 Node.js 的软件包(如 WebPack,Gulp 或 Grunt)执行构建和测试任务。Node.js 是一个轻量级的跨平台执行环境,可以使大多数现代开发工具无缝工作。由于其性质,Node 位于您的主机操作系统之外的技术堆栈的最底层。保持 Node 的版本最新以获得安全性、速度和功能更新的好处非常重要。
Node.js 有两个分支:长期支持(LTS)版本和当前版本。奇数版本是一次性的、风险的发布,不计划进行 LTS 阶段。偶数版本首先作为当前版本发布,然后进入 LTS 阶段。
为了最大的稳定性和避免意外问题,我强烈建议坚持使用 Node 的 LTS 版本:
- 通过运行此命令检查您当前的版本:
node -v
v8.9.0
您可以在nodejs.org上查看有关最新发布的更多信息。除了计划发布,这个网站通常会包含有关各种 Node.js 发布的临时关键安全补丁的信息。
- 如果您使用奇数或非 LTS 发布频道,请删除您现有的 Node 安装:
在 Windows 上,请确保您以管理员权限运行 PowerShell:
PS> choco uninstall node
在 macOS 上,如果您的环境设置正确,您不需要在命令中添加sudo:
$ brew uninstall --ignore-dependencies node
- 在 Windows 上,要升级到最新的 LTS 版本,请执行以下命令:
PS> choco upgrade nodejs-lts
- 在 macOS 上,如果您还没有安装 Node 8,您首先需要执行以下操作:
$ brew install node@8
- 如果您已经在版本 8 上,则执行以下操作:
$ brew upgrade node@8
请注意,计划在 2018 年 10 月发布版本 10 作为下一个 LTS 版本,因此在运行 brew install 命令之前,您需要牢记这一点。
如果您在 macOS 上,请参考下一节,了解使用n工具更轻松地管理您的 Node 版本的方法。否则,请跳转到更新 Npm部分。
n - 用于 macOS 的 Node 版本管理器
在 macOS 上,HomeBrew 没有 Node 的 LTS 特定频道,如果最新版本是奇数版本,您将发现自己处于一个不理想的位置。如果您错误地执行了brew upgrade node并升级到奇数版本,要从这个错误中恢复最好是很烦人的。这个过程包括通过运行类似于这样的命令来潜在地破坏其他 CLI 工具:
$ brew uninstall --ignore-dependencies node
在通过 brew 进行初始 Node 安装后,我强烈建议利用功能丰富、交互式的 Node 版本管理工具n,由前 Node 维护者 TJ Holowaychuk 创建:
- 安装
n:
$ npm install -g n
- 执行
n,它将显示您计算机上先前下载的所有 Node 版本的列表,并标记当前版本:
$ n
...
node/8.2.1
node/8.3.0
node/8.4.0
ο node/8.9.0
- 执行
n lts以安装最新的 LTS 版本:
$ n lts
install : node-v8.9.3
mkdir : /usr/local/n/versions/node/8.9.3
fetch : https://nodejs.org/dist/v8.9.3/node-v8.9.3-darwin-x64.tar.gz
######################################################################## 100.0%
installed : v8.9.3
使用n,您可以快速在不同的 Node 版本之间切换。
在本节中,我们将介绍如何保持 npm 的最新状态。
更新 npm 和全局 npm 包
如果 Node 是您技术栈中最低级别的工具,那么 npm 和全局 npm 包将被视为坐落在 Angular 和 Node 之间的下一层。
每次更新 Node 版本时,您还会获得一个新版本的 npm,它与 Node 捆绑在一起。但是,npm 的发布计划与 Node 的不一致。有时,会有显著的性能和功能增益,需要特定升级您的 npm 版本,例如 npm v5.0.0 引入的数量级速度改进,或者 npm v5.2.0 引入的减少全局包需求的 npx 工具:
- 在 Windows 上,您需要使用
npm-windows-upgrade工具来升级您的 npm 版本:
- 安装
npm-windows-upgrade:
PS> npm install --global --production npm-windows-upgrade
如果在安装工具时遇到错误,请参考Npm fails to install a global tool on Windows部分,解决系统设置的任何问题。
- 在提升的 shell 中执行
npm-windows-upgrade,您将获得一系列选项,如下所示:
PS> npm-windows-upgrade
npm-windows-upgrade v4.1.0
? Which version do you want to install?
6.0.1-next.0
> 6.0.0
6.0.0-next.2
6.0.0-next.1
6.0.0-next.0
5.10.0-next.0
5.9.0-next.0
(Move up and down to reveal more choices)
- 选择一个稳定的版本,例如
6.0.0:
PS>
? Which version do you want to install? 6.0.0
Checked system for npm installation:
According to PowerShell: C:\Program Files\nodejs
According to npm: C:\Users\duluc\AppData\Roaming\npm
Decided that npm is installed in C:\Program Files\nodejs
Upgrading npm... \
Upgrade finished. Your new npm version is 6.0.0\. Have a nice day!
- 验证您的安装:
PS> npm -v
6.0.0
- 在 macOS 上,升级您的 npm 版本很简单:
- 执行
npm install -g npm:
$ npm install -g npm
/usr/local/bin/npm -> /usr/local/lib/node_modules/npm/bin/npm-cli.js
/usr/local/bin/npx -> /usr/local/lib/node_modules/npm/bin/npx-cli.js
+ npm@6.0.0
updated 1 package in 18.342s
请注意,安装全局包,如前面所示,不应需要使用sudo。
- 如果需要
sudo,执行以下操作:
$ which npm
/usr/local/bin/npm
- 找到此文件夹的所有者和权限:
$ ls -ld /usr/local/bin/npm
lrwxr-xr-x 1 youruser group 38 May 5 11:19 /usr/local/bin/npm -> ../lib/node_modules/npm/bin/npm-cli.js
如您所见,正确的配置看起来像您自己的用户,以粗体显示为youruser,对该文件夹具有读/写/执行权限,也以粗体显示为rwx,其中npm位于其中。如果不是这种情况,请使用sudo chown -R $USER /usr/local/bin/npm来拥有该文件夹,然后使用chmod -R o+rwx /usr/local/bin/npm来确保您的用户具有完全权限。
- 验证您的安装:
$ npm -v
6.0.0
保持任何全局安装的软件包最新也很重要;请参考下一节,了解如何将全局安装保持在最低限度,并解决 Windows 上的安装问题。
全局 Npm 包
如本节和第二章中所述,在设置 Angular 项目时,您应该避免将任何项目特定工具安装为全局包。这包括诸如typescript、webpack、gulp或grunt等工具。npx工具使您能够方便地运行 CLI 命令,例如使用特定版本的tsc,而对性能的影响很小。如第二章中所讨论的,全局安装项目特定工具会对您的开发环境产生不利影响。
我确实提到了一些我仍然继续全局安装的工具,比如来自升级 Node部分的n工具,或者rimraf,这是一个跨平台递归删除工具,在 Windows 10 不配合删除您的node_modules文件夹时非常方便。这些工具是非项目特定的,而且基本稳定,不需要频繁更新。
事实是,除非工具提醒您升级自己,否则您很可能永远不会主动这样做。我们在第三章中使用的 now CLI 工具,为生产发布准备 Angular 应用,以在云中发布我们的 Docker 容器,是一个很好的例子,它始终保持警惕,以确保自己与以下消息保持最新:
^(─────────────────────────────────────────
│ Update available! 8.4.0 → 11.1.7 │
│ Changelog: https://github.com/zeit/now-cli/releases/tag/11.1.7 │
│ Please download binaries from https://zeit.co/download │
─────────────────────────────────────────)
您可以通过执行以下操作升级全局工具:
$ npm install -g now@latest
请注意,@latest请求将升级到下一个主要版本,如果可用的话,而不会引起太多轰动。虽然主要版本包含令人兴奋和有用的新功能,但它们也有破坏旧功能的风险,而您可能正在依赖这些功能。
这应该完成您的升级。然而,特别是在 Windows 上,很容易使您的 Node 和 npm 安装处于破损状态。以下部分涵盖了常见的故障排除步骤和您可以采取的操作,以恢复您的 Windows 设置。
Npm 在 Windows 上无法安装全局工具
Npm 可能无法安装全局工具;请考虑以下讨论的症状、原因和解决方案:
症状:当您尝试安装全局工具时,您可能会收到一个包含拒绝删除消息的错误消息,类似于下面显示的消息:
PS C:\WINDOWS\system32> npm i -g now
npm ERR! path C:\Users\duluc\AppData\Roaming\npm\now.cmd
npm ERR! code EEXIST
npm ERR! Refusing to delete C:\Users\duluc\AppData\Roaming\npm\now.cmd: node_modules\now\download\dist\now symlink target is not controlled by npm C:\Users\duluc\AppData\Roaming\npm\node_modules\now
npm ERR! File exists: C:\Users\duluc\AppData\Roaming\npm\now.cmd
npm ERR! Move it away, and try again.
npm ERR! A complete log of this run can be found in:
npm ERR! C:\Users\duluc\AppData\Roaming\npm-cache\_logs\2017-11-11T21_30_28_382Z-debug.log
原因:在 Windows 上,如果您曾经执行过npm install -g npm或使用 choco 升级过您的 Node 版本,您的 npm 安装很可能已经损坏。
解决方案 1:使用npm-windows-upgrade工具恢复您的环境:
- 执行 npm 升级例程:
PS> npm install --global --production npm-windows-upgrade
PS> npm-windows-upgrade
- 使用
rimraf删除有问题的文件和目录:
PS> npm i -g rimraf
rimraf C:\Users\duluc\AppData\Roaming\npm\now.cmd
rimraf C:\Users\duluc\AppData\Roaming\npm\now
- 尝试重新安装:
PS> npm i -g now@latest
如果这不能解决您的问题,请尝试解决方案 2。
解决方案 2:如果您安装了非 LTS nodejs 或者没有正确配置 npm,请尝试以下步骤:
- 卸载非 LTS nodejs 并重新安装它:
PS> choco uninstall nodejs
PS> choco install nodejs-lts --force -y
-
按照
github.com/npm/npm/wiki/Troubleshooting#upgrading-on-windows中的指南安装npm-windows-upgrade。 -
在具有管理员权限的 Powershell 中执行此操作:
PS> Set-ExecutionPolicy Unrestricted -Scope CurrentUser -Force
PS> npm install --global --production npm-windows-upgrade
PS> npm-windows-upgrade
- 执行
npm-windows-upgrade:
PS> npm-windows-upgrade
npm-windows-upgrade v4.1.0
? Which version do you want to install? 5.5.1
Checked system for npm installation:
According to PowerShell: C:\Program Files\nodejs
According to npm: C:\Users\duluc\AppData\Roaming\npm
Decided that npm is installed in C:\Program Files\nodejs
Upgrading npm... -
Upgrade finished. Your new npm version is 5.5.1\. Have a nice day!
-
注意根据 npm 文件夹。
-
转到此文件夹,并确保此文件夹中不存在
npm或npm.cmd。 -
如果有,删除。
-
确保此文件夹在
PATH中。
单击“开始”,搜索“环境变量”。单击“编辑系统环境变量”。在“系统属性”窗口中,单击“环境变量”。选择带有路径的行。单击“编辑”。
编辑环境变量对话框
-
尝试重新安装您的全局工具。
-
如果问题仍然存在,您可能需要使用 PowerShell 命令删除全局的
npm文件夹,如下所示:
PS> cmd /C "rmdir /S /Q C:\Users\duluc\AppData\Roaming\npm"
- 转到该文件夹:
PS> dir C:\Users\duluc\AppData\Roaming\npm
- 执行 npm:
PS> npm@5.5.1 C:\Program Files\nodejs\node_modules\npm
- 重新执行
npm-windows-upgrade例程:
PS> npm install --global --production npm-windows-upgrade
PS> npm-windows-upgrade
- 重新安装工具:
PS> npm i -g now
C:\Users\duluc\AppData\Roaming\npm\now -> C:\Users\duluc\AppData\Roaming\npm\node_modules\now\download\dist\now
> now@8.4.0 postinstall C:\Users\duluc\AppData\Roaming\npm\node_modules\now
> node download/install.js
> For the source code, check out: https://github.com/zeit/now-cli
> Downloading Now CLI 8.4.0 [====================] 100%
+ now@8.4.0
将来不要运行npm i -g npm。
更新 Angular
使用 Node 和 npm 最新版本,您现在可以准备升级您的 Angular 版本了。Angular 生态系统经过精心设计,使您的版本更新尽可能轻松。次要版本更新应该是直接和快速的,从版本6.0.0开始;主要版本升级应该更容易,因为 Angular CLI 附带了新的ng update命令。配合update.angular.io上发布的更新指南和特定于您升级路径的各种辅助工具,更新 Angular 是直接的。在本节中,我们将介绍如何更新您的 Angular 应用程序,假设从版本 5.2 升级到 6.0 的情景。指南应该基本保持不变,任何变化或将来的更改都记录在update.angular.io/中。
请记住,Angular 不建议在升级时跳过主要版本号,因此如果您使用的是版本 4,则首先需要升级到 5,然后再升级到 6。不要延迟更新您的框架版本,认为可以通过跳跃到最新版本来获得一些效率。
Angular 更新指南
按照这一步骤指南准备、执行和测试您的 Angular 版本升级过程。
了解您当前的版本
让我们首先检查package.json,以便您了解您正在使用的各种依赖项的版本。所有@angular包应该是相同的次要版本,例如5.2,如图所示:
package.json
"@angular/animations": "5.2.5",
"@angular/cdk": "⁵.2.2",
"@angular/common": "5.2.5",
"@angular/compiler": "5.2.5",
"@angular/core": "5.2.5",
"@angular/flex-layout": "².0.0-beta.12",
"@angular/forms": "5.2.5",
"@angular/http": "5.2.5",
"@angular/material": "⁵.2.2",
"@angular/platform-browser": "5.2.5",
"@angular/platform-browser-dynamic": "5.2.5",
"@angular/router": "5.2.5",
"core-js": "².4.1",
...
"rxjs": "⁵.5.6",
"ts-enum-util": "².0.0",
"zone.js": "⁰.8.20"
},
"devDependencies": {
"@angular/cli": "1.7.0",
"@angular/compiler-cli": "5.2.5",
"@angular/language-service": "5.2.5",
...
使用 Angular 更新指南
现在您已经了解了您当前的版本,可以使用更新指南了:
-
选择您的应用程序的复杂性:
-
基本:没有动画,没有 HTTP 调用
-
中级:如果您正在使用 Angular Material 或进行 HTTP 调用或使用 RxJS,通常作为 1-2 人开发团队并交付小型应用程序
-
高级:多人团队,交付中大型应用程序
大多数应用程序将属于中等复杂性;我强烈建议选择此选项。如果您已经在文档中深入实现了 Angular 功能,通过利用文档中提到的功能来实现任何自定义行为,确保在 HTTP、渲染、路由等方面实现任何自定义行为——一定要先浏览高级列表,以确保您没有使用已弃用的功能。
- 在更新指南上,选择从哪个版本升级到哪个版本。在这种情况下,选择从 5.2 升级到 6.0,如图所示:
-
点击“显示我如何更新!”
-
请注意屏幕上显示的指示,分为更新前、更新中和更新后三个不同的部分
现在是困难的部分,我们需要遵循说明并应用它们。
更新您的 Angular 应用程序
更新软件是有风险的。有几种策略可以减少您在更新应用程序时的风险。这是您在应用程序中构建大量自动化测试的主要原因;然而,随着时间的推移,您的实施,包括 CI 和 CD 系统,可能会恶化。版本更新是重新评估您的自动化系统的健壮性并进行必要投资的好时机。在开始更新过程之前,请考虑以下升级前清单。
升级前清单
以下是在开始升级之前要运行的一些方便的检查项目清单:
-
确保
@angular版本一直匹配到最后一个补丁。 -
确保您的 CI 和 CD 管道正常运行,没有失败或禁用的测试。
-
在升级之前对应用程序进行烟雾测试。确保所有主要功能正常运行,没有控制台错误或警告。
-
在升级之前解决任何发现的质量问题。
-
按顺序和有条不紊地遵循更新指南。
-
准备好回滚更新。
让我们从更新前的活动开始更新过程。
在更新之前
Angular 更新指南建议在“更新前”部分采取特定步骤,如下所示:
Angular 更新指南 - 更新前
在尝试更新之前,您可能需要对代码进行几种不同的更新。
命名空间更改:上述列表中的第一项通知我们某些动画服务和工具的命名空间可能已经更改。这些更改应该是低风险的,并且可以通过在 VS Code 中使用全局搜索工具快速完成。让我们看看如何快速观察你的应用程序中所有'@angular/core'的用法。看下一张截图:
'@angular/core'的搜索结果
在这种情况下,没有与动画相关的用法,所以我们可以继续。
重命名和替换更新:在版本 4 中,有一个要求,即将OpaqueTokens类型替换为InjectionTokens。对于这些类型的更改,再次使用全局搜索工具查找和替换必要的代码。
在使用全局搜索工具查找和替换代码时,确保您启用了匹配大小写(由 Aa 表示)和匹配整个单词(由 Ab|表示),以防止意外的替换。看一下以下截图,看看这两个选项处于启用状态时的情况:
匹配大小写和匹配整个单词已启用
功能性更改:弃用的功能提前一个主要版本发出信号,需要重写应用程序代码中受影响部分。如果您一直在大量使用HttpModule和Http,那么您的代码将需要进行严重的改造:
-
首先,使用全局搜索发现实际用法的实例。
-
在angular.io上搜索新引入的服务,例如
HttpClient或HttpClientModule:
Angular.io 文档页面
- 单击标题下的相关链接,其中包含有关新服务的丰富和上下文的信息。
新的服务通常伴随着新的好处,比如改进的编码体验,更好的可测试性或性能。
-
重新实现必要的代码。
-
执行下一节中提到的后续更新检查表。
这些功能性变化可以同时成为巨大的生产力助推器,但也会极大地增加及时升级到新版本 Angular 的摩擦。然而,您可以通过提前准备来降低变更成本,并最大程度地获得这些变化的好处。
在这种情况下,LocalCast Weather 应用程序没有使用已弃用的模块,因为恰好是在发布HttpClient服务后不久开始开发该应用程序。然而,如果我没有关注 Angular 社区,我就不会知道这个变化。出于这个原因,我强烈建议关注blog.angular.io。
此外,您可以定期检查 Angular 更新工具。该工具可能不会被迅速更新;然而,它是所有即将到来的变化的一个很好的摘要资源。
在更新工具中,如果您选择未来版本的 Angular,您将收到警告消息:
警告:当前主要版本之后的发布计划尚未最终确定,可能会更改。这些建议是基于计划的弃用。
这是保持领先并提前规划资源围绕 Angular 更新的一个很好的方法。
完成“更新前”阶段后,考虑在进入下一阶段之前查看后续更新检查表。
更新期间
以下是指南中关于ng update工具的更新期间部分:
Angular 更新指南-更新期间
相比之下,Angular 6 之前的升级看起来是这样的:
Angular 更新指南-在 Angular 6 之前
如果你对手动更新更感兴趣,请参考手动更新部分。在这一部分,我详细介绍了你应该执行的步骤,以更全面地进行自动升级。在第二章中,创建一个本地天气 Web 应用程序,我们避免安装 Angular CLI,这就是这种策略的好处所在。你可以继续在现有的 Angular 4 或 Angular 5 项目上工作,而不必担心 CLI 向后兼容性问题:
-
确保你已经更新到了最新的 Node LTS 版本,就像本章前面展示的那样
-
确保你使用的是 npm 的最新版本,就像本章前面展示的那样
-
在你的终端中,
cd进入项目文件夹 -
清理你的
node_modules文件夹:
$ rimraf node_modules
重要的是要注意,Node 或 npm 的版本更改可能会影响你的node_modules依赖项在计算机上的安装或存储方式。在升级到更低级别的工具,比如 Node 或 npm 之后,最好清除node_modules并在你的项目中重新安装你的包。在你的持续集成(CI)服务器上,这意味着使现有的包缓存无效。
- 重新安装依赖项:
$ npm install
- 卸载全局安装的
@angular/cli、webpack、jasmine或typescript的版本:
$ npm uninstall -g @angular/cli webpack jasmine typescript
- 在你的项目中更新到最新的 CLI 版本:
$ npm i -D @angular/cli@latest
> @angular/cli@6.0.0 postinstall /Users/du/dev/local-weather-app/node_modules/@angular/cli
> node ./bin/ng-update-message.js
===================================================================
The Angular CLI configuration format has been changed, and your
existing configuration can be updated automatically by running
the following command:
ng update @angular/cli
===================================================================
- 根据前面的消息建议更新项目配置:
$ npx ng update @angular/cli
master!
Updating karma configuration
Updating configuration
Removing old config file (.angular-cli.json)
Writing config file (angular.json)
Some configuration options have been changed, please make sure to update any npm scripts which you may have modified.
DELETE .angular-cli.json
CREATE angular.json (3644 bytes)
UPDATE karma.conf.js (1007 bytes)
UPDATE src/tsconfig.spec.json (324 bytes)
UPDATE package.json (3874 bytes)
UPDATE tslint.json (3024 bytes)
...
added 620 packages from 669 contributors in 24.956s
- 尝试执行
ng update:
$ npx ng update
We analyzed your package.json, there are some packages to update:
Name Version Command to update
-------------------------------------------------------------------
@angular/core 5.1.0 -> 6.0.0 ng update @angular/core
@angular/material 5.0.0 -> 6.0.0 ng update @angular/material
rxjs 5.5.2 -> 6.1.0 ng update rxjs
There might be additional packages that are outdated.
Or run ng update --all to try to update all at the same time.
- 尝试执行
ng update --all:
$ npx ng update --all
你可能会收到一个错误消息,说找到了不兼容的 peer 依赖。列出了一个或多个具体的问题。在解决所有问题之前,你将无法使用ng update。
在下一节中,我将介绍解决 peer 依赖错误的策略。如果你没有这种错误,可以跳过这一节。
不兼容的 peer 依赖
我将介绍一些在升级过程中遇到的不兼容的 peer 依赖错误,以及解决这些错误的不同策略。请注意,我将从简单的情况开始,并演示可能需要的研究量,因为你需要的依赖项可能不仅仅是你的包的最新发布版本。
- 包
karma-jasmine-html-reporter缺少 peer 依赖"jasmine" @ "³.0.0"。
这是一个简单的错误,只需简单地更新到最新版本的jasmine即可解决:
$ npm i -D jasmine
- 包
@angular/flex-layout与"rxjs"有不兼容的对等依赖关系(需要"⁵.5.0",将安装"6.1.0")。
这个错误需要一些对生态系统的研究和理解。截至 Angular 6,我们知道所有库都是版本同步的,因此我们需要这个库的 6.x 版本。让我们使用npm info来发现当前可用的版本:
$ npm info @angular/flex-layout
...
dist-tags:
latest: 5.0.0-beta.14 next: 6.0.0-beta.15
published a month ago by angular <devops+npm@angular.io>
截至目前,该库仍处于 beta 版本,最新版本为 5.0.0,因此简单地更新到最新版本的@angular/flex-layout是行不通的。在这种情况下,我们需要安装包的@next版本,如下所示:
$ npm i @angular/flex-layout@next
您将收到一堆依赖警告,显示需要 Angular 6 包。一旦更新完成,这些错误将消失。
- 包"@angular/compiler-cli"与"typescript"有不兼容的对等依赖关系(需要">=2.7.2 <2.8",将安装"2.8.3")。
Angular CLI 依赖于特定版本的 Typescript。如果执行npm info typescript,则最新版本的 Typescript 可能比所需版本更新。在这种情况下,正如前面的错误消息所报告的那样,它是2.8.3。错误消息确实向我们指出了具体需要的版本,如果你看一下 requires 语句。下限2.7.2似乎是正确的安装版本,所以让我们安装它,如下所示:
$ npm install -D typescript@2.7.2
理论上,我们所有的操作都应该解决所有对等依赖问题。实际上,我注意到这些错误有时会在使用npx ng update --all时仍然存在,因此我们将继续通过运行单独的更新命令来进行更新。
在非 macOS 操作系统上,您可能会持续遇到与 fsevents 相关的警告,例如npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.1.3。这是一个可选的包,仅在 macOS 上使用。避免看到这个错误的简单方法是运行npm install --no-optional命令。
继续更新。
我们将逐步更新 Angular:
- 让我们从 Angular Core 开始更新:
$ npx ng update @angular/core
Updating package.json with dependency rxjs @ "6.1.0" (was "5.5.6")...
Updating package.json with dependency @angular/language-service @ "6.0.0" (was "5.2.5")...
Updating package.json with dependency @angular/compiler-cli @ "6.0.0" (was "5.2.5")...
Updating package.json with dependency @angular/router @ "6.0.0" (was "5.2.5")...
Updating package.json with dependency @angular/forms @ "6.0.0" (was "5.2.5")...
Updating package.json with dependency @angular/platform-browser @ "6.0.0" (was "5.2.5")...
Updating package.json with dependency @angular/animations @ "6.0.0" (was "5.2.5")...
Updating package.json with dependency zone.js @ "0.8.26" (was "0.8.20")...
Updating package.json with dependency @angular/platform-browser-dynamic @ "6.0.0" (was "5.2.5")...
Updating package.json with dependency @angular/common @ "6.0.0" (was "5.2.5")...
Updating package.json with dependency @angular/core @ "6.0.0" (was "5.2.5")...
Updating package.json with dependency @angular/compiler @ "6.0.0" (was "5.2.5")...
Updating package.json with dependency @angular/http @ "6.0.0" (was "5.2.5")...
UPDATE package.json (5530 bytes)
...
added 12 packages from 37 contributors and updated 14 packages in 54.204s
请注意,此命令还会更新rxjs。
- 更新 Angular Material:
$ npx ng update @angular/material
Updating package.json with dependency @angular/cdk @ "6.0.0" (was "5.2.2")...
Updating package.json with dependency @angular/material @ "6.0.0" (was "5.2.2")...
UPDATE package.json (5563 bytes)
...
确保您查看第五章中的 Material Update Tool 和手动更新 Angular Material 的策略,使用 Angular Material 增强 Angular 应用。
- 更新其他依赖项,包括使用
npm update更新类型:
$ npm update
+ codelyzer@4.3.0
+ karma-jasmine@1.1.2
+ jsonwebtoken@8.2.1
+ core-js@2.5.5
+ prettier@1.12.1
+ karma-coverage-istanbul-reporter@1.4.2
+ typescript@2.8.3
+ @types/jsonwebtoken@7.2.7
+ ts-enum-util@2.0.2
+ @types/node@6.0.108
请注意,typescript已更新到其最新版本2.8.3,这对于 Angular 6 来说是不可接受的,正如前一节所述。通过执行npm install -D typescript@2.7.2回滚到版本2.7.2。
- 解决任何 npm 错误和警告。
你已经完成了主要的 Angular 依赖项更新。考虑在继续升级后部分之前执行“升级后检查清单”。
升级后
“升级后”阶段通知需要在主要 Angular 依赖项更新后进行的更改,并有时告诉我们在升级我们的 Angular 版本后可以获得的进一步好处。观察下一步:
Angular 升级指南-升级后
在这种情况下,我们必须解决与我们升级到 RxJS 相关的弃用。幸运的是,Angular 团队知道这可能是一个痛苦的过程,因此他们建议使用一个自动化工具来帮助我们入门:
-
不要全局安装该工具
-
执行迁移工具,如下所示:
$ npx rxjs-tslint -p .\src\tsconfig.app.json
Running the automatic migrations. Please, be patient and wait until the execution completes.
Found and fixed the following deprecations:
Fixed 2 error(s) in C:/dev/lemon-mart/src/app/common/common.ts
Fixed 6 error(s) in C:/dev/lemon-mart/src/app/auth/auth.service.ts
Fixed 1 error(s) in C:/dev/lemon-mart/src/app/common/ui.service.ts
...
WARNING: C:/dev/lemon-mart/src/app/auth/auth-http-interceptor.ts[2, 1]: duplicate RxJS import
WARNING: C:/dev/lemon-mart/src/app/auth/auth-http-interceptor.ts[4, 27]: outdated import path
WARNING: C:/dev/lemon-mart/src/app/auth/auth.service.fake.ts[2, 1]: duplicate RxJS import
...
- 手动解决任何警告;考虑以下示例:
example
import { BehaviorSubject, Observable, of } from 'rxjs'
import { ErrorObservable } from 'rxjs/observable/ErrorObservable'
import { IfObservable } from 'rxjs/observable/IfObservable'
import { catchError } from 'rxjs/operators'
在前面的示例中,我们只需要根据 RxJS 6 文档从'rxjs'和'rxjs/operators'导入,因此删除另外两个导入。此外,ErrorObservable和IfObservable导入被任何一行代码引用,因此很容易识别并删除。
一些警告可能掩盖了与新的 RxJS 函数的错误或不兼容性,因此逐一检查它们非常重要。
- 移除
rxjs-compat:
$ npm uninstall rxjs-compat
- 构建和测试您的代码,以确保通过执行
npm run predocker:build进行构建。
predocker:build以生产模式构建您的 Angular 应用程序,并通过执行以下命令运行您的单元测试和端到端测试:
$ npm run build -- --prod && npm test -- --watch=false && npm run e2e
解决任何错误。如果您遇到与您的代码无关的神秘错误,请尝试删除node_modules并重新安装软件包。
如果一切正常工作,恭喜你,你已经完成了升级!在你打开起泡酒之前,执行“升级后检查清单”。
升级后检查清单
更新后的清单在确保在进行大规模代码更改后没有引入任何退化的情况下非常有用。建议在更新过程的每个阶段之后执行此清单。可能并不总是可能或可行执行整个清单,但在对代码基进行重大更改后,如果有必要,更新你的单元测试,并逐步执行以下清单:
-
构建和烟雾测试你的 Angular 应用
-
提交你的更改
-
每次提交时,确保 CI 流水线保持正常
-
如果进行功能性更改,可能需要遵循你的组织的发布周期程序,其中可能包括由 QA 团队进行手动测试
-
建议逐个实施和部署这些更改,并将它们部署到生产环境
-
收集性能数据,如下一节所述
在一类更改后提交你的代码,这样可以在出现问题时回滚或挑选进一步的升级提交。
出于各种原因,你可能需要手动升级 Angular,这在下一节中有所涉及。
手动更新
最好对手动升级的工作原理有一个大致的了解,因为你可能无法使用具有自动更新功能的 Angular CLI 版本;你可能需要完全退出你的项目或者工具可能包含错误。这里讨论的版本号是从更新指南中复制的示例。
为了举例,我将演示从 Angular 4 到 Angular 5 的潜在升级:
-
遵循指南和本章的更新说明
-
确保 Node 和 npm 是最新的
-
为了升级到版本
5.0.0,执行以下命令:
$ npm install @angular/animations@'⁵.0.0' @angular/common@'⁵.0.0' @angular/compiler@'⁵.0.0' @angular/compiler-cli@'⁵.0.0' @angular/core@'⁵.0.0' @angular/forms@'⁵.0.0' @angular/http@'⁵.0.0' @angular/platform-browser@'⁵.0.0' @angular/platform-browser-dynamic@'⁵.0.0' @angular/platform-server@'⁵.0.0' @angular/router@'⁵.0.0' typescript@2.4.2 rxjs@'⁵.5.2'
- 接着执行
--save-exact命令,以防 TypeScript 被意外升级:
$ npm install typescript@2.4.2 --save-exact
- 确保你的
package.json文件已经更新到正确的版本:
"dependencies": {
"@angular/animations": "⁵.0.0",
"@angular/common": "⁵.0.0",
"@angular/compiler": "⁵.0.0",
"@angular/core": "⁵.0.0",
"@angular/forms": "⁵.0.0",
"@angular/http": "⁵.0.0",
"@angular/platform-browser": "⁵.0.0",
"@angular/platform-browser-dynamic": "⁵.0.0",
"@angular/platform-server": "⁵.0.0",
"@angular/router": "⁵.0.0",
"core-js": "².5.1",
"rxjs": "⁵.5.2",
"zone.js": "⁰.8.17"
},
"devDependencies": {
"@angular/cli": "¹.5.0",
"@angular/compiler-cli": "⁵.0.0",
"@angular/language-service": "⁴.4.3",
...
"typescript": "2.4.2"
},
注意,TypeScript 版本中的插入符号和波浪号已被移除,以防止任何意外的升级,因为 Angular 工具对任何给定 TypeScript 发布的特定功能非常敏感。
注意,@angular/cli 和 @angular/compiler-cli 已经更新到它们的最新版本;然而,工具没有更新 @angular/language-service。这突显了手动检查的重要性,因为你的工具链中的每个工具都容易受到小错误的影响。
- 通过执行以下命令更新
@angular/language-service:
$ npm install @angular/language-service@⁵.0.0
- 验证
package.json中是否有正确的文件版本:
"@angular/language-service": "⁵.0.0",
您已完成更新您的软件包。
- 按照指南和本章的更新说明进行操作。
在升级您的 Angular 应用程序后,测试您的更改对性能的影响是一个好主意。
性能测试
在更新之前和之后测试您的 Angular 应用程序的性能,以确保您的性能数字保持预期。在下面的情况中,由于平台级别的改进,我们自动获得了性能优势。首先,让我们比较一下 Angular v4 和 v5:
| 类别 | ** Angular 4** | ** Angular 5** | ** % 差异** |
|---|---|---|---|
| JavaScript Assets Delivered (gzipped) | 83.6 KB | 72.6 KB | 13% smaller |
| 首页渲染时间(Fiber) | 0.57 秒 | 0.54 秒 | 5% 更快 |
| 首页渲染时间(快速 3G) | 1.27 秒 | 1.18 秒 | 7% 更快 |
Angular 4.4.3 vs 5.0.0
Angular 6 的改进趋势持续下去:
| 类别 | ** Angular 5** | ** Angular 6** | ** % 差异** |
|---|---|---|---|
| JavaScript Assets Delivered (gzipped) | 72.6 KB | 64.1 KB | 12% 更小 |
| 首页渲染时间(Fiber) | 0.54 秒 | 0.32 秒 | 40% 更快 |
| 首页渲染时间(快速 3G) | 1.18 秒 | 0.93 秒 | 21% 更快 |
Angular 5.0.0 vs 6.0.0
这种趋势应该在未来的更新中继续,使用 Ivy 渲染引擎的目标大小为 3KB。我们将在第五章中介绍这些性能数字的重要性,使用 Angular Material 增强 Angular 应用程序。
解决安全漏洞
有时您会收到关于某些软件包的安全漏洞的通知,通过博客或者如果您使用 GitHub,您可能会在您的存储库上看到这样的警告:
GitHub.com 漏洞扫描
这是一个特定的问题,当我的 Angular 应用程序版本为 5.0.0,我的 CLI 版本为 1.5.0 时出现的。如果您查看这个依赖项,您可以看到依赖的软件包,并获得更多关于这个问题的细节。
GitHub.com 安全公告
在这种情况下,handlebars 的易受攻击版本 1.3.0 是由 Angular 5.0 包之一引起的。
进一步研究 Angular 的 GitHub 问题表明,问题实际上是由@angular/cli 版本 1.5.0引起的。参考是github.com/angular/angular/issues/20654。
这是尝试更新到 Angular、Material 或 CLI 的最新次要版本更新的好方法,在这种情况下是版本 5.1.0 和 1.6.0:
$ npm install @angular/animations@⁵.1.0 @angular/common@⁵.1.0 @angular/compiler@⁵.1.0 @angular/compiler-cli@⁵.1.0 @angular/core@⁵.1.0 @angular/forms@⁵.1.0 @angular/http@⁵.1.0 @angular/platform-browser@⁵.1.0 @angular/platform-browser-dynamic@⁵.1.0 @angular/platform-server@⁵.1.0 @angular/router@⁵.1.0 @angular/language-service@⁵.1.0 @angular/cli@¹.6.0
这次更新解决了 GitHub 显示的安全警告。如果你无法通过升级解决你的问题,请在 GitHub 上创建一个新问题,并密切关注 Angular 的即将发布的补丁或次要版本,直到问题得到解决。
更新你的 Web 服务器
你的堆栈顶部是你托管 Web 应用程序的 Web 服务器。这是一个实时的生产系统,很可能暴露在互联网上,因此风险最大。应该谨慎地保持最新状态。
理想情况下,你的发布流水线类似于第三章*,为生产发布准备 Angular 应用程序*中描述的流水线,其中你的前端应用程序由一个容器化的低配置实例提供。这可以是我发布和维护的minimal-node-web-server,也可以是基于 Nginx 的实例。在任何情况下,通过更改基础镜像旁边列出的版本号来升级是很简单的:
Dockerfile
FROM duluca/minimal-node-web-server:8.6.0
WORKDIR /usr/src/app
COPY dist public
指定你正在使用的基础 Docker 镜像的版本号总是一个好主意。否则,它将默认为最新行为,这在这种情况下可能意味着一个不适合生产的奇数版本。也就是说,minimal-node-web-server遵循了最佳安全实践的层层叠加,减少了攻击面,使成功攻击你的 Web 应用程序变得非常困难。与这一安全最佳实践主题一致,minimal-node-web-server永远不会将奇数节点版本作为默认行为。
如果你的内容是通过 IIS、Apache 或 Tomcat 等 Web 服务器安装提供的,你必须遵循和跟踪这些技术的安全公告。然而,很可能另一个人或另一个部门将负责升级这台服务器,这可能会导致从几天到几个月的延迟,这在互联网时间中是永远的。
你处于最高风险,如果你通过同一应用服务器提供静态网页内容,比如你的 SPA,同时也实现了后端 API。即使你的架构可能是解耦的,如果在你的依赖树中升级任何工具或应用程序对你的应用的任何其他部分产生副作用,这意味着你在保护或改进前端应用性能方面存在重大摩擦。
一个真正解耦的架构还将允许前端以不同的速度扩展,而不同于你的后端基础设施,这可以带来巨大的成本效益。例如,假设你的前端提供大量静态信息,并且很少需要轮询后端。在高负载时,你可能需要三个前端服务器实例来处理所有请求,但只需要一个后端服务器实例,因为调用很少。
更新 Docker 镜像
在升级应用程序及其依赖项或简单添加新功能后,您需要更新并发布新的 Docker 镜像。
-
在
package.json中,将版本属性更新为1.1.0或将您的版本与当前的 Angular 版本匹配 -
执行
npm run docker:debug来构建并验证您的更新是否正确工作 -
最后,执行
npm run docker:publish将您的新镜像推送到存储库
在发布图像后,采取必要步骤将图像部署到服务器或云提供商,如第三章中所述,准备 Angular 应用程序进行生产发布,以及第十一章中所述,AWS 上高可用云基础设施。
摘要
在本章中,我们讨论了保持整个依赖栈的最新状态的重要性,从 Node 和 npm 等开发工具到 Angular。我们看了看如何使用 ng update 和 Angular Update Guide 来尽可能地减少 Angular 更新的痛苦。我们还涵盖了手动更新、性能测试、处理超出安全漏洞和补丁的问题,包括保持 Web 服务器最新的必要性。保持相对最新的系统具有直接的成本效益。差距越小,维护的工作量就越小。然而,随着时间的推移,升级系统的成本呈指数级增长。作为非直接的好处,我们可以列举出由更好的性能带来的客户满意度,这是影响亚马逊等公司数百万美元的指标。工具中的新功能也对开发人员的生产力和幸福感产生深远影响,这有助于留住人才,减少新功能的成本,从而可能提高客户满意度。保持最新状态无疑是一个积极的反馈循环。
在下一章中,我们将讨论如何通过将 Angular Material 添加到项目中,使您的本地天气应用程序看起来更加出色。在这个过程中,您将了解用户控制或 UI 组件库可能对应用程序产生的负面性能影响,包括基本的 Material 组件、Angular Flex 布局、可访问性、排版、主题设置以及如何更新 Angular Material。
第五章:使用 Angular Material 增强 Angular 应用
在第三章*,为生产发布准备 Angular 应用*中,我们提到了提供高质量应用程序的需求。目前,该应用程序的外观和感觉非常糟糕,只适用于上世纪 90 年代创建的网站。用户或客户对您的产品或工作的第一印象非常重要,因此我们必须能够创建一个外观出色且在移动和桌面浏览器上提供出色用户体验的应用程序。
作为全栈开发人员,很难专注于应用程序的完善。随着应用程序功能集的迅速增长,情况会变得更糟。编写支持视图的优秀且模块化的代码很有趣,但在匆忙中退回到 CSS hack 和内联样式来改进应用程序是没有乐趣的。
Angular Material 是一个与 Angular 密切协调开发的令人惊叹的库。如果您学会如何有效地利用 Angular Material,您创建的功能将从一开始就看起来和运行得很好,无论您是在小型还是大型应用程序上工作。Angular Material 将使您成为一个更有效的 Web 开发人员,因为它附带了各种您可以利用的用户控件,并且您不必担心浏览器兼容性。作为额外的奖励,编写自定义 CSS 将变得罕见。
在本章中,您将学习以下内容:
-
如何配置 Angular Material
-
使用 Angular Material 升级 UX
Angular Material
Angular Material 项目的目标是提供一系列有用且标准的高质量用户界面(UI)组件。该库实现了谷歌的 Material Design 规范,在谷歌的移动应用程序、网络属性和 Android 操作系统中普遍存在。Material Design 确实具有特定的数字和盒状外观和感觉,但它不仅仅是另一个 CSS 库,就像 Bootstrap 一样。考虑在此处使用 Bootstrap 编码的登录体验:
Bootstrap 登录体验
请注意,输入字段及其标签位于不同的行上,复选框是一个小目标,错误消息显示为短暂的弹出通知,提交按钮只是坐落在角落里。现在考虑给定的 Angular Material 示例:
Angular Material 登录体验
输入字段及其标签最初是组合在一起的,以紧凑的形式吸引用户的注意力。复选框对触摸友好,提交按钮会拉伸以占用可用空间,以获得更好的默认响应式用户体验。一旦用户点击字段,标签就会收起到输入字段的左上角,如图所示:
Angular Material 动画和错误
此外,验证错误消息会内联显示,并与标签颜色变化结合,使用户注意力集中在输入字段上。
Material Design 帮助您设计具有自己品牌和样式的模块化 UI,同时定义动画,使用户在使用您的应用程序时拥有更好的用户体验(UX)。人类大脑下意识地跟踪对象及其位置。任何帮助过渡或由人类输入引起的反应的动画都会减少用户的认知负担,因此允许用户专注于处理内容,而不是试图弄清您特定应用程序的怪癖。
模块化 UI 设计和流畅的动作的结合创造了出色的用户体验。看看 Angular Material 如何实现一个简单的按钮。
Angular Material 按钮动画
在上面的截图中,请注意按钮上的点击动画是从用户实际点击的位置开始的。然而微妙,这创造了一种连续的动作,导致了对给定动作的适当反应。当按钮在移动设备上使用时,这种特效变得更加明显,从而导致更加自然的人机交互。大多数用户无法表达什么使直观的用户体验实际上直观,设计和体验中的这些微妙但至关重要的线索在允许您为用户设计这样的体验方面取得了巨大进步。
Angular Material 还旨在成为 Angular 高质量 UI 组件的参考实现。如果您打算开发自定义控件,Angular Material 的源代码应该是您首要的资源。术语“高质量”经常被使用,量化其含义非常重要。Angular Material 团队在他们的网站上恰当地表达了这一点。
我们所说的“高质量”是什么意思?
国际化和可访问性,以便所有用户都可以使用它们。简单直观的 API,不会让开发人员困惑,并且在各种用例中表现如预期,没有错误。行为经过充分的单元测试和集成测试。在 Material Design 规范的范围内可定制。性能成本最小化。代码清晰,有文档,可以作为 Angular 开发人员的示例。浏览器和屏幕阅读器支持。
Angular Material 支持所有主要浏览器的最近两个版本:Chrome(包括 Android)、Firefox、Safari(包括 iOS)和 IE11 / Edge。
构建 Web 应用程序,特别是那些也兼容移动设备的应用程序,确实很困难。有很多细微之处需要注意。Angular Material 将这些细微之处抽象出来,包括支持所有主要浏览器,这样您就可以专注于创建您的应用程序。Angular Material 不是一时的潮流,也不应轻视。如果使用正确,您可以大大提高生产率和工作质量的感知。
在您的项目中,不一定总是能够使用 Angular Material。我建议使用 PrimeNG(www.primefaces.org/primeng)或 Clarity(vmware.github.io/clarity)作为组件工具包,可以满足您大部分,如果不是全部,用户控制需求。要避免的一件事是从不同来源获取大量用户控件,最终得到一个杂乱的库,其中有数百个怪癖和错误需要学习、维护或解决。
Angular Material 的设置和性能
Angular Material 默认配置为优化最终交付的包大小。在 Angular.JS 和 Angular Material 1.x 中,将加载整个依赖库。然而,在 Angular Material 6 中,我们能够指定我们打算使用的组件,从而实现显著的性能改进。
在下表中,您可以看到典型的 Angular 1.x + Angular Material 1.x 与 Angular 6 + Material 6 应用程序在高速低延迟的光纤连接下性能特征的改进:
| 光纤网络 | Angular 6 + Material 6 | Angular 1.5 + Material 1.1.5 | % 差异 |
|---|---|---|---|
| 首页渲染时间* | 0.61 秒 | 1.69 秒** | ~2.8 倍更快 |
| 基本级别资产交付* | 113 KB | 1,425 KB | 缩小 12.6 倍 |
*图像或其他媒体内容未包含在结果中,以进行公平比较
*平均值:较低质量的基础设施导致初始渲染时间为 0.9 到 2.5 秒
在高速低延迟连接的理想条件下,Angular 6 + Material 6 应用程序在一秒内加载。然而,当我们切换到更常见的中等速度和高延迟的快速 3G 移动网络时,差异变得更加明显,如下表所示:
| 快速 3G 移动网络 | Angular 6 + Material 6 | Angular 1.5 + Material 1.1.5 | ** % 差异** |
|---|---|---|---|
| 首页渲染时间* | 1.94 秒 | 11.02 秒 | 5.7 倍更快 |
| 基本级别资产交付* | 113 KB | 1,425 KB | 缩小 12.6 倍 |
*图像或其他媒体内容未包含在结果中,以进行公平比较
尽管应用程序的大小差异保持一致,但您可以看到移动网络引入的额外延迟导致传统的 Angular 应用程序速度显着下降到不可接受的水平。
将所有组件添加到 Material 6 将导致约 1.3 MB 的额外负载需要传递给用户。正如您可以从之前的比较中看到的,这必须以任何代价避免。为了提供可能最小的应用程序,尤其是在移动和与销售相关的场景中,每 100 毫秒的加载时间对用户保留都有影响,您可以逐个加载和包含模块。Webpack 的摇树过程将模块分成不同的文件,从而减少初始下载大小。在未来的构建中,预计 Angular 的大小将进一步缩小,可能会减少上表中提到的大小一半。
安装 Angular Material
让我们开始任务,并使用 Angular Material 改进天气应用程序的用户体验。让我们将改进应用程序用户体验的任务移动到我们的 Waffle.io 看板上的进行中。在这里,您可以看到我的看板的状态:
Waffle.io 看板
自动
在 Angular 6 中,您可以自动将 Angular Material 添加到您的项目中,从而在过程中节省大量时间:
- 执行
add命令,如下所示:
$ npx ng add @angular/material
Installing packages for tooling via npm.
+ @angular/material@6.0.1
added 1 package in 15.644s
Installed packages for tooling via npm.
UPDATE package.json (1381 bytes)
UPDATE angular.json (3694 bytes)
UPDATE src/app/app.module.ts (502 bytes)
UPDATE src/index.html (474 bytes)
UPDATE node_modules/@angular/material/prebuilt-themes/indigo-pink.css (56678 bytes)
added 1 package in 13.031s
请注意,index.html文件已经被修改以添加图标库和默认字体,如下所示:
src/index.html
<head>
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500" rel="stylesheet">
...
</head>
还要注意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。重要的是要了解组成 Angular Material 的各种组件;在接下来的章节中,我们将介绍手动安装和配置步骤。您可以跳到Angular Flex Layout部分,但我强烈建议浏览一下手动步骤,因为我介绍了创建一个 Angular 模块来组织您的 Material 模块的概念。
手动
我们将从安装所有必需的库开始。从 Angular 5 开始,Angular Material 的主要版本应该与您的 Angular 安装版本匹配,而在 Angular 6 中,版本应该同步:
-
在终端中,执行
npm install @angular/material @angular/cdk @angular/animations hammerjs -
观察
package.json版本:
package.json
"dependencies": {
"@angular/animations": "6.0.0",
"@angular/cdk": "6.0.0",
"@angular/material": "6.0.0",
"hammerjs": "².0.8",
...
在这种情况下,所有库的主要和次要版本都是 5.0。如果您的主要和次要版本不匹配,您可以重新运行npm install命令以安装特定版本,或者选择通过将包的 semver 版本附加到安装命令来升级您的 Angular 版本:
$ npm install @angular/material@6.0.0 @angular/cdk@6.0.0 @angular/animations@6.0.0
如果您使用类似 Bash 的 shell,可以使用括号语法来节省一些输入,以避免重复命令的部分,比如npm install @angular/{material,cdk,animations}@6.0.0。
如果您需要更新 Angular 的版本,请参考第四章中的更新 Angular部分,保持与 Angular 更新同步。
理解 Material 的组件
让我们看看我们究竟安装了什么:
-
@angular/material是官方的 Material 2 库。 -
@angular/cdk是一个对等依赖项,除非您打算构建自己的组件,否则不会直接使用它。 -
@angular/animations启用了一些 Material 2 模块的动画。可以省略它以保持应用程序的大小最小。您可以使用NoopAnimationsModule来禁用需要此依赖项的模块中的动画。结果,您将失去一些 Angular Material 的 UX 优势。 -
hammerjs启用了手势支持;如果您的目标是任何触摸设备,不仅仅是手机和平板电脑,还包括混合式笔记本电脑,这一点非常重要。
手动配置 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:
src/app/material.module.ts
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
@NgModule({
imports: [CommonModule],
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 'hammerjs'
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
@NgModule({
...
imports: [..., MaterialModule, BrowserAnimationsModule],
}
-
修改
material.module.ts以导入按钮、工具栏和图标的基本组件 -
移除
CommonModule:
src/app/material.module.ts
import { MatButtonModule, MatToolbarModule, MatIconModule } from '@angular/material'
import { NgModule } from '@angular/core'
@NgModule({
imports: [MatButtonModule, MatToolbarModule, MatIconModule],
exports: [MatButtonModule, MatToolbarModule, MatIconModule],
})
export class MaterialModule {}
Material 现在已导入到应用程序中,现在让我们配置一个主题并将必要的 CSS 添加到我们的应用程序中。
导入主题
为了使用 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 图标 Web 字体添加到应用程序中,您可以访问一个很好的默认图标集。这个库大小为 48 kb,非常轻量级。
- 对于图标支持,请在
index.html中导入字体:
src/index.html
<head>
...
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head>
在www.google.com/design/icons/上发现并搜索图标。
要获得更丰富的图标集,请访问MaterialDesignIcons.com。这个图标集包含了 Material 图标的基本集,以及丰富的第三方图标,包括来自社交媒体网站的有用图像,以及涵盖了很多领域的丰富的操作。这个字体大小为 118 kb。
Angular Flex Layout
在您可以有效使用 Material 之前,您必须了解其布局引擎。如果您已经做了一段时间的 Web 开发,您可能遇到过 Bootstrap 的 12 列布局系统。这对我大脑以 100%的方式分配事物的数学障碍。Bootstrap 还要求严格遵守 div 列、div 行的层次结构,必须从顶层 HTML 精确管理到底部。这可能会导致非常沮丧的开发体验。在下面的截图中,您可以看到 Bootstrap 的 12 列方案是什么样子的:
Bootstrap 的 12 列布局方案
Bootstrap 的自定义网格布局系统在当时是革命性的,但随后 CSS3 Flexbox 出现了。结合媒体查询,这两种技术允许创建响应式用户界面。然而,有效地利用这些技术是非常费力的。从 Angular v4.1 开始,Angular 团队推出了其 Flex 布局系统,它可以正常工作。
GitHub 上的 Angular Flex Layout 文档恰如其分地解释了如下内容:
Angular Flex Layout 提供了一个复杂的布局 API,使用 FlexBox CSS + mediaQuery。这个模块为 Angular(v4.1 及更高版本)开发人员提供了使用自定义布局 API、mediaQuery observables 和注入的 DOM flexbox-2016 CSS 样式的组件布局功能。
Angular 的出色实现使得使用 FlexBox 非常容易。正如文档进一步解释的那样:
布局引擎智能地自动应用适当的 FlexBox CSS 到浏览器视图层次结构。这种自动化还解决了许多传统的、手动的、仅使用 Flexbox CSS 的应用程序所遇到的复杂性和解决方法。
该库非常强大,可以容纳您能想象到的任何类型的网格布局,包括与您可能期望的所有 CSS 功能的集成,比如calc()函数。在下图中,您可以看到如何使用 CSS Flexbox 描述列:
Angular Flex Layout 方案
令人振奋的消息是,Angular Flex 布局与 Angular Material 没有任何耦合,并且可以独立使用。这是非常重要的解耦,解决了使用 AngularJS 与 Material v1 时的一个主要痛点,其中 Material 的版本更新经常会导致布局中的错误。
更多详情,请查看:github.com/angular/flex-layout/wiki。
在发布时,@angular/flex-layout还没有发布稳定版本。该项目的 GitHub 活动表明,稳定版本将与 Angular 6 的发布同步。此外,CSS Grid 有望取代 CSS Flexbox,因此,该库使用的基础技术可能会发生变化。我希望这个库作为布局引擎的抽象层。
响应式布局
您设计和构建的所有 UI 都应该是面向移动设备的 UI。这不仅仅是为了服务于手机浏览器,还包括笔记本电脑用户可能会将您的应用与其他应用并排使用的情况。要正确实现移动设备优先设计有许多微妙之处。
以下是Mozilla 圣杯布局,它演示了“根据不同屏幕分辨率动态更改布局的能力”,同时优化移动设备的显示内容。
您可以在mzl.la/2vvxj25了解有关 Flexbox 基本概念的更多信息。
这是大屏幕上 UI 外观的表示:
Mozilla 大屏幕上的圣杯布局
同样的布局在小屏幕上表示如下:
Mozilla 小屏幕上的圣杯布局
Mozilla 的参考实现需要 85 行代码来完成这种响应式 UI。Angular Flex 布局只需一半的代码就能完成同样的任务。
安装 Angular Flex 布局
让我们安装并将 Angular Flex 布局添加到我们的项目中:
- 在终端中,执行
npm i @angular/flex-layout
在发布时,@angular/flex-layout的当前版本是5.0.0-beta.14,这会导致许多对等依赖错误。为了避免这些错误,请执行npm i @angular/flex-layout@next来安装版本6.0.0-beta.15,如第四章中所述,与 Angular 更新保持最新。
- 更新
app.module.ts,如下所示:
src/app.module.ts
import { FlexLayoutModule } from '@angular/flex-layout'
imports: [
...
FlexLayoutModule,
],
布局基础
Bootstrap 和 CSS FlexBox 与 Angular Flex 布局是不同的东西。如果你学会了 Angular Flex 布局,你将编写更少的布局代码,因为 Angular Material 大多数时候会自动做正确的事情,但是一旦你意识到一旦你离开 Angular Flex 布局的保护茧,你将不得不写更多的代码来让事情运转起来,你会感到失望。然而,你的技能仍然会转化,因为概念基本上是相同的。
让我们在接下来的部分中回顾一下 Flex 布局 API。
DOM 容器的 Flex 布局 API
这些指令可以用在诸如<div>或<span>之类的 DOM 容器上,比如<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="15px">...</div>:
| HTML API | 允许的值 |
|---|---|
fxLayout | | Use: row | column | row-reverse | column-reverse |
| fxLayoutAlign | 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>:
| HTML API | 允许的值 |
|---|---|
fxFlex | "" | px | % | vw | vh | |
fxFlexOrder | int |
fxFlexOffset | % | px | vw | vh |
fxFlexAlign | start | baseline | center | end |
fxFlexFill | none |
任何元素的 Flex 布局 API
以下指令可以应用于任何 HTML 元素,以显示、隐藏或更改所述元素的外观和感觉,比如<div fxShow fxHide.lt-sm></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 的信息。我们将在第十章,Angular 应用程序设计和技巧中介绍响应式 API。您可以在github.com/angular/flex-layout/wiki/Responsive-API上阅读更多关于响应式 API 的信息。
向您的应用程序添加 Material 组件
现在我们已经安装了各种依赖项,我们可以开始修改我们的 Angular 应用程序以添加 Material 组件。我们将添加一个工具栏,Material 设计卡片元素,并涵盖基本布局技术以及辅助功能和排版方面的问题。
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 原理图承诺使向您的 Angular 应用程序添加各种 Material 模块和组件变得不那么繁琐;然而,如提供的那样,这些原理图并不适用于创建灵活、可扩展和良好架构的代码库,正如本书所追求的那样。
目前,我建议将这些原理图用于快速原型设计或实验目的。
现在,让我们开始手动向 LocalCast Weather 添加一些组件。
使用 Material 工具栏修改着陆页面
在我们开始对 app.component.ts 进行进一步更改之前,让我们将组件切换为使用内联模板和内联样式,这样我们就不必在相对简单的组件中来回切换文件。
-
更新
app.component.ts以使用内联模板 -
移除
app.component.html和app.component.css
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.ts中的h1标签:
**src/app/app.component.ts**
<h1>
LocalCast Weather </h1>
- 使用
mat-toolbar更新h1标签:
src/app/app.component.ts
<mat-toolbar>
<span>LocalCast Weather</span>
</mat-toolbar>
- 观察结果;您应该看到一个工具栏,如图所示:
本地天气工具栏
- 使用更引人注目的颜色更新
mat-toolbar:
src/app/app.component.ts
<mat-toolbar color="primary">
为了更加原生的感觉,工具栏与浏览器的边缘接触是很重要的。这在大屏和小屏格式上都很有效。此外,当您将可点击的元素(如汉堡菜单或帮助按钮)放在工具栏的最左侧或最右侧时,您将避免用户点击空白空间的可能性。这就是为什么 Material 按钮实际上具有比视觉表示更大的点击区域。这在打造无挫折的用户体验方面有很大的不同:
src/styles.css
body {
margin: 0;
}
这对于这个应用来说并不适用,但是,如果您正在构建一个密集的应用程序,您会注意到您的内容将一直延伸到应用程序的边缘,这并不是一个理想的结果。考虑将您的内容区域包裹在一个 div 中,并使用 css 应用适当的边距,如下所示:
src/styles.css
.content-margin {
margin-left: 8px;
margin-right: 8px;
}
在下一个截图中,您可以看到应用了主色的边到边工具栏:
带有改进工具栏的本地天气
在 Material Card 中表示天气
Material 卡片是一个很好的容器,用来表示当前的天气信息。卡片元素被一个投影阴影所包围,将内容与周围区域分隔开来:
- 在
material.module中导入MatCardModule:
src/app/material.module.ts
import { ..., MatCardModule} from '@angular/material'
...
@NgModule({
imports: [..., MatCardModule],
exports: [..., MatCardModule],
})
- 在
app.component中用<mat-card>包围<app-current-weather>:
src/app/app.component.ts
<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>
- 观察如图所示的几乎无法区分的卡片元素:
带有不可区分卡片的本地天气
为了更好地布局屏幕,我们需要切换到 Flex 布局引擎。首先从组件模板中移除训练轮:
- 从周围的
<div>中移除style="text-align:center":
要在页面中心放置一个元素,我们需要创建一行,为中心元素分配一个宽度,并在两侧创建两个额外的列,这些列可以灵活地占据空白空间,如下所示:
src/app/app.component.ts
<div fxLayout="row">
<div fxFlex></div>
<div fxFlex="300px">
...
</div>
<div fxFlex></div>
</div>
-
用前面的 HTML 包围
<mat-card> -
注意卡片元素已正确居中,如下所示:
带居中卡片的 LocalCast 天气
阅读卡片文档,并查看 Material 文档站点上的示例,您会注意到mat-card提供了容纳标题和内容的元素。我们将在接下来的部分中实现这一点。
在material.angular.io上,您可以通过单击括号图标查看任何示例的源代码,或者通过单击箭头图标在 Plunker 中启动一个可工作的示例。
可访问性
利用这样的 Material 特性可能会感觉不必要;然而,在设计应用程序时,您必须考虑响应性、样式、间距和可访问性问题。Material 团队已经付出了很多努力,以便您的代码在大多数情况下能够正确运行,并为尽可能多的用户群提供高质量的用户体验。这可能包括视力受损或键盘主导用户,他们必须依赖专门的软件或键盘功能(如标签)来浏览您的应用程序。利用 Material 元素为这些用户提供了关键的元数据,使他们能够浏览您的应用程序。
Material 声称支持以下屏幕阅读器软件:
-
Windows 上的 IE / FF / Chrome 上的 NVDA 和 JAWS
-
iOS 上的 Safari 和 Safari / Chrome 上的 VoiceOver
-
Android 上的 Chrome TalkBack
卡头和内容
现在,让我们实现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>
<mat-card fxFlex="300px">
<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 fxFlex></div>
</div>
在 Material 中,少即是多。您会注意到我们能够删除中心的div,并直接在中心卡上应用fxFlex。所有 Material 元素都原生支持 Flex 布局引擎,这在复杂的 UI 中具有巨大的积极维护影响。
应用mat-card-header后,您可以看到这个结果:
带标题和内容的 LocalCast 天气卡
请注意,卡片内的字体现在与 Material 的 Roboto 字体匹配。然而,Current Weather 不再像以前那样引人注目。如果你在 mat-card-title 内部添加回 h2 标签,Current Weather 在视觉上会显得更大;然而,字体将不再与你的应用程序的其余部分匹配。要解决这个问题,你必须了解 Material 的排版特性。
Material 排版
Material 的文档恰如其分地将其表述如下:
排版是一种排列字体的方式,使文本在显示时易于辨认、可读和吸引人。
Material 提供了不同级别的排版,具有不同的字体大小、行高和字重特性,你可以应用到任何 HTML 元素上,而不仅仅是提供的组件。
在下表中是你可以使用的 CSS 类,用于应用 Material 的排版,比如 <div class="mat-display-4">Hello, Material world!</div>:
| 类名 | 用法 |
|---|---|
display-4, display-3, display-2 和 display-1 | 大的、一次性的标题,通常位于页面顶部(例如,主标题) |
headline | 对应 <h1> 标签的章节标题 |
title | 对应 <h2> 标签的章节标题 |
subheading-2 | 对应 <h3> 标签的章节标题 |
subheading-1 | 对应 <h4> 标签的章节标题 |
body-1 | 基本正文文本 |
body-2 | 更粗的正文文本 |
caption | 较小的正文和提示文本 |
button | 按钮和锚点 |
你可以在 material.angular.io/guide/typography 阅读更多关于 Material 排版的信息。
应用排版
有多种方式可以应用排版。一种方式是利用 mat-typography 类,并使用相应的 HTML 标签如 <h2>:
src/app/app.component.ts
<mat-card-header class="mat-typography">
<mat-card-title><h2>Current Weather</h2></mat-card-title>
</mat-card-header>
另一种方式是直接在元素上应用特定的排版,比如 class="mat-title":
src/app/app.component.ts
<mat-card-title><div class="mat-title">Current Weather</div></mat-card-title>
请注意,class="mat-title" 可以应用到 div、span 或带有相同结果的 h2 上。
作为一个一般的经验法则,通常更好的做法是实现更具体和局部化的选项,即第二种实现方式。
更新标语为居中对齐的标题
我们可以使用 fxLayoutAlign 居中应用程序的标语,并给它一个柔和的 mat-caption 排版,如下所示:
- 实现布局更改和标题排版:
**src/app/app.component.ts**
<div fxLayoutAlign="center">
<div class="mat-caption">Your city, your forecast, right now!</div>
</div>
- 观察结果,如下所示:
LocalCast 天气中心标语居中
更新当前天气卡片布局
仍然有更多工作要做,以使 UI 看起来像设计,特别是当前天气卡片的内容,如下所示:
为了设计布局,我们将利用 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 <div>
3 <span>{{current.city}}, {{current.country}}</span>
4 <span>{{current.date | date:'fullDate'}}</span>
5 </div>
6 <div>
7 <img [src]='current.image'>
8 <span>{{current.temperature | number:'1.0-0'}}℉</span>
9 </div>
10 <div>
11 {{current.description}}
12 </div>
13 </div>
让我们逐步通过文件并更新它:
-
将第 3、4 和 8 行的
<span>元素更新为<div> -
用
<div>包装<img>元素 -
在第 2 行和第 6 行有多个子元素的
<div>元素上添加fxLayout="row"属性 -
城市和国家列大约占据了屏幕的 2/3,因此在第 3 行的
<div>元素上添加fxFlex="66%" -
在第 4 行的下一个
<div>元素上添加fxFlex,以确保它占据其余的水平空间 -
在新的
<div>元素周围添加fxFlex="66%",以包围<img>元素 -
在第 4 行的下一个
<div>元素上添加fxFlex
模板的最终状态应该如下所示:
src/app/current-weather/current-weather.component.html
1 <div *ngIf="current">
2 <div fxLayout="row">
3 <div fxFlex="66%">{{current.city}}, {{current.country}}</div>
4 <div fxFlex>{{current.date | date:'fullDate'}}</div>
5 </div>
6 <div fxLayout="row">
7 <div fxFlex="66%">
8 <img [src]='current.image'>
9 </div>
10 <div fxFlex>{{current.temperature | number:'1.0-0'}}℉</div>
11 </div>
12 <div>
13 {{current.description}}
14 </div>
15 </div>
您可以在添加 Angular Flex 属性时更详细; 但是,您写的代码越多,将来需要维护的内容就越多,这会使未来的更改变得更加困难。例如,第 12 行的<div>元素不需要fxLayout="row",因为<div>隐式地换行。同样,在第 4 行和第 7 行,右侧列不需要显式的fxFlex属性,因为它将自动被左侧元素挤压。
从网格放置的角度来看,所有元素现在都在正确的单元格中,如下所示:
带有布局脚手架的 LocalCast 天气
对齐元素
现在,我们需要对齐和样式化每个单独的单元格以匹配设计。日期和温度需要右对齐,描述需要居中:
- 要右对齐日期和温度,请在
current-weather.component.css中创建一个名为.right的新 css 类:
src/app/current-weather/current-weather.component.css
.right {
text-align: right
}
-
在第 4 行和第 10 行的
<div>元素中添加class="right" -
以与之前章节中应用标语居中的方式居中
<div>元素的描述 -
观察元素是否正确对齐,如下所示:
具有正确对齐的 LocalCast 天气
样式元素
最终确定元素的样式通常是前端开发中最耗时的部分。我建议进行多次尝试,首先以最小的努力实现设计的足够接近版本,然后让您的客户或团队决定是否值得额外投入更多时间来完善设计:
- 添加一个新的 css 属性:
src/app/current-weather/current-weather.component.css
.no-margin {
margin-bottom: 0
}
-
对于城市名称,在第 3 行,添加'class="mat-title no-margin"'
-
对于日期,在第 4 行,添加"mat-subheading-2 no-margin"到'class="right"'
-
将日期格式从'fullDate'更改为'EEEE MMM d'以匹配设计
-
修改
<img>,在第 8 行添加style="zoom: 175%" -
对于温度,在第 10 行,追加"mat-display-3 no-margin"
-
对于描述,在第 12 行,添加'class="mat-caption"'
这是模板的最终状态:
src/app/current-weather/current-weather.component.html
<div *ngIf="current">
<div fxLayout="row">
<div fxFlex="66%" class="mat-title no-margin">{{current.city}}, {{current.country}}</div>
<div fxFlex class="right mat-subheading-2 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'}}℉</div>
</div>
<div fxLayoutAlign="center" class="mat-caption">
{{current.description}}
</div>
</div>
- 观察您的代码的样式化输出如何改变,如图所示:
带有样式的 LocalCast 天气
微调样式
标语可以从顶部和底部边距中受益。这是我们可能会在整个应用程序中使用的常见 CSS,所以让我们把它放在'styles.css'中:
- 实现'vertical-margin':
src/styles.css
.vertical-margin {
margin-top: 16px;
margin-bottom: 16px;
}
- 应用'vertical-margin':
src/app/app.component.ts
<div class="mat-caption vertical-margin">Your city, your forecast, right now!</div>
当前天气与城市名称具有相同的样式;我们需要区分这两者。
- 在'app.component.ts'中,使用'mat-headline'排版更新当前天气:
src/app/app.component.ts
<mat-card-title><div class="mat-headline">Current Weather</div></mat-card-title>
- 图像和温度没有居中,因此在第 6 行的围绕这些元素的行中添加'fxLayoutAlign="center center"':
src/app/current-weather/current-weather.component.html
<div fxLayout="row" fxLayoutAlign="center center">
- 观察您的应用程序的最终设计,应该是这样的:
LocalCast 天气最终设计
微调以匹配设计
这是一个你可能会花费大量时间的领域。如果我们遵循 80-20 原则,像素完美的微调通常最终成为需要花费 80%的时间来完成的最后 20%。让我们来看看我们的实现与设计之间的差异以及弥合差距需要付出的努力:
日期需要进一步定制。缺少数字序数th; 为了实现这一点,我们需要引入第三方库,如 moment,或者实现我们自己的解决方案,并将其绑定到模板上的日期旁边:
- 更新'current.date'以附加序数:
src/app/current-weather/current-weather.component.html
{{current.date | date:'EEEE MMM d'}}{{getOrdinal(current.date)}}
- 实现一个
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]
: ''
}
...
}
请注意,getOrdinal的实现归结为一个复杂的一行代码,不太可读,很难维护。如果这样的函数对您的业务逻辑至关重要,应该进行大量的单元测试。
在撰写本文时,Angular 6 不支持日期模板中的新行换行;理想情况下,我们应该能够将日期格式指定为'EEEE\nMMM d',以确保换行始终保持一致。
温度的实现需要使用<span>元素将数字与单位分开,用<p>包围,以便可以将上标样式应用到单位,例如<span class="unit">℉</span>,其中 unit 是一个 CSS 类,使其看起来像一个上标元素。
- 实现一个
unitCSS 类:
src/app/current-weather/current-weather.component.css
.unit {
vertical-align: super;
}
- 应用
unit:
src/app/current-weather/current-weather.component.html
...
7 <div fxFlex="55%">
...
10 <div fxFlex class="right no-margin">
11 <p class="mat-display-3">{{current.temperature | number:'1.0-0'}}
12 <span class="mat-display-1 unit">℉</span>
13 </p>
我们需要通过调整第 7 行的fxFlex值来实验预报图像应该有多少空间。否则,温度会溢出到下一行,并且您的设置还会受到浏览器窗口大小的影响。例如,60%在小浏览器窗口下效果很好,但当最大化时会导致溢出。然而,55%似乎满足了两个条件:
调整后的 LocalCast 天气
与往常一样,可以进一步调整边距和填充以进一步定制设计。然而,每一次偏离库都会在以后产生可维护性后果。除非您真的要围绕显示天气数据构建业务,否则应该在项目结束时推迟任何进一步的优化,如果时间允许,如果经验是任何指导,您将不会进行这种优化。
通过两个负的 margin-bottom hack,你可以获得一个与原始设计非常接近的设计,但我不会在这里包含这些 hack,而是留给读者在 GitHub 存储库中发现。这些 hack 有时是必要的恶,但总的来说,它们指向设计和实现现实之间的脱节。在调整部分之前的解决方案是甜蜜点,Angular Material 在这里蓬勃发展:
调整和 hack 后的 LocalCast 天气
更新单元测试
为了保持您的单元测试运行,您需要将MaterialModule导入到任何使用 Angular Material 的组件的spec文件中:
*.component.spec.ts
...
beforeEach(
async(() => {
TestBed.configureTestingModule({
...
imports: [..., MaterialModule, NoopAnimationsModule],
}).compileComponents()
})
)
你还需要更新任何测试,包括 e2e 测试,以搜索特定的 HTML 元素。
例如,由于应用程序的标题 LocalCast Weather 不再在h1标签中,你必须更新spec文件,以在span元素中查找它:
src/app/app.component.spec.ts
expect(compiled.querySelector('span').textContent).toContain('LocalCast Weather')
同样,在 e2e 测试中,你需要更新你的页面对象函数,以从正确的位置检索文本:
e2e/app.po.ts
getParagraphText() {
return element(by.css('app-root mat-toolbar span')).getText()
}
自定义主题
正如我们之前讨论的,Material 默认提供了一些默认主题,如深紫色-琥珀色、蓝紫色-粉色、粉色-蓝灰色和紫色-绿色。然而,你的公司或产品可能有自己的配色方案。为此,你可以创建一个自定义主题,改变你的应用程序的外观。
为了创建一个新的主题,你必须实现一个新的 scss 文件:
-
在
src下创建一个名为localcast-theme.scss的新文件 -
Material 主题指南,位于
material.angular.io/guide/theming,包括一个最新的起始文件。我将进一步解释文件的内容 -
首先包含基础主题库:
src/localcast-theme.scss
@import '~@angular/material/theming';
- 导入
mat-core()mixin,其中包括各种 Material 组件使用的所有通用样式:
src/localcast-theme.scss
@include mat-core();
mat-core()应该只在你的应用程序中包含一次;否则,你将在应用程序中引入不必要和重复的 css 负载。
mat-core()包含必要的 scss 函数,可以将自定义颜色注入到 Material 中,例如 mat-palette、mat-light-theme 和 mat-dark-theme。
至少,我们必须定义一个新的主色和一个强调色。然而,定义新的颜色并不是一个简单的过程。Material 需要定义一个调色板,mat-palette,它需要由一个复杂的颜色对象种子化,不能简单地被一个简单的十六进制值如#BFB900所覆盖。
要选择你的颜色,可以使用位于material.io/color的 Material Design Color Tool。这是工具的截图:
Material.io 颜色工具
- 使用 Material Palette,选择一个主色和一个次要颜色:
-
我的主要选择是红色,色调值为
500 -
我的次要选择是蓝紫色,色调值为
A400
-
通过浏览左侧的 6 个预构建屏幕,观察你的选择如何应用到 Material 设计应用程序
-
评估你的选择对可访问性的影响,如下所示:
Material.io 颜色工具可访问性选项卡该工具警告我们,我们的选择导致不合格的文本,当白色文本用于主要颜色时。您应该注意避免在主要颜色上显示白色文本,或更改您的选择。
mat-palette的接口如下所示:
mat-palette($base-palette, $default: 500, $lighter: 100, $darker: 700)
- 使用工具的默认色调定义主要和次要的
mat-palette对象:
src/localcast-theme.scss
$localcast-primary: mat-palette($mat-red, 500);
$localcast-accent: mat-palette($mat-indigo, A400);
- 创建一个新主题并应用它:
src/localcast-theme.scss
$localcast-app-theme: mat-light-theme($localcast-primary, $localcast-accent);
@include angular-material-theme($localcast-app-theme);
-
在
angular.json中,找到apps.styles属性 -
在删除
styles.input属性的同时,在列表前加上localcast-theme.scss
angular.json
...
"styles": [
"src/localcast-theme.scss",
"src/styles.css"
],
...
即使您的主题是 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。
您的应用程序现在应该是这样的:
带有自定义主题的 LocalCast 天气
我们现在可以将 UX 任务移动到已完成的列中:
Waffle.io 看板状态
高级主题
为了创建更多定制的主题,您应该考虑使用 Material Design 主题调色板生成器mcg.mbitson.com。这将生成定义自定义颜色调色板以创建真正独特主题所需的代码。
您还可以在meyerweb.com/eric/tools/color-blend找到颜色混合器,以找到两种颜色之间的中间点。
更新 Angular Material
在第四章中,与 Angular 更新保持最新,我们利用了ng update进行自动升级体验,并介绍了手动和系统化的更新包方法。我们将在更新 Angular Material 时采用类似的策略。
您可以使用ng update来快速且无痛的升级体验,应该如下所示:
$ npx ng update @angular/material
Updating package.json with dependency @angular/cdk @ "6.0.0" (was "5.2.2")...
Updating package.json with dependency @angular/material @ "6.0.0" (was "5.2.2")...
UPDATE package.json (5563 bytes)
此外,我发现了 Angular 团队在 github.com/angular/material-update-tool 发布的 material-update-tool。目前这个工具被宣传为一个特定的 Angular Material 5.x 到 6.0 的更新工具,因此它可能在未来成为 ng update 的一部分,就像 rxjs-tslint 工具一样。您可以按照下面的示例运行该工具:
$ npx angular-material-updater -p .\src\tsconfig.app.json
√ Successfully migrated the project source files. Please check above output for issues that couldn't be automatically fixed.
如果您幸运并且一切顺利,可以随意跳过本节的其余部分。在本节的其余部分中,我将介绍我在开发此示例时遇到的涉及发布候选版本和 Beta 版本的特定情况,这突显了手动更新的必要性。首先,我们将了解当前版本,然后发现最新可用版本,最后,更新和测试升级,就像我们手动更新 Angular 时所做的那样。
对您当前的版本进行盘点
观察 package.json 中的 Angular Material 包版本:
package.json
"dependencies": {
"@angular/core": "⁵.0.0",
...
"@angular/animations": "⁵.0.0",
"@angular/cdk": "⁵.0.0-rc0",
"@angular/flex-layout": "².0.0-beta.10-4905443",
"@angular/material": "⁵.0.0-rc0",
"hammerjs": "².0.8",
},
在这种特殊情况下,我在 RC 阶段安装了 Material 5.0.0。建议不要发布 Beta 或 RC 库。由于我们的 @angular/core 包指示我们使用的是 Angular 版本 5.0.0,我们将目标升级到最新的 Angular Material 5.x.x 版本。
检查最新可用版本
我们将利用 npm CLI 工具来发现 Angular Material 的最新可用版本:
- 执行
npm info @angular/material并观察输出:
{
name: '@angular/material',
description: 'Angular Material',
'dist-tags': { latest: '5.0.0' },
versions:
[ ...
'5.0.0-rc.0',
'5.0.0-rc.1',
'5.0.0-rc.2',
'5.0.0-rc.3',
'5.0.0-rc0',
'5.0.0' ],
...
time: {
created: ...
'5.0.0-rc0': '2017-11-06T20:15:29.863Z',
'5.0.0-rc.1': '2017-11-21T00:38:56.394Z',
'5.0.0-rc.0': '2017-11-27T19:21:19.781Z',
'5.0.0-rc.2': '2017-11-28T00:13:13.487Z',
'5.0.0-rc.3': '2017-12-05T21:20:42.674Z',
'5.0.0': '2017-12-06T20:19:25.466Z'
}
您可以观察到,结合输出中更深层的时间信息,自 5.0.0-rc0 发布以来已经推出了 5 个新版本,最终版本是库的主要版本 5.0.0 发布。
如果 Material 库有其他主要版本可用,比如 6.0.0,您仍应坚持使用 5.x.x 版本,因为我们的 @angular/core 版本是 5.x.x。一般来说,您应该保持 Angular 和 Material 的主要版本相同。
-
研究
@angular/core、@angular/animations、@angular/cdk、@angular/flex-layout、@angular/material和hammerjs的最新可用版本。 -
为了减少您需要筛选的信息量,对每个包执行
npm info <package-name>versions -
将您的发现记录在类似以下的表中;我们将讨论如何确定您的目标版本:
| 包 | 当前 | 最新 | 目标 |
|---|---|---|---|
| @angular/core | 5.0.0 | 5.1.0 | 5.0.0 |
| @angular/animations | 5.0.0 | 5.1.0 | 5.0.0 |
| @angular/cdk | 5.0.0-rc0 | 5.0.0 | 5.0.0 |
| @angular/flex-layout | 2.0.0-beta.10-4905443 | 2.0.0-rc.1 | 2.x.x |
| @angular/material | 5.0.0-rc0 | 5.0.0 | 5.0.0 |
| hammerjs | 2.0.8 | 2.0.8 | 2.x.x |
研究结果表明,发布了新的 Angular 小版本,这是有用的信息。在确定目标版本时,要保守。遵循以下指导:
-
在更新 Material 时不要更新 Angular 组件
-
如果您打算同时更新 Angular 组件,请分阶段进行,并确保在每个单独阶段之后执行测试
-
将任何 Beta 或 RC 软件包更新到其最新可用版本
-
当软件包的新版本可用时,保持在软件包的相同主要版本中
-
除非文档另有建议,否则遵循这些指南
更新 Angular Material
现在我们知道要升级到哪个版本,让我们继续进行:
- 执行以下命令以将 Material 及其相关组件更新到其目标版本:
$ npm install @angular/material@⁵.0.0 @angular/cdk@⁵.0.0 @angular/animations@⁵.0.0 @angular/flex-layout@².0.0-rc.1
-
验证您的
package.json以确保版本与预期版本匹配 -
解决任何 NPM 警告(详见第四章,与 Angular 更新保持最新,更新 Angular部分)
在这种特定情况下,我收到了无法满足的@angular/flex-layout包的对等依赖警告。在 GitHub 上进一步调查(github.com/angular/flex-layout/issues/508)显示这是一个已知问题,通常可以从 Beta 或 RC 包中预期到。这意味着可以安全地忽略这些警告。
升级完成后,请确保执行“后续更新清单”,详见第四章,与 Angular 更新保持最新。
摘要
在本章中,您了解了什么是 Angular Material,如何使用 Angular Flex 布局引擎,UI 库对性能的影响,以及如何将特定的 Angular Material 组件应用于您的应用程序。您意识到了过度优化 UI 设计的陷阱,以及如何向应用程序添加自定义主题。我们还讨论了如何保持 Angular Material 的最新状态。
在下一章中,我们将更新天气应用程序,以响应用户输入使用响应式表单,并保持我们的组件解耦,同时还使用BehaviorSubject在它们之间实现数据交换。在下一章之后,我们将完成天气应用程序,并将重点转移到构建更大的业务应用程序。
第六章:响应式表单和组件交互
到目前为止,您一直在努力组合构成 Angular 应用程序的基本元素,比如模块、组件、管道、服务、RxJS、单元测试、环境变量,甚至更进一步地学习如何使用 Docker 交付您的 Web 应用程序,并使用 Angular Material 使其看起来更加精致。
为了构建真正动态的应用程序,我们需要构建能够实现丰富用户交互并利用现代网络功能的功能,比如LocalStorage和GeoLocation。您还需要熟练掌握新的 Angular 语法,以有效地利用绑定、条件布局和重复元素。
您需要能够使用 Angular 表单来创建带有验证消息的输入字段,使用搜索即时输入功能创建引人入胜的搜索体验,为用户提供自定义其偏好的方式,并能够在本地和服务器上持久保存这些信息。您的应用程序可能会有多个共享数据的组件。
随着您的应用程序不断发展,并且有更多的人参与其中或者与同事交流您的想法,仅仅用手绘草图就变得越来越困难。这意味着我们需要一个更专业的模拟,最好是一个交互式的模拟,以最好地展示应用程序的计划用户体验。
在本章中,您将做以下事情:
- 了解这些:
-
双向绑定
-
模板驱动表单
-
熟练掌握组件之间的交互
-
能够创建这些:
-
交互式原型
-
使用 Angular 响应式表单进行输入字段和验证
交互式原型
外观确实很重要。无论您是在开发团队工作还是作为自由职业者,您的同事、老板或客户总是会更认真地对待一个精心准备的演示。在第二章中,创建本地天气 Web 应用程序,我提到了成为全栈开发人员的时间和信息管理挑战。我们必须选择一个可以在最少的工作量下取得最佳结果的工具。这通常意味着选择付费工具,但 UI/UX 设计工具很少是免费或便宜的。
原型工具将帮助您创建一个更好、更专业的应用程序模拟。无论您选择哪种工具,都应该支持您选择使用的 UI 框架,在这种情况下是 Material。
如果一张图片价值千言万语,那么你的应用的交互式原型价值千行代码。应用的交互式模型将帮助你在编写一行代码之前审查想法,并节省大量的代码编写。
MockFlow WireFramePro
我选择了 MockFlow WireFramePro,mockflow.com,作为一个易于使用、功能强大且在线支持 Material design UI 元素的工具,它允许你创建多个页面,然后将它们链接在一起,以创建一个工作应用程序的幻觉。
最重要的是,在发布时,MockFlow 允许永远免费使用一个完整功能集和功能。这将给你一个机会真正审查工具的有用性,而不受人为限制或者试用期的影响,试用期总是比你预期的要快得多。
Balsamiq 是更知名的线框工具。然而,balsamiq.com没有提供免费使用,但如果你正在寻找一个没有月费的工具,我强烈推荐 Balsamiq 的桌面应用 Mockups,它只需要一次购买费用。
构建模型
我们首先添加一个新任务来创建一个交互式原型,在任务结束时,我会将所有工件附加到这个任务上,这样它们就存储在 GitHub 上,所有团队成员都可以访问,也可以从 Wiki 页面链接进行持久性文档化。让我们将这个新任务拉到进行中的列,并查看来自 Waffle.io 的看板板的状态:
WireframePro 作为一个拖放设计界面非常直观,所以我不会详细介绍工具的工作原理,但我会强调一些技巧:
-
创建你的项目
-
选择一个组件包,可以是手绘 UI 或者 Material design
-
将每个屏幕作为一个新页面添加,如下所示:
MockFlow.com WireFrame Pro
我建议坚持手绘 UI 的外观和感觉,因为它能够为你的观众设定正确的期望。如果你在与客户的第一次会议上展示了一个非常高质量的模型,你的第一个演示将是一个低调的陈述。你最多只能满足期望,最坏的情况下,会让你的观众感到失望。
主屏幕
这是主屏幕的新模型:
LocalCast Weather Wireframe
您会注意到一些不同之处,比如应用工具栏与浏览器栏的混合以及重复元素的故意模糊。我做出这些选择是为了减少我需要在每个屏幕上花费的设计时间。我只是使用水平和垂直线对象来创建网格。
搜索结果
搜索屏幕同样故意保持模糊,以避免必须维护任何详细信息。令人惊讶的是,您的观众更有可能关注您的测试数据,而不是关注设计元素。
通过含糊不清,我们故意让观众的注意力集中在重要的事情上。以下是搜索屏幕的模拟:
LocalCast 天气搜索线框图
设置窗格
设置窗格是一个单独的屏幕,其中包含从主屏幕复制并应用了 85%不透明度的元素,以创建类似模型的体验。设置窗格本身只是一个带有黑色边框和纯白背景的矩形。
看一下以下的模拟:
LocalCast 天气设置线框图
添加交互性
能够点击模拟并了解导航工作流程的感觉是一个无法或缺的工具,可以获得早期用户反馈。这将为您和您的客户节省大量的沮丧、时间和金钱。
要将元素链接在一起,请按照以下步骤操作:
-
选择主屏幕上的可点击元素,如齿轮图标
-
在链接子标题下,点击选择页面
-
在弹出窗口中,选择设置
-
点击创建链接,如此截图所示:
WireFrame Pro - 添加链接
现在,当您点击齿轮图标时,工具将显示设置页面,这将在同一页面上创建侧边栏实际显示的效果。要返回主屏幕,您可以将齿轮图标和侧边栏外部的部分链接回该页面,以便用户可以来回导航。
导出功能原型
一旦您的原型完成,您可以将其导出为各种格式:
- 选择导出线框图按钮,如下所示:
WireFrame Pro - 导出线框图
- 现在选择您的文件格式,如下所示:
WireFrame Pro - 文件格式
我更喜欢 HTML 格式,因为它更灵活;然而,您的工作流程和需求会有所不同。
-
如果您选择了 HTML,您将获得一个 ZIP 捆绑包的所有资产。
-
解压捆绑包并使用浏览器导航到它;您应该会得到您线框的交互版本,如图所示:
WireFrame Pro - 交互式线框交互元素在以下截图中以黄色突出显示。您可以使用屏幕左下角的“显示链接”选项启用或禁用此行为。
您甚至可以使用minimal-nginx-server或minimal-node-server对原型 HTML 项目进行容器化,并使用相同的技术在 Zeit Now 上进行托管,这与第三章中讨论的准备 Angular 应用程序进行生产发布的技术完全相同。
现在将所有资产添加到 GitHub 问题的评论中,包括 ZIP 捆绑包,我们准备继续下一个任务。让我们将“添加城市搜索卡…”移动到“进行中”,如我们看板中所示:
Waffle.io 看板
使用用户输入进行搜索
现在,我们将在应用程序的主屏幕上实现搜索栏。用户故事中指出显示当前位置的天气预报信息,这可能意味着具有地理位置功能。然而,正如您可能注意到的,地理位置被列为一个单独的任务。挑战在于,使用原生平台功能如地理位置,您永远无法保证获得实际的位置信息。这可能是由于移动设备的信号丢失问题,或者用户可能拒绝分享他们的位置信息。
首先,我们必须提供良好的基线用户体验,并实现增值功能,如地理位置功能。我们将实现搜索即时输入功能,同时向用户提供反馈,如果服务无法检索到预期的数据。
最初,实现类型搜索机制可能是直观的;然而,OpenWeatherMapAPI 并没有提供这样的端点。相反,它们提供昂贵且在兆字节范围内的大量数据下载。
我们需要实现自己的应用服务器来公开这样一个端点,以便我们的应用可以有效地查询,同时使用最少量的数据。
OpenWeatherMap 的免费端点确实带来了一个有趣的挑战,其中两位数的国家代码可能会伴随城市名称或邮政编码以获得最准确的结果。这是一个很好的机会,可以为用户实现反馈机制,如果对于给定的查询返回了多个结果。
我们希望应用程序的每次迭代都是一个潜在的可发布的增量,并且避免在任何给定时间做太多事情。
我们将执行以下操作:
-
添加 Angular 表单控件
-
使用 Angular Material Input,如在
material.angular.io/components/input中记录的那样。 -
将搜索栏创建为其自己的组件
-
扩展现有的端点以接受邮政编码,并使国家代码在
weather.service中变为可选项 -
节流请求
添加 Angular 响应式表单
您可能会想为什么我们要添加 Angular 表单,因为我们只添加了一个单个输入字段,而不是具有多个输入的表单。作为一个一般的经验法则,任何时候您添加任何输入字段,它都应该包装在<form>标签中。Forms模块包含FormControl,它使您能够编写支持输入字段背后的后备代码,以响应用户输入,并根据需要提供适当的数据、验证或响应消息。
Angular 中有两种类型的表单:
-
模板驱动: 这些表单类似于您可能熟悉的 AngularJS 中的表单,其中表单逻辑主要在 HTML 模板中。我个人不喜欢这种方法,因为很难测试这些行为,而且庞大的 HTML 模板很快就难以维护。
-
响应式: 响应式表单的行为由控制器中编写的 TypeScript 代码驱动。这意味着您的验证逻辑可以进行单元测试,并且更好的是可以在整个应用程序中重复使用。在
angular.io/guide/reactive-forms中了解更多关于响应式表单的信息。
让我们首先将ReactiveFormsModule导入到我们的应用程序中:
src/app/app.module.ts
...
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
...
@NgModule({
...
imports: [
...
FormsModule,
ReactiveFormsModule,
...
响应式表单是使 Angular Material 团队能够编写更丰富的工具的核心技术,例如可以根据将来的 TypeScript 接口自动生成输入表单的工具。
添加和验证组件
我们将使用 Material 表单和输入模块创建一个citySearch组件:
- 将
MatFormFieldModule和MatInputModule添加到material.module中,以便在应用程序中可用:
src/app/material.module.ts
import {
...
MatFormFieldModule,
MatInputModule,
} from '@angular/material'
...
@NgModule({
imports: [
...
MatFormFieldModule,
MatInputModule,
],
exports: [
...
MatFormFieldModule,
MatInputModule,
],
})
我们正在添加MatFormFieldModule,因为每个输入字段都应该包装在<mat-form-field>标签中,以充分利用 Angular Material 的功能。在高层次上,<form>封装了键盘、屏幕阅读器和浏览器扩展用户的许多默认行为;<mat-form-field>实现了简单的双向数据绑定,这种技术应该适度使用,并且还允许优雅的标签、验证和错误消息显示。
- 创建新的
citySearch组件:
$ npx ng g c citySearch --module=app.module
由于我们添加了material.module.ts文件,ng无法猜测应将城市搜索功能模块添加到哪里,导致出现错误,例如More than one module matches。因此,我们需要使用--module选项提供要将citySearch添加到的模块。使用--skip-import选项跳过将组件导入到任何模块中。
- 创建一个基本模板:
src/app/city-search/city-search.component.html
<form>
<mat-form-field>
<mat-icon matPrefix>search</mat-icon>
<input matInput placeholder="Enter city or zip" aria-label="City or Zip" [formControl]="search">
</mat-form-field>
</form>
- 导入并实例化
FormControl的实例:
src/app/city-search/city-search.component.ts
import { FormControl } from '@angular/forms'
...
export class CitySearchComponent implements OnInit {
search = new FormControl()
...
响应式表单有三个级别的控件:
-
FormControl是与输入字段具有一对一关系的最基本元素 -
FormArray表示重复的输入字段,表示对象的集合 -
FormGroup用于将单独的FormControl或FormArray对象注册为您向表单添加更多输入字段时
最后,FormBuilder对象用于更轻松地编排和维护FormGroup的操作,这将在第十章中进行介绍,Angular 应用设计和示例。
- 在包含
app-current-weather的外部行的标题之间,在app.component中添加app-city-search:
src/app/app.component.ts
...
</div>
<div fxLayoutAlign="center">
<app-city-search></app-city-search>
</div>
<div fxLayout="row">
...
- 通过在浏览器中查看应用程序来测试组件的集成,如下所示:
带有搜索字段的 LocalWeather 应用
如果没有错误,现在我们可以开始添加FormControl元素并将它们连接到搜索端点。
向天气服务添加搜索
到目前为止,我们一直在通过名称和国家代码传递参数来获取城市的天气。通过允许用户输入邮政编码,我们必须使我们的服务更灵活,以接受两种类型的输入。
OpenWeatherMap 的 API 接受 URI 参数,因此我们可以使用 TypeScript 联合类型重构现有的getCurrentWeather函数,并使用类型守卫,我们可以提供不同的参数,同时保持类型检查:
- 重构
weather.service中的getCurrentWeather函数以处理邮政编码和城市输入:
app/src/weather/weather.service.ts
getCurrentWeather(
search: string | number,
country?: string
): Observable<ICurrentWeather> {
let uriParams = ''
if (typeof search === 'string') {
uriParams = `q=${search}`
} else {
uriParams = `zip=${search}`
}
if (country) {
uriParams = `${uriParams},${country}`
}
return this.getCurrentWeatherHelper(uriParams)
}
我们将城市参数重命名为search,因为它可以是城市名称或邮政编码。然后,我们允许其类型为string或number,并根据运行时的类型,我们将使用q或zip。如果存在,我们还将country设置为可选,并仅在查询中追加它。
getCurrentWeather现在嵌入了业务逻辑,因此是单元测试的良好目标。遵循单一职责原则,从 SOLID 原则中,我们将 HTTP 调用重构为自己的函数,称为getCurrentWeatherHelper。
- 将 HTTP 调用重构为
getCurrentWeatherHelper。
在下一个示例中,请注意使用反引号字符`而不是单引号字符',它利用了允许在JavaScript中嵌入表达式的模板文字功能:
src/app/weather/weather.service.ts
private getCurrentWeatherHelper(uriParams: string): Observable<ICurrentWeather> {
return this.httpClient
.get<ICurrentWeatherData>(
`${environment.baseUrl}api.openweathermap.org/data/2.5/weather?` +
`${uriParams}&appid=${environment.appId}`
)
.pipe(map(data => this.transformToICurrentWeather(data)))
}
作为积极的副作用,getCurrentWeatherHelper 遵循了开闭原则,因为我们可以通过提供不同的uriParams 来改变函数的行为,所以它对扩展是开放的,并且对修改是封闭的,因为它不需要经常被修改。
为了证明后一点,让我们实现一个新的函数,根据纬度和经度获取当前天气。
- 实现
getCurrentWeatherByCoords:
src/app/weather/weather.service.ts
getCurrentWeatherByCoords(coords: Coordinates): Observable<ICurrentWeather> {
const uriParams = `lat=${coords.latitude}&lon=${coords.longitude}`
return this.getCurrentWeatherHelper(uriParams)
}
如你所见,getCurrentWeatherHelper 可以在不做任何修改的情况下容易地进行扩展。
- 确保您更新
IWeatherService和之前所做的更改保持一致。
作为遵循 SOLID 设计原则的结果,我们更容易地对流控制逻辑进行鲁棒的单元测试,最终编写出更具韧性、更便宜维护的代码。
实现搜索:
现在,让我们将新的服务方法与输入字段连接起来:
- 更新
citySearch以注入weatherService并订阅输入更改:
src/app/city-search/city-search.component.ts
...
export class CitySearchComponent implements OnInit {
search = new FormControl()
constructor(private weatherService: WeatherService) {}
...
ngOnInit() {
this.search.valueChanges
.subscribe(...)
}
在此时,我们将所有输入都视为string。用户输入可以是城市、邮政编码,或用逗号分隔的城市和国家代码,或邮政编码和国家代码。而城市或邮政编码是必需的,国家代码是可选的。我们可以使用String.split函数来解析任何可能的逗号分隔输入,然后使用String.trim去除字符串的开头和结尾的任何空格。然后,我们通过遍历它们并使用Array.map来确保我们去除字符串的所有部分。
然后,我们使用三元运算符?:来处理可选参数,只有在存在值时才传递一个值,否则将其保留为未定义。
- 实现搜索处理程序:
src/app/city-search/city-search.component.ts
this.search.valueChanges
.subscribe((searchValue: string) => {
if (searchValue) {
const userInput = searchValue.split(',').map(s => s.trim())
this.weatherService.getCurrentWeather(
userInput[0],
userInput.length > 1 ? userInput[1] : undefined
).subscribe(data => (console.log(data)))
}
})
- 为用户添加有关可选国家功能的提示:
src/app/city-search/city-search.component.html
...
<mat-form-field>
...
<mat-hint>Specify country code like 'Paris, US'</mat-hint>
</mat-form-field>
...
在这一点上,订阅处理程序将调用服务器并将输出记录到控制台。
观察在 Chrome Dev Tools 中如何工作。注意search函数运行的频率以及我们未处理服务错误的情况。
使用节流/防抖限制用户输入:
如此,我们在每次按键输入时都向服务器发送请求。这不是期望的行为,因为它会导致糟糕的用户体验,耗尽电池寿命,造成浪费的网络请求,并在客户端和服务器端都引起性能问题。用户可能会打错字;他们可能会改变主意,然后很少有输入的前几个字符会产生有用的结果。
我们仍然可以监听每个按键输入,但不必对每个按键输入做出反应。通过利用节流/防抖,我们可以限制生成的事件数量到一个预定的时间间隔,并依然保持输入时搜索的功能。
请注意,throttle和debounce不是功能等效的,它们的行为会因框架而异。除了节流,我们希望捕获用户输入的最后一次输入。在lodash框架中,throttle 函数可以实现此需求,而在RxJS中,debounce 可以实现。请注意,此差异可能在将来的框架更新中得到修复。
可以很容易地使用RxJS/debounceTime将节流注入到可观察流中。
使用pipe实现debounceTime:
src/app/city-search/city-search.component.ts
import { debounceTime } from 'rxjs/operators'
this.search.valueChanges
.pipe(debounceTime(1000))
.subscribe(...)
debounceTime最多每秒运行一次搜索,但在用户停止输入后也会运行最后一次搜索。相比之下,RxJS/throttleTime每秒只会运行一次搜索,并不一定捕获用户输入的最后几个字符。
RxJS 还具有throttle和debounce函数,您可以使用它们来实现自定义逻辑以限制不一定是基于时间的输入。
由于这是一个时间和事件驱动的功能,不可行进行断点调试。您可以在 Chrome Dev Tools | Network 选项卡中监视网络调用,但要获得有关搜索处理程序实际被调用的次数的更实时感觉,请添加一个console.log语句。
在代码中使用活动的console.log语句并不是一个好的实践。正如第三章为生产发布准备 Angular 应用中介绍的,console.log是一种低级的调试方法。这些语句使得很难阅读实际代码,这本身就具有很高的可维护性成本。所以,无论它们是被注释掉还是不是,都不要在代码中使用console.log语句。
实现输入验证和错误消息
FormControl是高度可定制的。它允许您设置默认初始值,添加验证器,或在模糊、更改和提交事件上监听更改,如下所示:
example
new FormControl('Bethesda', { updateOn: 'submit' })
我们不会用一个值来初始化FormControl,但我们需要实现一个验证器来禁止一个字符的输入:
- 从
@angular/forms导入Validators:
src/app/city-search/city-search.component.ts
import { FormControl, Validators } from '@angular/forms'
- 修改
FormControl以添加最小长度验证器:
src/app/city-search/city-search.component.ts
search = new FormControl('', [Validators.minLength(2)])
- 修改模板以显示验证错误消息:
src/app/city-search/city-search.component.html
...
<form style="margin-bottom: 32px">
<mat-form-field>
...
<mat-error *ngIf="search.invalid">
Type more than one character to search
</mat-error>
</mat-form-field>
</form>
...
请注意增加一些额外的间距以为长度较长的错误消息腾出空间。
如果您处理不同类型的错误,模板中的hasError语法可能会变得重复。您可能希望实现一个更可扩展的解决方案,可以通过代码进行自定义,如下所示:
example
<mat-error *ngIf="search.invalid">{{getErrorMessage()}}</mat-error>
getErrorMessage() {
return this.search.hasError('minLength') ? 'Type more than one character to search' : '';
}
- 修改
search函数以不使用无效输入执行搜索:
src/app/city-search/city-search.component.ts
this.search.valueChanges.pipe(debounceTime(1000)).subscribe((searchValue: string) => {
if (!this.search.invalid) {
...
不仅仅是简单检查searchValue是否已定义且不是空字符串,我们可以通过调用this.search.invalid来利用验证引擎进行更健壮的检查。
通过双向绑定实现模板驱动的表单
与响应式表单相对应的是模板驱动的表单。如果您熟悉 AngularJS 中的ng-model,您会发现新的ngModel指令是其 API 兼容的替代品。
在幕后,ngModel实现了一个自动将自身附加到FormGroup的FormControl。ngModel可以在<form>级别或单个<input>级别使用。您可以在angular.io/api/forms/N…上了解更多关于ngModel的信息。
在本地天气应用中,我在app.component.ts中包含了一个名为app-city-search-tpldriven的组件的注释。您可以取消app.component中的注释以进行实验。让我们看看替代模板实现是什么样的:
src/app/city-search-tpldriven/city-search-tpldriven.component.html
...
<input matInput placeholder="Enter city or zip" aria-label="City or Zip"
[(ngModel)]="model.search" (ngModelChange)="doSearch($event)"
minlength="2" name="search" #search="ngModel">
...
<mat-error *ngIf="search.invalid">
Type more than one character to search
</mat-error>
...
注意ngModel与[()]的“香蕉箱”双向绑定语法的使用。
组件中的差异实现如下:
src/app/city-search-tpldriven/city-search-tpldriven.component.ts
import { NgModel, Validators} from '@angular/forms'
...
export class CitySearchTpldrivenComponent implements OnInit {
model = {
search: '',
}
...
doSearch(searchValue) {
const userInput = searchValue.split(',').map(s => s.trim())
this.weatherService
.getCurrentWeather(userInput[0], userInput.length > 1 ? userInput[1] : undefined)
.subscribe(data => console.log(data))
}
正如你所看到的,大部分逻辑是在模板中实现的,程序员需要保持对模板中的内容和控制器的活跃心智模型,并在两个文件之间来回切换,以对事件处理程序和验证逻辑进行更改。
此外,我们丢失了输入限制以及在输入无效状态时阻止服务调用的能力。当然,仍然可以实现这些功能,但它们需要繁琐的解决方案,而且并不完全适合新的 Angular 语法和概念。
启用组件交互
为了更新当前天气信息,我们需要city-search组件与current-weather组件进行交互。在 Angular 中,有四种主要的技术来实现组件之间的交互:
-
全局事件
-
父组件监听从子组件冒泡上来的信息
-
在模块内部工作的同级、父级或子级的组件,它们基于类似的数据流
-
父组件向子组件传递信息
全局事件
这是从编程早期开始就一直被利用的技术。在 JavaScript 中,你可能通过全局函数委托或 jQuery 的事件系统来实现这一点。在 AngularJS 中,你可能创建了一个服务并在其中存储值。
在 Angular 中,你仍然可以创建一个根级别的服务,在其中存储值,使用 Angular 的EventEmitter类(实际上是为指令而设计的),或使用rxjs/Subscription来为自己创建一个复杂的消息总线。
作为模式,全局事件容易被滥用,而不是帮助维护一个解耦的应用架构,随着时间的推移,它会导致全局状态。全局状态甚至是在控制器级别的本地状态,函数读取和写入任何给定类的变量,都是编写可维护和可单元测试软件的头号敌人。
最终,如果你将所有应用程序数据存储或者路由所有事件都在一个服务中以启用组件交互,那么你只是在发明一个更好的捕鼠夹。这是一种应该尽量避免的反模式。在后面的章节中,您将发现本质上我们仍然会使用服务来实现组件间的交互;然而,我想指出的是在灵活的架构和全局或集中式解耦方法之间存在一个细微的界限,后者无法很好地扩展。
使用事件发射器的子父关系
你的子组件应该完全不知道它的父组件。这是创建可重用组件的关键。
我们可以使用 app 组件作为父元素,实现城市搜索组件和当前天气组件之间的通信,让 app 模块控制器来协调数据。
让我们看看这个实现会是怎样的:
city-search组件通过@Output属性公开了一个EventEmitter:
src/app/city-search/city-search.component.ts
import { Component, Output, EventEmitter } from '@angular/core'
export class CitySearchComponent implements OnInit {
...
@Output() searchEvent = new EventEmitter<string>()
...
this.search.valueChanges.debounceTime(1000).subscribe((searchValue: string) => {
if (!this.search.invalid) {
this.searchEvent.emit(this.searchValue)
}
})
...
}
app组件使用该信息,并调用weatherService,设置currentWeather变量:
src/app/app.component.ts
template: `
...
<app-city-search (searchEvent)="doSearch($event)"></app-city-search>
...
`
export class AppComponent {
currentWeather: ICurrenWeather
constructor() { }
doSearch(searchValue) {
const userInput = searchValue.split(',').map(s => s.trim())
this.weatherService
.getCurrentWeather(userInput[0], userInput.length > 1 ? userInput[1] : undefined)
.subscribe(data => this.currentWeather = data)
}
}
我们已经成功地向上传递了信息,现在我们必须能够将它传递给 current-weather 组件。
使用输入绑定的父子关系
按照定义,父组件将意识到它正在使用哪些子组件。由于 currentWeather 属性与 current-weather 组件上的 current 属性绑定,结果传递下来并显示。这是通过创建一个 @Input 属性来实现的:
src/app/current-weather/current-weather.component.ts
import { Component, Input } from '@angular/core'
...
export class CurrentWeatherComponent implements OnInit {
@Input() current: ICurrentWeather
...
}
然后你可以更新 app 组件,将数据绑定到 current 天气上:
src/app/app.component.ts
template: `
...
<app-current-weather [current]="currentWeather"></app-current-weather>
...
`
这种方式可能适用于创建耦合度较高的组件或用户控件,且不需要消耗外部数据的情况。一个很好的例子就是向 current-weather 组件添加预测信息,如下所示:
天气预报线框图
每周的每一天都可以作为一个组件来实现,使用 *ngFor 进行重复,并且将这些信息合理地绑定到 current-weather 的子组件上是非常合理的:
example
<app-mini-forecast *ngFor="let dailyForecast of forecastArray
[forecast]="dailyForecast">
</app-mini-forecast>
通常,如果你在使用数据驱动的组件,父子或者子父通信模式将导致架构不够灵活,使得组件的重用或重新排列变得非常困难。考虑到不断变化的业务需求和设计,这是一个重要的教训需要牢记。
使用主题进行兄弟交互
组件互动的主要原因是发送或接收用户提供或从服务器接收的数据更新。在 Angular 中,你的服务公开 RxJS.Observable 端点,这些是数据流,你的组件可以订阅它们。RxJS.Observer 补充了 RxJS.Observable 作为 Observable 发出的事件的消费者。RxJS.Subject 将这两套功能合并到一个易于使用的对象中。您可以使用主题来描述属于特定数据集的流,比如正在显示的当前天气数据:
src/app/weather/weather.service.ts
import { Subject } from 'rxjs'
...
export class WeatherService implements IWeatherService {
currentWeather: Subject<ICurrentWeather>
...
}
currentWeather 仍然是一个数据流,并不仅仅代表一个数据点。你可以通过订阅来订阅 currentWeather 数据的变化,或者可以按照以下方式发布对它的更改:
example
currentWeather.subscribe(data => (this.current = data))
currentWeather.next(newData)
Subject 的默认行为非常类似于通用的发布-订阅机制,比如 jQuery 事件。但是,在组件以不可预知的方式加载或卸载的异步世界中,使用默认的 Subject 并不是很有用。
有三种不同类型的 Subject:
-
ReplaySubject: 它将记住和缓存数据流中发生的所有数据点,以便订阅者可以在任何给定时间重放所有事件 -
BehaviorSubject: 它只记住最后一个数据点,同时继续监听新的数据点 -
AsyncSubject: 这是一次性事件,不希望再次发生
ReplaySubject 可能会对您的应用程序造成严重的内存和性能影响,所以应该谨慎使用。在 current-weather 的情况下,我们只对显示最新收到的天气数据感兴趣,但通过用户输入或其他事件,我们可以接收新数据,因此我们可以保持 current-weather 组件最新。 BehaviorSubject 将是满足这些需求的合适机制:
- 在
weatherService中定义BehaviorSubject并设置默认值:
app/src/weather/weather.service.ts
import { BehaviorSubject } from 'rxjs'
...
export class WeatherService implements IWeatherService {
currentWeather = new BehaviorSubject<ICurrentWeather>({
city: '--',
country: '--',
date: Date.now(),
image: '',
temperature: 0,
description: '',
})
...
}
- 将
current-weather组件更新为订阅新的BehaviorSubject:
app/src/current-weather/current-weather.component.ts
...
ngOnInit() {
this.weatherService.currentWeather.subscribe(data => (this.current = data))
}
...
- 将
city-search组件更新为发布其接收到的数据到BehaviorSubject:
app/src/city-search/city-search.component.ts
...
this.weatherService
.getCurrentWeather(
userInput[0],
userInput.length > 1 ? userInput[1] : undefined
)
.subscribe(data => this.weatherService.currentWeather.next(data))
...
- 在浏览器中测试您的应用程序;它应该如下所示:
土耳其布尔萨的天气信息
当您输入一个新的城市时,组件应该更新为该城市的当前天气信息。
在应用程序首次加载时,默认体验看起来有些问题。至少有两种不同的处理方式。首先是在app组件级别隐藏整个组件,如果没有数据显示。为了使其工作,我们将不得不将weatherService注入到app组件中,最终导致不太灵活的解决方案。另一种方法是能够更好地处理current-weather组件中缺少的数据。
为了使应用程序更好,您可以在应用程序启动时实现地理位置功能,以获取用户当前位置的天气。您还可以利用window.localStorage来存储上次显示的城市或从window.geolocation在初始启动时检索的上次位置。
在继续之前,不要忘记执行npm test和npm run e2e。读者可以自行修复单元测试和端到端测试。
摘要
这一章完成了我们对本地天气应用程序的工作。我们可以将城市搜索功能任务移动到完成列,如我们看板中所示:
Waffle.io 看板状态
在本章中,您学会了如何创建一个交互式原型,而不需要编写一行代码。然后,您使用MatInput、验证器、响应式表单和数据流驱动处理程序创建了一个搜索即时响应的功能。您还了解了不同的策略来实现组件间的交互和数据共享。最后,您了解了双向绑定和基于模板的表单。
LocalCast Weather 是一个简单的应用程序,我们用它来介绍 Angular 的基本概念。正如您所见,Angular 非常适合构建这样的小型和动态应用程序,同时向最终用户提供最少量的框架代码。您应该考虑利用 Angular 甚至用于快速而简单的项目,这在构建更大型的应用程序时也是一个很好的实践。在下一章中,您将使用路由器优先的方法来创建一个更复杂的业务线(LOB)应用程序,设计和构建可扩展的 Angular 应用程序,其中包括一流的身份验证和授权、用户体验以及涵盖大多数 LOB 应用程序需求的众多技巧。