Angular-企业就绪的-Web-应用-七-

70 阅读1小时+

Angular 企业就绪的 Web 应用(七)

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

译者:飞龙

协议:CC BY-NC-SA 4.0

第十三章:AWS 上的高可用云基础设施

互联网是一个充满敌意的环境。有好的和坏的参与者。坏参与者可能会试图在您的安全中找到漏洞,或者试图通过 分布式拒绝服务DDoS)攻击使您的网站崩溃。如果您幸运的话,好的参与者会喜欢您的网站,并且不会停止使用它。他们会对您的网站提出改进建议,但他们也可能遇到错误,并且由于高流量,您的网站可能会因为过于热情而变得缓慢。在互联网上的实际部署需要大量的专业知识才能正确完成。作为一名全栈开发者,您只能了解硬件、软件和网络的一些细微差别。幸运的是,随着云服务提供商的出现,许多这种专业知识已经转化为软件配置,而复杂的硬件和网络问题则由提供商处理。

云服务提供商的最佳特性之一是云的可扩展性,这指的是您的服务器会自动扩展以应对意外的流量高峰,并在流量恢复正常水平时缩减规模以节省成本。亚马逊网络服务AWS)不仅超越了基本的云可扩展性,还引入了高可用性和容错概念,允许实现弹性本地和全球部署。我选择向您介绍 AWS,是因为其庞大的功能,这些功能远远超出了我在本书中将要涉及的内容。使用 Route 53,您可以获得免费的 DDoS 保护;使用 API Gateway,您可以创建 API 密钥;使用 AWS Lambda,您每月只需花费几美元就可以处理数百万笔交易;使用 CloudFront,您可以在世界各大城市的秘密边缘位置缓存您的内容。此外,蓝绿部署允许您实现软件的无中断部署。

总体而言,您将在本章中学到的工具和技术适用于任何云服务提供商,并且正迅速成为任何全栈开发者的关键知识。我们将讨论以下主题:

  • 创建和保护 AWS 账户

  • 合理配置基础设施,包括简单的负载测试以优化实例

  • 配置和部署到 AWS

    • ECS Fargate 脚本化蓝绿部署

    • 账单

本书示例代码的最新版本可在以下 GitHub 仓库链接找到。该仓库包含代码的最终和完整状态。每个部分都包含信息框,以帮助您找到 GitHub 上正确的文件名或分支,以便您可以使用它来验证您的进度。

第十三章 的示例代码移除了之前章节中所有可选和替代实现,并且仅启用与 lemon-mart-server 的身份验证。这样,读者可以参考 lemon-mart 项目的干净实现。

基于 lemon-mart第十三章 示例:

  1. 克隆github.com/duluca/lemon-mart上的仓库。

  2. 使用 config.docker-integration.yml 验证您的 config.yml 实现。

  3. 在根目录上执行 npm install 以安装依赖项。

  4. 要运行 CircleCI Docker 集成配置,请执行 git checkout deploy_aws。请参阅github.com/duluca/lemon-mart/pull/27的拉取请求。

  5. 本章的代码示例位于子文件夹:

    projects/ch13 
    
  6. 要运行本章的 Angular 应用程序,请执行:

    npx ng serve ch13 
    
  7. 要运行本章的 Angular 单元测试,请执行:

    npx ng test ch13 --watch=false 
    
  8. 要运行本章的 Angular e2e 测试,请执行:

    npx ng e2e ch13 
    
  9. 要为本章构建一个生产就绪的 Angular 应用程序,请执行:

    npx ng build ch13 --prod 
    

注意,存储库根目录下的 dist/ch13 文件夹将包含编译结果。

请注意,书中或 GitHub 上的源代码可能并不总是与 Angular CLI 生成的代码相匹配。由于生态系统不断演变,书中代码与 GitHub 上的代码在实现上也可能存在细微差异。随着时间的推移,示例代码发生变化是自然的。在 GitHub 上,您可能会找到更正、修复以支持库的新版本,或者观察多种技术并行的实现。您只需实现书中推荐的理想解决方案即可。如果您发现错误或有疑问,请创建问题或提交 GitHub 上的拉取请求,以惠及所有读者。

您可以在附录 C中了解更多关于更新 Angular 的信息,保持 Angular 和工具始终如一。您可以从static.packt-cdn.com/downloads/9781838648800_Appendix_C_Keeping_Angular_and_Tools_Evergreen.pdfexpertlysimple.io/stay-evergreen在线找到此附录。

AWS 是一个非常流行的服务,AWS 账户更是黑客攻击的热门目标。让我们从创建一个安全的 AWS 账户开始。

创建安全的 AWS 账户

账户访问和控制在任何云服务中都是至关重要的,包括 AWS。在初始账户创建后,您将拥有您的根凭据,即您的电子邮件和密码组合。

让我们从创建 AWS 账户开始:

  1. 首先导航到console.aws.amazon.com

  2. 如果您还没有,请创建一个新账户。

  3. 如果你刚接触 AWS,你可以在注册页面这里获得各种服务的 12 个月免费层访问权限,如图所示:

    图 13.1:AWS 账户注册

    您的 AWS 账单与您的根凭据相关联。如果遭到破坏,在您恢复访问之前,您的账户可能会遭受大量损失。

  4. 确保您已在您的根凭据上启用了双因素认证。

    为了增加另一层安全性,从现在开始,您需要停止使用根凭证登录您的 AWS 账户。您可以使用 AWS 身份和访问管理IAM)模块创建用户账户。如果这些账户被泄露,与您的根账户不同,您可以轻松快速地删除或替换它们。

  5. 导航到 IAM 模块

  6. 创建一个新的具有全局管理员权限的用户账户。

  7. 使用这些凭证登录 AWS 控制台。

  8. 您还应该为这些凭证启用双因素认证。

  9. 一个安全的账户设置如下所示,每个状态都报告为绿色:img/B14091_13_02.png

    图 13.2:安全设置后的 AWS IAM 模块

与用户账户合作的主要好处是程序性访问。对于每个用户账户,您都可以创建一个公共访问 ID 和私有访问密钥对。当您与第三方合作,例如托管持续集成服务、自己的应用程序代码或 CLI 工具时,您使用程序性访问密钥来连接到您的 AWS 资源。当不可避免地访问密钥泄露时,禁用旧密钥并创建新密钥既快速又方便。

此外,用户账户访问可以通过非常细粒度的权限进行严格控制。您还可以创建具有一组权限的角色,并进一步控制 AWS 服务与某些外部服务之间的通信。

在创建用户账户和角色时,始终采取最小权限原则。当与不熟悉 AWS 的客户、承包商或同事合作时,这可能是一项令人沮丧的练习;然而,这是一项值得做的练习。

您的安全性和可靠性取决于您最薄弱的环节,因此您必须计划失败,并且最重要的是,定期练习恢复计划。

保护机密

密码和私钥泄露的发生频率可能比你想象的要高。您的密钥可能在未加密的公共 Wi-Fi 网络上被泄露;您可能不小心将它们检查到代码仓库中,或者使用像电子邮件这样极其不安全的通信方式。

然而,意外代码检查入是最大的问题,因为大多数初级开发者没有意识到在源代码控制系统中删除不是一种选择。

作为开发者,有一些值得注意的最佳实践需要遵循,以保护您的机密:

  1. 在公共 Wi-Fi 上始终使用 VPN 服务,例如tunnelbear.com

  2. 利用位于您用户主目录下的 .aws/credentials 文件来创建配置文件并存储访问密钥。

  3. 作为团队规范,在项目的根目录下创建一个.env文件,并将其添加到.gitignore中,以存储 CI 服务器可能后来注入的任何机密。

  4. 在推送之前始终审查提交。

  5. 考虑注册一个可以监控您的代码库以查找机密的服务的账户,例如 GitGurdian 在gitguardian.com/,这对于开源项目是免费的。

注意,GitGuardian 将 Firebase 和 OpenWeather API 密钥标记为泄露。这是一个误报,因为所涉及的密钥是公钥,必须发布以使你的应用程序正确运行。

每次都遵循这些惯例将使你养成一个良好的习惯,即永远不要将秘密检查到代码库中。在下一节中,我们将深入探讨云环境中的资源考虑因素。

适当规模的基础设施

优化你的基础设施的目的是在保护你公司收入的同时,最大限度地减少运营基础设施的成本。你的目标应该是确保用户不会遇到高延迟,否则称为不良性能,更糟糕的是,未满足或丢弃的请求,同时让你的企业成为一个可持续的事业。

网络应用程序性能的三个支柱如下:

  1. CPU 利用率

  2. 内存使用

  3. 网络带宽

我故意将磁盘访问排除在关键考虑指标之外,因为只有特定的工作负载在应用程序服务器或数据存储上执行时才会受到影响。只要应用程序资源由内容分发网络CDN)提供,磁盘访问很少会影响服务网络应用程序的性能。尽管如此,仍然要注意任何意外的失控磁盘访问,例如频繁创建临时和日志文件。例如,Docker 可能会输出日志,这些日志很容易填满驱动器。

在理想情况下,CPU、内存和网络带宽的使用应该均匀地利用,大约在可用容量的 60-80%左右。如果你遇到由于各种其他因素(如磁盘 I/O、缓慢的第三方服务或低效的代码)引起的性能问题,很可能会出现你的某个指标达到或接近最大容量,而其他两个则处于闲置或严重未充分利用的状态。这是一个机会,可以使用更多的 CPU、内存或带宽来补偿性能问题,并均匀利用可用资源。

目标是 60-80%的利用率背后的原因是,为了给一个新的实例(服务器或容器)的配置和准备就绪以服务用户留出一些时间。在你预定义的阈值被超过后,当一个新的实例正在配置时,你可以继续服务越来越多的用户,从而最小化未满足的请求。

在整本书中,我一直在劝阻过度工程化或完美解决方案。在当今复杂的 IT 环境中,几乎不可能预测你将在哪里遇到性能瓶颈。你的工程可能非常容易地花费超过$100,000 的工程时数,而你的问题的解决方案可能只需要几百美元的新硬件,无论是网络交换机、固态驱动器、CPU 还是更多的内存。

如果您的 CPU 过于繁忙,您可能想在代码中引入更多的账务逻辑,通过索引、哈希表或字典,您可以在内存中缓存它们以加快逻辑的后续或中间步骤。例如,如果您不断运行数组查找操作以定位记录的特定属性,您可以在该记录上执行操作,将记录的 ID 和/或属性保存到您在内存中保持的哈希表中,从而将您的运行时成本从 O(n) 降低到 O(1)

在前面的示例之后,您可能会使用过多的内存,特别是使用哈希表。在这种情况下,您可能希望更积极地卸载或转移缓存到较慢但更丰富的数据存储中,例如使用您的备用网络带宽的 Redis 实例。

如果您的网络利用率过高,您可能想调查使用带有过期链接的 CDN、客户端缓存、限制请求以及为滥用配额的客户设置 API 访问限制,或者优化您的实例,使其网络容量与其 CPU 或内存容量不成比例地更多。

优化实例

在前面的示例中,我展示了使用我的 duluca/minimal-node-web-server Docker 镜像来托管我们的 Angular 应用。尽管 Node.js 是一个非常轻量级的服务器,但它并不是为了仅仅作为 Web 服务器而优化的。此外,Node.js 具有单线程执行环境,这使得它不适合同时为许多并发用户提供服务静态内容。

您可以通过执行 docker stats 来观察 Docker 镜像正在使用的资源:

$ docker stats
CONTAINER ID  CPU %  MEM USAGE / LIMIT  MEM %  NET I/O  BLOCK I/O    PIDS
27d431e289c9  0.00%  1.797MiB / 1.9GiB  0.09%  13.7kB / 285kB  0B / 0B  2 

这里是 Node 和基于 NGINX 的服务器在空闲时系统资源利用率的比较结果:

服务器镜像大小内存使用
duluca/minimal-nginx-web-server16.8 MB1.8 MB
duluca/minimal-node-web-server71.8 MB37.0 MB

然而,空闲时的值只能讲述故事的一部分。为了更好地理解,我们必须进行简单的负载测试,以查看负载下的内存和 CPU 利用率。

简单负载测试

为了更好地理解我们服务器的性能特性,让我们对它们施加一些负载并对其进行压力测试:

  1. 使用 docker run 启动您的容器:

    $ docker run --name <imageName> -d -p 8080:<internal_port>
    <imageRepo> 
    

    如果您使用 npm 脚本来运行 Docker,请执行以下命令以启动您的容器:

    $ npm run docker:debug 
    
  2. 执行以下 bash 脚本来启动负载测试:

    $ curl -L http://bit.ly/load-test-bash | bash -s 100 "http://localhost:8080" 
    

    此脚本将每秒向服务器发送 100 个请求,直到您终止它。

  3. 执行 docker stats 来观察性能特性。

这里是 CPU 和内存利用率的总体观察:

CPU 利用率统计最大内存
duluca/minimal-nginx-web-server2%15%60%2.4 MB
duluca/minimal-node-web-server20%45%130%75 MB

如您所见,两个服务器在提供相同内容时存在显著的性能差异。请注意,这种基于每秒请求数的测试对于比较分析很有用,但不一定反映实际使用情况。

很明显,我们的 NGINX 服务器将为我们提供最佳性价比。有了最优解决方案,让我们在 AWS 上部署应用程序。

部署到 AWS ECS Fargate

AWS 弹性容器服务ECS)Fargate 是一种成本效益高且易于配置的云部署容器的方法。

ECS 由四个主要部分组成:

  1. 一个容器仓库,弹性容器注册库ECR),您可以在其中发布您的 Docker 镜像。

  2. 服务、任务和任务定义,其中您定义作为服务运行的容器作为任务定义的运行时参数和端口映射。

  3. 一个集群,一组 EC2 实例,其中可以部署和扩展任务。

  4. Fargate,一种管理集群服务,它抽象化了 EC2 实例、负载均衡器和安全组的问题。

在 AWS 控制台右上角,务必选择离您的用户最近的区域。对我来说,这是 us-east-1 区域。

我们的目标是创建一个高度可用的蓝绿部署,这意味着在服务器故障或甚至在部署期间,我们的应用程序至少有一个实例将处于运行状态。这些概念在第十四章Google Analytics 和高级云操作可扩展环境中的每用户成本部分中进行了详细探讨。

