如何创建和部署 Angular Material 应用程序

已发表: 2022-03-10
快速总结↬在 Netlify 上创建完全基于 Angular 的 Angular 8 Web 应用程序和 QR 码生成器应用程序的演练。

Angular 是创建新 Web 应用程序时的流行选择之一。 此外,“材料设计”规格已成为当今创造极简且引人入胜的体验的首选。 因此,任何新的“Angular”项目大多使用“Angular Material Design Library”来使用遵循材料设计规范的组件。 从流畅的动画到适当的交互反馈,所有这些都已作为 Angular 官方材料设计库的一部分提供。

开发 Web 应用程序后,下一步就是部署它。 这就是“Netlify”出现的地方。 凭借其非常易于使用的界面、自动部署、用于 A/B 测试的流量拆分和各种其他功能,Netlify 无疑是一个很棒的工具。

本文将介绍使用官方 Angular Material Design 库创建 Angular 8 Web 应用程序。 我们将在 Netlify 上创建一个完全基于 Angular 的 QR 码生成器 Web 应用程序。

本教程的文件可以在 GitHub 上找到,这里部署了一个演示版本。

入门

  1. 安装 Angular 8,
  2. 创建一个 GitHub 帐户,
  3. 在您的计算机上安装 Git,
  4. 创建一个 Netlify 帐户。

注意我将使用 VSCode 和 Microsoft Windows 作为首选的 IDE 和操作系统,尽管对于任何其他操作系统上的任何其他 IDE,这些步骤都是相似的。

完成以上先决条件后,开始吧!

跳跃后更多! 继续往下看↓

模拟与规划

在我们开始创建项目之前,提前计划是有益的:我们希望在我们的应用程序中使用什么样的 UI? 会有可重复使用的部件吗? 应用程序将如何与外部服务交互?

首先,检查 UI 模拟。

主页(大预览)
创建二维码页面(大预览)
历史页面(大预览)

这些是将包含在应用程序中的三个不同页面。 主页将是我们应用程序的起点。 创建 QR 页面应处理新 QR 码的创建。 历史页面将显示所有保存的二维码。

模型不仅提供了应用程序的外观和感觉,而且还分离了每个页面的职责。

一个观察结果(来自模拟)是顶部导航栏似乎在所有页面中都很常见。 因此,导航栏可以创建为可重用组件并重用。

现在我们已经对应用程序的外观以及可以重用的内容有了一定的了解,让我们开始吧。

创建一个新的 Angular 项目

启动 VSCode,然后在 VSCode 中打开一个终端窗口以生成一个新的 Angular 项目。

VSCode 中的终端(大预览)

终端将使用提示中显示的默认路径打开。 您可以在继续之前更改为首选目录; 在 Windows 的情况下,我将使用cd命令。

导航到首选路径(大预览)

展望未来,angular-cli 有一个命令来生成新项目ng new <project-name> 。 只需使用您喜欢的任何花哨的项目名称,然后按 Enter,例如ng new qr

这将触发 angular-cli 魔法; 它将提供一些选项来配置项目的某些方面,例如,添加角度路由。 然后,根据选择的选项,它将生成整个项目骨架,无需任何修改即可运行。

对于本教程,输入Yes作为路由并选择CSS作为样式。 这将生成一个新的 Angular 项目:

创建一个新的 Angular 项目(大预览)

我们现在已经有了一个完整的 Angular 项目。 为了确保一切正常,我们可以通过在终端中输入以下命令来运行项目: ng serve 。 哦,但是等等,这会导致错误。 可能发生了什么?

ng 服务错误(大预览)

别担心。 每当您使用 angular-cli 创建一个新项目时,它都会在以ng new qr命令中指定的项目名称命名的文件夹中生成整个骨架。 在这里,我们必须将当前工作目录更改为刚刚创建的目录。 在 Windows 中,使用命令cd qr更改目录。

现在,尝试在ng serve的帮助下再次运行该项目:

项目运行(大预览)

打开 Web 浏览器,转到 URL https://localhost:4200 以查看正在运行的项目。 默认情况下,命令ng serve在端口 4200 上运行应用程序。

提示要在不同的端口上运行它,我们使用命令ng serve --port <any-port>例如, ng serve --port 3000

这可以确保我们的基本 Angular 项目启动并运行。 让我们继续前进。

我们需要将项目文件夹添加到 VSCode。 转到“文件”菜单并选择“打开文件夹”并选择项目文件夹。 项目文件夹现在将显示在左侧的 Explorer 视图中。

添加角度材料库

要安装 Angular 材质库,请在终端窗口中使用以下命令: ng add @angular/material 。 这将(再次)询问一些问题,例如您想要哪个主题,是否需要默认动画,是否需要触摸支持等等。 我们将只选择默认的Indigo/Pink主题, Yes添加HammerJS库和浏览器动画。

添加 Angular 材质(大预览)

上述命令还配置了整个项目以启用对材料组件的支持。

  1. 它将项目依赖项添加到package.json
  2. 它将 Roboto 字体添加到index.html文件中,
  3. 它将 Material Design 图标字体添加到您的index.html
  4. 它还添加了一些全局 CSS 样式:
    • 去除身体的边缘,
    • 设置height: 100%进入 HTML 和正文,
    • 将 Roboto 设置为默认应用程序字体。