配置 ECS Fargate

您可以在 AWS服务菜单下访问 ECS 功能,选择弹性容器服务链接。

如果这是您第一次登录,您必须完成一个教程,其中您将被强制创建一个示例应用程序。我建议您完成教程,并在之后删除您的示例应用程序。为了删除服务,您需要将您服务的任务数量更新为 0。此外,删除默认集群以避免任何意外费用。

创建 Fargate 集群

让我们先配置一个 Fargate 集群,它作为配置其他 AWS 服务时的锚点。我们的集群最终将运行一个集群服务,我们将在接下来的章节中逐步构建。

AWS Fargate 是实施云中可扩展容器编排解决方案的一个很好的选择。近年来,Kubernetes 作为首选解决方案已经变得非常流行。Kubernetes 是 AWS ECS 的开源替代品,它为容器编排提供了更丰富的功能,适用于本地、云和云混合部署。虽然 AWS 确实提供了 Amazon Elastic Container Service for Kubernetes (Amazon EKS),但与纯 Kubernetes 相比,RedHat 的开源 OpenShift 平台更容易使用,并且自带电池(即无需额外配置)。

让我们创建集群:

  1. 导航到弹性容器服务

  2. 点击集群 | 创建集群

  3. 选择仅网络...由 AWS Fargate 提供模板。

  4. 点击下一步,您将看到创建集群步骤,如图所示:图片

    图 13.3:AWS ECS 创建集群

  5. 集群名称输入为fargate-cluster

  6. 创建一个VPC以隔离您的资源与其他 AWS 资源。

  7. 点击创建集群以完成设置。

您将看到您的操作摘要,如下所示:

图片

图 13.4:AWS ECS Fargate 集群

现在您已经在自己的虚拟专用云VPC)中创建了一个集群,您可以在弹性容器服务 | 集群下查看它。

创建容器仓库

接下来,我们需要设置一个仓库,我们可以在此处发布我们在本地或 CI 环境中构建的容器镜像:

本节假设您已根据第九章使用 Docker 的 DevOps中详细说明的设置 Docker 和 npm 脚本。您可以通过执行npm i -g mrm-task-npm-docker并使用npx mrm npm-docker应用这些脚本来获取这些脚本的最新版本。

  1. 导航到弹性容器服务

  2. 点击仓库 | 创建仓库

  3. 将仓库名称输入为lemon-mart

  4. 复制屏幕上生成的仓库 URI

  5. 将 URI 粘贴到您的应用程序的package.json文件中作为新的imageRepo变量:

    **package.json**
    ...
    "config": {
      "imageRepo": "000000000000.dkr.ecr.us-east-1.amazonaws.com/lemon-mart",
      ...
    } 
    
  6. 点击创建仓库

  7. 点击下一步,然后点击完成以完成设置。

在摘要屏幕上,您将获得有关如何使用 Docker 与您的仓库一起使用的进一步说明。在本章的后面部分,我们将介绍将为我们处理这些任务的脚本:

图片

图 13.5:AWS ECS 仓库

您可以在弹性容器服务 | 仓库下查看您的新仓库。

我们将在即将到来的AWS 的 npm 脚本部分中介绍如何发布您的镜像。

让我们继续设置 ECS。

创建任务定义

在我们的仓库中定义了容器目标后,我们可以定义一个任务定义,它包含运行我们的容器所需的所有元数据,例如端口映射、保留 CPU 和内存分配:

  1. 导航到弹性容器服务

  2. 点击任务定义 | 创建新的任务定义

  3. 选择Fargate启动类型兼容性。

  4. 任务定义名称输入为lemon-mart-task

  5. 选择任务角色为无(您可以在稍后添加一个以启用对其他 AWS 服务的访问)。

  6. 任务内存输入为0.5 GB

  7. 任务 CPU输入为0.25 CPU

  8. 点击添加容器

    • 容器名称输入为lemon-mart

    • 对于镜像,粘贴之前生成的镜像仓库 URI,但向其追加:latest标签,以便始终从仓库中拉取最新镜像,例如000000000000.dkr.ecr.us-east-1.amazonaws.com/lemon-mart:latest

    • 为 NGINX 设置软限制128 MB,或为 Node.js 设置256 MB

    • 端口映射下,指定容器端口为 NGINX 的80或 Node.js 的3000

  9. 接受剩余的默认设置。

  10. 点击添加;这是在创建之前您的任务定义将看起来如何:img/B14091_13_06.png

    图 13.6:AWS ECS 任务定义

  11. 点击创建以完成设置。

弹性容器服务 | 任务定义下查看您的新任务定义

注意,默认设置将启用 AWS CloudWatch 日志记录,这是一种您可以事后访问容器实例控制台日志的方式。在这个例子中,将创建一个名为/ecs/lemon-mart-task的 CloudWatch 日志组。

云监控 | 日志下查看您的新日志组。

如果您添加的容器需要持久化数据,任务定义允许您定义一个卷并将文件夹挂载到您的 Docker 容器中。我发布了一个关于如何配置 AWS 弹性文件系统(EFS)与您的 ECS 容器的指南,请参阅bit.ly/mount-aws-efs-ecs-container

创建一个弹性负载均衡器

在高可用性部署中,您希望运行两个容器实例,正如我们刚才创建的任务定义所定义的,跨越两个不同的可用区(AZs)。对于这种动态扩展和缩减,我们需要配置一个应用程序负载均衡器(ALB)来处理请求路由和排空:

  1. 在另一个选项卡上,导航到EC2 | 负载均衡器 | 创建负载均衡器

  2. 创建一个应用程序负载均衡器

  3. 名称设置为lemon-mart-alb

    为了在监听器下支持 SSL 流量,您可以在端口443上添加一个新的 HTTPS 监听器。通过 AWS 服务和向导,可以方便地实现 SSL 设置。在 ALB 配置过程中,AWS 提供了链接到这些向导的选项,以创建您的证书。然而,这是一个复杂的过程,并且可能因您现有的域名托管和 SSL 证书设置而有所不同。在这本书中,我将跳过与 SSL 相关的配置。您可以在我在bit.ly/setupAWSECSCluster发布的指南中找到与 SSL 相关的步骤。

  4. 可用区下,选择为您的fargate-cluster创建的VPC

  5. 选择列出的所有 AZs。

  6. 展开标签并添加一个键/值对,以便能够识别 ALB,例如"App": "LemonMart"

  7. 点击下一步:配置安全设置

    如果您添加了 HTTPS 监听器,您将看到配置证书的选项。

    如果配置证书,请点击从 ACM 选择证书(AWS 证书管理器)并选择默认 ELB 安全策略

    如果您从未创建过证书,请点击从 ACM 请求新的证书链接来创建一个。如果您之前创建过证书,请转到证书管理器创建一个新的。然后,刷新并选择您的证书。

  8. 点击下一步:配置安全组

  9. 创建一个新的集群特定安全组lemon-mart-sg,仅允许端口80入站或如果使用 HTTPS 则允许端口443入站。

    在下一节创建集群服务时,请确保此处创建的安全组是在服务创建期间选择的。否则,您的 ALB 将无法连接到您的实例。

  10. 点击下一步:配置路由

  11. 为新的目标组命名为lemon-mart-target-group

  12. 将协议类型从instance更改为ip

  13. 健康检查下,如果通过 HTTP 提供服务,请保留默认路由/

    健康检查对于扩展和部署操作至关重要。这是 AWS 可以用来检查实例是否成功创建的机制。

    如果部署 API 和/或重定向所有 HTTP 调用到 HTTPS,请确保您的应用程序定义了一个自定义路由,该路由不会被重定向到 HTTPS。在 HTTP 服务器上,GET /healthCheck返回一个简单的 200 消息,表示I'm healthy并验证这不会重定向到 HTTPS。否则,您将经历很多痛苦和苦难,试图找出问题所在,因为所有健康检查都会失败,部署也会莫名其妙地失败。duluca/minimal-node-web-server提供 HTTPS 重定向,以及一个开箱即用的 HTTP-only /healthCheck端点。使用duluca/minimal-nginx-web-server,您需要提供自己的配置。

  14. 点击下一步:注册目标

  15. 不要注册任何目标IP 范围。ECS Fargate 会神奇地为您管理这些。如果您自己这样做,您将配置一个半损坏的基础设施。

  16. 点击下一步:审查;您的 ALB 设置应类似于图中所示![img/B14091_13_07.png]

    图 13.7:AWS 应用程序负载均衡器设置

  17. 点击创建以完成设置。

在下一节创建集群服务时,您将使用lemon-mart-alb

创建集群服务

现在,我们将通过在集群中使用任务定义和创建的 ALB 来整合所有内容:

  1. 导航到弹性容器服务

  2. 点击集群 | fargate-cluster

  3. 服务选项卡下,点击创建

  4. 选择启动类型Fargate

  5. 选择您之前创建的任务定义。

    注意,任务定义是版本化的,例如lemon-mart-task:1。如果您要更改任务定义,AWS 将创建lemon-mart-task:2。您需要使用这个新版本更新服务,以便您的更改生效。

  6. 服务名称输入为lemon-mart-service

  7. 对于任务数量,选择2

  8. 对于最小健康百分比,选择50

  9. 对于最大百分比,选择200

    保持滚动更新部署类型,因为我们将实现自己的蓝绿部署策略。

  10. 点击下一步 步骤

    为了在部署期间实现高可用性,将最小健康百分比设置为100。Fargate 定价基于每秒的使用量,因此在部署应用程序时,您将为额外的实例支付额外费用,而旧的实例正在被取消配置。

  11. 配置网络下,选择与您之前集群相同的VPC

  12. 选择所有可用的子网;对于高可用性,至少应该有两个。

  13. 选择您在上一节中创建的安全组,命名为lemon-mart-sg。(如果您看不到它,请刷新页面。)

  14. 选择负载均衡器类型为应用程序负载均衡器

  15. 选择lemon-mart-alb选项。

  16. 通过点击添加到负载均衡器按钮,将容器端口(例如803000)添加到 ALB。

  17. 选择您已经定义的生产监听端口

  18. 选择您已经定义的目标组lemon-mart-target-group

  19. 取消选择启用服务发现集成

  20. 点击下一步

  21. 如果您希望实例在达到一定容量限制时自动扩展和缩小,请设置自动扩展

    我建议在服务初始设置期间跳过自动扩展的设置,以便更容易地调试任何潜在的配置问题。您可以在稍后回来设置它。自动任务扩展策略依赖于警报,例如 CPU 利用率。在第十四章Google Analytics 和高级云操作可扩展环境中的每用户成本部分,您可以了解如何计算您的最佳目标服务器利用率并根据这个数字设置警报。

  22. 点击下一步并审查您的更改,如图所示:

    图 13.8:AWS Fargate 集群服务设置

  23. 最后,点击创建服务以完成设置。

弹性容器服务 | 集群 | fargate-cluster | lemon-mart-service下观察您的新服务。在您将镜像发布到容器仓库之前,您的 AWS 服务将无法部署实例,因为健康检查将不断失败。发布镜像后,您需要确保在服务的事件标签页中没有错误。

AWS 是一个复杂的野兽,有了 Fargate,您可以避免很多复杂性。然而,如果您有兴趣使用自己的 EC2 实例设置自己的 ECS 集群,您可以通过 1-3 年的预留实例获得显著的折扣。我有一个 75+步的设置指南可供参考,链接为bit.ly/setupAWSECSCluster

我们手动执行了许多步骤来创建我们的集群。AWS CloudFormation 通过提供可定制的配置模板来解决这个问题,您可以根据自己的需求进行定制,或者从头开始编写自己的模板。如果您想认真对待 AWS,这种代码即基础设施的设置绝对是您应该采取的方式。

对于生产部署,确保您的配置由 CloudFormation 模板定义,这样它就可以在部署相关的错误发生时轻松重新部署。不是如果,而是当部署相关的错误发生时。

配置 DNS

要将域名或子域名连接到您的应用程序,您必须配置您的 DNS 以指向 ALB。AWS 提供 Route 53 服务来管理您的域名。

Route 53 使动态地将域名或子域名分配给 ALB 变得容易:

  1. 导航到Route 53 | 托管区域

  2. 如果您已经注册了域名,请选择它;否则,使用创建托管区域注册它。

    注意,您需要将域名的名称服务器重新分配到 AWS 以使此操作生效。

  3. 点击创建记录集

  4. 输入名称lemonmart

  5. 别名设置为yes

  6. 从负载均衡器列表中选择lemon-mart-alb

  7. 点击创建以完成设置:img/B14091_13_09.png

    图 13.9:Route 53 – 创建记录集

现在,您的网站将可以通过您刚刚定义的子域名访问,例如,lemonmart.angularforenterprise.com

如果您不使用 Route 53,请不要慌张。在您的域名提供商网站上,编辑Zone文件以创建指向 ALB DNS 地址的 A 记录,然后您就完成了。

获取 ALB DNS 名称

为了获取您的负载均衡器的 DNS 地址,执行以下步骤:

  1. 导航到EC2 | 负载均衡器

  2. 选择lemon-mart-alb

  3. 描述选项卡中,注意 DNS 名称;考虑以下示例:

    DNS name:
    lemon-mart-alb-1871778644.us-east-1.elb.amazonaws.com (A Record) 
    

现在我们已经配置了 AWS ECS Fargate,让我们准备我们的 Angular 应用程序以便部署到 AWS。

添加 npm 脚本用于 AWS

就像 Docker 的 npm 脚本一样,我开发了一套名为npm 脚本用于 AWS的脚本,这些脚本在 Windows 10 和 macOS 上运行。这些脚本将允许您以壮观、无停机、蓝绿方式上传和发布您的 Docker 镜像。您可以通过执行以下步骤获取这些脚本的最新版本并在项目中自动配置它们:

我们正在lemon-mart项目中配置这些设置。

  1. 安装 AWS ECS 任务的 npm 脚本:

    npm i -g mrm-task-npm-aws 
    
  2. 应用 npm 脚本用于 Docker 的配置:

    npx mrm npm-aws 
    

现在,让我们配置脚本:

  1. 确保在您的项目中设置了mrm-task-npm-docker脚本。

  2. 创建一个.env文件并设置AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEY

    **.env**
    AWS_ACCESS_KEY_ID=your_own_key_id
    AWS_SECRET_ACCESS_KEY=your_own_secret_key 
    
  3. 确保您的.env文件已包含在.gitignore文件中,以保护您的机密信息。

  4. 安装或升级到最新的 AWS CLI:

    • 在 macOS 上,brew install awscli

    • 在 Windows 上,choco install awscli

  5. 使用您的凭证登录 AWS CLI:

    1. 运行aws configure

    2. 您需要从配置 IAM 账户时获取您的访问密钥 ID秘密访问密钥

    3. 默认区域名称设置为us-east-1

  6. 更新package.json以添加一个新的config属性,包含以下配置属性:

    **package.json**
      ...
      "config": {
        ...
        "awsRegion": "us-east-1",
        "awsEcsCluster": "fargate-cluster",
        "awsService": "lemon-mart-service"
      },
    ... 
    

    确保您已从配置 npm 脚本用于 Docker 时更新package.json,以便imageRepo属性具有您的新 ECS 存储库的地址。

  7. 确保已将 AWS 脚本添加到package.json中,如图所示:

    **package.json**
    ...
    "scripts": {
      ...
      "aws:login:win": "cross-conf-env 
         aws ecr get-login --no-include-email --region 
         $npm_package_config_awsRegion > 
         dockerLogin.cmd && call dockerLogin.cmd && 
         del dockerLogin.cmd",
      "aws:login:mac": "eval $(aws ecr get-login 
         --no-include-email --region $npm_package_config_awsRegion)",
        "aws:login": "run-p -cs aws:login:win aws:login:mac", 
    } 
    

通过执行aws --version来检查您的 AWS CLI 版本。根据您的版本,您的aws:login可能需要不同。前面的脚本显示了 AWS CLI v1 的登录脚本。如果您有 v2,您的登录命令将类似于以下脚本:

在 macOS / Linux 上

aws ecr get-login-password --region $npm_package_config_awsRegion | docker login --username AWS --password-stdin $npm_package_config_imageRepo

在 Windows 上

(Get-ECRLoginCommand).Password | docker login --username AWS --password-stdin $npm_package_config_imageRepo:latest

npm run aws:login调用平台特定的命令,自动执行通常需要多步操作的动作,从 AWS CLI 工具获取docker login命令,如下所示:

**example**
$ npm run aws:login 
docker login -u AWS -p eyJwYXl...3ODk1fQ== https://00000000000.dkr.ecr.us-east-1.amazonaws.com
$ docker login -u AWS -p eyJwYXl...3ODk1fQ== https://00000000000.dkr.ecr.us-east-1.amazonaws.com
WARNING! Using --password via the CLI is insecure. Use --password-stdin.
Login Succeeded 

您首先执行aws ecr get-login,然后复制粘贴生成的docker login命令并执行它,以便您的本地 Docker 实例指向 AWS ECR。现在让我们看看我们如何部署构建的容器:

**package.json**
...
"scripts": {
...
"aws:deploy": "cross-conf-env docker run 
   --env-file ./.env silintl/ecs-deploy
   -c $npm_package_config_awsEcsCluster 
   -n $npm_package_config_awsService 
   -i $npm_package_config_imageRepo:latest 
   -r $npm_package_config_awsRegion --timeout 1000", 
}
...
example
$ docker image build . -f nginx.Dockerfile 
   -t 000000000.dkr.ecr.us-east-1.amazonaws.com/lemon-mart:latest
$ npm run docker:publish
$ npm run aws:deploy
Using image name: 0000000.dkr.ecr.us-east-1.amazonaws.com/lemon-mart:latest
Current task definition: arn:aws:ecs:us-east-1: 0000000:task-definition/lemon-mart-task:7
New task definition: arn:aws:ecs:us-east-1: 0000000:task-definition/lemon-mart-task:8
Service updated successfully, new task definition running.
Waiting for service deployment to complete...
Service deployment successful. 

我们首先构建我们的 Web 应用的 NGINX 版本 Docker 镜像,因为我们正在 ECS 上监听端口80。然后,将容器发布到 ECR,最后执行npm run aws:deploy,它使用运行蓝色/绿色部署的silintl/ecs-deploy Docker 容器。

使用 ECS 命令如何进行蓝色/绿色部署的详细信息超出了本书的范围。要查看更多使用原生 AWS ECS 命令的示例,请参阅github.com/aws-samples/ecs-blue-green-deploymentaws-samples存储库。

我们可以将我们的命令组合在一起,作为一个单独的release命令来执行,如下所示:

**package.json**
...
"scripts": {
  ...
  "aws:release": "run-s -cs aws:login docker:publish aws:deploy"
}
... 

最后,npm run aws:release简单地按照正确的顺序从 npm 脚本中运行aws:logindocker:publishaws:deploy命令。

发布

您的项目配置为在 AWS 上部署。您主要需要使用我们创建的两个命令来构建和发布镜像:

  1. 执行docker:debug来测试、构建、标记、运行、跟踪并在浏览器中启动您的应用程序以测试镜像:

    $ npm run docker:debug 
    
  2. 执行aws:release以配置 Docker 登录 AWS,发布最新的镜像构建,并在 ECS 上发布:

    $ npm run aws:release 
    

    注意,当连续运行多个命令,其中一个命令以状态1退出时,npm 将其视为失败。然而,这并不一定意味着您的操作失败。始终滚动查看终端输出,以查看是否抛出了任何真实错误。

  3. 验证您的任务在服务级别上是否运行和运行:img/B14091_13_10.png

    图 13.10:AWS ECS 服务

    确保运行计数和期望计数相同。不匹配或部署时间非常长通常意味着您的新容器上的健康检查失败。查看事件选项卡以获取更多信息。您的容器可能无法启动,或者您可能监听的是错误的端口。

  4. 验证您的实例在任务级别上是否运行:img/B14091_13_11.png

    图 13.11:AWS ECS 任务实例

    注意 公共 IP 地址并导航到它;例如,http://54.164.92.137,你应该能看到你的应用程序或 LemonMart 正在运行。

  5. 验证在 DNS 层面上 负载均衡器 设置是否正确。

  6. 导航到 ALB DNS 地址,例如 lemon-mart-alb-681490029.us-east-1.elb.amazonaws.com,并确认应用程序渲染如下:图片

    图 13.12:LemonMart 在 AWS Fargate 上运行

Et voilà!你的网站应该已经上线并运行。

在随后的版本中,在你第一次之后,你将能够观察到蓝绿部署的实际操作,如下所示:

图片

图 13.13:AWS 服务在蓝绿部署期间

有两个任务正在运行,同时还有两个新的任务正在配置。当新的任务正在验证时,运行计数将上升到四个任务。在新任务验证并通过旧任务的连接被释放后,运行计数将回到两个。

你可以通过配置 CircleCI 并使用安装了 awscli 工具的容器以及运行 AWS 的 npm 脚本来自动化你的部署。使用这种技术,你可以实现持续部署到预发布环境或持续交付到生产环境。

使用 CircleCI 部署到 AWS

第九章使用 Docker 的 DevOps 中,我们基于多阶段 Dockerfile 实现了一个 CircleCI 管道,这产生了一个 tar 和 gzip 压缩的 Docker 镜像。我们还介绍了如何使用 CircleCI 实现部署步骤。利用本章所学,我们可以结合两种策略,以便使用 CircleCI 部署到 AWS。

对于 AWS 部署,你可以使用 aws-cli orb 和一个 deploy 作业。deploy 作业将包含从缓存中恢复构建的 Docker 镜像、登录 AWS 并将镜像推送到你的 AWS ECS 容器仓库的步骤。

lemon-mart 仓库中,本节使用的 config.yml 文件名为 .circleci/config.docker-integration.yml。你还可以在 CircleCI 上找到本章的 YML 文件对应的 pull request,网址为 github.com/duluca/lemon-mart/pull/27,使用分支 deploy_aws

为了推送容器,我们通过运行 npm run aws:deploy 进行部署。让我们在 config.yml 文件中添加一个新的 deploy 作业。

CircleCI 账户设置组织设置 下,添加一个名为 aws 的新 上下文。将 AWS_ACCOUNT_IDAWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEYAWS_DEFAULT_REGION 环境变量作为上下文的一部分设置。

在这里查看配置更改:

**.circleci/config.yml**
version: 2.1
orbs:
  **aws-cli: circleci/aws-cli@1.0.0**
...
jobs:
  ...
  **deploy:**
 **executor: aws-cli/default**
    working_directory: ~/repo
    steps:
      - attach_workspace:
          at: /tmp/workspace
      - checkout
      - setup_remote_docker
      - aws-cli/setup
      - run: npm ci
      - run:
          name: Restore .env files
          command: |
            set +H
            DOT_ENV=AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID\\nAWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY
            echo -e $DOT_ENV > .env
      - run:
          name: Sign Docker into AWS ECR
          command: |
            aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com/lemon-mart
      - run:
          name: Push it to ECR
          command: |
            docker load < /tmp/workspace/built-image.tar.gz
            ECR_URI=$AWS_ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com/lemon-mart
            docker image tag lemon-mart:$CIRCLE_BRANCH $ECR_URI:$CIRCLE_BRANCH
            docker image tag $ECR_URI:$CIRCLE_BRANCH $ECR_URI:latest
            docker image push $ECR_URI:$CIRCLE_BRANCH
            docker image push $ECR_URI:latest      
      - run:
          name: Deploy
          **command: npm run aws:deploy** 

我们使用 aws-cli/setup 作业配置 aws-cli orb。然后执行 npm ci,这样我们就可以稍后运行我们的 npm 脚本。我们使用 CircleCI 环境变量恢复 .env 文件。我们使用 AWS ECR 登录信息配置 Docker,这样我们就可以将其容器推送到它。我们将前一步骤的 built-image.tar.gz 存储起来,并使用 docker load 命令加载它。我们给镜像打标签并推送到 ECR。最后,我们执行 npm run aws:deploy,这触发了我们的蓝绿部署。

最后但同样重要的是,我们更新 workflows 以包括 deploy 作业并配置我们之前定义的 aws context

**.circleci/config.yml**
...
workflows:
  version: 2
  build-and-deploy:
    jobs:
      - build
      - deploy:
          **context: aws**
          requires:
            - build 

在 CircleCI 中正确配置上下文至关重要。如果配置错误,您可能会发现自己陷入一个卡夫卡式的错误迷宫、糟糕的文档和痛苦的境地。不要说我没有警告过你。

请在此处查看成功部署的截图:

图 13.14:成功部署到 AWS Fargate 集群的 CircleCI

注意,部署步骤需要近 7 分钟。这是因为蓝绿部署确保我们的新部署是健康的,然后它会从现有容器中移除连接到新容器,默认设置下这需要 5 分钟。如果新部署不健康,部署步骤将在 10 分钟后超时并失败。

恭喜!现在我们可以以无停机、蓝绿的方式持续部署到 AWS。这一切都很棒,但一个基本的高度可用配置的成本是多少呢?

为了避免收费,请删除 lemon-mart-service。为此,您需要首先将您服务的任务数量更新为 0。此外,删除为您创建的默认集群,以避免任何未预见的费用。

让我们在下一节中检查费用。

AWS 账单

我在 AWS Fargate 上高度可用的 LemonMart 部署每月大约花费 45 美元。以下是详细费用分解:

描述费用
亚马逊简单存储服务 (S3)$0.01
AWS 数据传输$0.02
亚马逊云监控$0.00
亚马逊 EC2 容器服务 (ECS Fargate)$27.35
亚马逊弹性计算云 (EC2 负载均衡器实例)$16.21
亚马逊 EC2 容器注册库 (ECR)$0.01
亚马逊 Route 53$0.50
总计$44.10

注意,账单非常详细,但它确实准确地分解了我们最终使用的所有 AWS 服务。主要成本是运行我们的 Web 服务器在 EC2 容器服务ECS)上的两个实例,以及在 弹性计算云EC2)上运行负载均衡器。客观地说,每月 45 美元可能看起来很多,用于托管一个 Web 应用程序。如果您愿意设置自己的集群并使用专用的 EC2 服务器,您可以通过 1 年或 3 年的分期付款来节省高达 50% 的成本。在 Heroku 上具有两个实例的类似、高度可用的部署起价为每月 50 美元,您还可以获得其他丰富的功能。同样,Vercel Now 上的两个实例将花费每月 30 美元。请注意,Heroku 和 Vercel Now 都不提供对物理上不同的可用区的访问。另一方面,Digital Ocean 允许您在不同的数据中心中部署服务器;然而,您必须自己编写基础设施代码。对于每月 15 美元,您可以在三个服务器上设置自己的高度可用集群,并能够在其上托管多个网站。

摘要

在本章中,您学习了在正确保护您的 AWS 账户时需要注意的细微差别和各种安全考虑。我们讨论了调整基础设施规模的概念。您以隔离的方式进行了简单的负载测试,以找出两个 Web 服务器之间性能的相对差异。拥有优化的 Web 服务器后,您配置了 AWS ECS Fargate 集群,以实现高度可用的云基础设施。使用 npm 脚本进行 AWS,您学习了如何编写可重复且可靠的零停机蓝/绿部署脚本。最后,您了解了在 AWS 和其他云服务提供商(如 Heroku、Vercel Now 和 Digital Ocean)上运行您的基础设施的基本成本。

在下一章和最后一章中,我们将完成对全栈 Web 开发者在将应用程序部署到网络上时应了解的主题范围的覆盖。我们将向 LemonMart 添加 Google Analytics 以衡量用户行为,利用高级负载测试来了解部署良好配置的可扩展基础设施的财务影响,并使用自定义分析事件来衡量重要应用程序功能的实际使用情况。

练习

使用 LemonMart 的 docker-compse.yml 文件部署其服务器基础设施到 AWS ECS。作为额外奖励,配置 AWS ECS 以使用 AWS 弹性文件系统EFS)持久化您的 MongoDB 数据:

  1. docs.aws.amazon.com/AmazonECS/latest/developerguide/ECS_CLI_installation.html 安装 ECS CLI。

  2. mrm-task-npm-aws 脚本添加到 lemon-mart-server 的根目录。

  3. 在版本 3.0 中创建一个新的 docker-compose.aws.yml 文件,并将其更新为引用您已发布的容器版本。

  4. 使用 npm run aws:publish:compose 命令部署您的应用程序。