为了确保一切正常,此时您可以再次运行该项目,尽管您不会注意到任何新内容。

添加主页

我们的项目骨架现在已经准备好了。 让我们从添加主页开始。

(大预览)

我们想让我们的主页保持简单,就像上图一样。 这个主页使用了一些有角度的材质组件。 让我们剖析一下。

  1. 顶部栏是一个简单的 HTML nav元素,其中包含材质样式按钮mat-button ,其子元素为图像和文本。 条形颜色与添加 Angular 材质库时选择的原色相同;
  2. 居中的图像;
  3. 另一个mat-button ,只有一个文本作为它的子元素。 这个按钮将允许用户导航到历史页面;
  4. 一个计数徽章, matBadge ,附在上面的按钮上,显示用户保存的二维码数量;
  5. 右下角的浮动操作按钮mat-fab具有所选主题的强调色。

离题一点,让我们先添加其他必需的组件和服务。

添加标题

正如之前计划的那样,导航栏应该被重用,让我们将它创建为一个单独的角度组件。 在 VSCode 中打开终端并输入ng gc header (ng generate component header 的缩写),然后按 Enter。 这将创建一个名为“header”的新文件夹,其中包含四个文件:

  • header.component.css :用于为该组件提供样式;
  • header.component.html :用于添加 HTML 元素;
  • header.component.spec.ts :用于编写测试用例;
  • header.component.ts :添加基于 Typescript 的逻辑。
标头组件(大预览)

要使标头看起来像在模拟中一样,请在header.component.html中添加以下 HTML:

 <nav class="navbar" [class.mat-elevation-z8]=true> <div> <button *ngIf="showBackButton" aria-hidden=false mat-icon-button routerLink="/"> <mat-icon> <i class="material-icons md-32">arrow_back</i> </mat-icon> </button> <span>{{currentTitle}}</span> </div> <button *ngIf="!showBackButton" aria-hidden=false mat-button class="button"> <img src="../../assets/qr-icon-white.png"> <span>QR Generator</span> </button> <button *ngIf="showHistoryNav" aria-hidden=false mat-button class="button" routerLink="/history"> <span>History</span> </button> </nav>

提示要为任何材质组件添加高程,请使用[class.mat-elevation-z8]=true可以通过更改z值来更改高程值,在本例中为z8例如,要将海拔更改为 16,请使用[class.mat-elevation-z16]=true

在上面的 HTML 片段中,使用了两个 Angular 材质元素: mat-iconmat-button/mat-icon-button 。 它们的用法非常简单; 首先,我们需要将这两个作为模块添加到我们的app.module.ts中,如下所示:

mat-iconmat-button的模块导入(大预览)

这将允许我们在任何组件的任何位置使用这两个 Angular 材质元素。

要添加材质按钮,使用以下 HTML 片段:

 <button mat-button> Material Button </button>

Angular 材质库中提供了不同类型的材质按钮元素,例如mat-raised-buttonmat-flat-buttonmat-fab等; 只需将上述代码片段中的mat-button替换为任何其他类型即可。

材质按钮的类型(大预览)

另一个元素是mat-icon ,用于显示材质图标库中可用的图标。 在开始添加 Angular 材质库时,还添加了对材质图标库的引用,这使我们能够使用大量图标中的图标。

用法很简单:

 <mat-icon> <i class="material-icons md-32">arrow_back</i> </mat-icon>

嵌套的<i>标签可用于更改图标大小(此处为md-32 ),这将使图标大小的高度和宽度为 32px。 该值可以是md-24md-48等。 嵌套<i>标记的值是图标的名称。 (可以在此处找到任何其他图标的名称。)

可访问性

无论何时使用图标或图像,它们都必须为可访问性目的或屏幕阅读器用户提供足够的信息。 ARIA(Accessible Rich Internet Applications)定义了一种使残障人士更容易访问 Web 内容和 Web 应用程序的方法。

需要注意的一点是,具有原生语义的 HTML 元素(例如nav )不需要 ARIA 属性。 屏幕阅读器已经知道nav是一个导航元素并照此阅读。

ARIA 规范分为三类:角色、状态和属性。 假设一个div用于在 HTML 代码中创建一个进度条。 它没有任何原生语义; ARIA 角色可以将这个小部件描述为一个进度条,ARIA 属性可以表示它的特性,例如它可以被拖动。 ARIA state 将描述其当前状态,例如进度条的当前值。 请参阅下面的片段:

 <div role="progressbar" aria-valuenow="75" aria-valuemin="0" aria-valuemax="100"> </div>

同样,使用了一个非常常用的 aria 属性: aria-hidden=true/false 。 值 true 使该元素对屏幕阅读器不可见。

由于此应用程序中使用的大多数 UI 元素都具有原生语义含义,因此使用的唯一 ARIA 属性是指定 ARIA 可见性状态。有关详细信息,请参阅此。