你可以使用 Minimal MEAN 项目和相关的 GitHub gists 作为指南,在 github.com/duluca/minimal-mean#continuous-integration-and-hosting

进一步阅读

问题

尽可能好地回答以下问题,以确保你已理解本章的关键概念,而无需使用 Google。你需要帮助回答这些问题吗?请参阅 附录 D自我评估答案,在线位于 static.packt-cdn.com/downloads/9781838648800_Appendix_D_Self-Assessment_Answers.pdf 或访问 expertlysimple.io/angular-self-assessment

  1. 适当规模的基础设施有哪些好处?

  2. 使用 AWS ECS Fargate 相比 AWS ECS 的好处是什么?

  3. 你记得关闭你的 AWS 基础设施以避免额外收费吗?

  4. 什么是蓝/绿部署?

第十四章:Google Analytics 和高级云操作

你已经设计、开发和部署了一个世界级的网络应用程序;然而,这只是你应用程序故事的开端。网络是一个不断演变、充满活力、生机勃勃的环境,需要关注才能继续作为企业取得成功。在第十三章AWS 上的高可用云基础设施中,我们讨论了云基础设施的基本概念和拥有成本。

在本章中,我们将深入了解如何真正理解用户实际使用我们的应用程序,我们将使用 Google Analytics 来获取这些信息。然后,我们将利用这些信息创建逼真的负载测试来模拟实际用户行为,以了解我们服务器单个实例的真实容量。了解单个服务器的容量后,我们可以微调我们的基础设施扩展方式,以减少浪费并讨论各种扩展策略的影响。

最后,我们将讨论高级分析概念,例如自定义事件,以获得对用户行为的更精细理解和跟踪。

在本章中,你将学习以下主题:

  • 收集分析

  • 预算和扩展

  • 高级负载测试以预测容量

  • 可靠的云扩展

  • 使用自定义分析事件测量实际使用情况

在本章中,你将设置 Google Analytics、Google Tag Manager 和 OctoPerf 账户。

本书样本代码的最新版本可以在 GitHub 上的以下存储库链接中找到。该存储库包含代码的最终和完成状态。你可以在本章结束时通过查找projects文件夹下的代码末尾快照来验证你的进度。

第十四章的情况下:

  1. 克隆仓库:github.com/duluca/lemon-mart

    在根目录下执行npm install以安装依赖项。

  2. 本章的代码示例可以在以下子文件夹中找到:

    projects/ch14 
    
  3. 要运行本章的 Angular 应用程序,请执行以下命令:

    npx ng serve ch14 
    
  4. 要运行本章的 Angular 单元测试,请执行以下命令:

    npx ng test ch14 --watch=false 
    
  5. 要运行本章的 Angular e2e 测试,请执行以下命令:

    npx ng e2e ch14 
    
  6. 要构建本章的生产就绪 Angular 应用程序,请执行以下命令:

    npx ng build ch14 --prod 
    

    注意,存储库根目录下的dist/ch14文件夹将包含编译结果。

    请注意,书中或 GitHub 上的源代码可能并不总是与 Angular CLI 生成的代码相匹配。由于生态系统不断演变,书中代码与 GitHub 上代码之间的实现可能也存在细微差异。随着时间的推移,示例代码发生变化是自然的。在 GitHub 上,您可能会找到更正、修复以支持库的新版本,或者为读者提供观察的多种技术的并排实现。读者只需实现书中推荐的理想解决方案即可。如果您发现错误或有问题,请创建一个 GitHub 上的问题或提交一个 pull request,以惠及所有读者。

    您可以在 附录 C 中了解更多关于更新 Angular 的信息,保持 Angular 和工具常青。您可以从 static.packt-cdn.com/downloads/9781838648800_Appendix_C_Keeping_Angular_and_Tools_Evergreen.pdfexpertlysimple.io/stay-evergreen 在线找到此附录。

让我们从了解网络分析的基本知识开始。

收集分析数据

现在我们网站已经上线并运行,我们需要开始收集指标以了解其使用情况。指标对于运营网络应用程序至关重要。

Google Analytics 有许多方面。以下是主要三个方面:

  1. 获取,衡量访客如何到达您的网站

  2. 行为,衡量访客如何与您的网站互动

  3. 转化,衡量访客如何在您的网站上完成各种目标

这是我的网站 thejavascriptpromise.com/ 上的 行为 | 概览 页面的一个查看:

图 14.1:Google Analytics 行为概览

thejavascriptpromise.com/ 是一个简单的单页 HTML 网站,因此指标相当简单。让我们回顾一下屏幕上的各种指标:

  1. 页面浏览量显示访客数量。

  2. 独立页面浏览量显示独立访客数量。

  3. 平均页面停留时间显示每位用户在网站上花费的时间。

  4. 跳出率显示用户在没有导航到子页面或以任何方式与网站互动(如点击带有自定义事件的链接或按钮)的情况下离开网站。

  5. %退出率表示用户在查看特定页面或一组页面后离开网站的频率。

在 2017 年,网站大约有 1,090 位独立访客,平均每位访客在网站上花费约 2.5 分钟,即 157 秒。鉴于这是一个单页网站,跳出率和%退出率在任何有意义的层面上都不适用。后来,我们使用独立访客数量来计算每位用户的成本。

作为比较,书中提到的 LemonMart 应用在 2018 年 4 月至 2020 年 4 月之间共服务了 162,396 个柠檬:

图 14.2:LemonMart 行为概览

除了页面浏览量之外,Google Analytics 还可以捕获特定事件,例如点击按钮触发服务器请求。这些事件可以在 事件 | 概览 页面上查看,如下所示:

图 14.3:Google Analytics 事件概览

也可以在服务器端捕获指标,但这将提供随时间推移的请求统计。您将需要额外的代码和状态管理来跟踪特定用户的行为,以便您可以计算随时间推移的用户统计。通过在客户端使用 Google Analytics 实现此类跟踪,您可以获得对用户来源、所做行为、是否成功以及何时离开您的应用程序的更详细理解,而无需在您的后端添加不必要的代码复杂性和基础设施负载。

将 Google Tag Manager 添加到您的 Angular 应用程序中

让我们在您的 Angular 应用程序中开始捕获分析数据。Google 正在逐步淘汰与 Google Analytics 一起提供的旧版 ga.jsanalytics.js 产品,用其新的、更灵活的全局站点标签 gtag.js 取代这些产品,该标签与 Google Tag Manager 一起提供。这绝不是 Google Analytics 的终结;相反,它代表了对一个更容易配置和管理分析工具的转变。全局站点标签可以通过 Google Tag Manager 远程配置和管理。标签是发送到客户端的 JavaScript 跟踪代码片段,它们可以启用对新的指标的跟踪以及与多个分析工具的集成,而无需更改已部署的代码。您仍然可以使用 Google Analytics 来分析和查看您的分析数据。Google Tag Manager 的另一个主要优点是它具有版本控制功能,这意味着您可以在各种条件下触发不同类型的标签进行实验,而无需担心对您的分析配置造成不可逆的损害。

设置 Google Tag Manager

让我们从为您的应用程序设置一个 Google Tag Manager 账户开始:

  1. tagmanager.google.com/ 登录 Google Tag Manager。

  2. 按照以下步骤添加一个带有 Web 容器的新的账户:

    图 14.4:Google Tag Manager

  3. 按照网站上的说明,将生成的脚本粘贴到 index.html 文件的顶部或接近顶部的 <head><body> 部分:

    **src/index.html**
    <head>
    <!-- Google Tag Manager -->
    <script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start': new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0], j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
    })(window,document,'script','dataLayer','GTM-56D4F6K');</script>
    <!-- End Google Tag Manager -->
    ...
    </head>
    <body>
    <!-- Google Tag Manager (noscript) -->
    <noscript><iframe src="img/ns.html?id=GTM-56D4F6K" height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
    <!-- End Google Tag Manager (noscript) -->
    <app-root></app-root>
    </body> 
    

    注意,如果用户在其浏览器中禁用了 JavaScript 执行,则 <noscript> 标签才会执行。这样,我们可以收集此类用户的指标,而不是对他们的存在视而不见。

  4. 提交并发布您的标签管理器容器。

  5. 您应该看到您的标签管理器的初始设置已完成,如下截图所示:

    图 14.5:已发布的标签

  6. 验证您的 Angular 应用程序运行时没有错误。

注意,如果你没有发布你的标签管理器容器,你将在开发控制台或网络标签加载gtm.js时看到404错误。

设置 Google Analytics

现在,让我们通过 Google Analytics 生成一个跟踪 ID。这是你应用的通用唯一标识符,用于关联你的分析数据:

  1. analytics.google.com登录 Google Analytics。

  2. 打开管理控制台,使用屏幕左下角的齿轮图标,如图所示:

    图 14.6:Google Analytics 管理控制台

  3. 创建一个新的分析账户。

  4. 使用前一张截图中的步骤作为指南,执行以下步骤:

    1. 添加一个新的属性LemonMart

    2. 将属性配置为你的偏好。

    3. 点击跟踪代码

    4. 复制以UA-xxxxxxxxxx-1开头的跟踪 ID

    5. 忽略提供的gtag.js代码。

拥有你的跟踪 ID 后,我们可以配置 Google Tag Manager,使其能够收集分析数据。

在标签管理器中配置 Google Analytics 标签

现在,让我们将我们的 Google Analytics ID 连接到 Google Tag Manager:

  1. tagmanager.google.com,打开工作区标签。

  2. 点击添加新标签

  3. 将其命名为Google Analytics

  4. 点击标签配置并选择通用分析

  5. Google Analytics 设置下添加一个新变量。

  6. 将上一节复制的跟踪 ID 粘贴。

  7. 点击触发器并添加所有页面触发器。

  8. 点击保存,如图所示:

    图 14.7:创建 Google Analytics 标签

  9. 提交并发布你的更改,并观察带有单个标签的版本摘要,如图所示:

    图 14.8:显示一个标签的版本摘要

  10. 现在刷新你的 Angular 应用,你将在/home路由上。

  11. 在一个私有窗口中,打开你的 Angular 应用的新实例并导航到/manager/home路由。

  12. analytics.google.com/,打开实时 | 概览面板,如图所示:

    图 14.9:Google Analytics 实时概览

  13. 注意,两个活跃用户正在被跟踪。

  14. 顶部活跃页面下,你应该能看到用户所在的页面。

通过利用 Google Tag Manager 和 Google Analytics 一起,我们能够在不更改 Angular 应用内部任何代码的情况下完成页面跟踪。

搜索引擎优化SEO)是分析的重要组成部分。为了更好地理解爬虫如何感知你的 Angular 网站,请使用位于www.google.com/webmasters/tools的 Google Search Console 仪表板来识别优化。此外,考虑使用 Angular Universal 在服务器端渲染某些动态内容,以便爬虫可以索引你的动态数据源,并为你的网站带来更多流量。

预算和扩展

在第十三章的AWS 计费部分,AWS 上的高可用云基础设施中,我们介绍了运营一个 Web 服务器的月度成本,从每月 5 美元到每月 45 美元不等,从单服务器实例场景到高可用基础设施。对于大多数需求,预算讨论将从这一月度数字开始和结束。你可以执行如高级负载测试部分所建议的负载测试,以预测每服务器的用户容量,并大致了解你可能需要多少服务器。在一个有数十个服务器全天候运行的动态扩展云环境中,这是一种过于简单化的预算计算方法。

如果你运营的是一个规模较大的网站,事情往往会变得复杂。你将需要在不同的技术栈上运行多个服务器,服务于不同的目的。很难衡量或证明为看似过剩的容量或不必要的高性能服务器预留多少预算。无论如何,你需要能够根据你服务的用户数量来传达你基础设施的效率,并确保你的基础设施经过精细调整,这样你就不会因为一个无响应的应用程序而失去用户,或者因为使用了比你需要的更多容量而过度付费。

因此,我们将采取以用户为中心的方法,将我们的 IT 基础设施成本转换为每用户成本指标,这样业务和营销部门都能理解。

在下一节中,我们将探讨计算你基础设施的每用户成本意味着什么,以及当云扩展介入时,这些计算如何变化。我们将以我的一个网站为例。

计算每用户成本

我们将利用 Google Analytics 中的行为指标,目标是计算给定时间段的每用户成本:

每用户成本的计算方法如下:

图片

使用之前从thejavascriptpromise.com/获取的数据,让我们将数据输入公式来计算每用户成本/月

这个网站部署在 DigitalOcean 上的 Ubuntu 服务器上,因此每月的基础设施成本(包括每周备份)为每月 6 美元。从 Google Analytics 中,我们知道 2017 年有 1,090 位独立访客:

图片

在 2017 年,我每个用户支付了 7 美分。这笔钱花得值吗?每月 6 美元,我并不介意。在 2017 年,thejavascriptpromise.com/ 部署在一个传统的服务器设置上,作为一个静态网站,无法扩展或缩减。这些条件使得使用独立访客指标和找到每个用户的成本变得非常直接。正是这种简单的计算方式,也导致了基础设施的低效。如果我要在相同的基础设施上服务 100 万用户,我的年度成本将达到 70,000 美元。如果我能通过谷歌广告为每 1000 个用户赚取 100 美元,我的网站每年将赚取 100,000 美元。在扣除税费、开发费用以及我们不合理的主机费用后,运营可能会亏损。

如果你想要利用云扩展,即根据当前用户需求动态扩展或缩减实例,那么上述公式很快就会变得毫无用处,因为你必须考虑配置时间和目标服务器利用率。

配置时间是你的云提供商从零开始启动新服务器所需的时间。目标服务器利用率是给定服务器的最大使用指标,其中必须发出扩展警报,以便在当前服务器达到最大容量之前,新服务器已经准备好。为了计算这些变量,我们必须对我们的服务器执行一系列负载测试。

页面浏览量是确定单页应用(如 Angular)中用户行为的过于简单的方法,在这些应用中,页面浏览量不一定与请求相关,或者反之亦然。如果我们仅仅基于页面浏览量执行负载测试,我们就无法得到一个关于你的平台在负载下可能如何表现的现实模拟。

用户行为,或者说用户实际如何使用你的应用,可以极大地影响你的性能预测,并使预算数字大幅波动。你可以使用谷歌分析自定义事件来捕捉由你的平台提供各种类型请求的复杂动作集。在本章的后面部分,我们将探讨如何在“测量实际使用”部分测量实际使用情况。

初始时,你将没有上述任何指标,而且任何你可能拥有的指标,在你对软件或硬件栈进行任何有意义的更改时都会被无效化。因此,定期执行负载测试以模拟真实用户负载是至关重要的。

高级负载测试

为了能够预测容量,我们需要运行负载测试。在第十三章,“AWS 上的高可用云基础设施”中,我讨论了一种简单的负载测试技术,即向服务器发送大量网络请求。在相对比较场景中,这对于测试原始功率来说效果不错。然而,实际用户在浏览你的网站时会产生数十个不同间隔的请求,这导致了对后端服务器的各种 API 调用。

我们必须能够模拟虚拟用户,并在我们的服务器上释放大量用户以找到服务器的瓶颈。OctoPerf 是一个易于使用的服务,用于执行此类负载测试,位于 octoperf.com。OctoPerf 提供了一个免费层,允许在无限次测试运行中使用两个负载生成器进行 50 个并发用户/测试。

OctoPerf 是一个理想的工具,可以帮助我们快速开始高级测试功能。让我们创建一个账户,看看它能为我们做什么:

  1. 创建一个 OctoPerf 账户。

  2. 登录并添加一个新的 LemonMart 项目,如图所示:图片

    图 14.10:在 OctoPerf 中添加项目

    OctoPerf 允许您创建具有不同使用特性的多个虚拟用户。由于它是一个基于 URL 的设置,任何基于点击的用户操作也可以通过直接调用应用程序服务器 URL 并传递测试参数来模拟。

  3. 创建两个虚拟用户,一个作为 经理,导航到基于经理的页面,另一个作为 POS 用户,坚持使用 POS 功能。

  4. 点击 创建场景图片

    图 14.11:POS 用户场景

  5. 将场景命名为 晚间高峰

  6. 您可以添加 经理POS 用户 类型的混合,如图所示:图片

    图 14.12:晚间高峰场景

  7. 点击 启动 50 VUs 按钮以开始负载测试。

    您可以观察实时达到的 用户数每秒点击数,如下面的屏幕截图所示:

    图片

    图 14.13:晚间高峰负载测试进行中

  8. ECS 服务指标还给我们提供了一个关于实时利用率的总体概念,如下面的屏幕截图所示:图片

    图 14.14:ECS 实时指标

  9. 分析负载测试结果。

您可以通过点击 ECS 服务指标 中的 CPUUtilization 链接或通过导航到 CloudWatch | 指标 部分来从 ECS 获取更准确的结果,如下所示:

图片

图 14.15:AWS CloudWatch 指标

如前图所示,在 10 分钟的持续用户负载为 50 的情况下,CPU 利用率相当稳定,大约为 1.3%。在此期间,没有请求错误,如 OctoPerf 的统计摘要所示:

图片

图 14.16:OctoPerf 统计摘要

理想情况下,我们会测量每秒最大用户数,直到错误开始产生。然而,鉴于只有 50 个虚拟用户和我们已经拥有的信息,我们可以预测在 100% 利用率下可以处理多少用户:

图片

我们的负载测试结果显示,我们的基础设施可以处理每秒 3,846 个用户。鉴于这一信息,我们可以在下一节计算可扩展环境中的每用户成本。然而,性能和可靠性是相辅相成的。你选择如何架构你的基础设施也将为预算提供重要信息,因为所需的可靠性水平将决定你必须始终保留的最小实例数量。

可靠的云扩展

可靠性可以用你组织的恢复点目标(RPO)和恢复时间目标(RTO)来表示。RPO 代表你愿意丢失多少数据,而 RTO 代表在发生故障的情况下,你可以多快重建你的基础设施。

假设你运营一个电子商务网站。在每周工作日的中午左右,你达到销售高峰。每次用户将商品添加到购物车时,你都会在服务器端缓存中存储这些商品,以便用户可以在家后继续购物。此外,你每分钟处理数百笔交易。生意兴隆,你的基础设施正在扩展,一切都很顺利。与此同时,一只饥饿的老鼠或过度充电的闪电云决定攻击你的数据中心。最初,一个看似无害的电源单元出现故障,但没关系,因为附近的电源单元可以填补空缺。然而,这是午餐高峰期;数据中心上的其他网站也面临着高流量。结果,几个电源单元过热并发生故障。没有足够的电源单元来填补空缺,因此,电源单元一个接一个地过热并开始故障,引发了一系列连锁反应,最终导致整个数据中心崩溃。与此同时,一些用户刚刚点击了加入购物车,其他人点击了支付按钮,还有一些人正准备到达你的网站。如果你的 RPO 是 1 小时,意味着你每小时持久化一次购物车缓存,那么你将不得不与那些夜间购物者失去宝贵的数据和潜在的销售。如果你的 RTO 是 1 小时,那么你需要 1 小时的时间才能让你的网站重新上线并运行,你可以放心,那些刚刚点击购买按钮或到达无响应网站的大部分客户那天不会在你的网站上购买。

一个周全的 RPO(恢复点目标)和 RTO(恢复时间目标)是关键的商业需求,但它们还必须与正确的基础设施相匹配,这样才能以成本效益的方式实现你的目标。AWS 由全球二十多个地区组成,每个地区至少包含它们的可用区(AZ)。每个 AZ 是一个物理上分离的基础设施,不会受到另一个 AZ 故障的影响。

在 AWS 上,高可用配置意味着您的应用程序至少在两个可用区(AZ)上运行,因此如果服务器实例失败,或者整个数据中心失败,您已经在物理上分开的数据中心中有一个实例已经运行,能够无缝地接收传入的请求。

容错架构意味着您的应用程序部署在多个区域。即使整个区域因自然灾害、DDoS 攻击或软件更新不当而完全中断,您的基础设施仍然屹立不倒,并且能够响应用户请求。您的数据通过多层安全措施和备份的备份进行保护。

AWS 提供优质的服务,包括Shield服务,用于保护您的网站免受 DDoS 攻击,Pilot Light服务以最低的基础设施在另一个区域保持等待休眠状态,如果需要,可以扩展到全容量,同时保持运营成本较低,以及Glacier服务,以经济实惠的方式存储大量数据长达数年。

高可用配置始终需要在多 AZ 设置中至少有两个实例。对于容错设置,您需要在至少两个区域中拥有两个高可用配置。大多数 AWS 云服务,如用于数据存储的 DynamoDB 或用于缓存的 Redis,默认都是高可用的,包括无服务器技术如 Lambda。Lambda 按使用量收费,并且可以以成本效益的方式扩展以满足任何需求。如果您可以将重计算任务移动到 Lambda,您可以在过程中显著降低服务器利用率和扩展需求。在规划您的基础设施时,您应该考虑所有这些变量,以设置满足您需求的正确可扩展环境。

可扩展环境中的每用户成本

在可扩展环境中,您不能指望 100%的利用率。部署新服务器需要时间。利用率达到 100%的服务器无法及时处理额外的传入请求,这会导致用户看到请求丢失或错误请求。因此,相关的服务器必须在达到 100%利用率之前发送触发信号,以确保不会丢失任何请求。在本章早期,我建议在扩展之前设定 60-80%的目标利用率。确切的数字将很大程度上取决于您选择的特定软件和硬件堆栈。

根据您自定义的利用率目标,我们可以计算出您的基础设施平均每个实例预期要服务的用户数量。利用这些信息,您可以计算出更准确的每用户成本,这应该能够根据您的具体需求正确调整您的 IT 预算。过度支出和不足支出同样糟糕。您可能正在放弃比可接受水平更多的增长、安全、数据、可靠性和弹性。

在下一节中,我们将介绍如何计算最佳目标服务器利用率指标,以便您可以计算更准确的每用户成本。然后,我们将探讨在预先计划的时间框架和软件部署过程中可能发生的扩展。

计算目标服务器利用率

首先,计算您自定义的服务器利用率目标,这是您的服务器经历增加流量并触发新服务器配置的时间点,以便原始服务器不会达到 100%的利用率并丢弃请求。考虑以下公式:

让我们通过一个具体的例子来展示公式的应用:

  1. 对您的实例进行负载测试以确定每个实例的用户容量:

    负载测试结果:每秒 3,846 用户。

    每秒请求数和每秒用户数并不相同,因为一个用户可能需要多次请求来完成一个操作,并且可能每秒执行多个请求。需要像 OctoPerf 这样的高级负载测试工具来执行真实且多样化的工作负载,并测量用户容量相对于请求容量的情况。

  2. 测量实例配置速度,从创建/冷启动到首次满足请求:

    测量的实例配置速度:60 秒。

    为了测量这个速度,您可以放下秒表。根据您的具体设置,AWS 在 ECS 服务事件标签页、CloudWatch 和 CloudTrail 中提供事件和应用日志,以关联足够的信息来找出何时请求了新实例以及实例准备就绪以处理请求所需的时间。

    例如,在ECS 服务事件标签页中,以目标注册事件作为开始时间。一旦任务已启动,点击任务 ID 以查看创建时间。使用任务 ID,检查 CloudWatch 中的任务日志以查看任务首次处理 Web 请求的时间作为结束时间,然后计算持续时间。

  3. 测量第 95 百分位数的用户增长率,排除已知的容量增加:

    第 95 百分位数用户增长率:每秒 10 用户。

    第 95 百分位数是一个常用的指标来计算整体网络使用情况。这意味着 95%的时间内,使用量将低于所声明的数量,这使得它是一个很好的数字用于规划,正如 Barb Dijker 在她的文章《这个 95 百分位数是什么意思?》中所解释的,该文章可在www2.arnes.si/~gljsentvid10/pct.html找到。

    如果您没有先前的指标,最初定义用户增长率将最多是一个有根据的猜测。然而,一旦您开始收集数据,您就可以更新您的假设。此外,不可能运营一个能够以经济有效的方式应对任何可想象到的异常值的基础设施,而不会丢弃请求。根据您的指标,您应该有意识地做出业务决策,确定应该忽略多少百分比的异常值作为可接受的业务风险。

  4. 让我们将数字代入公式:

图片

自定义目标利用率率,向下取整,将是 84%。将你的扩展触发器设置为 84%将避免实例过度配置,同时防止用户请求丢失。

在考虑到这个自定义目标利用率的情况下,让我们更新包含扩展的每个用户成本公式:

图片

因此,如果我们的基础设施每月为 150 个用户提供服务,成本为 100 美元,在 100%的利用率下,你计算出的每个用户的成本将是 0.67 美元/用户/月。如果你考虑扩展,成本将如下所示:

图片

不丢失请求的扩展将比原始的 0.67 美元/用户/月多 16%,达到 0.79 美元/用户/月。然而,重要的是要记住,你的基础设施并不总是如此高效。在较低的利用率目标下,或者当这些目标与扩展触发器配置不当时,成本可以轻易地翻倍、三倍或四倍于原始成本。这里的最终目标是找到最佳平衡点,这意味着你将为每个用户支付正确的金额。

没有规定的每个用户成本你应该追求。然而,如果你提供的服务在扣除所有其他运营成本和利润率后,每个用户每月收费 5 美元,并且你仍有剩余预算并且你的用户抱怨性能不佳,那么你可能支出不足。然而,如果你正在侵蚀你的利润率,或者更糟糕的是,仅仅收支平衡,那么你可能支出过多,或者你可能需要重新考虑你的商业模式。

有几个其他因素会影响你的每个用户成本,包括我们稍后将讨论的蓝绿部署。你还可以通过利用预定配置来提高扩展的效率。

预定配置

动态扩展和收缩然后再次扩展是定义云计算的关键。然而,目前可用的算法仍然需要一些规划,如果你知道一年中的某些日子、周或月将需要不寻常的高资源容量。面对突然涌入的新流量,你的基础设施将尝试动态扩展,但如果流量的增长速度是对数级的,即使优化了服务器利用率目标也无济于事。服务器将频繁达到并运行在 100%的利用率,导致请求丢失或错误。为了防止这种情况发生,你应该在可预测的高需求期间主动提供额外的容量。

蓝绿部署

在第十三章“AWS 上的高可用云基础设施”中,你配置了无停机时间的蓝绿部署。蓝绿部署是可靠的代码部署,确保你的网站持续在线,同时最大限度地降低不良部署的风险。

假设您有一个高可用性部署,这意味着在任何给定时间都有两个实例处于活动状态。在蓝/绿部署期间,将配置两个额外的实例。一旦这些额外的实例准备好满足请求,它们的健康状态将使用您预定义的健康指标来确定。

如果发现您的新实例是健康的,这意味着它们处于正常工作状态。将有一段时间,比如说 5 分钟,在这段时间内,原始实例中的连接会被耗尽并重新路由到新实例。在这段时间内,原始实例将被取消配置。

如果发现新实例不健康,则这些新实例将被取消配置,导致部署失败。然而,服务将保持不间断的可用性,因为原始实例将保持完整,在整个过程中继续为用户提供服务。

使用指标修订估计

压力测试和预测用户增长率可以帮助您了解系统在生产环境中的行为。收集更细粒度的指标和数据对于修订您的估计并确定更准确的 IT 预算至关重要。

测量实际使用

如我们之前讨论的,仅跟踪页面浏览量并不能反映用户向服务器发送的请求数量。使用 Google Tag Manager 和 Google Analytics,您可以轻松跟踪页面浏览量以外的更多内容。

到出版时为止,以下是一些您可以在各个类别中配置的默认事件。随着时间的推移,此列表将不断增长:

  • 页面浏览:用于跟踪用户在页面资源加载和页面完全渲染时是否停留:

    • 页面浏览;在首次机会触发

    • DOM 就绪;当 DOM 结构加载完成时

    • 窗口加载完成;当所有元素都加载完成时

  • 点击:用于跟踪用户与页面的点击交互:

    • 所有元素

    • 仅链接

  • 用户参与度:跟踪用户行为:

    • 元素可见性;元素是否已被显示

    • 表单提交;是否提交了表单

    • 滚动深度;他们滚动了页面多远

    • YouTube 视频;如果他们播放了嵌入的 YouTube 视频

  • 其他事件跟踪:

    • 自定义事件;由程序员定义以跟踪单个或多个步骤的事件,例如用户完成结账流程的步骤

    • 历史更改;用户是否在浏览器的历史记录中导航回

    • JavaScript 错误;是否生成了 JavaScript 错误

    • 计时器;用于触发或延迟基于时间的分析事件