header.component.html确实包含一些根据当前页面隐藏和显示后退按钮的逻辑。 此外,主页按钮还包含应添加到/assets文件夹的图像/徽标。 从此处下载图像并将其保存在/assets文件夹中。

对于导航栏的样式,在header.component.css中添加以下 css :

 .navbar { position: fixed; top: 0; left: 0; right: 0; z-index: 2; background: #3f51b5; display: flex; flex-wrap: wrap; align-items: center; padding: 12px 16px; } .button { color: white; margin: 0px 10px; }

由于我们希望保持标头组件在其他组件之间可重用,因此要决定应该显示什么,我们将需要这些作为来自其他组件的参数。 这需要使用@Input()装饰器,它将绑定到我们在header.component.html中使用的变量。

header.component.ts文件中添加这些行:

 // Add these three lines above the constructor entry. @Input() showBackButton: boolean; @Input() currentTitle: string; @Input() showHistoryNav: boolean; constructor() { }

上述三个绑定将作为参数从头组件将使用的其他组件传递。 一旦我们继续前进,它的用法将更加清晰。

继续前进,我们需要创建一个可以由 Angular 组件表示的主页。 因此,让我们从创建另一个组件开始; 在终端中键入ng gc home以自动生成 home 组件。 如前所述,将创建一个名为“home”的新文件夹,其中包含四个不同的文件。 在继续修改这些文件之前,让我们将一些路由信息添加到角度路由模块。

添加路由

Angular 提供了一种将 URL 映射到特定组件的方法。 每当发生一些导航时,Angular 框架都会根据app-routing.module.ts文件中的信息监控 URL; 它初始化映射的组件。 这样不同的组件就不需要承担初始化其他组件的责任。 在我们的例子中,应用程序具有三个页面,可通过单击不同的按钮进行导航。 我们通过利用 Angular 框架提供的路由支持来实现这一点。

home 组件应该是应用程序的起点。 让我们将此信息添加到app-routing.module.ts文件中。

路由主页组件(大预览)

path属性设置为空字符串; 这使我们能够将应用程序 URL 映射到主页组件,例如显示 Google 主页的google.com

提示路径值从不以/ ”开头,而是使用空字符串,即使路径可能类似于search/coffee

回到主页组件,将home.component.html的内容替换为:

 <app-header [showBackButton]="false" [currentTitle]=""></app-header> <app-profile></app-profile> <!-- FAB Fixed --> <button mat-fab class="fab-bottom-right" routerLink="/create"> <mat-icon> <i class="material-icons md-48">add</i> </mat-icon> </button>

home 组件包含三个部分:

  1. 可重用的标头组件<app-header>
  2. 配置文件组件<app-profile> ,
  3. 右下角的浮动操作按钮。

上面的 HTML 片段展示了如何在其他组件中使用可重用的 header 组件; 我们只使用组件选择器并传入所需的参数。

Profile 组件被创建用作主页的主体——我们将很快创建它。

带有+图标的浮动操作按钮是屏幕右下方的一种mat-fab类型的 Angular 材质按钮。 它具有routerLink属性指令,该指令使用app-routing.module.ts中提供的路由信息​​进行导航。 在这种情况下,按钮的路由值为/create ,它将被映射到创建组件。

要使创建按钮浮动在右下角,请在home.component.css中添加以下 CSS 代码:

 .fab-bottom-right { position: fixed; left: auto; bottom: 5%; right: 10%; }

由于配置文件组件应该管理主页正文,我们将保持home.component.ts不变。

添加配置文件组件

打开终端,输入ng gc profile并回车生成配置文件组件。 如前所述,该组件将处理主页的主体。 打开profile.component.html并将其内容替换为:

 <div class="center profile-child"> <img class="avatar" src="../../assets/avatar.png"> <div class="profile-actions"> <button mat-raised-button matBadge="{{historyCount}}" matBadgeOverlap="true" matBadgeSize="medium" matBadgeColor="accent" color="primary" routerLink="/history"> <span>History</span> </button> </div> </div>

上面的 HTML 片段展示了如何使用材质库的matBadge元素。 为了能够在这里使用它,我们需要按照通常的练习将MatBadgeModule添加到app.module.ts文件中。 徽章是 UI 元素(例如按钮、图标或文本)的小型图形状态描述符。 在这种情况下,它与按钮一起使用以显示用户保存的 QR 计数。 Angular 材质库徽章具有各种其他属性,例如使用matBadgeSize设置徽章的位置,使用matBadgePosition指定尺寸,使用matBadgeColor设置徽章颜色。

还需要向 assets 文件夹中添加一项图片资源:下载。 将其保存到项目的/assets文件夹中。

打开profile.component.css并添加:

 .center { top: 50%; left: 50%; position: absolute; transform: translate(-50%, -50%); } .profile-child { display: flex; flex-direction: column; align-items: center; } .profile-actions { padding-top: 20px; } .avatar { border-radius: 50%; width: 180px; height: 180px; }

上面的 CSS 将按计划实现 UI。

继续前进,我们需要某种逻辑来更新历史计数值,因为它将反映在前面使用的matBadge中。 打开profile.component.ts并适当地添加以下代码段:

 export class ProfileComponent implements OnInit { historyCount = 0; constructor(private storageUtilService: StorageutilService) { } ngOnInit() { this.updateHistoryCount(); } updateHistoryCount() { this.historyCount = this.storageUtilService.getHistoryCount(); } }

我们添加了StorageutilService ,但直到现在我们还没有创建这样的服务。 忽略错误,我们已经完成了我们的配置文件组件,该组件也完成了我们的主页组件。 创建存储实用程序服务后,我们将重新访问此配置文件组件。 好的,那么让我们这样做吧。

本地存储

HTML5 提供了 Web 存储功能,可用于在本地存储数据。 与 cookie 相比,这提供了更多的存储空间——至少 5MB 和 4KB。 有两种类型的 Web 存储具有不同的范围和生命周期:本地会话。 前者可以永久存储数据,而后者是临时的,用于单个会话。 选择类型的决定可以基于用例,在我们的场景中,我们希望跨会话保存,因此我们将使用本地存储。

每条数据都存储在一个键/值对中。 我们将使用生成 QR 的文本作为键,将编码为 base64 字符串的 QR 图像作为值。 创建一个实体文件夹,在该文件夹内创建一个新的qr-object.ts文件并添加代码片段,如下所示:

二维码实体模型(大预览)

课堂内容:

 export class QR { text: string; imageBase64: string; constructor(text: string, imageBase64: string) { this.imageBase64 = imageBase64; this.text = text; } }

每当用户保存生成的 QR 时,我们将创建上述类的对象并使用存储实用程序服务保存该对象。

创建一个新的服务文件夹,我们将创建许多服务,最好将它们组合在一起。

服务文件夹(大预览)

将当前工作目录更改为服务cd services ,以使用ng gs <any name>创建新服务。 这是ng generate service <any name>的简写,键入ng gs storageutil并按 Enter

这将创建两个文件:

  • storageutil.service.ts
  • storageutil.service.spec.ts

后者用于编写单元测试。 打开storageutil.service.ts并添加:

 private historyCount: number; constructor() { } saveHistory(key : string, item :string) { localStorage.setItem(key, item) this.historyCount = this.historyCount + 1; } readHistory(key : string) : string { return localStorage.getItem(key) } readAllHistory() : Array<QR> { const qrList = new Array<QR>(); for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); const value = localStorage.getItem(key); if (key && value) { const qr = new QR(key, value); qrList.push(qr); } } this.historyCount = qrList.length; return qrList; } getHistoryCount(): number { if (this.historyCount) { return this.historyCount; } this.readAllHistory(); return this.historyCount; } deleteHistory(key : string) { localStorage.removeItem(key) this.historyCount = this.historyCount - 1; }