大多数这些事件不需要任何额外的编码来实现,因此我们将实现一个自定义事件来演示您如何通过自定义编码捕获任何单个事件或一系列事件。通过一系列事件捕获工作流程可以揭示您应该将开发精力集中在哪些方面。

关于 Google Tag Manager 事件、触发器或技巧和窍门,我建议您查看 Simo Ahava 的博客www.simoahava.com/以获取更多信息。

创建自定义事件

在此示例中,我们将捕获客户成功结账并完成销售的事件。我们将实现两个事件,一个用于结账发起,另一个用于交易成功完成:

  1. 登录您的 Google Tag Manager 工区tagmanager.google.com

  2. 触发器菜单下,单击新建,如图所示:

    图 14.17:标签管理器工区

  3. 为您的触发器命名。

  4. 单击空触发器卡以选择事件类型。

  5. 选择自定义事件

  6. 创建一个名为checkoutCompleted的自定义事件,如图所示:

    图 14.18:自定义结账事件

    通过选择某些自定义事件选项,您可以限制或控制特定事件的收集,即仅在特定页面或域名上,例如在lemonmart.com上。在以下截图中,您可以看到一个自定义规则,该规则会过滤掉在lemonmart.com上未发生的任何结账事件,以排除开发或测试数据:

    图 14.19:某些自定义事件

  7. 保存您的新事件。

  8. 为名为Checkout Initiated的事件重复此过程。

  9. 添加两个新的 Google Analytics 事件标签,如下截图所示:

    图 14.20:新的自定义事件标签

  10. 配置事件并将其与您创建的相关触发器附加到它,如下截图所示:

    图 14.21:触发器设置

  11. 提交并发布您的工区。

我们现在已准备好在我们的分析环境中接收自定义事件。

在 Angular 中添加自定义事件