导入 qr-object 类以更正任何错误。 要使用本地存储功能,无需导入任何新内容,只需使用关键字localStorage根据键保存或获取值。

现在再次打开profile.component.ts文件并导入StorageutilService类以正确完成配置文件组件。

运行项目,我们可以看到主页按计划启动。

添加创建二维码页面

我们已经准备好了主页,尽管创建/添加按钮没有做任何事情。 不用担心,实际的逻辑已经写好了。 我们使用routerLink指令将 URL 的基本路径更改为/create ,但app-routing.module.ts文件中没有添加任何映射。

让我们创建一个组件来处理新二维码的创建,输入ng gc create-qr并按回车键生成一个新组件。

打开app-routing.module.ts文件并将以下条目添加到routes数组中:

 { path: 'create', component: CreateQrComponent },

这会将CreateQRComponent映射到 URL /create

打开create-qr.components.html并将内容替换为:

 <app-header [showBackButton]="showBackButton" [currentTitle]="title" [showHistoryNav]="showHistoryNav"></app-header> <mat-card class="qrCard" [class.mat-elevation-z12]=true> <div class="qrContent"> <!--Close button section--> <div class="closeBtn"> <button mat-icon-button color="accent" routerLink="/" matTooltip="Close"> <mat-icon> <i class="material-icons md-48">close</i> </mat-icon> </button> </div> <!--QR code image section--> <div class="qrImgDiv"> <img *ngIf="!showProgressSpinner" src={{qrCodeImage}} width="200px" height="200px"> <mat-spinner *ngIf="showProgressSpinner"></mat-spinner> <div class="actionButtons" *ngIf="!showProgressSpinner"> <button mat-icon-button color="accent" matTooltip="Share this QR"> <mat-icon> <i class="material-icons md-48">share</i> </mat-icon> </button> <button mat-icon-button color="accent" (click)="saveQR()" matTooltip="Save this QR"> <mat-icon> <i class="material-icons md-48">save</i> </mat-icon> </button> </div> </div> <!--Textarea to write any text or link--> <div class="qrTextAreaDiv"> <mat-form-field> <textarea matInput [(ngModel)]="qrText" cdkTextareaAutosize cdkAutosizeMinRows="4" cdkAutosizeMaxRows="4" placeholder="Enter a website link or any text..."></textarea> </mat-form-field> </div> <!--Create Button--> <div class="createBtnDiv"> <button class="createBtn" mat-raised-button color="accent" matTooltip="Create new QR code" matTooltipPosition="above" (click)="createQrCode()">Create</button> </div> </div> </mat-card>

上面的代码片段使用了许多 Angular 材质库元素。 按照计划,它有一个标头组件引用,其中传递了所需的参数。 接下来是创建页面的主体; 它由一张 Angular 材质卡或mat-card组成,使用[class.mat-elevation-z12]=true时,居中并提升到 12px。

素材卡只是另一种容器,可以用作任何其他div标签。 尽管材质库提供了一些属性来在mat-card中布置明确定义的信息,例如图像位置、标题、副标题、描述和操作,如下所示。

卡片示例(大预览)

在上面的 HTML 片段中,我们使用mat-card就像任何其他容器一样。 使用的另一个材质库元素是matTooltip ; 它只是另一个易于使用的工具提示,当用户悬停或长按元素时显示。 只需使用下面的代码片段来显示工具提示:

 matTooltip="Any text you want to show"

它可以与图标按钮或任何其他 UI 元素一起使用,以传达额外的信息。 在应用程序上下文中,它显示有关关闭图标按钮的信息。 要更改工具提示的位置,使用matTooltipPosition

 matTooltip="Any text you want to show" matTooltipPosition="above"

除了matTooltip之外, mat-spinner用于显示加载进度。 当用户单击“创建”按钮时,会进行网络调用。 这是显示进度微调器的时候。 当网络调用返回结果时,我们只是隐藏微调器。 它可以像这样简单地使用:

 <mat-spinner *ngIf="showProgressSpinner"></mat-spinner>

showProgressSpinner是一个布尔变量,用于显示/隐藏进度微调器。 该库还提供了一些其他参数,例如[color]='accent'来更改颜色, [mode]='indeterminate'来更改进度微调器类型。 不确定的进度微调器不会显示任务的进度,而确定的进度微调器可以具有不同的值来反映任务进度。 这里使用了一个不确定的微调器,因为我们不知道网络调用需要多长时间。

材质库提供了一个符合材质指南的 textarea 变体,但它只能用作mat-form-field的后代。 材质 textarea 的使用与默认的 HTML 一样简单,如下所示:

 <mat-form-field> <textarea matInput placeholder="Hint text"></textarea> </mat-form-field>

matInput是一个指令,它允许本机input标签与mat-form-field一起使用。 placeholder属性允许为用户添加任何提示文本。

提示使用cdkTextareaAutosize textarea 属性使其可自动调整大小。 使用cdkAutosizeMinRowscdkAutosizeMaxRows设置行和列以及所有三个一起使 textarea 自动调整大小,直到达到最大行和列限制设置。

要使用所有这些材质库元素,我们需要将它们添加到app.module.ts文件中。

创建 QR 模块导入(大预览)

HTML 中使用了占位符图像。 下载并保存到/assets文件夹。

上面的 HTML 还需要 CSS 样式,所以打开create-qr.component.ts文件并添加以下内容:

 .qrCard { display: flex; flex-direction: column; align-items: center; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 20%; height: 65%; padding: 50px 20px; } .qrContent { display: flex; flex-direction: column; align-items: center; width: 100%; } .qrTextAreaDiv { width: 100%; display: flex; flex-direction: row; justify-content: center; padding: 0px 0px; position: absolute; bottom: 10%; } .createBtn { left: 50%; transform: translate(-50%, 0px); width: 80%; } .createBtnDiv { position: absolute; bottom: 5%; width: 100%; } .closeBtn { display: flex; flex-direction: row-reverse; align-items: flex-end; width: 100%; margin-bottom: 20px; } .closeBtnFont { font-size: 32px; color: rgba(0,0,0,0.75); } .qrImgDiv { top: 20%; position: absolute; display: flex; flex-direction: column; align-items: center; justify-content: center; width: 100%; } .actionButtons { display: flex; flex-direction: row; padding-top: 20px; }