现在,让我们编辑 Angular 代码以触发事件:

  1. 考虑带有结账按钮的 POS 模板:

    **src/app/pos/pos/pos.component.html**
    <p>
      <img
        src=”https://user-images.githubusercontent.com/822159/36186684-9f05fef8-110e-11e8-991f-fae6ca60fe5d.png” />
    </p>
    <p>
      <button mat-icon-button (click)=”checkout(currentTransaction)”>
        <mat-icon>shopping_cart</mat-icon> Checkout Customer
      </button>
    </p> 
    

    以下图中的圆形结账按钮位于左下角:

    图 14.22:带有结账按钮的 POS 页面

    可选地,您可以直接在模板中添加一个onclick事件处理器,例如在结账按钮上onclick="dataLayer.push({'event': 'checkoutInitiated'})"。这会将checkoutInitiated事件推送到由gtm.js提供的dataLayer对象。

  2. 定义一个ITransaction接口:

    **src/app/pos/transaction/transaction.ts**
    ...
    export interface ITransaction {
      paymentType: TransactionType
      paymentAmount: number
      transactionId?: string
    }
    ... 
    
  3. 定义一个TransactionType枚举:

    **src/app/pos/transaction/transaction.enum.ts**
    ...
    export enum TransactionType {
      Cash,
      Credit,
      LemonCoin,
    }
    ... 
    
  4. 实现一个具有processTransaction函数的TransactionService

    **src/app/pos/transaction/transaction.service.ts**
    ...
    @Injectable({
      providedIn: ‘root’,
    })
    export class TransactionService {
      constructor() {}
      processTransaction(transaction: ITransaction)
          : Observable<string> {
        return new
          BehaviorSubject<string>(‘5a6352c6810c19729de860ea’)
          .asObservable()
      }
    }
    ... 
    

    ‘5a6352c6810c19729de860ea’是一个表示交易 ID 的随机字符串。

    PosComponent中声明一个用于推送dataLayer事件的接口:

    **src/app/pos/pos/pos.component.ts**
    ...
    interface IEvent {
    event: ‘checkoutCompleted’ | ‘checkoutInitiated’
    }
    declare let dataLayer: IEvent[]
    ... 
    

    导入依赖项并初始化currentTransaction

    **src/app/pos/pos/pos.component.ts**
    ...
    export class PosComponent implements OnInit, OnDestroy {
      private subs = new SubSink()
      currentTransaction: ITransaction
      constructor(
        private transactionService: TransactionService,
        private uiService: UiService
      ) {}
       ngOnInit() {
        this.currentTransaction = {
          paymentAmount: 25.78,
          paymentType: TransactionType.Credit,
        } as ITransaction
      }
      ngOnDestroy() {
        this.subs.unsubscribe()
      }
    ... 
    

    在发起服务调用之前创建checkout函数以调用checkoutInitiated

    使用setTimeout模拟假交易并在超时结束时调用checkoutCompleted事件:

    **src/app/pos/pos/pos.component.ts**
    export class PosComponent implements OnInit {
    ...
      checkout(transaction: ITransaction) {
        this.uiService.showToast(‘Checkout initiated’)
        dataLayer.push({
          event: ‘checkoutInitiated’,
        })
        this.subs.sink = this.transactionService
          .processTransaction(transaction)
          .pipe(
            filter((tx) => tx != null || tx !== undefined),
            tap((transactionId) => {
              this.uiService.showToast(‘Checkout completed’)
              dataLayer.push({
                event: ‘checkoutCompleted’,
              })
            })
          )
          .subscribe()
    } 
    

为了防止在分析收集过程中丢失任何数据,请考虑覆盖失败情况,例如添加多个checkoutFailed事件,以涵盖各种失败情况。

现在,我们已经准备好看到分析的实际应用:

运行您的应用。

在 POS 页面上,点击结账按钮。

在 Google Analytics 中,观察实时 | 事件标签以查看事件发生的情况。

5-10 分钟后,事件也会在行为 | 事件标签下显示,如下所示:

图 14.23:Google Analytics 顶级事件

使用自定义事件,您可以跟踪您网站上发生的各种细微的用户行为。通过收集checkoutInitiatedcheckoutCompleted事件,您可以计算有多少发起的结账被完成,从而计算转化率。在销售点系统中,这个比率应该非常高;否则,这可能意味着您可能存在系统性的问题。

高级分析事件

在结账开始时,可以收集与每个事件相关的附加元数据,例如支付金额或类型,或者当结账完成时的transactionId

要使用这些更高级的功能,我建议您查看angulartics2,它可以在www.npmjs.com/package/angulartics2找到。angulartics2是一个对供应商无关的 Angular 分析库,可以使用流行的供应商,如 Google Tag Manager、Google Analytics、Adobe、Facebook、百度等,如工具主页上所强调的,如下所示:

图 14.24:Angulartics2

angulartics2与 Angular 路由和 UI 路由集成,能够在每个路由的基础上实现自定义规则和异常。该库使得实现自定义事件和通过数据绑定进行元数据跟踪变得容易。

查看以下示例:

<div angulartics2On="click" angularticsEvent="DownloadClick"
  angularticsCategory="{{ song.name }}"
  [angularticsProperties]="{label: 'Fall Campaign'}">
</div> 

我们可以跟踪一个名为DownloadClickclick事件,该事件将附加一个category和一个label,以便在 Google Analytics 中进行丰富的事件跟踪。

在掌握高级分析之后,您可以使用实际的使用数据来指导您如何改进或托管您的应用。这个主题结束了一个旅程,这个旅程始于本书开头创建铅笔草图,涵盖了全栈网络开发者必须熟悉的各种工具、技术和技术,以便在今天的网络中取得成功。我们深入研究了 Angular、Angular Material、Docker 和自动化,以使您成为最富有生产力的开发者,交付最高质量的网络应用,同时在这个过程中处理大量的复杂性。祝您好运!

摘要

在本章中,你完善了你对开发 Web 应用程序的知识。你学习了如何使用 Google Tag Manager 和 Google Analytics 来捕获你的 Angular 应用程序的页面浏览量。使用高级指标,我们讨论了你可以如何计算每用户的 Infrastructure 成本。然后我们探讨了高可用性和扩展对预算的影响的细微差别。我们涵盖了复杂用户工作流程的负载测试,以估计任何给定服务器可以同时托管多少用户。使用这些信息,我们计算了目标服务器利用率以微调你的扩展设置。

我们的所有预发布计算基本上都是估计和有根据的猜测。我们讨论了你可以用来衡量你应用程序实际使用的各种指标和自定义事件。当你的应用程序上线并开始收集这些指标时,你可以更新你的计算以更好地理解你基础设施的可行性和成本效益。

恭喜!你已经完成了你的旅程。我希望你玩得开心!请随意使用这本书作为参考,包括附录。

关注我的 Twitter @duluca 并关注 expertlysimple.io 以获取更新。

进一步阅读

  • Google Analytics 和 Google Tag Manager 博客,由 Simo Ahava 提供:www.simoahava.com

问题

尽可能好地回答以下问题,以确保你已理解本章的关键概念,而无需使用 Google。你需要帮助回答这些问题吗?请参阅 附录 D自我评估答案,在线位于 static.packt-cdn.com/downloads/9781838648800_Appendix_D_Self-Assessment_Answers.pdf 或访问 expertlysimple.io/angular-self-assessment

  1. 负载测试的好处是什么?

  2. 关于可靠的云扩展,有哪些考虑因素?

  3. 测量用户行为的价值是什么?

附录 A

Angular 调试

"一个表述清晰的问题已经解决了一半。" 20 世纪初通用汽车公司研究部门负责人查尔斯·凯特林(Charles Kettering)曾说过,为了有效地找到解决问题的方法,你必须首先能够很好地解释它。换句话说,你必须首先投入时间去理解问题是什么,当你做到了这一点,你就已经解决了一半的问题。

有效的调试对于理解为什么或如何你的软件失败至关重要。与使用console.log相比,有更好的方法来调试你的 JavaScript 代码。本附录将介绍各种工具和技术,以介绍断点调试和可以帮助你更好地理解应用程序状态的浏览器扩展。

在本附录中,我们涵盖了以下内容:

  • 最有用的快捷键

  • 浏览器中的错误排查

  • Karma、Jasmine 和单元测试错误

  • 使用开发者工具进行调试

  • 使用 VS Code 进行调试

  • 使用 Angular Augury 进行调试

  • 使用 Redux DevTools 进行调试

  • RxJS 调试

让我们从学习一个将使你更加高效的生产力快捷键开始。

最有用的快捷键

在不熟悉或大型代码库中找到自己的位置可能会很困难,令人困惑,甚至令人烦恼。有一个键盘快捷键可以解决这个问题,这个快捷键在多个工具中共享,如 VS Code 和 Chrome/Edge 开发者工具(dev tools)。

要在 VS Code 或开发者工具中的面板中搜索并打开文件,请使用以下快捷键:

在 macOS 上:图片 + P

在 Windows 上:Ctrl + P

你会很快发现,这将是你最常使用的快捷键。

浏览器中的错误排查

在本节中,你将故意引入一个容易犯的错误,这样你就可以熟悉在开发应用程序时可能发生的真实错误,并深入了解使你成为有效开发者的工具。

请参考第四章自动化测试、持续集成和发布到生产,以及 LocalCast 天气应用,以更好地理解以下代码示例。

LocalCast 天气应用的最新版本可以在 GitHub 上找到:github.com/duluca/local-weather-app

让我们假设我们在从OpenWeatherMap.org的 API 文档页面复制粘贴 URL 时犯了一个无辜的错误,并且忘记在前面添加http://。这是一个容易犯的错误:

**src/app/weather/weather.service.ts**
...
return this.httpClient
  .get<ICurrentWeatherData>(
`api.openweathermap.org/data/2.5/weather?q=${city},${country}&appid=${environment.appId}`
  ).pipe(map(data => this.transformToICurrentWeather(data)))
... 

你的应用将成功编译,但当你检查浏览器中的结果时,你不会看到任何天气数据。实际上,看起来CurrentWeather组件根本就没有渲染,正如你在以下图片中可以看到的:

图片

图 1:CurrentWeather 无法渲染

为了找出原因,你需要调试你的 Angular 应用。

利用浏览器开发者工具

作为一名开发者,我使用 Edge 或 Google Chrome 浏览器,因为它们具有跨平台和一致的开发者工具,以及有用的扩展。

作为最佳实践,我使用 VS Code 和浏览器并排编码,同时开发工具也在浏览器中打开。有多个很好的理由来实践并排开发:

  • 快速反馈循环:使用实时重新加载,你可以非常快地看到你更改的最终结果

  • 笔记本电脑:现在很多开发者都在笔记本电脑上进行大部分的开发工作,第二块显示器是一种奢侈

  • 关注响应式设计:由于我的工作空间有限,我始终关注移动优先开发,在之后修复桌面布局问题

  • 关注网络活动:使我能够快速看到任何 API 调用错误,并确保请求的数据量符合我的预期

  • 关注控制台错误:使我能够快速反应并解决当引入新错误时

  • 禁用缓存:这样你知道你总是得到所有的更改,而不是与浏览器的缓存作斗争

观察并排开发的样貌:

图片

图 2:运行实时重新加载的并排开发

最终,你应该做最适合你的事情。在使用并排设置的情况下,我经常发现自己需要打开和关闭 VS Code 的资源管理器,并根据具体任务调整开发工具窗格的大小。要切换 VS Code 的资源管理器,请点击前一张截图中圈出的资源管理器图标。

就像你可以使用npm start进行带有实时重新加载的并排开发一样,你也可以使用npm test获得相同类型的快速反馈循环,用于单元测试:

图片

图 3:单元测试并行的开发

使用并排单元测试设置,你可以成为开发单元测试的高效者。

优化开发工具

为了使带有实时重新加载的并排开发工作得很好,你需要优化默认的开发工具体验:

图片

图 4:优化后的 Chrome 开发者工具

观察前一张截图,你会注意到许多设置和信息发射器被突出显示:

  1. 默认打开网络选项卡,以便你可以看到网络流量流动。

  2. 通过点击 按钮打开开发工具设置。

  3. 点击右侧图标,以便开发工具停靠在 Chrome 的右侧。这种布局提供了更多的垂直空间,因此你可以一次性看到更多的网络流量和控制台事件。作为额外的好处,左侧大致呈现移动设备的尺寸和形状。

  4. 开启大请求行并关闭概览,以便看到每个请求的更多 URL 和参数,并获得更多垂直空间。

  5. 选择禁用缓存选项,这将强制在开发工具打开时刷新页面时重新加载每个资源。这可以防止奇怪的缓存错误破坏你的日子。

  6. 你主要会关注查看对各种 API 的 XHR 调用,因此点击XHR以过滤结果。

  7. 注意,你可以在右上角查看控制台错误的数量为12。理想情况下,控制台错误的数量应该始终为 0。

  8. 注意,请求行中的顶部条目表明存在状态码为404 Not Found的错误。

  9. 由于我们正在调试 Angular 应用程序,已经加载了Augury扩展。这个工具将在本章后面更详细地介绍,使用更复杂的 LemonMart 应用程序。

在你的优化开发工具环境中,你现在可以有效地调试和解决之前的应用程序错误。

故障排除网络问题

在这个阶段,应用程序存在三个可见问题:

  • 组件详情未显示

  • 存在许多控制台错误

  • API 调用返回404 Not Found错误

首先检查任何网络错误,因为网络错误通常会导致连锁反应:

  1. 网络选项卡中点击失败的 URL

  2. 在 URL 右侧打开的详细信息面板中,点击预览选项卡

  3. 你应该看到这个:

    Cannot GET /api.openweathermap.org/data/2.5/weather 
    

    仅通过观察这个错误信息,你可能会错过忘记添加http://前缀到 URL 的事实。这个错误很微妙,绝对不是显而易见的。

  4. 将鼠标悬停在 URL 上,观察完整的 URL,如图所示:完整 URL 截图

    图 5:检查网络错误

如你所见,现在错误非常明显。在这个视图中,我们可以看到完整的 URL,并且很明显,weather.service.ts中定义的 URL 不是完全限定的,因此 Angular 正在尝试从其父服务器(localhost:5000)加载资源,而不是通过网络到正确的服务器。

调查控制台错误

在修复这个问题之前,了解失败的 API 调用的连锁反应是值得的:

  1. 观察控制台错误:控制台错误截图

    图 6:开发者工具控制台错误上下文

    这里值得注意的第一个元素是名为ERROR CONTEXT的对象,它有一个名为DebugContext_的属性。DebugContext_包含错误发生时 Angular 应用程序当前状态的详细快照。DebugContext_中包含的信息比 AngularJS 生成的几乎无用的错误信息多得多。

    值为(...)的属性是属性获取器,你必须点击它们来加载它们的详细信息。例如,如果你点击componentRenderElement的省略号,它将被app-current-weather元素填充。你可以展开元素来检查组件的运行时条件。

  2. 现在滚动到控制台顶部

  3. 观察第一个错误:

    ERROR TypeError: Cannot read property 'city' of undefined 
    

你可能之前遇到过TypeError。这种错误是由于尝试访问未定义的对象属性而引起的。在这种情况下,CurrentWeatherComponent.current因为没有 HTTP 调用失败而没有分配给一个对象。由于current未初始化,模板盲目地尝试绑定到其属性,如{{current.city}},我们得到一条消息说属性'city'的未定义无法读取。这就是可能在你应用程序中产生许多不可预测副作用的那种连锁反应。你必须积极编码以防止这种情况发生。

Karma、Jasmine 和单元测试错误

当使用ng test命令运行测试时,你可能会遇到一些高级错误,这些错误可能会掩盖实际潜在错误的根本原因。

解决错误的通用方法应该是从内到外,首先解决子组件的问题,最后留下父组件和根组件。

网络错误

网络错误可能由多种潜在问题引起:

NetworkError: Failed to execute 'send' on 'XMLHttpRequest': Failed to load 'ng:///DynamicTestModule/AppComponent.ngfactory.js'. 

从内到外工作,你应该实现服务的测试双倍,并向适当的组件提供伪造品,如前一小节所述。然而,即使在父组件中正确提供了伪造品,你也可能仍然会遇到错误。请参阅处理通用错误事件的章节,以揭示潜在问题。

通用错误事件

错误事件是隐藏潜在原因的通用错误:

[object ErrorEvent] thrown 

为了暴露通用错误的根本原因,实现一个新的test:debug脚本:

  1. 如下所示,在package.json中实现test:debug

    **package.json**
    ...
      "scripts": {
      ...
      "test:debug": "ng test --source-map",
      ...
    } 
    
  2. 执行npm run test:debug

  3. 现在,Karma 运行器可能会揭示根本问题

  4. 如果需要,根据堆栈跟踪找到可能引起问题的子组件

    如果这种策略不起作用,你可能可以通过断点调试你的单元测试来获取更多关于错误原因的信息。

使用开发工具进行调试

关于是否将console.log输出到控制台;这是一个问题。记录在案,让我声明,console.log语句永远不会被提交到你的仓库。一般来说,它们是浪费时间,因为它们需要编辑、构建和运行代码来带来价值,更不用说清理代码的成本了。

调试的首选方法是断点调试,这是一种暂停代码执行的方法,在代码运行时检查和操作其状态。你可以有条件地设置断点,逐行遍历你的代码,甚至可以在控制台中执行语句来尝试新想法。

Angular 9 和 Ivy 带来了许多调试改进,使得调试异步代码和模板成为可能。此外,Angular 9 生成的堆栈跟踪在确定错误根本原因方面要远比以前更有用。

有一些特殊用例,其中console.log语句可能很有用。这些主要是并行操作且依赖于及时用户交互的异步工作流程。在这些情况下,控制台日志可以帮助你更好地理解事件流和各个组件之间的交互。你可以在本章后面的调试 RxJS部分看到这一点。

对于常见情况,我们应该坚持使用断点调试。使用开发者工具,我们可以观察属性在被设置时的状态,并且能够即时更改它们的值,以强制代码在if-elseswitch语句中执行分支逻辑。

假设HomeComponent上存在一些基本逻辑,它根据从AuthService检索到的isAuthenticated值设置一个displayLogin boolean,如下所示:

**src/app/home/home.component.ts**
...
import { AuthService } from '../auth.service'
...
export class HomeComponent implements OnInit {
  displayLogin = true
  constructor(private authService: AuthService) {}
  ngOnInit() {
    this.displayLogin = !this.authService.isAuthenticated()
  }
} 

现在观察displayLogin的值和isAuthenticated函数在被设置时的状态,然后观察displayLogin值的改变:

  1. 切换到开发者工具中的标签

  2. 使用最有用的快捷键,Ctrl + P图片 + P,搜索HomeComponent

  3. ngOnInit函数内的第一行设置一个断点

  4. 刷新页面

  5. 你会看到你的断点被触发,如这里用蓝色突出显示的图片

    图 7:Chrome 开发者工具断点调试

  6. 悬停在this.displayLogin上并观察其值被设置为true

  7. 如果你悬停在this.authService.isAuthenticated()上,你将无法观察到其值

    当你的断点被触发时,你可以在控制台中访问当前的状态作用域,这意味着你可以执行函数并观察其值。

  8. 在控制台执行isAuthenticated()

    > this.authService.isAuthenticated() true 
    

    你会观察到它返回true,这正是this.displayLogin被设置的值。你仍然可以在控制台中强制改变displayLogin的值。

  9. displayLogin设置为false

    > this.displayLogin = false false 
    

如果你观察displayLogin的值,无论是通过悬停在其上还是从控件中检索它,你会发现该值被设置为false

利用断点调试的基础知识,你可以调试复杂场景而无需更改源代码。你也可以调试模板以及复杂的回调,使用 RxJS 语句。

使用 Visual Studio Code 进行调试

你也可以直接在 Visual Studio Code 中调试你的 Angular 应用程序、Karma 和 Protractor 测试。首先,你需要配置调试器以与 Chrome 调试环境一起工作,如图所示:

图片

图 8:VS Code 调试设置

  1. 点击调试面板

  2. 展开无配置下拉菜单并点击添加配置...

  3. 选择环境复选框中,选择Chrome

    这将在.vscode/launch.json文件中创建一个默认配置。我们将修改此文件以添加三个单独的配置。

  4. launch.json的内容替换为以下配置:

    **.vscode/launch.json**
    {
      "version": "0.2.0",
      "configurations": [
        {
          "name": "Debug npm start with Chrome",
          "type": "chrome",
          "request": "launch",
          "url": "http://localhost:5000/#",
          "webRoot": "${workspaceRoot}",
          "runtimeArgs": [
            "--remote-debugging-port=9222"
          ],
          "sourceMaps": true,
          "preLaunchTask": "npm: start"
        },
        {
          "name": "Debug npm start with Edge",
          "type": "edge",
          "request": "launch",
          "version": "dev",
          "url": "http://localhost:5000/#",
          "webRoot": "${workspaceRoot}",
          "sourceMaps": true,
          "preLaunchTask": "npm: start"
        },
        {
          "name": "Debug npm test with Chrome",
          "type": "chrome",
          "request": "launch",
          "url": "http://localhost:9876/debug.html",
          "webRoot": "${workspaceRoot}",
          "runtimeArgs": [
            "--remote-debugging-port=9222"
          ],
          "sourceMaps": true,
          "preLaunchTask": "npm: test"
        },
        {
          "name": "Debug npm test with Edge",
          "type": "edge",
          "request": "launch",
          "version": "dev",
          "url": "http://localhost:9876/debug.html",
          "webRoot": "${workspaceRoot}",
          "sourceMaps": true,
          "preLaunchTask": "npm: test"
        },
        {
          "name": "npm run e2e",
          "type": "node",
          "request": "launch",
          "program": "${workspaceRoot}/node_modules/protractor/bin/protractor",
          "protocol": "inspector",
          "args": [
            "${workspaceRoot}/protractor.conf.js"
          ]
        }
      ]
    } 
    

    注意,我们还为微软基于 Chromium 的新 Edge 浏览器添加了调试器。

  5. 在开始调试器之前,执行相关的 CLI 命令,如npm startnpm testnpm run e2e

  6. 调试页面,在调试下拉菜单中,选择npm start并点击绿色的播放图标

  7. 观察到一个 Chrome 实例已启动

  8. .ts文件上设置断点

  9. 在应用程序中执行操作以触发断点

  10. 如果一切顺利,Chrome 将报告代码已在 Visual Studio Code 中暂停

    更多信息,请参阅 GitHub 上 VS Code 菜谱的 Angular CLI 部分github.com/Microsoft/vscode-recipes

使用 Angular Augury 进行调试

Augury 是一个用于调试和性能分析的 Chrome DevTools 扩展,它是一个专为帮助开发者可视化导航组件树、检查路由状态以及通过源映射在生成的 JavaScript 代码和开发者编写的 TypeScript 代码之间启用断点调试而设计的工具。

注意,Augury 与 Angular 9 的 Ivy 渲染引擎不完全兼容。为了某些功能(如路由树和 NgModules)能够工作,您需要暂时在项目中禁用 Ivy。

您可以通过更新项目根目录下的tsconfig.app.json来关闭 Ivy,向其中添加以下属性:

"angularCompileOptions": {
  "enableIvy": false
} 

您可以从augury.angular.io下载 Augury。一旦安装,当您为 Angular 应用程序打开 Chrome DevTools 时,您会注意到一个名为 Augury 的新选项卡,如图所示:

图 9:Chrome DevTools Augury

Augury 提供了有用的关键信息,以了解您的 Angular 应用程序在运行时的行为:

  1. 当前 Angular 版本列在下面,在这种情况下,版本为9.1.7

  2. 组件树显示了在应用程序中渲染的所有 Angular 组件

  3. 路由树显示了应用程序中配置的所有路由

  4. NgModules显示了应用程序的AppModule和功能模块

组件树

组件树选项卡显示了所有应用程序组件之间的关系以及它们如何相互交互:

  1. 按如下方式选择特定的组件,例如HomeComponent

    图 10:Augury 组件树

    右侧的属性选项卡将显示一个名为查看源代码的链接,您可以使用它来调试您的组件。进一步向下,您将能够观察到组件属性的状态,例如displayLogin boolean,包括注入到组件中的服务和它们的状态。

    您可以通过双击值来更改任何属性的值。

    例如,如果您想将displayLogin的值更改为false,只需双击包含true值的蓝色框,并输入false。您将能够在 Angular 应用程序中观察到您更改的效果。

    为了观察 HomeComponent 的运行时组件层次结构,你可以观察 注入器图

  2. 点击如图所示的 注入器图 选项卡:

图 11:Augury 注入器图

此视图显示了所选组件是如何被渲染的。在这种情况下,我们可以观察到 HomeComponent 是在 AppComponent 中被渲染的。这种可视化在追踪不熟悉的代码库中特定组件的实现或存在深层组件树的情况下非常有帮助。

路由树

路由树 选项卡将显示路由的当前状态。这可以是一个非常有助于可视化路由和组件之间关系的工具,如图所示:

图 12:Augury 路由树

上述路由树展示了具有主-详细视图的深度嵌套路由结构。你可以通过点击圆形节点来查看渲染给定组件所需的绝对路径和参数。

如你所见,对于 PersonDetailsComponent,确定渲染主-详细视图的这一部分所需的确切参数集可能会变得复杂。

NgModules

NgModules 选项卡显示了 AppModule 和任何其他当前已加载到内存中的功能模块:

  1. 启动应用程序的 /home 路由并在地址栏中按回车键,以便 Augury 注册导航事件

  2. 观察如图所示的 NgModules 选项卡:

    图 13:Augury NgModules

    你会注意到所有根级模块,包括 AppModule,都已经加载。然而,由于我们的应用程序具有懒加载架构,我们还没有加载任何功能模块。

  3. 导航到 ManagerModule 中的一个页面并在地址栏中按回车键

  4. 然后,导航到 UserModule 中的一个页面并在地址栏中按回车键

  5. 最后,导航回 /home 路由并在地址栏中按回车键

  6. 观察如图所示的 NgModules 选项卡:

    图 14:Augury 包含三个模块的 NgModules

  7. 现在,你会注意到 ManagerModuleUserModule 以及所有相关的模块已经被加载到内存中。

NgModules 是一个重要的工具,可以可视化你的设计和架构的影响。

使用 Redux DevTools 进行调试

有两种主要的策略用于调试和从 NgRx 获取仪表化。

  1. 实现一个控制台日志记录器进行调试

  2. 配置 Store DevTools 以进行丰富的仪表化

让我们从简单的调试解决方案开始。

实现 NgRx 控制台日志记录器

app.module 中,StoreModule 被配置为将一个 MetaReducer 注入到你的配置中。元减法器能够监听在动作-减法器管道中发生的所有事件,从而赋予你预处理动作的能力。我们可以使用这个钩子来实现一个简单的日志记录器。

  1. reducers/index.ts 中实现一个名为 logger 的函数:

    **src/app/reducers/index.ts**
    export function logger(reducer: ActionReducer<AppState>): ActionReducer<AppState> {
      return (state, action) => {
        const result = reducer(state, action)
        console.groupCollapsed(action.type)
        console.log('prev state', state)
        console.log('action', action)
        console.log('next state', result)
        console.groupEnd()
        return result
      }
    } 
    
  2. 使用 metaReducers 配置 logger,并且仅在非生产模式下:

    **src/app/reducers/index.ts**
    export const metaReducers: MetaReducer<AppState>[] =
    !environment.production
      ? [logger]
      : [] 
    

现在尝试一下,你应该能在你的控制台中观察到 NgRx,如下所示:

图 15:带有 NgRx 日志的控制台视图

配置 NgRx Store DevTools

NgRx Store Devtools 包也可以在开发过程中协助我们的调试工作,或者在生成生产构建时提供仪表化。通过运行以下命令添加该包:

$ npx ng add @ngrx/store-devtools 

你会注意到该包会自动在app.module中添加生产仪表化规则,以便只捕获最后 25 个事件。这是为了避免性能问题。

一旦安装,为了利用生成的仪表化并能够调试 NgRx,你将需要安装 Chrome 或 Firefox 的 Redux DevTools 扩展,可以在github.com/zalmoxisus/redux-devtools-extensionextension.remotedev.io找到。

一旦启动你的应用程序,激活扩展,你应该能看到 Redux DevTools 随着时间的推移正在捕获详细的仪表化,如下所示:

图 16:Redux DevTools

Redux DevTools 为你提供了回放事件和查看状态变化的能力。这在上一个截图的右下象限中得到了演示。你可以观察到当前城市为布尔萨,其前一个值为贝塞斯达

调试 RxJS

调试 RxJS 管道内部发生情况的主要策略有两个:

  1. 深入事件流并记录特定点的流事件数据

  2. 在开发者工具中执行断点调试

让我们从使用tap操作符开始。

深入 RxJS 事件流

第六章表单、可观察对象和主题中,我们介绍了 RxJS 的tap操作符,作为将用户输入从搜索输入的更改事件流中引导出来的方式,并最终调用我们的doSearch函数。当 RxJS 流似乎没有按预期行为时,你可以结合tap操作符和console.log来记录每个事件的详细数据,这样你就可以随着时间的推移看到它。由于tap根据操作顺序中的位置捕获流中的数据,一旦添加到流中,你就可以简单地使用 VS Code 的行移动键盘快捷键来移动它并测试流程。

要移动代码行上下,在 Windows 上使用Alt + Alt + ,在 macOS 上使用![](https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/09919dd6569643e9b7010ba94fd77494~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1773212829&x-signature=3w9WahsEPyJ081osZoedle58AkE%3D) + ![](https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/09919dd6569643e9b7010ba94fd77494~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1773212829&x-signature=3w9WahsEPyJ081osZoedle58AkE%3D) +

CitySearchComponent中的以下tap将记录来自输入的每个更改事件:

this.search.valueChanges 
  .pipe( 
 **tap(console.log),**
    debounceTime(1000), 
    filter(() => !this.search.invalid), 
    tap((searchValue: string) => this.doSearch(searchValue)) 
  ).subscribe() 

图 17:RxJS 记录每个事件

如果我们将tap向下移动一行,那么我们不会得到每个用户输入,而只会得到防抖事件:

this.search.valueChanges 
  .pipe( 
    debounceTime(1000), 
    **tap(console.log),** 
    filter(() => !this.search.invalid), 
    tap((searchValue: string) => this.doSearch(searchValue)) 
  ).subscribe() 

图 18:RxJS 仅记录防抖事件

再向下移动一行,我们看到经过防抖和过滤后的事件:

this.search.valueChanges 
  .pipe( 
    debounceTime(1000), 
    filter(() => !this.search.invalid), 
    **tap(console.log),** 
    tap((searchValue: string) => this.doSearch(searchValue)) 
  ).subscribe() 

图 19:RxJS 忽略无效输入

注意,由于一个字符无效,过滤器捕获了这个事件并没有让它通过,因此我们在控制台中看不到任何数据。

虽然在控制台中可能会非常混乱,但你也可以同时记录很多东西,使用回调而不是仅仅传递console.log可调用函数:

this.search.valueChanges 
  .pipe( 
    debounceTime(1000), 
    tap(debouncedData => 
      console.log('debounced: ', debouncedData)
    ), 
    filter(() => !this.search.invalid), 
    tap(debouncedAndFilteredData => 
      console.log(
        'debounced + filtered: ', 
        debouncedAndFilteredData
      )
    ), 
    tap((searchValue: string) => this.doSearch(searchValue)) 
  ).subscribe() 

图 20:RxJS 记录多个事件

接下来,让我们看看如何利用断点调试。

断点调试 RxJS 事件流

参考本章前面的使用 DevTools 进行调试部分,了解更多关于断点调试的信息。在调试 RxJS 时,重要的是要理解调试器中蓝色光标的作用。

当一行代码有多个可以暂停执行的位置时,这些位置会用方形光标表示。这些光标可以是开启的(深色,实心)或关闭的(浅色,透明),以指示你希望在代码行的哪个位置让浏览器停止,如下面的截图所示:

图 21:RxJS 断点调试

光标可以用来在浏览器中停止流,在执行工作的回调函数内部,以检查数据或甚至操作它。在上面的例子中,我已经将断点调试器设置为在过滤器函数内部中断,这样我就可以检查搜索输入字段的当前状态。

通过实验调试来了解更多相关信息。

进一步的建议

如果你仍然遇到困难,不要沮丧。即使是最好的人有时也会在小事上花费无数小时。然而,如果你需要,你可以寻求帮助。Angular 在网上有一个丰富且支持性强的社区。

你寻求帮助的方式非常重要。首先使用stackblitz.com/复现你的问题。一半的时间,你会在复现问题的过程中找到你的问题。接下来,在stackoverflow.com上提问。如果你能提供 StackBlitz 链接到你的问题,你的问题很可能会得到快速解答。如果最终发现可能是框架本身的问题,那么在正确的存储库(换句话说,Angular、CLI、Material、NgRx、RxJS 等)上创建一个 GitHub 问题,详细说明你的问题。如果你在框架中发现了 bug,那么你就正式为开源项目的发展做出了贡献:

否则,你也可以使用 Twitter 并搜索@angular#angular标签以寻求帮助。我个人是 Google 的大粉丝。我的哲学是,别人可能遇到了和我一样的问题,当我说这句话的时候——一个良好的 Google 查询就是问题解决了一半。

附录 B

Angular 快速参考

这里有一份快速参考表,帮助你熟悉常见的 Angular 语法和 CLI 命令。花点时间复习并熟悉新的 Angular 语法、主要组件、CLI 框架和常见管道。

如果你之前使用过 AngularJS,你会发现这个列表特别有用,因为你需要学习一些旧的语法。

如果你刚开始使用 Angular 或者不是 CLI 命令的粉丝,可以查看 Nx Console 在 nx.dev/angular/cli/console,这是一个出色的桌面应用程序,也是 Visual Studio Code 的扩展,它可以为你编写 CLI 参数。话虽如此,我仍然建议你首先熟悉 CLI 命令,并强迫自己使用它们一段时间,这样你就能更好地理解 Nx Console 的工作原理。

绑定,或数据绑定,指的是代码中的变量与在 HTML 模板或其他组件中显示或输入的值之间自动的单一或双向连接:

类型语法数据方向
插值属性属性类样式{{expression}} [target]="expression" bind-target="expression"单向,从数据源到视图目标
事件(target)="statement" on-target="statement"单向,从视图目标到数据源
双向[(target)]="expression" bindon-target="expression"双向

内置指令

指令封装了可以应用于 HTML 元素或其他组件的编码行为:

名称语法目的
结构指令*ngIf``*ngFor``*ngSwitch控制 HTML 的结构布局以及元素是否被添加或从 DOM 中移除
属性指令[class] [style] [(model)]监听并修改其他 HTML 元素、属性、属性和组件的行为,例如 CSS 类、HTML 样式和 HTML 表单元素

常见管道

管道(在 AngularJS 中称为过滤器)修改了数据绑定值在 HTML 模板中的显示方式:

名称目的用法
异步管理对可观察对象的订阅,并提供对模板中变量的同步访问someVariable$ &#124; async as someVariable
日期根据区域规则格式化日期{{date_value &#124; date[:format]}}
文本转换将文本转换为大写、小写或标题格式{{value &#124; uppercase}}``{{value &#124; lowercase}}``{{value &#124; titlecase }}
小数根据区域规则格式化数字{{number &#124; number[:digitInfo]}}
百分比根据区域规则格式化数字为百分比{{number &#124; percent[:digitInfo]}}
货币根据区域规则使用货币代码和符号格式化数字为货币{{number &#124; currency [:currencyCode [:symbolDisplay [:digitInfo]]]}}

启动命令,主要组件和 CLI 框架

启动命令帮助生成新项目或添加依赖。Angular CLI 命令通过自动生成样板代码轻松创建主要组件。要查看完整命令列表,请访问 github.com/angular/angular-cli/wiki

启动命令

这里是最基本的命令,您可能会随着时间的推移记住并频繁使用。请记住,不要像在 第三章创建基本的 Angular 应用程序 中所述那样全局安装 @angular/cli

名称用途CLI 命令
新建创建一个新的 Angular 应用程序,包含初始化的 Git 仓库、package.json,已配置路由并启用 Ivy。从父文件夹运行。npx @angular/cli new project-name --routing
更新更新 Angular、RxJS 和 Angular Material 依赖项。如有必要,重写代码以保持兼容性。npx ng update
安装 Angular Material安装并配置 Angular Material 依赖项。npx ng add @angular/material

主要组件模板

在您的日常工作流程中使用以下命令添加新组件、服务和其他主要组件到您的 Angular 应用程序中。这些命令将为您节省大量时间并帮助您避免简单的配置错误:

名称用途CLI 命令
模块创建一个新的 @NgModule 类。使用 -- routing 为子模块添加路由。可选地,使用 --module 将新模块导入父模块。ng g module my-module ng g m my-module
组件创建一个新的 @Component 类。使用 -- module 指定父模块。可选地,使用 --flat 跳过目录创建,-t 用于内联模板,-s 用于内联样式。ng g component my-component ng g c my-component
指令创建一个新的 @Directive 类。可选地,使用 --module 将指令作用域限定为指定的子模块。ng g directive my-directive ng g d my-directive
管道创建一个新的 @Pipe 类。可选地,使用 --module 将管道作用域限定为指定的子模块。ng g pipe my-pipe ng g p my-pipe
服务创建一个新的 @Injectable 类。使用 --module 为指定的子模块提供服务。服务不会自动导入到模块中。可选地使用 --flat false 在目录下创建服务。ng g service my-service ng g s my-service
守卫创建一个新的 @Injectable 类,该类实现了路由生命周期钩子 CanActivate。使用 --module 为指定的子模块提供守卫。守卫不会自动导入到模块中。ng g guard my-guard ng g g my-guard

为了正确地在一个自定义模块(如my-module)下搭建一些之前列出的组件,你可以在你打算生成的名称之前添加模块名称,例如,ng g c my-module/my-new-component。Angular CLI 将正确连接并将新组件放置在my-module文件夹下。

TypeScript 搭建

如果你不太熟悉 TypeScript 语法,这些 TypeScript 特定的搭建将帮助你创建类、接口和枚举,这样你就可以利用面向对象编程原则来减少代码重复,并在类中而不是在组件中实现代码行为,如计算属性:

名称用途CLI 命令
创建一个基本类ng g class my-class
接口创建一个基本接口ng g interface my-interface
枚举创建一个基本枚举ng g enum my-enum

常见的 RxJS 函数/操作符

为了成为一名有效的 Angular 开发者,你需要成为 RxJS 的大师。以下是一些最常见和有用的 RxJS 操作符,供快速参考:

函数

名称用途
pipe接受一个或多个可观察对象作为输入,并生成一个可观察对象作为输出,允许你构建自定义数据流。
subscribe必须激活一个可观察对象。从subscribe操作中提取可观察数据流的值是一种反模式。可以使用异步管道或tap函数来检查或使用当前值。
unsubscribe释放资源并取消可观察对象的执行。不取消订阅可能导致性能问题和内存泄漏。使用异步管道或SubSink库来管理订阅。

操作符

名称用途
of将提供的值转换为可观察序列。对于将同步代码集成到可观察数据流中非常有用。
from从数组、可迭代对象或承诺创建一个可观察对象。
map允许你遍历可观察对象发出的每个值。
merge创建一个输出可观察对象,同时并发地发出所有给定输入可观察对象的所有值。对于基于多个可观察对象触发操作非常有用。
combineLatest将多个可观察对象中的值与每个可观察对象的最新值组合。当与merge操作符一起使用时非常有用。
filter过滤数据流中的值。用于忽略 null 值或仅在满足某些条件时执行管道的其余部分。
concat顺序地从一个或多个可观察对象中发出值。对于同步多个操作非常有用。类似concatMap的变体也可以扁平化可观察对象,这对于遍历集合的值非常有用。
take给定一个计数,在消耗指定次数后自动完成可观察对象。
catchError捕获可观察对象上的错误,通过返回一个新的可观察对象或抛出一个错误来处理。
scan使用累加器函数,它可以增量地处理数据。也就是说,随着数字的增加,可以得到一个运行的小计。这对于需要更新的长时间运行的操作非常有用。

感谢 Jan-Niklas Wortmann 对本节的审阅。在 Twitter 上关注他:@niklas_wortmann

进一步阅读