让我们用逻辑连接 UI。 打开create-qr.component.ts文件并添加以下代码,保留那些已经存在的行:

 export class CreateQrComponent implements OnInit { qrCodeImage = '../../../assets/download.png'; showProgressSpinner = false; qrText: string; currentQR; showBackButton = true; title = 'Generate New QR Code'; showHistoryNav = true; constructor(private snackBar: MatSnackBar, private restutil: RestutilService, private storageService: StorageutilService) { } ngOnInit() { } createQrCode() { //Check if any value is given for the qr code text if (!!this.qrText) { //Make the http call to load qr code this.loadQRCodeImage(this.qrText); } else { //Show snackbar this.showSnackbar('Enter some text first') } } public loadQRCodeImage(text: string) { // Show progress spinner as the request is being made this.showProgressSpinner = true; // Trigger the API call this.restutil.getQRCode(text).subscribe(image =>{ // Received the result - as an image blob - require parsing this.createImageBlob(image); }, error => { console.log('Cannot fetch QR code from the url', error) // Hide the spinner - show a proper error message this.showProgressSpinner = false; }); } private createImageBlob(image: Blob) { // Create a file reader to read the image blob const reader = new FileReader(); // Add event listener for "load" - invoked once the blob reading is complete reader.addEventListener('load', () => { this.qrCodeImage = reader.result.toString(); //Hide the progress spinner this.showProgressSpinner = false; this.currentQR = reader.result.toString(); }, false); // Read image blob if it is not null or undefined if (image) { reader.readAsDataURL(image); } } saveQR() { if (!!this.qrText) { this.storageService.saveHistory(this.qrText, this.currentQR); this.showSnackbar('QR saved') } else { //Show snackbar this.showSnackbar('Enter some text first') } } showSnackbar(msg: string) { //Show snackbar this.snackBar.open(msg, '', { duration: 2000, }); } }

为了向用户提供上下文信息,我们还使用了材料设计库中的MatSnackBar 。 这显示为屏幕下方的弹出窗口,并在消失前停留几秒钟。 这不是一个元素,而是一个可以从 Typescript 代码调用的服务。

上面的方法名称为showSnackbar的代码片段展示了如何打开一个snackbar,但是在它可以使用之前,我们需要在app.module.ts文件中添加MatSnackBar条目,就像我们为其他材质库元素所做的那样。

提示在最近的 Angular 材质库版本中,没有直接的方法可以更改快餐栏样式。 相反,必须对代码进行两次添加。

首先,使用下面的 CSS 来改变背景和前景色:

 ::ng-deep snack-bar-container.snackbarColor { background-color: rgba(63, 81, 181, 1); } ::ng-deep .snackbarColor .mat-simple-snackbar { color: white; }

其次,使用一个名为panelClass的属性将样式设置为上述 CSS 类:

 this.snackBar.open(msg, '', { duration: 2000, panelClass: ['snackbarColor'] });

上述两种组合将允许对材料设计库快餐栏组件进行自定义样式。

这样就完成了如何创建 QR 页面的步骤,但还缺少一个。 检查create-qr.component.ts文件,它将显示有关缺失部分的错误。 这个难题缺少的部分是RestutilService ,它负责从第三方 API 获取 QR 码图像。

在终端中,通过输入ng gs restutil并按 Enter 将当前目录更改为 services。 这将创建 RestUtilService 文件。 打开restutil.service.ts文件并添加以下代码段:

 private edgeSize = '300'; private BASE_URL = 'https://api.qrserver.com/v1/create-qr-code/?data={data}!&size={edge}x{edge}'; constructor(private httpClient: HttpClient) { } public getQRCode(text: string): Observable { // Create the url with the provided data and other options let url = this.BASE_URL; url = url.replace("{data}", text).replace(/{edge}/g, this.edgeSize); // Make the http api call to the url return this.httpClient.get(url, { responseType: 'blob' }); } private edgeSize = '300'; private BASE_URL = 'https://api.qrserver.com/v1/create-qr-code/?data={data}!&size={edge}x{edge}'; constructor(private httpClient: HttpClient) { } public getQRCode(text: string): Observable { // Create the url with the provided data and other options let url = this.BASE_URL; url = url.replace("{data}", text).replace(/{edge}/g, this.edgeSize); // Make the http api call to the url return this.httpClient.get(url, { responseType: 'blob' }); }

上述服务从第三方 API 获取 QR 图像,由于响应不是 JSON 类型,而是图像,因此我们在上述代码段中将responseType指定为'blob'

Angular 提供了HttpClient类来与任何支持 HTTP 的服务器进行通信。 它提供了许多功能,例如在触发请求之前过滤请求、获取响应、通过回调和其他方式处理响应。 要使用它,请在app.module.ts文件中为HttpClientModule添加一个条目。

最后将该服务导入create-qr.component.ts文件,完成二维码的创建。

可是等等! 上面的创建二维码逻辑有问题。 如果用户一次又一次地使用相同的文本生成二维码,就会导致网络调用。 解决此问题的一种方法是缓存基于请求,因此如果请求文本相同,则从缓存中提供响应。

缓存请求

Angular 提供了一种简化的 HTTP 调用方式 HttpClient 以及 HttpInterceptor 来检查和转换与服务器之间的 HTTP 请求或响应。 它可用于身份验证或缓存以及许多此类事情,可以添加多个拦截器并将其链接起来以进行进一步处理。 在这种情况下,如果 QR 文本相同,我们将拦截请求并从缓存中提供响应。

创建一个拦截器文件夹,然后创建一个文件cache-interceptor.ts

缓存拦截器(大预览)

将以下代码片段添加到文件中:

 import { Injectable } from '@angular/core'; import { HttpInterceptor, HttpResponse, HttpRequest, HttpHandler, HttpEvent } from '@angular/common/http'; import { tap } from 'rxjs/operators'; import { of, Observable } from 'rxjs'; @Injectable({ providedIn: 'root' }) export class RequestCachingService implements HttpInterceptor { private cacheMap = new Map<string, HttpResponse<any>>(); constructor() { } intercept(req: HttpRequest , next: HttpHandler): Observable<HttpEvent<any>> { const cachedResponse = this.cacheMap.get(req.urlWithParams); if (cachedResponse) { return of(cachedResponse); } return next.handle(req).pipe(tap(event => { if (event instanceof HttpResponse) { this.cacheMap.set(req.urlWithParams, event); } })) } } import { Injectable } from '@angular/core'; import { HttpInterceptor, HttpResponse, HttpRequest, HttpHandler, HttpEvent } from '@angular/common/http'; import { tap } from 'rxjs/operators'; import { of, Observable } from 'rxjs'; @Injectable({ providedIn: 'root' }) export class RequestCachingService implements HttpInterceptor { private cacheMap = new Map<string, HttpResponse<any>>(); constructor() { } intercept(req: HttpRequest , next: HttpHandler): Observable<HttpEvent<any>> { const cachedResponse = this.cacheMap.get(req.urlWithParams); if (cachedResponse) { return of(cachedResponse); } return next.handle(req).pipe(tap(event => { if (event instanceof HttpResponse) { this.cacheMap.set(req.urlWithParams, event); } })) } }

在上面的代码片段中,我们有一个映射,键是请求 URL,响应是值。 我们检查当前 URL 是否存在于地图中; 如果是,则返回响应(其余部分自动处理)。 如果 URL 不在地图中,我们添加它。

我们还没有完成。 An entry to the app.module.ts is required for its proper functioning. Add the below snippet:

 import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; import { CacheInterceptor } from './interceptor/cache-interceptor'; providers: [ { provide: HTTP_INTERCEPTORS, useClass: CacheInterceptor, multi: true } ],

This adds the caching feature to our application. Let's move on to the third page, the History page.

Adding The History Page

All the saved QR codes will be visible here. To create another component, open terminal type ng gc history and press Enter.

Open history.component.css and add the below code:

 .main-content { padding: 5% 10%; } .truncate { width: 90%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .center-img { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); display: flex; flex-direction: column; align-items: center; }

Open history.component.html and replace the content with this:

 <app-header [showBackButton]="showBackButton" [currentTitle]="title" [showHistoryNav]="showHistoryNav"></app-header> <div class="main-content"> <mat-grid-list cols="4" rowHeight="500px" *ngIf="historyList.length > 0"> <mat-grid-tile *ngFor="let qr of historyList"> <mat-card> <img mat-card-image src="{{qr.imageBase64}}"> <mat-card-content> <div class="truncate"> {{qr.text}} </div> </mat-card-content> <mat-card-actions> <button mat-button (click)="share(qr.text)">SHARE</button> <button mat-button color="accent" (click)="delete(qr.text)">DELETE</button> </mat-card-actions> </mat-card> </mat-grid-tile> </mat-grid-list> <div class="center-img" *ngIf="historyList.length == 0"> <img src="../../assets/no-see.png" width="256" height="256"> <span>Nothing to see here</span> </div> </div>

As usual, we have the header component at the top. Then, the rest of the body is a grid list that will show all the saved QR codes as individual mat-card . For the grid view, we are using mat-grid-list from the Angular material library. As per the drill, before we can use it, we have to first add it to the app.module.ts file.

Mat 网格列表充当一个容器,其中包含多个名为mat-grid-tile子图块。 在上面的 HTML 片段中,每个 tile 都是使用mat-card创建的,它使用它的一些属性来通用放置其他 UI 元素。 我们可以提供number of columnsrowHeight ,用于自动计算宽度。 在上面的代码片段中,我们提供了列数和rowHeight值。

当历史记录为空时,我们使用占位符图像,下载并添加到资产文件夹。

要实现填充所有这些信息的逻辑,请打开history.component.ts文件并将以下代码段添加到HistoryComponent类中:

 showBackButton = true; title = 'History'; showHistoryNav = false; historyList; constructor(private storageService: StorageutilService, private snackbar: MatSnackBar ) { } ngOnInit() { this.populateHistory(); } private populateHistory() { this.historyList = this.storageService.readAllHistory(); } delete(text: string) { this.storageService.deleteHistory(text); this.populateHistory(); } share(text: string) { this.snackbar.open(text, '', {duration: 2000,}) }

上面的逻辑只是获取所有保存的 QR 并用它填充页面。 用户可以删除保存的 QR,这将从本地存储中删除该条目。

所以这完成了我们的历史组件......还是这样? 我们仍然需要为这个组件添加路由映射。 打开app-routing.module.ts并为历史页面添加一个映射:

 { path: 'history', component: HistoryComponent },

整个路由数组现在应该是这样的:

 const routes: Routes = [ { path: '', component: HomeComponent }, { path: 'create', component: CreateQrComponent }, { path: 'history', component: HistoryComponent }, ];

现在是运行应用程序以检查完整流程的好时机,因此打开终端并键入ng serve并按 Enter。 然后,转到localhost:4200以验证应用程序的工作。

添加到 GitHub

在继续部署步骤之前,最好将项目添加到 GitHub 存储库。

  1. 打开 GitHub。
  2. 创建一个新的存储库。
  3. GitHub 新存储库(大预览)
  4. 在 VS Code 中,使用终端并按照快速入门指南中提到的第一组命令推送所有项目文件。
  5. 在 GitHub 中添加项目(大预览)

只需刷新页面以检查所有文件是否可见。 从此时起,任何 git 更改(例如提交、拉/推)都将反映在这个新创建的存储库中。

Netlify 和部署

我们的应用程序运行在我们的本地机器上,但是为了让其他人可以访问它,我们应该将它部署在云平台上并注册到一个域名。 这就是 Netlify 发挥作用的地方。 它提供了持续部署服务、与 GitHub 的集成以及更多可以从中受益的功能。 现在,我们希望启用对我们应用程序的全局访问。 让我们开始吧。

  1. 在 Netlify 上注册。
  2. 在仪表板中,单击“从 Git 新建站点”按钮。
  3. Netlify 新站点(大预览)
  4. 在下一个屏幕中单击 GitHub。
  5. Netlify 选择 git 提供者(大预览)
  6. 授权 Netlify 能够访问您的 GitHub 存储库。
  7. Netlify GitHub 授权(大预览)
  8. 搜索并选择新创建的qr存储库。
  9. Netlify GitHub 存储库选择(大预览)
  10. 在下一步中,Netlify 允许我们选择 GitHub 存储库分支进行部署。 通常使用master分支,但也可以有一个单独的release分支,其中仅包含与发布相关的稳定功能。
  11. Netlify 构建和部署(大预览)

由于这是一个 Angular Web 应用程序,因此添加ng build --prod作为构建命令。 如angular.json文件中所述,发布的目录将是dist/qr

Angular 构建路径(大预览)

现在单击Deploy site按钮,该按钮将使用命令ng build --prod触发项目构建,并将文件输出到dist/qr

由于我们向 Netlify 提供了路径信息,它会自动选择正确的文件来为 Web 应用程序提供服务。 默认情况下,Netlify 会向我们的应用程序添加一个随机域。

已部署 Netlify 站点(大预览)

您现在可以单击上述页面中提供的链接,以便从任何地方访问该应用程序。 最后,应用程序已部署完毕。

自定义域

在上图中,显示了我们应用程序的 URL,而子域是随机生成的。 让我们改变它。

单击Domain settings按钮,然后在自定义域部分中单击三点菜单并选择Edit site name

自定义域(大预览)

这将打开一个弹出窗口,可以在其中输入新的站点名称; 此名称在 Netlify 域中应该是唯一的。 输入任何可用的站点名称,然后单击保存

网站名称(大预览)

现在,指向我们应用程序的链接将更新为新的站点名称。

拆分测试

Netlify 提供的另一个很酷的功能是拆分测试。 它支持流量拆分,以便不同的用户集与不同的应用程序部署进行交互。 我们可以将新功能添加到不同的分支,并将流量拆分到此分支部署,分析流量,然后将功能分支与主部署分支合并。 让我们配置它。

启用拆分测试的先决条件是拥有至少两个分支的 GitHub 存储库。 前往之前在 GitHub 中创建的应用程序存储库,然后创建一个新分支a .

创建新分支(大预览)

存储库现在将有一个master分支和a分支。 Netlify 需要配置为进行分支部署,因此打开 Netlify 仪表板并单击Settings 。 在左侧,单击Build & Deploy ,然后单击Continuous Deployment ,然后在右侧的Deploy contexts部分中单击Edit settings

分支部署(大预览)

Branch deploys子部分中,选择“让我添加单个分支”选项,然后输入分支名称并保存。

部署分支是 Netlify 提供的另一个有用的功能; 我们可以选择要部署的 GitHub 存储库分支,我们还可以在合并之前对master分支的每个拉取请求启用预览。 这是一个简洁的功能,使开发人员能够在将代码更改添加到主部署分支之前实际测试他们的更改。

现在,单击页面顶部的Split Testing选项卡选项。 此处将介绍拆分测试配置。

拆分测试(大预览)

我们可以选择分支(生产分支除外)——在本例中a 。 我们也可以玩转流量分割的设置。 根据每个分支分配的流量百分比,Netlify 会将一些用户重新路由到使用a分支部署的应用程序,而将其他用户重新路由到master分支。 配置完成后,点击Start test按钮启用流量拆分。

提示Netlify 可能无法识别连接的 GitHub 存储库有多个分支,并且可能会出现以下错误:

拆分测试错误(大预览)

要解决此问题,只需从Build & Deploy选项重新连接到存储库。

Netlify 还提供了许多其他功能。 我们刚刚介绍了它的一些有用功能,以演示如何轻松配置 Netlify 的不同方面。

这将我们带到了旅程的终点​​。 我们已经成功地创建了一个基于 Web 应用程序的 Angular Material 设计,并将其部署在 Netlify 上。

结论

Angular 是一个伟大且流行的 Web 应用程序开发框架。 使用官方的 Angular 材料设计库,可以更轻松地创建符合材料设计规范的应用程序,以便与用户进行非常自然的交互。 而且,用一个好的框架开发的应用程序应该使用一个好的平台进行部署,而Netlify就是这样。 随着不断的发展、强大的支持和众多的功能,它无疑是一个将 Web 应用程序或静态站点带给大众的绝佳平台。 希望这篇文章能够帮助您开始一个新的 Angular 项目,从构思到部署。

延伸阅读

  • 角度架构
  • 更多 Angular 材质组件
  • 有关 Netlify 功能的更多信息