Angular 快速指南



Angular - 简介

Angular 是一个基于 TypeScript 的全栈 Web 框架,用于构建 Web 和移动应用程序。其中一个主要优势是 Angular 支持适用于任何屏幕分辨率的 Web 应用程序。Angular 应用程序与手机、平板电脑、笔记本电脑或台式机完全兼容。Angular 拥有一个出色的 Web 开发人员用户界面库,其中包含可重用的 UI 组件。

此功能有助于我们创建单页面应用程序 (SPA)。SPA 是响应式且快速的应用程序。例如,如果您在单页面中有一个按钮,并且单击该按钮,则操作将在当前页面中动态执行,而无需从服务器加载新页面。Angular 是基于 TypeScript 的面向对象编程,并且也支持服务器端编程功能。

Angular 版本比较

众所周知,Google 发布了 Angular 的版本以改进移动和 Web 开发功能。所有发布的版本都向后兼容,并且可以轻松更新到较新版本。让我们来看一下已发布版本的比较。

AngularJS

AngularJs 是一个非常强大的 JavaScript 框架。它于 2010 年 10 月发布。AngularJS 基于模型视图控制器 (MVC) 架构,并自动处理适合每个浏览器的 JavaScript 代码。

Angular 2.0

Angular 2.0 于 2016 年 9 月发布。它是 AngularJS 的重新设计和重写版本。AngularJs 侧重于控制器,但版本 2 将重点转移到组件。组件是应用程序的主要构建块。它支持渲染、更新页面和构建 Google Android 和 iOS 的跨平台原生移动应用程序的速度特性。

Angular 4.0

Angular 4.0 于 2017 年 3 月发布。它更新到 TypeScript 2.2,支持 ng if-else 条件,而 Angular 2 仅支持 if 条件。Angular 4.0 引入了动画包、Http 搜索参数,最后 Angular 4 应用程序更小更快。

Angular 5.0

Angular 5.0 于 2017 年 11 月发布。它支持一些突出功能,例如 HTTPClient API、Lambda 支持、改进的编译器和构建优化器。

Angular 6.0

Angular 6.0 于 2018 年 5 月发布。此版本添加的功能包括更新的 Angular CLI、更新的 CDK、更新的 Angular Material、多个验证器以及 Reactive JS 库的使用。

Angular 7.0

Angular 7.0 于 2018 年 10 月发布。一些突出功能包括 Google 支持的社区、基于 POJO 的开发、模块化结构、声明式用户界面和模块化结构。

Angular 新功能

Angular 提供了以下新的吸引人的功能 -

  • Bazel 支持 - 如果您的应用程序使用多个模块和库,Bazel 并发构建有助于加快应用程序加载速度。

  • 延迟加载 - Angular 将 AppRoutingModule 拆分为更小的包并在 DOM 中加载数据。

  • 差异加载 - 当您创建应用程序时,Angular CLI 会生成模块,这将自动加载,然后浏览器将呈现数据。

  • Web 工作线程 - 它在后台运行,不会影响页面的性能。

  • CLI 工作流程改进 - Angular CLI 命令 ng-build、ng-test 和 ng-run 扩展到第三方库。

  • 路由器向后兼容性 - Angular 路由器向后兼容性功能有助于为大型项目创建路径,以便用户可以借助延迟编码轻松添加其代码。

  • 选择加入使用共享 - 用户可以选择共享 Angular CLI 使用数据。

应用

下面列出了一些使用 Angular 框架的热门网站 -

  • Weather.com - 它是领先的天气预报网站之一。

  • Youtube - 它是 Google 托管的视频和共享网站。

  • Netflix - 它是一家技术和媒体服务提供商。

  • PayPal - 它是一个在线支付系统。

Angular - 安装

本章介绍如何在您的机器上安装 Angular。在开始安装之前,让我们先验证先决条件。

先决条件

众所周知,Angular 是用 TypeScript 编写的。我们需要 Nodenpm 将文件编译成 JavaScript,然后才能部署我们的应用程序。为此,必须在您的系统中安装 Node.js。希望您已经在机器上安装了 Node.js

我们可以使用以下命令检查它 -

node --version

您可以看到节点的版本。如下所示 -

v14.2.0

如果未安装 Node,您可以访问以下链接下载并安装 -

https://node.org.cn/en/download/。

Angular 安装

Angular CLI 安装基于非常简单的步骤。安装时间不超过五分钟。

npm 用于安装 Angular CLI。安装 Node.js 后,npm 也会安装。如果要验证它,请键入以下命令

npm -v

您可以在下面看到版本 -

6.14.4

让我们使用 npm 如下安装 Angular CLI -

npm install -g @angular/cli@^8.0.0

要验证 Angular 是否已正确安装在您的机器上,请键入以下命令 -

ng version

您可以看到以下响应 -

Angular CLI: 8.3.26 
Node: 14.2.0 
OS: win32 x64 
Angular: ... 
Package                    Version 
------------------------------------------------------
@angular-devkit/architect  0.803.26 
@angular-devkit/core       8.3.26 
@angular-devkit/schematics 8.3.26 
@schematics/angular        8.3.26 
@schematics/update         0.803.26 
rxjs                       6.4.0

Angular - 创建第一个应用程序

让我们创建一个简单的 Angular 应用程序并分析基本 Angular 应用程序的结构。

让我们使用以下命令检查 Angular 框架是否已安装在我们的系统中以及已安装的 Angular 版本的版本 -

ng --version

这里,

ng 是用于创建、管理和运行 Angular 应用程序的 CLI 应用程序。它用 JavaScript 编写并在 NodeJS 环境中运行。

结果将显示 Angular 版本的详细信息,如下所示 -

Angular CLI: 8.3.26 
Node: 14.2.0 
OS: win32 x64 
Angular: ... 
Package                    Version 
------------------------------------------------------
@angular-devkit/architect  0.803.26 
@angular-devkit/core       8.3.26 
@angular-devkit/schematics 8.3.26 
@schematics/angular        8.3.26 
@schematics/update         0.803.26 
rxjs                       6.4.0

因此,Angular 已安装在我们的系统中,版本为 8.3.26

让我们创建一个 Angular 应用程序来检查我们每天的支出。让我们将 ExpenseManager 作为我们新应用程序的选择。使用以下命令创建新应用程序。

cd /path/to/workspace 
ng new expense-manager

这里,

newng CLI 应用程序的命令之一。它将用于创建新应用程序。它会提出一些基本问题以创建新应用程序。让应用程序选择默认选项就足够了。关于如下所述的路由问题,请指定 No。我们将在后面的 路由章节中了解如何创建路由。

Would you like to add Angular routing? No

回答完基本问题后,ng CLI 应用程序将在 expense-manager 文件夹下创建一个新的 Angular 应用程序。

让我们进入我们新创建的应用程序文件夹。

cd expense-manager

让我们检查应用程序的部分结构。应用程序的结构如下 -

| favicon.ico 
| index.html 
| main.ts 
| polyfills.ts 
| styles.css 
| 
+---app 
|  app.component.css 
|  app.component.html 
|  app.component.spec.ts 
|  app.component.ts 
|  app.module.ts 
| 
+---assets 
|  .gitkeep 
| 
+---environments 
   environment.prod.ts 
   environment.ts

这里,

  • 我们只显示了应用程序中最重要的文件和文件夹。

  • favicon.icoassets 是应用程序的图标和应用程序的根资产文件夹。

  • polyfills.ts 包含对浏览器兼容性有用的标准代码。

  • environments 文件夹将包含应用程序的设置。它包括生产和开发设置。

  • main.ts 文件包含启动代码。

  • index.html 是应用程序的基本 HTML 代码。

  • styles.css 是基本 CSS 代码。

  • app 文件夹包含 Angular 应用程序代码,我们将在后续章节中详细学习。

让我们使用以下命令启动应用程序 -

ng serve
10% building 3/3 modules 0 activei wds: Project is running at https://:4200/webpack-dev-server/
i wds: webpack output is served from /

i wds: 404s will fallback to //index.html 
chunk {main} main.js, main.js.map (main) 49.2 kB [initial] [rendered] 
chunk {polyfills} polyfills.js, polyfills.js.map (polyfills) 269 kB [initial] [rendered] 
chunk {runtime} runtime.js, runtime.js.map (runtime) 6.15 kB [entry] [rendered] 
chunk {styles} styles.js, styles.js.map (styles) 9.75 kB [initial] [rendered] 
chunk {vendor} vendor.js, vendor.js.map (vendor) 3.81 MB [initial] [rendered] 
Date: 2020-05-26T05:02:14.134Z - Hash: 0dec2ff62a4247d58fe2 - Time: 12330ms 
** Angular Live Development Server is listening on localhost:4200, open your 
browser on https://:4200/ ** 
i wdm: Compiled successfully.

这里,serve 是用于使用本地开发 Web 服务器编译和运行 Angular 应用程序的子命令。ng server 将启动一个开发 Web 服务器并在端口 4200 下提供应用程序。

让我们启动浏览器并打开 https://:4200。浏览器将显示如下所示的应用程序 -

Browser Application

让我们将应用程序的标题更改为更好地反映我们的应用程序。打开 src/app/app.component.ts 并将代码更改为如下所示 -

export class AppComponent { 
   title = 'Expense Manager'; 
}

我们的最终应用程序将在浏览器中呈现,如下所示 -

Browser Application

我们将在后续章节中更改应用程序并学习如何编写 Angular 应用程序代码。

Angular - 架构

让我们在本节中了解 Angular 框架的架构。

Angular 框架基于四个核心概念,它们分别是 -

  • 组件。
  • 带有 数据绑定指令 的模板。
  • 模块。
  • 服务和依赖注入。

组件

Angular 框架架构的核心是 Angular 组件。Angular 组件是每个 Angular 应用程序的构建块。每个 Angular 应用程序都由一个或多个 Angular 组件 组成。它基本上是一个普通的 JavaScript/TypeScript 类,以及一个 HTML 模板和一个关联的名称。

HTML 模板可以访问其对应的 JavaScript/TypeScript 类中的数据。组件的 HTML 模板可以使用其选择器的值(名称)包含其他组件。Angular 组件可以有一个可选的 CSS 样式与其关联,并且 HTML 模板也可以访问 CSS 样式。

Component

让我们分析一下 ExpenseManager 应用程序中的 AppComponent 组件。AppComponent 代码如下所示:

// src/app/app.component.ts 
import { Component } from '@angular/core'; @Component({ 
   selector: 'app-root', 
   templateUrl: './app.component.html', 
   styleUrls: ['./app.component.css'] 
}) 
export class AppComponent { 
   title = 'Expense Manager'; 
}

@Component 是一个装饰器,用于将普通的 TypeScript 类转换为 Angular 组件

app-root 是组件的选择器/名称,它使用组件装饰器的 selector 元数据指定。app-root 可以被应用程序根文档 src/index.html 使用,如下所示

<!doctype html> 
<html lang="en"> 
   <head> 
      <meta charset="utf-8"> 
      <title>ExpenseManager</title> 
      <base href="/"> 
      <meta name="viewport" content="width=device-width, initial-scale=1"> 
      <link rel="icon" type="image/x-icon" href="favicon.ico"> 
   </head> 
   <body> 
      <app-root></app-root> 
   </body> 
</html>

app.component.html 是与组件关联的 HTML 模板文档。组件模板使用 @Component 装饰器的 templateUrl 元数据指定。

app.component.css 是与组件关联的 CSS 样式文档。组件样式使用 @Component 装饰器的 styleUrls 元数据指定。

AppComponent 属性(title)可以在 HTML 模板中使用,如下所示:

{{ title }}

模板

模板基本上是 HTML 的超集。模板包含 HTML 的所有功能,并提供额外的功能来将组件数据绑定到 HTML 中,并动态生成 HTML DOM 元素。

模板的核心概念可以分为两部分,如下所示:

数据绑定

用于将组件中的数据绑定到模板。

{{ title }}

这里,titleAppComponent 中的一个属性,它使用 插值 绑定到模板。

指令

用于包含逻辑以及启用复杂 HTML DOM 元素的创建。

<p *ngIf="canShow">
   This sectiom will be shown only when the *canShow* propery's value in the corresponding component is *true* </p> 
<p [showToolTip]='tips' />

这里,ngIfshowToolTip(只是一个例子)是指令。ngIf 仅当 canShow 为真时才创建段落 DOM 元素。类似地,showToolTip属性指令,它为段落元素添加了工具提示功能。

当用户将鼠标悬停在段落上时,将显示一个工具提示。工具提示的内容来自其对应组件的 tips 属性。

模块

Angular 模块基本上是相关功能/功能的集合。Angular 模块将多个组件和服务分组到单个上下文中。

例如,动画相关功能可以分组到单个模块中,并且 Angular 已经为动画相关功能提供了一个模块,即 BrowserAnimationModule 模块。

一个 Angular 应用程序可以拥有任意数量的模块,但只有一个模块可以设置为根模块,它将引导应用程序,然后在必要时调用其他模块。一个模块也可以配置为访问其他模块的功能。简而言之,任何模块中的组件都可以访问任何其他模块中的组件和服务。

下图描述了模块与其组件之间的交互。

Module

让我们检查一下 Expense Manager 应用程序的根模块。

import { BrowserModule } from '@angular/platform-browser'; 
import { NgModule } from '@angular/core'; 
import { AppComponent } from './app.component'; @NgModule({ 
   declarations: [ 
      AppComponent 
   ], 
   imports: [ 
      BrowserModule 
   ], 
   providers: [], 
   bootstrap: [AppComponent] 
}) 
export class AppModule { }

这里,

  • NgModule 装饰器用于将普通的 TypeScript/JavaScript 类转换为 Angular 模块

  • declarations 选项用于将组件包含到 AppModule 模块中。

  • bootstrap 选项用于设置 AppModule 模块的根组件。

  • providers 选项用于为 AppModule 模块包含服务。

  • imports 选项用于将其他模块导入到 AppModule 模块中。

下图描述了模块、组件和服务之间的关系

服务

服务是提供非常特定功能的普通 TypeScript/JavaScript 类。服务将执行一项单一任务并将其做好。服务的目的是可重用性。与其在组件内部编写功能,不如将其分离到服务中,这样也可以在其他组件中使用。

此外,服务使开发人员能够组织应用程序的业务逻辑。基本上,组件使用服务来完成自己的工作。依赖注入用于在组件中正确初始化服务,以便组件可以在必要时无需任何设置即可访问服务。

Angular 应用程序的工作流程

我们已经学习了 Angular 应用程序的核心概念。让我们看看典型 Angular 应用程序的完整流程。

Angular application

src/main.ts 是 Angular 应用程序的入口点。

src/main.ts 引导 AppModule(src/app.module.ts),它是每个 Angular 应用程序的根模块。

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

AppModule 引导 AppComponent(src/app.component.ts),它是每个 Angular 应用程序的根组件。

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

这里,

AppModule 通过 imports 选项加载模块。

AppModule 还使用 依赖注入 (DI) 框架加载所有注册的服务。

AppComponent 渲染其模板 (src/app.component.html) 并使用相应的样式 (src/app.component.css)。AppComponent 名称 app-root 用于将其放置在 src/index.html 中。

<!doctype html> 
<html lang="en"> 
   <head> 
      <meta charset="utf-8"> 
      <title>ExpenseManager</title> 
      <base href="/"> 
      <meta name="viewport" content="width=device-width, initial-scale=1"> 
      <link rel="icon" type="image/x-icon" href="favicon.ico"> 
   </head> 
   <body> 
      <app-root></app-root> 
   </body> 
</html>

AppComponent 可以使用应用程序中注册的任何其他组件。

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

组件通过其模板中指令使用目标组件的选择器名称来使用其他组件。

<component-selector-name></component-selector-name>

此外,所有注册的服务都可通过 依赖注入 (DI) 框架访问所有 Angular 组件。

Angular - Angular 组件和模板

正如我们之前了解到的,组件是 Angular 应用程序的构建块。Angular 组件的主要工作是生成一个称为 视图的网页部分。每个组件都将有一个关联的模板,它将用于生成视图。

让我们在本节中学习组件和模板的基本概念。

添加组件

让我们在 ExpenseManager 应用程序中创建一个新组件。

打开命令提示符并转到 ExpenseManager 应用程序。

cd /go/to/expense-manager

使用以下指定的 ng generate component 命令创建一个新组件:

ng generate component expense-entry

输出

输出如下所示:

CREATE src/app/expense-entry/expense-entry.component.html (28 bytes) 
CREATE src/app/expense-entry/expense-entry.component.spec.ts (671 bytes) 
CREATE src/app/expense-entry/expense-entry.component.ts (296 bytes) 
CREATE src/app/expense-entry/expense-entry.component.css (0 bytes) 
UPDATE src/app/app.module.ts (431 bytes)

这里,

  • ExpenseEntryComponent 创建在 src/app/expense-entry 文件夹下。
  • 创建了组件类、模板和样式表。
  • AppModule 使用新组件更新。

将 title 属性添加到 ExpenseEntryComponent(src/app/expense-entry/expense-entry.component.ts)组件。

import { Component, OnInit } from '@angular/core'; @Component({ 
   selector: 'app-expense-entry', 
   templateUrl: './expense-entry.component.html', styleUrls: ['./expense-entry.component.css'] 
}) 
export class ExpenseEntryComponent implements OnInit {
   title: string;
   constructor() { } 
   ngOnInit() { 
      this.title = "Expense Entry" 
   } 
}

使用以下内容更新模板 src/app/expense-entry/expense-entry.component.html

<p>{{ title }}</p>

打开 src/app/app.component.html 并包含新创建的组件。

<h1>{{ title }}</h1>
<app-expense-entry></app-expense-entry>

这里,

app-expense-entry 是选择器值,可以用作常规 HTML 标签。

最后,应用程序的输出如下所示:

HTML Tag

在学习更多关于模板的知识的过程中,我们将更新组件的内容。

模板

Angular 组件的组成部分是 模板。它用于生成 HTML 内容。模板是具有附加功能的普通 HTML。

附加模板

模板可以使用 @component 装饰器的元数据附加到 Angular 组件。Angular 提供了两个元数据来将模板附加到组件。

templateUrl

我们已经知道如何使用 templateUrl。它期望模板文件的相对路径。例如,AppComponent 将其模板设置为 app.component.html。

templateUrl: './app.component.html',

模板

模板允许将 HTML 字符串放置在组件本身中。如果模板内容最少,那么将其放在 组件类本身中将更容易跟踪和维护。

@Component({ 
   selector: 'app-root', 
   templateUrl: `<h1>{{ title }}</h1>`, 
   styleUrls: ['./app.component.css'] 
}) 
export class AppComponent implements OnInit { 
   title = 'Expense Manager'; 
   constructor(private debugService : DebugService) {} ngOnInit() { 
      this.debugService.info("Angular Application starts"); 
   } 
}

附加样式表

Angular 模板可以使用与 HTML 类似的 CSS 样式。模板从两个来源获取其样式信息,a) 来自其组件 b) 来自应用程序配置。

组件配置

组件装饰器提供了两个选项,stylesstyleUrls,用于向其模板提供 CSS 样式信息。

  • Styles - styles 选项用于将 CSS 放置在组件本身中。
styles: ['h1 { color: '#ff0000'; }']
  • styleUrls - styleUrls 用于引用外部 CSS 样式表。我们也可以使用多个样式表。
styleUrls: ['./app.component.css', './custom_style.css']

应用程序配置

Angular 在项目配置 (angular.json) 中提供了一个选项来指定 CSS 样式表。在 angular.json 中指定的样式将适用于所有模板。让我们检查一下我们的 angular.json,如下所示:

{
"projects": { 
   "expense-manager": { 
      "architect": { 
         "build": { 
            "builder": "@angular-devkit/build-angular:browser", "options": { 
               "outputPath": "dist/expense-manager", 
               "index": "src/index.html", 
               "main": "src/main.ts", 
               "polyfills": "src/polyfills.ts", 
               "tsConfig": "tsconfig.app.json", 
               "aot": false, 
               "assets": [ 
                  "src/favicon.ico", 
                  "src/assets" 
               ], 
               "styles": [ 
                  "src/styles.css" 
               ], 
               "scripts": [] 
            }, 
         }, 
      } 
   }}, 
   "defaultProject": "expense-manager" 
}

这里,

styles 选项将 src/styles.css 设置为全局 CSS 样式表。我们可以包含任意数量的 CSS 样式表,因为它支持多个值。

包含 Bootstrap

让我们使用 styles 选项将 Bootstrap 包含到我们的 ExpenseManager 应用程序中,并将默认模板更改为使用 Bootstrap 组件。

打开命令提示符并转到 ExpenseManager 应用程序。

cd /go/to/expense-manager

使用以下命令安装 bootstrapJQuery

npm install --save bootstrap@4.5.0 jquery@3.5.1

这里,

我们安装了 JQuery,因为 Bootstrap 广泛使用 jquery 来实现高级组件。

选项 angular.json 并设置 bootstrap 和 jquery 库路径。

{ 
   "projects": { 
      "expense-manager": { 
         "architect": { 
            "build": {
               "builder":"@angular-devkit/build-angular:browser", "options": { 
                  "outputPath": "dist/expense-manager", 
                  "index": "src/index.html", 
                  "main": "src/main.ts", 
                  "polyfills": "src/polyfills.ts", 
                  "tsConfig": "tsconfig.app.json", 
                  "aot": false, 
                  "assets": [ 
                     "src/favicon.ico", 
                     "src/assets" 
                  ], 
                  "styles": [ 
                     "./node_modules/bootstrap/dist/css/bootstrap.css", "src/styles.css" 
                  ], 
                  "scripts": [ 
                     "./node_modules/jquery/dist/jquery.js", "./node_modules/bootstrap/dist/js/bootstrap.js" 
                  ] 
               }, 
            }, 
         } 
   }}, 
   "defaultProject": "expense-manager" 
}

这里,

scripts 选项用于包含 JavaScript 库。通过 scripts 注册的 JavaScript 将可用于应用程序中的所有 Angular 组件。

打开 app.component.html 并将内容更改为如下所示

<!-- Navigation --> 
<nav class="navbar navbar-expand-lg navbar-dark bg-dark static-top"> 
   <div class="container"> 
      <a class="navbar-brand" href="#">{{ title }}</a> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarResponsive" aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation"> 
         <span class="navbar-toggler-icon">
         </span> 
      </button> 
      <div class="collapse navbar-collapse" id="navbarResponsive"> 
         <ul class="navbar-nav ml-auto"> 
            <li class="nav-item active"> 
            <a class="nav-link" href="#">Home
               <span class="sr-only">(current)
               </span>
            </a> 
            </li> 
            <li class="nav-item"> 
            <a class="nav-link" href="#">Report</a> 
            </li> 
            <li class="nav-item"> 
            <a class="nav-link" href="#">Add Expense</a> 
            </li> 
            <li class="nav-item"> 
            <a class="nav-link" href="#">About</a> 
            </li> 
         </ul> 
      </div> 
   </div> 
</nav> 
<app-expense-entry></app-expense-entry>

这里,

使用 Bootstrap 导航和容器。

打开 src/app/expense-entry/expense-entry.component.html 并放置以下内容。

<!-- Page Content --> 
<div class="container"> 
   <div class="row"> 
      <div class="col-lg-12 text-center" style="padding-top: 20px;"> 
         <div class="container" style="padding-left: 0px; padding-right: 0px;"> 
            <div class="row"> 
            <div class="col-sm" style="text-align: left;"> {{ title }} 
            </div> 
            <div class="col-sm" style="text-align: right;"> 
               <button type="button" class="btn btn-primary">Edit</button> 
            </div> 
            </div> 
         </div> 
         <div class="container box" style="margin-top: 10px;"> 
         <div class="row"> 
         <div class="col-2" style="text-align: right;">  
            <strong><em>Item:</em></strong> 
         </div> 
         <div class="col" style="text-align: left;"> 
            Pizza 
         </div>
         </div> 
         <div class="row"> 
         <div class="col-2" style="text-align: right;">
            <strong><em>Amount:</em></strong> 
         </div> 
         <div class="col" style="text-align: left;"> 
            20 
         </div> 
         </div> 
         <div class="row"> 
         <div class="col-2" style="text-align: right;"> 
            <strong><em>Category:</em></strong> 
         </div> 
         <div class="col" style="text-align: left;"> 
            Food 
         </div> 
         </div> 
         <div class="row"> 
         <div class="col-2" style="text-align: right;"> 
            <strong><em>Location:</em></strong>
         </div> 
         <div class="col" style="text-align: left;"> 
            Zomato 
         </div> 
         </div> 
         <div class="row"> 
         <div class="col-2" style="text-align: right;"> 
            <strong><em>Spend On:</em></strong> 
         </div> 
         <div class="col" style="text-align: left;"> 
            June 20, 2020 
         </div> 
         </div> 
      </div> 
   </div> 
</div> 
</div>

重新启动应用程序。

应用程序的输出如下所示:

Restart Tag

我们将在下一章改进应用程序以处理动态费用条目。

Angular - 数据绑定

数据绑定处理如何将您的数据从组件绑定到 HTML DOM 元素(模板)。我们可以轻松地与应用程序交互,而无需担心如何插入您的数据。我们可以通过两种不同的方式建立连接:单向绑定和双向绑定。

在进入此主题之前,让我们在 Angular 中创建一个组件。

打开命令提示符并使用以下命令创建新的 Angular 应用程序:

cd /go/to/workspace 
ng new databind-app 
cd databind-app

使用 Angular CLI 创建一个 test 组件,如下所示:

ng generate component test

以上创建了一个新组件,输出如下所示:

CREATE src/app/test/test.component.scss (0 bytes) CREATE src/app/test/test.component.html (19 bytes) CREATE src/app/test/test.component.spec.ts (614 bytes) 
CREATE src/app/test/test.component.ts (262 bytes) UPDATE src/app/app.module.ts (545 bytes)

使用以下命令运行应用程序:

ng serve

单向数据绑定

单向数据绑定是组件及其模板之间的一种单向交互。如果您在组件中执行任何更改,则它会反映在 HTML 元素中。它支持以下类型:

字符串插值

通常,字符串插值是格式化或操作字符串的过程。在 Angular 中,插值用于将数据从组件显示到视图 (DOM)。它用 {{ }} 表达式表示,也称为胡子语法。

让我们在组件中创建一个简单的字符串属性并将数据绑定到视图。

将以下代码添加到 test.component.ts 文件中,如下所示:

export class TestComponent implements OnInit { 
   appName = "My first app in Angular"; 
}

转到 test.component.html 文件并添加以下代码:

<h1>{{appName}}</h1>

通过替换现有内容,将测试组件添加到您的 app.component.html 文件中,如下所示:

<app-test></app-test>

最后,使用以下命令启动您的应用程序(如果尚未启动):

ng serve

您可以在屏幕上看到以下输出:

String interpolation

事件绑定

事件是指鼠标点击、双击、悬停或任何键盘和鼠标操作等操作。如果用户与应用程序交互并执行某些操作,则会触发事件。它用括号 ()on- 表示。我们有不同的方法将事件绑定到 DOM 元素。让我们简要了解一下。

组件到视图绑定

让我们了解简单的按钮点击事件处理是如何工作的。

将以下代码添加到 test.component.ts 文件中,如下所示:

export class TestComponent { 
   showData($event: any){ 
      console.log("button is clicked!"); if($event) { 
         console.log($event.target); 
         console.log($event.target.value); 
      } 
   } 
}

$event*:指的是触发的事件。在这个场景中,*click* 是事件。$event 包含有关事件和目标元素的所有信息。这里,目标是按钮。$event.target 属性将包含目标信息。

我们有两种方法可以将组件方法调用到视图 (test.component.html)。第一种方法定义如下:

<h2>Event Binding</h2> 
<button (click)="showData($event)">Click here</button>

或者,您可以使用规范形式的 前缀 - on,如下所示:

<button on-click = "showData()">Click here</button>

这里,我们没有使用 $event,因为它可选项。

最后,使用以下命令启动您的应用程序(如果尚未启动):

ng serve

现在,运行您的应用程序,您会看到以下响应:

canonical

这里,当用户点击按钮时,事件绑定会理解按钮点击操作并调用组件 showData() 方法,因此我们可以得出结论,它是一种单向绑定。

属性绑定

属性绑定用于将组件属性中的数据绑定到 DOM 元素。它用 [] 表示。

让我们通过一个简单的示例来了解。

将以下代码添加到 test.component.ts 文件中。

export class TestComponent { 
   userName:string = "Peter"; 
}

在 view test.component.html 中添加以下更改,

<input type="text" [value]="userName">

这里,

userName 属性绑定到 DOM 元素 <input> 标签的一个属性。

最后,使用以下命令启动您的应用程序(如果尚未启动):

ng serve
Property binding

属性绑定

属性绑定用于将组件中的数据绑定到 HTML 属性。语法如下:

<HTMLTag [attr.ATTR]="Component data">

例如,

<td [attr.colspan]="columnSpan"> ... </td>

让我们通过一个简单的示例来了解。

将以下代码添加到 test.component.ts 文件中。

export class TestComponent { 
   userName:string = "Peter"; 
}

在 view test.component.html 中添加以下更改,

<input type="text" [value]="userName">

这里,

userName 属性绑定到 DOM 元素 <input> 标签的一个属性。

最后,使用以下命令启动您的应用程序(如果尚未启动):

ng serve
Attribute binding

类绑定

类绑定用于将组件中的数据绑定到 HTML 的 class 属性。语法如下:

<HTMLTag [class]="component variable holding class name">

类绑定提供了额外的功能。如果组件数据是布尔值,则只有在数据为 true 时才会绑定类。可以通过字符串(“foo bar”)和字符串数组提供多个类。还有更多选项可用。

例如,

<p [class]="myClasses">

让我们通过一个简单的示例来了解。

在 test.component.ts 文件中添加以下代码,

export class TestComponent { 
   myCSSClass = "red"; 
   applyCSSClass = false; 
}

在 view test.component.html 中添加以下更改。

<p [class]="myCSSClass">This paragraph class comes from *myClass* property </p> 
<p [class.blue]="applyCSSClass">This paragraph class does not apply</p>

在 test.component.css 中添加以下内容。

.red { 
   color: red; 
} 
.blue { 
   color: blue; 
}

最后,使用以下命令启动您的应用程序(如果尚未启动):

ng serve

最终输出将如下所示:

Class binding

样式绑定

样式绑定用于将组件中的数据绑定到 HTML 的 style 属性。语法如下:

<HTMLTag [style.STYLE]="component data">

例如,

<p [style.color]="myParaColor"> ... </p>

让我们通过一个简单的示例来了解。

将以下代码添加到 test.component.ts 文件中。

myColor = 'brown';

在 view test.component.html 中添加以下更改。

<p [style.color]="myColor">Text color is styled using style binding</p>

最后,使用以下命令启动您的应用程序(如果尚未启动):

ng serve

最终输出将如下所示:

Style binding

双向数据绑定

双向数据绑定是一种双向交互,数据双向流动(从组件到视图,以及从视图到组件)。简单的例子是 ngModel。如果您在属性(或模型)中进行任何更改,则它会反映在您的视图中,反之亦然。它是属性和事件绑定的组合。

NgModel

NgModel 是一个独立的指令。ngModel 指令将表单控件绑定到属性,并将属性绑定到表单控件。ngModel 的语法如下:

<HTML [(ngModel)]="model.name" />

例如,

<input type="text" [(ngModel)]="model.name" />

让我们尝试在测试应用程序中使用 ngModel

AppModule (src/app/app.module.ts) 中配置 FormsModule

import { FormsModule } from '@angular/forms'; @NgModule({ 
   imports: [ 
      BrowserModule, 
      FormsModule
   ] 
}) 
export class AppModule { }

FormModule 执行必要的设置以启用双向数据绑定。

更新 TestComponent 视图 (test.component.html),如下所示:

<input type="text" [(ngModel)]="userName" />
<p>Two way binding! Hello {{ userName }}!</p>

这里,

属性绑定到表单控件 ngModel 指令,如果您在文本框中输入任何文本,它将绑定到该属性。运行应用程序后,您将看到以下更改:

最后,使用以下命令启动您的应用程序(如果尚未启动):

ng serve

现在,运行您的应用程序,您会看到以下响应:

Way Data binding

现在,尝试将输入值更改为 Jack。在您键入时,输入下方文本将发生更改,最终输出将如下所示:

Two Way Data binding

我们将在后续章节中学习更多关于表单控件的知识。

工作示例

让我们在 ExpenseManager 应用程序中实现本章学习的所有概念。

打开命令提示符并转到项目根文件夹。

cd /go/to/expense-manager

创建 ExpenseEntry 接口 (src/app/expense-entry.ts) 并添加 id、amount、category、Location、spendOn 和 createdOn。

export interface ExpenseEntry { 
   id: number; 
   item: string; 
   amount: number; 
   category: string; 
   location: string; 
   spendOn: Date; 
   createdOn: Date; 
}

ExpenseEntry 导入到 ExpenseEntryComponent 中。

import { ExpenseEntry } from '../expense-entry';

创建一个 ExpenseEntry 对象,expenseEntry,如下所示:

export class ExpenseEntryComponent implements OnInit { 
   title: string; 
   expenseEntry: ExpenseEntry; 
   constructor() { } 
   ngOnInit() { 
      this.title = "Expense Entry"; 
      this.expenseEntry = { 
         id: 1, 
         item: "Pizza", 
         amount: 21, 
         category: "Food", 
         location: "Zomato", 
         spendOn: new Date(2020, 6, 1, 10, 10, 10), createdOn: new Date(2020, 6, 1, 10, 10, 10), 
      }; 
   } 
}

使用 expenseEntry 对象更新组件模板,src/app/expense-entry/expense-entry.component.html,如下所示:

<!-- Page Content --> 
<div class="container">
   <div class="row"> 
      <div class="col-lg-12 text-center" style="padding-top: 20px;"> 
         <div class="container" style="padding-left: 0px; padding-right: 0px;"> 
            <div class="row"> 
               <div class="col-sm" style="text-align: left;"> 
                  {{ title }} 
               </div> 
               <div class="col-sm" style="text-align: right;"> 
                  <button type="button" class="btn btn-primary">Edit</button> 
               </div> 
            </div> 
         </div> 
         <div class="container box" style="margin-top: 10px;"> 
            <div class="row"> 
               <div class="col-2" style="text-align: right;">  
                  <strong><em>Item:</em></strong> 
               </div> 
               <div class="col" style="text-align: left;"> 
                  {{ expenseEntry.item }} 
               </div> 
            </div> 
            <div class="row"> 
               <div class="col-2" style="text-align: right;"> 
                  <strong><em>Amount:</em></strong> 
               </div> 
               <div class="col" style="text-align: left;">
                  {{ expenseEntry.amount }} 
               </div> 
            </div> 
            <div class="row"> 
               <div class="col-2" style="text-align: right;"> 
                  <strong><em>Category:</em></strong> 
               </div> 
               <div class="col" style="text-align: left;"> 
                  {{ expenseEntry.category }} 
               </div> 
            </div> 
            <div class="row"> 
               <div class="col-2" style="text-align: right;"> 
                  <strong><em>Location:</em></strong> 
               </div> 
               <div class="col" style="text-align: left;"> 
                  {{ expenseEntry.location }} 
               </div> 
            </div> 
            <div class="row"> 
               <div class="col-2" style="text-align: right;">
                  <strong><em>Spend On:</em></strong>
               </div> 
               <div class="col" style="text-align: left;"> 
                  {{ expenseEntry.spendOn }} 
               </div> 
            </div> 
         </div> 
      </div> 
   </div> 
</div>
NgModel

Angular - 指令

Angular 指令是用于与应用程序交互的 DOM 元素。通常,指令是一个 TypeScript 函数。当此函数执行时,Angular 编译器会在 DOM 元素内检查它。Angular 指令以 ng- 开头,其中 ng 代表 Angular,并使用 @directive 装饰器扩展 HTML 标签。

指令使逻辑能够包含在 Angular 模板中。Angular 指令可以分为三类,如下所示:

属性指令

用于为现有的 HTML 元素添加新属性以更改其外观和行为。

<HTMLTag [attrDirective]='value' />

例如,

<p [showToolTip]='Tips' />

这里,showToolTip 指的是一个示例指令,当在 HTML 元素中使用时,它会在用户将鼠标悬停在 HTML 元素上时显示提示。

结构指令

用于在当前 HTML 文档中添加或删除 DOM 元素。

<HTMLTag [structuralDirective]='value' />

例如,

<div *ngIf="isNeeded"> 
   Only render if the *isNeeded* value has true value. 
</div>

这里,ngIf 是一个内置指令,用于在当前 HTML 文档中添加或删除 HTML 元素。Angular 提供了许多内置指令,我们将在后面的章节中学习。

基于组件的指令

组件可以用作指令。每个组件都有 InputOutput 选项来传递组件与其父 HTML 元素之间的信息。

<component-selector-name [input-reference]="input-value"> ... </component-selector-name>

例如,

<list-item [items]="fruits"> ... </list-item>

这里,list-item 是一个组件,items 是输入选项。我们将在后面的章节中学习如何创建组件和高级用法。

在进入本主题之前,让我们在 Angular 中创建一个示例应用程序 (directive-app) 来完成学习成果。

打开命令提示符并使用以下命令创建新的 Angular 应用程序:

cd /go/to/workspace 
ng new directive-app 
cd directive-app

使用 Angular CLI 创建一个 test 组件,如下所示:

ng generate component test

以上创建了一个新组件,输出如下所示:

CREATE src/app/test/test.component.scss (0 bytes) CREATE src/app/test/test.component.html (19 bytes) CREATE src/app/test/test.component.spec.ts (614 bytes) 
CREATE src/app/test/test.component.ts (262 bytes) UPDATE src/app/app.module.ts (545 bytes)

使用以下命令运行应用程序:

ng serve

DOM 概述

让我们简要了解一下 DOM 模型。DOM 用于定义访问文档的标准。通常,HTML DOM 模型被构建为对象的树。它是一个访问 html 元素的标准对象模型。

我们可以在 Angular 中出于以下原因使用 DOM 模型:

  • 我们可以使用 DOM 元素轻松导航文档结构。
  • 我们可以轻松添加 html 元素。
  • 我们可以轻松更新元素及其内容。

结构指令

结构指令通过添加或删除元素来更改 DOM 的结构。它用 * 符号表示,并带有三个预定义的指令 NgIf、NgForNgSwitch。让我们逐一简要了解一下。

NgIf 指令

NgIf 指令用于根据条件是否为真或假来显示或隐藏应用程序中的数据。我们可以将其添加到模板中的任何标签中。

让我们尝试在 directive-app 应用程序中使用 ngIf 指令。

test.component.html 中添加以下标签。

<p>test works!</p> 
<div *ngIf="true">Display data</div>

在您的 app.component.html 文件中添加测试组件,如下所示:

<app-test></app-test>

使用以下命令启动服务器(如果尚未启动):

ng serve

现在,运行您的应用程序,您会看到以下响应:

NgServe

如果将条件设置为 ngIf=“false”,则内容将被隐藏。

ngIfElse 指令

ngIfElsengIf 类似,不同的是它还提供了在失败场景中呈现内容的选项。

让我们通过一个示例了解 ngIfElse 的工作原理。

test.component.ts 文件中添加以下代码。

export class TestComponent implements OnInit { 
   isLogIn : boolean = false;
   isLogOut : boolean = true; 
}

test.component.html 文件中添加以下代码,如下所示:

<p>ngIfElse example!</p> 
<div *ngIf="isLogIn; else isLogOut"> 
   Hello you are logged in 
</div>
<ng-template #isLogOut> 
   You're logged out.. 
</ng-template>

最后,使用以下命令启动您的应用程序(如果尚未启动):

ng serve

现在,运行您的应用程序,您会看到以下响应:

NgApplication

这里,

isLogOut

值为 true,因此它转到 else 块并呈现 ng-template。我们将在本章后面学习 ng-template

ngFor 指令

ngFor 用于从项目列表中重复一部分元素。

让我们通过一个示例了解 ngFor 的工作原理。

在 test.component.ts 文件中添加列表,如下所示:

list = [1,2,3,4,5];

test.component.html 中添加 ngFor 指令,如下所示:

<h2>ngFor directive</h2> 
<ul> 
   <li *ngFor="let l of list">
      {{l}} 
   </li>
</ul>

这里,let 关键字创建一个局部变量,它可以在模板中的任何位置被引用。let l 创建一个模板局部变量以获取列表元素。

最后,使用以下命令启动您的应用程序(如果尚未启动):

ng serve

现在,运行您的应用程序,您会看到以下响应:

Ngdirective

trackBy

有时,ngFor 在大型列表中的性能较低。例如,当在列表中添加新项目或删除任何项目时,可能会触发多个 DOM 操作。要遍历大型对象集合,我们使用 trackBy

它用于跟踪何时添加或删除元素。它由 trackBy 方法执行。它有两个参数 index 和 element。Index 用于唯一标识每个元素。下面定义了一个简单的示例。

让我们通过一个示例了解 trackBy 与 ngFor 的配合使用。

将以下代码添加到 test.component.ts 文件中。

export class TestComponent { 
   studentArr: any[] = [ { 
      "id": 1, 
      "name": "student1" 
   }, 
   { 
      "id": 2,
      "name": "student2" 
   }, 
   { 
      "id": 3, "name": "student3"
   },
   { 
      "id": 4, 
      "name": "student4" 
   } 
   ]; 
   trackByData(index:number, studentArr:any): number { 
      return studentArr.id; 
   }

这里,

我们创建了,

trackByData()

方法以基于 id 的唯一方式访问每个学生元素。

test.component.html 文件中添加以下代码,以在 ngFor 内部定义 trackBy 方法。

<ul> 
   <li *ngFor="let std of studentArr; trackBy: trackByData">
      {{std.name}} 
   </li>
</ul>

最后,使用以下命令启动您的应用程序(如果尚未启动):

ng serve

现在,运行您的应用程序,您会看到以下响应:

Directive

这里,应用程序将打印学生姓名。现在,应用程序使用学生 id 而不是对象引用来跟踪学生对象。因此,DOM 元素不受影响。

NgSwitch 指令

NgSWitch 用于检查多个条件并保持 DOM 结构简单易懂。

让我们尝试在 directive-app 应用程序中使用 ngSwitch 指令。

test.component.ts 文件中添加以下代码。

export class TestComponent implements OnInit {  
   logInName = 'admin'; 
}

在 test.component.html 文件中添加以下代码,如下所示:

<h2>ngSwitch directive</h2> 
<ul [ngSwitch]="logInName"> 
   <li *ngSwitchCase="'user'"> 
      <p>User is logged in..</p> 
   </li> 
   <li *ngSwitchCase="'admin'"> 
      <p>admin is logged in</p> 
   </li> 
   <li *ngSwitchDefault> 
      <p>Please choose login name</p> 
   </li> 
</ul>

最后,使用以下命令启动您的应用程序(如果尚未启动):

ng serve

现在,运行您的应用程序,您会看到以下响应:

NgSwitch

这里,我们将 logInName 定义为 admin。因此,它与第二个 SwitchCase 匹配并打印上述与 admin 相关的消息。

属性指令

属性指令执行 DOM 元素或组件的外观或行为。一些示例包括 NgStyle、NgClass 和 NgModel。而 NgModel 是前一章中解释的双向属性数据绑定。

ngStyle

ngStyle 指令用于添加动态样式。以下示例用于将蓝色应用于段落。

让我们尝试在 directive-app 应用程序中使用 ngStyle 指令。

test.component.html 文件中添加以下内容。

<p [ngStyle]="{'color': 'blue', 'font-size': '14px'}"> 
   paragraph style is applied using ngStyle 
</p>

使用以下命令启动应用程序(如果尚未启动):

ng serve

现在,运行您的应用程序,您会看到以下响应:

ngStyle

ngClass

ngClass 用于在 HTML 元素中添加或删除 CSS 类。

让我们尝试在 directive-app 应用程序中使用 ngClass 指令。

使用以下命令创建一个类 User

ng g class User

您可以看到以下响应 -

CREATE src/app/user.spec.ts (146 bytes) 
CREATE src/app/user.ts (22 bytes)

转到 src/app/user.ts 文件并添加以下代码:

export class User { 
   userId : number; userName : string; 
}

这里,我们在 User 类中创建了两个属性 userIduserName

打开 test.component.ts 文件并添加以下更改:

import { User } from '../user'; 
export class TestComponent implements OnInit {  
   users: User[] = [ 
      { 
         "userId": 1, 
         "userName": 'User1' 
      }, 
      { 
         "userId": 2, 
         "userName": 'User2' 
      }, 
   ]; 
}

这里,我们声明了一个局部变量 users 并使用 2 个用户对象初始化它。

打开 test.component.css 文件并添加以下代码

.highlight { 
   color: red; 
}

打开您的 test.component.html 文件并添加以下代码:

<div class="container"> 
   <br/> 
   <div *ngFor="let user of users" [ngClass]="{ 
      'highlight':user.userName === 'User1' 
   }"> 
      {{ user.userName }} 
   </div> 
</div>

这里,

我们为 User1 应用了 ngClass,因此它将突出显示 User1

最后,使用以下命令启动您的应用程序(如果尚未启动):

ng serve

现在,运行您的应用程序,您会看到以下响应:

ngClass

自定义指令

Angular 提供了使用用户定义的指令扩展 Angular 指令的选项,这称为 自定义指令。让我们在本节中学习如何创建自定义指令。

让我们尝试在 directive-app 应用程序中创建自定义指令。

Angular CLI 提供以下命令来创建自定义指令。

ng generate directive customstyle

执行此命令后,您将看到以下响应:

CREATE src/app/customstyle.directive.spec.ts (244 bytes) 
CREATE src/app/customstyle.directive.ts (151 bytes) UPDATE src/app/app.module.ts (1115 bytes)

打开 app.module.ts。指令将通过 declarations 元数据在 AppModule 中配置。

import { CustomstyleDirective } from './customstyle.directive'; 
@NgModule({ 
   declarations: [ 
      AppComponent, 
      TestComponent, 
      CustomstyleDirective 
   ] 
})

打开 customstyle.directive.ts 文件并添加以下代码:

import { Directive, ElementRef } from '@angular/core'; 
@Directive({ 
   selector: '[appCustomstyle]' 
}) 
export class CustomstyleDirective {
   constructor(el: ElementRef) { 
      el.nativeElement.style.fontSize = '24px'; 
   } 
}

这里,constructor 方法使用 CustomStyleDirective 获取元素作为 el。然后,它访问 el 的样式并使用 CSS 属性将其字体大小设置为 24px

最后,使用以下命令启动您的应用程序(如果尚未启动):

ng serve

现在,运行您的应用程序,您会看到以下响应:

Custom directives

ng-template

ng-template 用于创建动态且可重用的模板。它是一个虚拟元素。如果您使用 ng-template 编译代码,则它将在 DOM 中转换为注释。

例如,

让我们在 test.component.html 页面中添加以下代码。

<h3>ng-template</h3> 
<ng-template>ng-template tag is a virtual element</ng-template>

如果您运行该应用程序,它将仅打印 h3 元素。检查您的页面源代码,模板以注释的形式显示,因为它是一个虚拟元素,因此它不会呈现任何内容。我们需要将 ng-template 与 Angular 指令一起使用。

通常,指令会发出与其关联的 HTML 标签。有时,我们不希望标签,而只希望内容。例如,在以下示例中,将发出 li。

<li *ngFor="let item in list">{{ item }}</li>

我们可以使用 ng-template 安全地跳过 li 标签。

ng-template 与结构指令

ng-template 应始终用于 ngIf、ngForngSwitch 指令内部以呈现结果。

让我们假设一个简单的代码。

<ng-template [ngIf]=true> 
   <div><h2>ng-template works!</h2></div> 
</ng-template>

这里,如果 ngIf 条件变为 true,它将打印 div 元素内部的数据。类似地,您也可以使用 ngForngSwitch 指令。

NgForOf 指令

ngForOf 也是一个结构指令,用于在集合中呈现项目。以下示例用于显示 ng-template 内部的 ngForOf 指令。

import { Component, OnInit } from '@angular/core'; 
@Component({ 
   selector: 'app-test', 
   template: ` 
   <div> 
   <ng-template ngFor let-item [ngForOf]="Fruits" let-i="index"> 
   <p>{{i}}</p> 
   </ng-template> 
   </div>` 
   , 
   styleUrls: ['./test.component.css'] 
}) 
export class TestComponent implements OnInit { 
   Fruits = ["mango","apple","orange","grapes"]; 
   ngOnInit() 
   { 
   } 
}

如果您运行该应用程序,它将显示每个元素的索引,如下所示:

0 
1 
2 
3

组件指令

组件指令基于组件。实际上,每个组件都可以用作指令。组件提供 @Input 和 @Output 装饰器来在父组件和子组件之间发送和接收信息。

让我们尝试在我们的directive-app应用程序中使用组件作为指令。

使用以下命令创建一个新的ChildComponent

ng generate component child
CREATE src/app/child/child.component.html (20 bytes) CREATE src/app/child/child.component.spec.ts (621 bytes) 
CREATE src/app/child/child.component.ts (265 bytes) CREATE src/app/child/child.component.css (0 bytes) UPDATE src/app/app.module.ts (466 bytes)

打开child.component.ts并添加以下代码:

@Input() userName: string;

在这里,我们正在为ChildComponent设置一个输入属性。

打开child.component.html并添加以下代码:

<p>child works!</p> 
<p>Hi {{ userName }}</p>

在这里,我们使用userName的值来欢迎用户。

打开test.component.ts并添加以下代码:

name: string = 'Peter';

打开test.component.html并添加以下代码:

<h1>Test component</h1> 
<app-child [userName]="name"><app-child>

在这里,我们在TestComponent内部使用AppComponent作为具有输入属性的指令。

最后,使用以下命令启动您的应用程序(如果尚未启动):

ng serve

现在,运行您的应用程序,您会看到以下响应:

[](images/directive-app/component_as_directive.PNG"

工作示例

让我们在ExpenseManager应用程序中添加一个新组件来列出费用条目。

打开命令提示符并转到项目根文件夹。

cd /go/to/expense-manager

启动应用程序。

ng serve

使用以下命令创建一个新组件ExpenseEntryListComponent

ng generate component ExpenseEntryList

输出

输出如下:

CREATE src/app/expense-entry-list/expense-entry-list.component.html (33 bytes) 
CREATE src/app/expense-entry-list/expense-entry-list.component.spec.ts (700 bytes) 
CREATE src/app/expense-entry-list/expense-entry-list.component.ts (315 bytes) 
CREATE src/app/expense-entry-list/expense-entry-list.component.css (0 bytes) 
UPDATE src/app/app.module.ts (548 bytes)

在这里,该命令创建了ExpenseEntryList组件并在AppModule中更新了必要的代码。

ExpenseEntry导入到ExpenseEntryListComponent组件(src/app/expense-entry-list/expense-entry-list.component)

import { ExpenseEntry } from '../expense-entry';

添加一个方法getExpenseEntries(),在ExpenseEntryListComponent (src/app/expense-entry-list/expense-entry-list.component)中返回费用条目的列表(模拟项目)。

getExpenseEntries() : ExpenseEntry[] { 
   let mockExpenseEntries : ExpenseEntry[] = [ 
      { id: 1, 
         item: "Pizza", 
         amount: Math.floor((Math.random() * 10) + 1), 
         category: "Food", 
         location: "Mcdonald", 
         spendOn: new Date(2020, 4, Math.floor((Math.random() * 30) + 1), 10, 10, 10), 
         createdOn: new Date(2020, 4, Math.floor((Math.random() * 30) + 1), 10, 10, 10) }, 
      { id: 1, 
         item: "Pizza", 
         amount: Math.floor((Math.random() * 10) + 1), 
         category: "Food", 
         location: "KFC", 
         spendOn: new Date(2020, 4, Math.floor((Math.random() * 30) + 1), 10, 10, 10), 
         createdOn: new Date(2020, 4, Math.floor((Math.random() * 30) + 1), 10, 10, 10) }, 
      { id: 1,
         item: "Pizza",
         amount: Math.floor((Math.random() * 10) + 1), 
         category: "Food", 
         location: "Mcdonald", 
         spendOn: new Date(2020, 4, Math.floor((Math.random() * 30) + 1), 10, 10, 10), 
         createdOn: new Date(2020, 4, Math.floor((Math.random() * 30) + 1), 10, 10, 10) }, 
      { id: 1, 
         item: "Pizza", 
         amount: Math.floor((Math.random() * 10) + 1), 
         category: "Food", 
         location: "KFC", 
         spendOn: new Date(2020, 4, Math.floor((Math.random() * 30) + 1), 10, 10, 10), 
         createdOn: new Date(2020, 4, Math.floor((Math.random() * 30) + 1), 10, 10, 10) }, 
      { id: 1, 
         item: "Pizza", 
         amount: Math.floor((Math.random() * 10) + 1), 
         category: "Food", 
         location: "KFC", 
         spendOn: new Date(2020, 4, Math.floor((Math.random() * 30) + 1), 10, 10, 10), 
         createdOn: new Date(2020, 4, Math.floor((Math.random() * 30) + 1), 10, 10, 10) 
      }, 
   ]; 
   return mockExpenseEntries; 
}

声明一个局部变量expenseEntries,并加载如下所示的费用条目的模拟列表:

title: string; 
expenseEntries: ExpenseEntry[]; 
constructor() { } 
ngOnInit() { 
   this.title = "Expense Entry List"; 
   this.expenseEntries = this.getExpenseEntries(); 
}

打开模板文件(src/app/expense-entry-list/expense-entry-list.component.html)并在表格中显示模拟条目。

<!-- Page Content -->
<div class="container"> 
   <div class="row"> 
      <div class="col-lg-12 text-center" style="padding-top: 20px;">
         <div class="container" style="padding-left: 0px; padding-right: 0px;"> 
            <div class="row"> 
               <div class="col-sm" style="text-align: left;"> 
                  {{ title }} 
               </div> 
               <div class="col-sm" style="text-align: right;"> 
                  <button type="button" class="btn btn-primary">Edit</button> 
               </div> 
            </div> 
         </div> 
         <div class="container box" style="margin-top: 10px;"> 
            <table class="table table-striped"> 
               <thead> 
                  <tr> 
                     <th>Item</th> 
                     <th>Amount</th> 
                     <th>Category</th> 
                     <th>Location</th> 
                     <th>Spent On</th> 
                  </tr> 
               </thead> 
               <tbody> 
                  <tr *ngFor="let entry of expenseEntries"> 
                     <th scope="row">{{ entry.item }}</th> 
                     <th>{{ entry.amount }}</th> 
                     <td>{{ entry.category }}</td> 
                     <td>{{ entry.location }}</td> 
                     <td>{{ entry.spendOn | date: 'short' }}</td> 
                  </tr> 
               </tbody> 
            </table> 
         </div> 
      </div> 
   </div> 
</div>

这里,

  • 使用bootstrap表格。tabletable-striped将根据Boostrap样式标准对表格进行样式设置。

  • 使用ngFor遍历expenseEntries并生成表格行。

打开AppComponent模板src/app/app.component.html并包含ExpenseEntryListComponent并删除ExpenseEntryComponent,如下所示:

... 
<app-expense-entry-list></app-expense-entry-list>

最后,应用程序的输出如下所示。

AppComponent

Angular - 管道

管道被称为过滤器。它有助于转换数据并在插值内管理数据,由{{ | }}表示。它接受数据、数组、整数和字符串作为输入,这些输入由“|”符号分隔。本章详细解释了管道。

添加参数

在您的test.component.ts文件中创建一个日期方法。

export class TestComponent { 
   presentDate = new Date(); 
}

现在,在您的test.component.html文件中添加以下代码。

<div> 
   Today's date :- {{presentDate}} 
</div>

现在,运行应用程序,它将显示以下输出:

Today's date :- Mon Jun 15 2020 10:25:05 GMT+0530 (IST)

这里,

日期对象被转换为易于阅读的格式。

添加日期管道

让我们在上面的html文件中添加日期管道。

<div> 
   Today's date :- {{presentDate | date }}
</div>

您可以看到以下输出:

Today's date :- Jun 15, 2020

日期中的参数

我们可以使用:字符在管道中添加参数。我们可以使用此参数显示短日期、完整日期或格式化日期。在test.component.html文件中添加以下代码。

<div> 
   short date :- {{presentDate | date:'shortDate' }} <br/>
   Full date :- {{presentDate | date:'fullDate' }} <br/> 
   Formatted date:- {{presentDate | date:'M/dd/yyyy'}} <br/> 
   Hours and minutes:- {{presentDate | date:'h:mm'}} 
</div>

您可以在屏幕上看到以下响应:

short date :- 6/15/20 
Full date :- Monday, June 15, 2020 
Formatted date:- 6/15/2020 
Hours and minutes:- 12:00

链式管道

我们可以将多个管道组合在一起。当某个场景与必须应用于数据转换的多个管道相关联时,这将非常有用。

在上面的示例中,如果要以大写字母显示日期,则可以同时应用DateUppercase管道。

<div> 
   Date with uppercase :- {{presentDate | date:'fullDate' | uppercase}} <br/>
   Date with lowercase :- {{presentDate | date:'medium' | lowercase}} <br/>  
</div>

您可以在屏幕上看到以下响应:

Date with uppercase :- MONDAY, JUNE 15, 2020 Date with lowercase :- jun 15, 2020, 12:00:00 am

这里,

Date、Uppercase和Lowercase是预定义的管道。让我们在下一节中了解其他类型的内置管道。

内置管道

Angular支持以下内置管道。我们将逐一简要讨论。

AsyncPipe

如果数据以可观察对象的形式出现,则Async管道订阅可观察对象并返回传输的值。

import { Observable, Observer } from 'rxjs';
export class TestComponent implements OnInit {
   timeChange = new Observable<string>((observer: Observer>string>) => {
      setInterval(() => observer.next(new 
      Date().toString()), 1000); 
   }); 
}

这里,

Async管道每秒执行一次订阅以进行时间更改,并在每次传递给它时返回结果。主要优点是,我们不需要在timeChange上调用subscribe,也不需要担心取消订阅,如果组件被删除。

在您的test.component.html中添加以下代码。

<div> 
   Seconds changing in Time: {{ timeChange | async }}
</div>

现在,运行应用程序,您可以在屏幕上看到秒数在变化。

CurrencyPipe

它用于将给定的数字转换为各个国家/地区的货币格式。在test.component.ts文件中考虑以下代码。

import { Component, OnInit } from '@angular/core'; @Component({ 
   selector: 'app-test', 
   template: ` 
   <div style="text-align:center"> 
      <h3> Currency Pipe</h3> 
      <p>{{ price | currency:'EUR':true}}</p> 
      <p>{{ price | currency:'INR' }}</p> 
   </div> 
   `, 
   styleUrls: ['./test.component.scss'] 
}) 
export class TestComponent implements OnInit { 
   price : number = 20000; ngOnInit() {

   } 
}

您可以在屏幕上看到以下输出:

Currency Pipe

€20,000.00 
₹20,000.00

SlicePipe

Slice管道用于返回数组的切片。它以索引作为参数。如果您只分配起始索引,则表示它将打印到值的末尾。如果您想打印特定范围的值,则可以分配起始索引和结束索引。

我们还可以使用负索引访问元素。下面显示了一个简单的示例:

test.component.ts

import { Component, OnInit } from '@angular/core'; @Component({ 
   selector: 'app-test', 
   template: ` 
   <div> 
      <h3>Start index:- {{Fruits | slice:2}}</h3> 
      <h4>Start and end index:- {{Fruits | slice:1:4}}</h4> 
      <h5>Negative index:- {{Fruits | slice:-2}}</h5> 
      <h6>Negative start and end index:- {{Fruits | slice:-4:-2}}</h6> 
   </div>
   `, 
   styleUrls: ['./test.component.scss'] 
}) 
export class TestComponent implements OnInit { 
   Fruits = ["Apple","Orange","Grapes","Mango","Kiwi","Pomegranate"]; 
   ngOnInit() {

   } 
}

现在运行您的应用程序,您可以在屏幕上看到以下输出:

Start index:- Grapes,Mango,Kiwi,Pomegranate
Start and end index:- Orange,Grapes,Mango 
Negative index:- Kiwi,Pomegranate 
Negative start and end index:- Grapes,Mango

这里,

  • {{Fruits | slice:2}}表示它从第二个索引值Grapes开始到值的末尾。

  • {{Fruits | slice:1:4}}表示从1到end-1开始,因此结果是从第一个到第三个索引值。

  • {{Fruits | slice:-2}}表示从-2开始到末尾,因为没有指定结束值。因此,结果是Kiwi、Pomegranate。

  • {{Fruits | slice:-4:-2}}表示从负索引-4是Grapes到end-1,即-3,因此索引[-4,-3]的结果是Grapes、Mango。

DecimalPipe

它用于格式化十进制值。它也被认为是CommonModule。让我们在test.component.ts文件中了解一个简单的代码,

import { Component, OnInit } from '@angular/core'; @Component({ 
   selector: 'app-test', 
   template: ` 
   <div style="text-align:center"> 
      <h3>Decimal Pipe</h3> 
      <p> {{decimalNum1 | number}} </p> 
      <p> {{decimalNum2 | number}} </p> 
   </div> 
   `, 
   styleUrls: ['./test.component.scss'] 
}) 
export class TestComponent implements OnInit { 
   decimalNum1: number = 8.7589623; 
   decimalNum2: number = 5.43; 
   ngOnInit() {

   } 
}

您可以在屏幕上看到以下输出:

Decimal Pipe 
8.759 
5.43

格式化值

我们可以在数字模式内应用字符串格式。它基于以下格式:

number:"{minimumIntegerDigits}.{minimumFractionDigits} - {maximumFractionDigits}"

让我们在我们的代码中应用上述格式,

@Component({ 
   template: ` 
   <div style="text-align:center"> 
      <p> Apply formatting:- {{decimalNum1 | number:'3.1'}} </p> 
      <p> Apply formatting:- {{decimalNum1 | number:'2.1-4'}} </p> 
   </div> 
   `, 
})

这里,

{{decimalNum1 | number:’3.1’}}表示三位小数点,至少一位小数,但对最大小数位数没有限制。它返回以下输出:

Apply formatting:- 008.759

{{decimalNum1 | number:’2.1-4’}}表示两位小数,至少一位,最多四位小数,因此它返回以下输出:

Apply formatting:- 08.759

PercentPipe

它用于将数字格式化为百分比。格式化字符串与DecimalPipe概念相同。下面显示了一个简单的示例:

import { Component, OnInit } from '@angular/core'; 
@Component({ 
   selector: 'app-test', 
   template: ` 
   <div style="text-align:center"> 
      <h3>Decimal Pipe</h3> 
      <p> {{decimalNum1 | percent:'2.2'}} </p> 
   </div> 
   `, 
   styleUrls: ['./test.component.scss'] 
}) 
export class TestComponent { 
   decimalNum1: number = 0.8178; 
}

您可以在屏幕上看到以下输出:

Decimal Pipe 
81.78%

JsonPipe

它用于将JavaScript对象转换为JSON字符串。在test.component.ts文件中添加以下代码,如下所示:

import { Component, OnInit } from '@angular/core'; 
@Component({ 
   selector: 'app-test', 
   template: ` 
   <div style="text-align:center"> 
      <p ngNonBindable>{{ jsonData }}</p> (1) 
      <p>{{ jsonData }}</p> 
      <p ngNonBindable>{{ jsonData | json }}</p> 
      <p>{{ jsonData | json }}</p> 
   </div> 
   `, 
   styleUrls: ['./test.component.scss'] 
}) 
export class TestComponent { 
   jsonData = { id: 'one', name: { username: 'user1' }} 
}

现在,运行应用程序,您可以在屏幕上看到以下输出:

{{ jsonData }} 
(1) 
[object Object] 
{{ jsonData | json }} 
{ "id": "one", "name": { "username": "user1" } }

创建自定义管道

正如我们已经看到的,Angular中提供了许多预定义的管道,但有时,我们可能希望以自定义格式转换值。本节说明了如何创建自定义管道。

使用以下命令创建自定义管道:

ng g pipe digitcount

执行上述命令后,您可以看到响应:

CREATE src/app/digitcount.pipe.spec.ts (203 bytes) CREATE src/app/digitcount.pipe.ts (213 bytes) 
UPDATE src/app/app.module.ts (744 bytes)

让我们使用管道来计算数字中的位数。打开digitcount.pipe.ts文件并添加以下代码:

import { Pipe, PipeTransform } from '@angular/core'; @Pipe({ 
   name: 'digitcount' 
}) 
export class DigitcountPipe implements PipeTransform { 
   transform(val : number) : number { 
      return val.toString().length; 
   } 
}

现在,我们添加了计算数字中位数的逻辑。让我们在test.component.ts文件中添加最终代码,如下所示:

import { Component, OnInit } from '@angular/core'; @Component({ 
   selector: 'app-test', 
   template: ` 
   <div> 
      <p> DigitCount Pipe </p> 
      <h1>{{ digits | digitcount }}</h1> 
   </div> 
   `, 
   styleUrls: ['./test.component.scss'] 
}) 
export class TestComponent implements OnInit { 
   digits : number = 100; 
   ngOnInit() { 
   } 
}

现在,运行应用程序,您可以看到以下响应:

DigitCount Pipe 
3

工作示例

让我们在我们的ExpenseManager应用程序中使用该管道。

打开命令提示符并转到项目根文件夹。

cd /go/to/expense-manager

启动应用程序。

ng serve

打开ExpenseEntryListComponent的模板src/app/expense-entry-list/expense-entry-list.component.html并在entry.spendOn中包含管道,如下所示:

<td>{{ entry.spendOn | date: 'short' }}</td>

在这里,我们使用了日期管道以短格式显示花费的日期。

最后,应用程序的输出如下所示:

Pipes

Angular - 响应式编程

响应式编程是一种处理数据流和更改传播的编程范式。数据流可以是静态的或动态的。静态数据流的一个示例是数组或数据集合。它将具有初始数量,并且不会更改。动态数据流的一个示例是事件发射器。事件发射器在事件发生时发出数据。最初,可能没有事件,但随着时间的推移,事件会发生,并且会发出。

响应式编程使数据流能够从一个称为Observable的源发出,并且发出的数据流能够通过称为订阅的过程被其他称为Observer的源捕获。这种Observable/Observer模式或简单的Observer模式极大地简化了编程环境中复杂的变化检测和必要的更新。

JavaScript本身不支持响应式编程。RxJs是一个JavaScript库,它使JavaScript能够进行响应式编程。Angular广泛使用RxJs库来执行以下高级概念:

  • 组件之间的数据传输。
  • HTTP客户端。
  • 路由器。
  • 响应式表单。

让我们在本节中使用RxJs库学习响应式编程。

Observable

如前所述,Observable是数据源,它们可以是静态的或动态的。Rxjs提供了许多方法来从常见的JavaScript对象创建Observable。让我们看看一些常见的方法。

of - 按顺序发出任意数量的值,最后发出完成通知。

const numbers$ = of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

这里,

  • numbers$是一个Observable对象,当订阅时,它将按顺序发出1到10。

  • 变量末尾的美元符号($)用于识别该变量是Observable。

range - 按顺序发出一定范围的数字。

const numbers$ = range(1,10)

from - 发出数组、promise或可迭代对象。

const numbers$ = from([1,2,3,4,5,6,7,8,9,10]);

ajax - 通过AJAX获取URL,然后发出响应。

const api$ = ajax({ url: 'https://httpbin.org/delay/1', method: 'POST', headers: { 'Content-Type': 'application/text' }, body: "Hello" });

这里,

https://httpbin.org 是一项免费的REST API服务,它将按以下指定返回提供的正文内容的JSON格式:

{ 
   "args": {}, 
   "data": "Hello", 
   "files": {}, 
   "form": {}, 
   "headers": { 
      "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", 
      "Accept-Encoding": "gzip, deflate, br", 
      "Accept-Language": "en-US,en;q=0.9", 
      "Host": "httpbin.org", "Sec-Fetch-Dest": "document", 
      "Sec-Fetch-Mode": "navigate", 
      "Sec-Fetch-Site": "none", 
      "Upgrade-Insecure-Requests": "1", 
      "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Safari/537.36", 
      "X-Amzn-Trace-Id": "Root=1-5eeef468-015d8f0c228367109234953c" 
   }, 
   "origin": "ip address", 
   "url": "https://httpbin.org/delay/1" 
}

fromEvent - 侦听HTML元素的事件,然后在侦听的事件触发时发出事件及其属性。

const clickEvent$ = fromEvent(document.getElementById('counter'), 'click');

Angular在内部广泛使用此概念来提供组件之间的数据传输以及响应式表单。

订阅过程

订阅Observable非常容易。每个Observable对象都将有一个方法subscribe用于订阅过程。Observer需要实现三个回调函数来订阅Observable对象。它们如下:

  • next - 接收和处理从Observable发出的值

  • error - 错误处理回调

  • complete - 当Observable中的所有数据都发出时调用的回调函数。

一旦定义了三个回调函数,就必须调用Observable的subscribe方法,如下所示:

const numbers$ = from([1,2,3,4,5,6,7,8,9,10]); 
// observer 
const observer = { 
   next: (num: number) => {      this.numbers.push(num); this.val1 += num }, 
      error: (err: any) => console.log(err), 
      complete: () => console.log("Observation completed") 
}; 
numbers$.subscribe(observer);

这里,

  • next - 方法获取发出的数字,然后将其推入局部变量this.numbers中。

  • next - 方法也将数字添加到局部变量this.val1中。

  • error - 方法只是将错误消息写入控制台。

  • complete 方法还会将完成消息写入控制台。

我们可以跳过errorcomplete 方法,只编写next 方法,如下所示:

number$.subscribe((num: number) => { this.numbers.push(num); this.val1 += num; });

操作符

Rxjs 库提供了一些操作符来处理数据流。一些重要的操作符如下:

filter - 使用回调函数过滤数据流。

const filterFn = filter( (num : number) => num > 5 ); 
const filteredNumbers$ = filterFn(numbers$); 
filteredNumbers$.subscribe( (num : number) => { 
this.filteredNumbers.push(num); this.val2 += num } );

map - 使用回调函数映射数据流并更改数据流本身。

const mapFn = map( (num : number) => num + num ); const mappedNumbers$ = mappedFn(numbers$);

pipe - 启用组合两个或多个操作符。

const filterFn = filter( (num : number) => num > 5 ); 
const mapFn = map( (num : number) => num + num ); const processedNumbers$ = numbers$.pipe(filterFn, mapFn); 
processedNumbers$.subscribe( (num : number) => { this.processedNumbers.push(num); this.val3 += num } );

让我们创建一个示例应用程序来尝试本章学习的反应式编程概念。

使用以下命令创建一个新的应用程序,reactive:

ng new reactive

将目录更改为我们新创建的应用程序。

cd reactive

运行应用程序。

ng serve

更改 AppComponent 组件代码(src/app/app.component.ts),如下所示:

import { Component, OnInit } from '@angular/core'; import { Observable, of, range, from, fromEvent } from 'rxjs'; 
import { ajax } from 'rxjs/ajax'; 
import { filter, map, catchError } from 'rxjs/operators'; 
@Component({ 
   selector: 'app-root', 
   templateUrl: './app.component.html', 
   styleUrls: ['./app.component.css'] 
}) 
export class AppComponent implements OnInit { 
   title = 'Reactive programming concept'; 
   numbers : number[] = []; 
   val1 : number = 0; 
   filteredNumbers : number[] = []; 
   val2 : number = 0; 
   processedNumbers : number[] = []; 
   val3 : number = 0; 
   apiMessage : string; 
   counter : number = 0; 
   ngOnInit() { 
      // Observable stream of data Observable<number>
      // const numbers$ = of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); 
      // const numbers$ = range(1,10); 
      const numbers$ = from([1,2,3,4,5,6,7,8,9,10]); 
      // observer 
      const observer = { 
         next: (num: number) => {this.numbers.push(num); this.val1 += num }, 
         error: (err: any) => console.log(err), 
         complete: () => console.log("Observation completed") 
      }; 
      numbers$.subscribe(observer); 
      const filterFn = filter( (num : number) => num > 5 ); 
      const filteredNumbers = filterFn(numbers$); 
      filteredNumbers.subscribe( (num : number) => {this.filteredNumbers.push(num); this.val2 += num } ); 
      const mapFn = map( (num : number) => num + num ); 
      const processedNumbers$ = numbers$.pipe(filterFn, mapFn); 
      processedNumbers$.subscribe( (num : number) => {this.processedNumbers.push(num); this.val3 += num } ); 
      const api$ = ajax({ 
         url: 'https://httpbin.org/delay/1', 
         method: 'POST', 
         headers: {'Content-Type': 'application/text' }, 
         body: "Hello" 
      }); 
      api$.subscribe(res => this.apiMessage = res.response.data ); 
      const clickEvent$ = fromEvent(document.getElementById('counter'), 'click'); 
      clickEvent$.subscribe( () => this.counter++ ); 
   } 
}

这里,

  • 使用 range、from、ajax 和 fromEvent 方法创建 Observable。
  • 使用 filter、map 和 pipe 操作符方法处理数据流。
  • 回调函数捕获发射的数据,处理它,然后将其存储在组件的局部变量中。

更改AppComponent 模板(src/app/app.component.html),如下所示:

<h1>{{ title }}</h1> 
<div> 
   The summation of numbers ( <span *ngFor="let num of numbers"> {{ num }} </span> ) is {{ val1 }} 
</div> 
<div> 
   The summation of filtered numbers ( <span *ngFor="let num of filteredNumbers"> {{ num }} </span> ) is {{ val2 }} 
</div> 
<div> 
   The summation of processed numbers ( <span *ngFor="let num of processedNumbers"> {{ num }} </span> ) is {{ val3 }} 
</div> 
<div> 
   The response from the API is <em>{{ apiMessage }}</em> </div> 
<div> 
   <a id="counter" href="#">Click here</a> to increment the counter value. The current counter value is {{ counter }} 
<div>

这里,

显示了Observer 回调函数处理的所有局部变量。

打开浏览器,https://:4200。

Pipes

点击Click here 链接五次。对于每个事件,都会发射该事件并转发到Observer。将调用 Observer 回调函数。回调函数为每次点击递增计数器,最终结果如下所示:

Observer

Angular - 服务和依赖注入

如前所述,服务在 Angular 应用程序中提供特定的功能。在给定的 Angular 应用程序中,可以使用一个或多个服务。类似地,Angular 组件可能依赖于一个或多个服务。

此外,Angular 服务可能依赖于其他服务才能正常工作。依赖项解析是开发任何应用程序中复杂且耗时的活动之一。为了降低复杂性,Angular 提供了依赖注入模式作为核心概念之一。

让我们学习如何在本章中在 Angular 应用程序中使用依赖注入。

创建 Angular 服务

Angular 服务是一个普通的 TypeScript 类,它具有一到多个方法(功能)以及@Injectable 装饰器。它使普通的 TypeScript 类能够用作 Angular 应用程序中的服务。

import { Injectable } from '@angular/core'; @Injectable() 
export class DebugService { 
   constructor() { } 
}

这里,@Injectable 装饰器将普通的 TypeScript 类转换为 Angular 服务。

注册 Angular 服务

要使用依赖注入,每个服务都需要注册到系统中。Angular 提供了多种注册服务的方法。它们如下:

  • ModuleInjector @ 根级别
  • ModuleInjector @ 平台级别
  • ElementInjector 使用 providers 元数据
  • ElementInjector 使用 viewProviders 元数据
  • NullInjector

ModuleInjector @ 根

ModuleInjector 强制服务只能在特定模块内使用。@Injectable 中可用的ProvidedIn 元数据用于指定可以使用该服务的模块。

该值应引用已注册的 Angular 模块之一(用@NgModule 装饰)。root 是一个特殊选项,它引用应用程序的根模块。示例代码如下:

import { Injectable } from '@angular/core'; @Injectable({ 
   providedIn: 'root', 
})
export class DebugService { 
   constructor() { } 
}

ModuleInjector @ 平台

Platform InjectorModuleInject 高一级,仅在高级和罕见的情况下使用。每个 Angular 应用程序都通过执行PreformBrowserDynamic().bootstrap 方法(参见main.js)开始,该方法负责引导 Angular 应用程序的根模块。

PreformBrowserDynamic() 方法创建由PlatformModule 配置的注入器。我们可以使用PlatformModule 提供的platformBrowser() 方法配置平台级服务。

NullInjector

NullInjector 比平台级ModuleInjector 高一级,并且位于层次结构的顶层。我们无法在NullInjector 中注册任何服务。当在层次结构中的任何位置都找不到所需的服务时,它会解析并简单地抛出错误。

ElementInjector 使用 providers

ElementInjector 强制服务只能在某些特定组件内使用。@Component 装饰器中可用的 providers 和ViewProviders 元数据用于指定对特定组件可见的服务列表。使用 providers 的示例代码如下:

ExpenseEntryListComponent

// import statement 
import { DebugService } from '../debug.service'; 
// component decorator 
@Component({ 
   selector: 'app-expense-entry-list', 
   templateUrl: './expense-entry-list.component.html', 
   styleUrls: ['./expense-entry-list.component.css'], 
   providers: [DebugService] })

这里,DebugService 仅在ExpenseEntryListComponent 及其视图内可用。要在其他组件中使用 DebugService,只需在必要的组件中使用providers 装饰器。

ElementInjector 使用 viewProviders

viewProvidersprovider 类似,但它不允许在使用ng-content 指令创建的组件内容内使用服务。

ExpenseEntryListComponent

// import statement 
import { DebugService } from '../debug.service'; 
// component decorator 
@Component({ 
   selector: 'app-expense-entry-list', 
   templateUrl: './expense-entry-list.component.html', 
   styleUrls: ['./expense-entry-list.component.css'], viewProviders: [DebugService] 
})

父组件可以通过其视图或内容使用子组件。下面提到了具有子组件和内容视图的父组件的示例:

父组件视图/模板

<div> 
   child template in view 
   <child></child> 
</div> 
<ng-content></ng-content>

子组件视图/模板

<div> 
   child template in view 
</div> 

父组件在模板(另一个组件)中的用法

<parent> 
   <!-- child template in content -->
   <child></child>
</parent> 

这里,

  • child 组件在两个地方使用。一个在父级的视图内。另一个在父级内容内。
  • 服务将在放置在父级视图内的子组件中可用。
  • 服务在放置在父级内容内的子组件中不可用。

解析 Angular 服务

让我们看看组件如何使用以下流程图解析服务。

Resolve Angular

这里,

  • 首先,组件尝试查找使用viewProviders 元数据注册的服务。
  • 如果未找到,组件尝试查找使用providers 元数据注册的服务。
  • 如果未找到,组件尝试查找使用ModuleInjector 注册的服务
  • 如果未找到,组件尝试查找使用PlatformInjector 注册的服务
  • 如果未找到,组件尝试查找使用NullInjector 注册的服务,它始终会抛出错误。

注入器的层次结构以及服务解析的工作流程如下:

Angular service

解析修饰符

正如我们在上一章中学习的那样,服务的解析从组件开始,并在找到服务或到达NUllInjector 时停止。这是默认解析,可以使用Resolution Modifier 更改它。它们如下:

Self()

Self() 在其当前ElementInjector 本身开始并停止对服务的搜索。

import { Self } from '@angular/core'; 
constructor(@Self() public debugService: DebugService) {}

SkipSelf()

SkipSelf() 与 Self() 正好相反。它跳过当前 ElementInjector 并从其父ElementInjector 开始搜索服务。

import { SkipSelf } from '@angular/core'; 
constructor(@SkipSelf() public debugService: DebugService) {}

Host()

Host() 在其宿主ElementInjector 中停止对服务的搜索。即使服务在更高级别可用,它也会在宿主处停止。

import { Host } from '@angular/core'; 
constructor(@Host() public debugService: DebugService) {}

Optional()

Optional() 在搜索服务失败时不会抛出错误。

import { Optional } from '@angular/core'; 
constructor(@Optional() private debugService?: DebugService) { 
   if (this.debugService) { 
      this.debugService.info("Debugger initialized"); 
   } 
}

依赖注入提供程序

依赖注入提供程序有两个作用。首先,它有助于为要注册的服务设置令牌。该令牌将用于引用和调用服务。其次,它有助于根据给定的配置创建服务。

如前所述,最简单的提供程序如下:

providers: [ DebugService ]

这里,DebugService 既是令牌,也是类,服务对象必须由此类创建。提供程序的实际形式如下:

providers: [ { provides: DebugService, useClass: DebugService }]

这里,provides 是令牌,useClass 是创建服务对象的类引用。

Angular 提供了一些其他提供程序,它们如下:

别名类提供程序

提供程序的目的是重用现有的服务。

providers: [ DebugService, 
   { provides: AnotherDebugService, userClass: DebugService }]

这里,只会创建一个DebugService 服务的实例。

值提供程序

Value 提供程序的目的是提供值本身,而不是要求 DI 创建服务对象的实例。它也可以使用现有的对象。唯一的限制是对象必须与引用的服务形状相同。

export class MyCustomService { 
   name = "My Custom Service" 
} 
[{ provide: MyService, useValue: { name: 'instance of MyCustomService' }]

这里,DI 提供程序仅返回在useValue 选项中设置的实例,而不是创建新的服务对象。

非类依赖项提供程序

它允许在 Angular DI 中使用字符串、函数或对象。

让我们看一个简单的例子。

// Create the injectable token 
import { InjectionToken } from '@angular/core'; 
export const APP_CONFIG = new InjectionToken<AppConfig>('app.config'); 
// Create value 
export const MY_CONFIG: AppConfig = { 
   title: 'Dependency Injection' 
}; 
// congfigure providers 
providers: [{ provide: APP_CONFIG, useValue: HERO_DI_CONFIG }] 
// inject the service 
constructor(@Inject(APP_CONFIG) config: AppConfig) {

工厂提供程序

Factory Providers 允许复杂的服务创建。它将对象的创建委托给外部函数。Factory Providers 还可以选择为工厂对象设置依赖项。

{ provide: MyService, useFactory: myServiceFactory, deps: [DebugService] };

这里,myServiceFactory 返回MyService 的实例。

Angular 服务用法

现在,我们知道如何创建和注册 Angular 服务了。让我们看看如何在组件内部使用 Angular 服务。使用 Angular 服务就像将构造函数的参数类型设置为服务提供程序的令牌一样简单。

export class ExpenseEntryListComponent implements OnInit {
   title = 'Expense List'; 
   constructor(private debugService : DebugService) {} 
   ngOnInit() { 
      this.debugService.info("Angular Application starts"); 
   } 
}

这里,

  • ExpenseEntryListComponent 构造函数设置了一个类型为 DebugService 的参数。

  • Angular 依赖注入 (DI) 将尝试查找应用程序中注册的任何类型为 DebugService 的服务。如果找到,它会将 DebugService 的实例设置为 ExpenseEntryListComponent 组件。如果未找到,它将抛出错误。

添加调试服务

让我们添加一个简单的Debug 服务,它将帮助我们在应用程序开发期间打印调试信息。

打开命令提示符并转到项目根文件夹。

cd /go/to/expense-manager

启动应用程序。

ng serve

运行以下命令以生成一个 Angular 服务,DebugService

ng g service debug

这将创建两个 TypeScript 文件(调试服务及其测试),如下所示:

CREATE src/app/debug.service.spec.ts (328 bytes) 
CREATE src/app/debug.service.ts (134 bytes)

让我们分析DebugService 服务的内容。

import { Injectable } from '@angular/core'; @Injectable({ 
   providedIn: 'root' 
}) 
export class DebugService { 
   constructor() { } 
}

这里,

  • @Injectable 装饰器附加到 DebugService 类,这使得 DebugService 能够在应用程序的 Angular 组件中使用。

  • providerIn 选项及其值 root 使 DebugService 能够在应用程序的所有组件中使用。

让我们添加一个方法 Info,它将消息打印到浏览器控制台。

info(message : String) : void { 
   console.log(message); 
}

让我们在ExpenseEntryListComponent 中初始化服务并使用它来打印消息。

import { Component, OnInit } from '@angular/core'; import { ExpenseEntry } from '../expense-entry'; import { DebugService } from '../debug.service'; @Component({ 
   selector: 'app-expense-entry-list', 
   templateUrl: './expense-entry-list.component.html', styleUrls: ['./expense-entry-list.component.css'] 
}) 
export class ExpenseEntryListComponent implements OnInit { 
   title: string; 
   expenseEntries: ExpenseEntry[]; 
   constructor(private debugService: DebugService) { } 
   ngOnInit() { 
      this.debugService.info("Expense Entry List 
      component initialized"); 
      this.title = "Expense Entry List"; 
      this.expenseEntries = this.getExpenseEntries(); 
   } 
   // other coding 
}

这里,

  • DebugService 使用构造函数参数初始化。设置类型为 DebugService 的参数 (debugService) 将触发依赖注入以创建一个新的 DebugService 对象并将其设置为 ExpenseEntryListComponent 组件。

  • 在 ngOnInit 方法中调用 DebugService 的 info 方法将在浏览器控制台中打印消息。

可以使用开发者工具查看结果,它看起来类似于以下所示:

Debug service

让我们扩展应用程序以了解服务的范围。

让我们使用以下命令创建一个DebugComponent

ng generate component debug
CREATE src/app/debug/debug.component.html (20 bytes) CREATE src/app/debug/debug.component.spec.ts (621 bytes) 
CREATE src/app/debug/debug.component.ts (265 bytes) CREATE src/app/debug/debug.component.css (0 bytes) UPDATE src/app/app.module.ts (392 bytes)

让我们删除根模块中的 DebugService。

// src/app/debug.service.ts
import { Injectable } from '@angular/core'; @Injectable() 
export class DebugService { 
   constructor() { 
   }
   info(message : String) : void {     
      console.log(message); 
   } 
}

在 ExpenseEntryListComponent 组件下注册 DebugService。

// src/app/expense-entry-list/expense-entry-list.component.ts @Component({ 
   selector: 'app-expense-entry-list', 
   templateUrl: './expense-entry-list.component.html', 
   styleUrls: ['./expense-entry-list.component.css'] 
   providers: [DebugService] 
})

这里,我们使用了 providers 元数据(ElementInjector) 来注册服务。

打开DebugComponent (src/app/debug/debug.component.ts) 并导入DebugService,并在组件的构造函数中设置一个实例。

import { Component, OnInit } from '@angular/core'; import { DebugService } from '../debug.service'; 
@Component({ 
   selector: 'app-debug', 
   templateUrl: './debug.component.html', 
   styleUrls: ['./debug.component.css'] 
}) 
export class DebugComponent implements OnInit { 
   constructor(private debugService: DebugService) { } 
   ngOnInit() { 
      this.debugService.info("Debug component gets service from Parent"); 
   } 
}

这里,我们没有注册DebugService。因此,如果用作父组件,则 DebugService 将不可用。当在父组件内部使用时,如果父组件可以访问该服务,则该服务可能从父组件获得。

打开ExpenseEntryListComponent 模板 (src/app/expense-entry-list/expense-entry-list.component.html) 并包含一个内容部分,如下所示

// existing content 
<app-debug></app-debug>
<ng-content></ng-content>

这里,我们包含了一个内容部分和 DebugComponent 部分。

让我们在 AppComponent 模板中将调试组件作为内容包含在ExpenseEntryListComponent 组件中。打开AppComponent 模板并将app-expense-entry-list 更改为以下内容:

// navigation code
<app-expense-entry-list>
<app-debug></app-debug>
</app-expense-entry-list>

这里,我们包含了DebugComponent 作为内容。

让我们检查应用程序,它将在页面末尾显示DebugService 模板,如下所示:

Debug

此外,我们可以在控制台中看到调试组件输出的两条调试信息。这表明调试组件从其父组件获取了服务。

让我们更改服务在**ExpenseEntryListComponent**中注入的方式以及它如何影响服务的作用域。将providers注入器更改为viewProviders注入。**viewProviders**不会将服务注入到内容子元素中,因此它应该会失败。

viewProviders: [DebugService]

检查应用程序,您会看到其中一个调试组件(用作内容子元素)抛出如下所示的错误:

Application

让我们从模板中删除调试组件并恢复应用程序。

打开**ExpenseEntryListComponent**模板(src/app/expense-entry-list/expense-entry-list.component.html)并删除以下内容

 
<app-debug></app-debug>
<ng-content></ng-content>

打开**AppComponent**模板并将**app-expense-entry-list**更改为如下所示:

// navigation code
<app-expense-entry-list>
</app-expense-entry-list>

将**ExpenseEntryListComponent**中的**viewProviders**设置更改为**providers**。

providers: [DebugService]

重新运行应用程序并检查结果。

Angular - Http客户端编程

Http客户端编程是每个现代Web应用程序中必不可少的特性。如今,许多应用程序通过REST API(HTTP协议上的功能)公开其功能。考虑到这一点,Angular团队提供了广泛的支持来访问HTTP服务器。Angular提供了一个单独的模块**HttpClientModule**和一个服务**HttpClient**来进行HTTP编程。

让我们在本节中学习如何使用**HttpClient**服务。开发人员应该具备HTTP编程的基本知识才能理解本章内容。

费用REST API

进行Http编程的先决条件是对Http协议和REST API技术的了解。Http编程涉及两部分:服务器和客户端。Angular提供支持来创建客户端应用程序。**Express**是一个流行的Web框架,它提供支持来创建服务器端应用程序。

让我们使用Express框架创建一个**费用Rest API**,然后使用Angular HttpClient服务从我们的**ExpenseManager**应用程序访问它。

打开命令提示符并创建一个新文件夹**express-rest-api**。

cd /go/to/workspace 
mkdir express-rest-api 
cd expense-rest-api

使用以下命令初始化一个新的节点应用程序:

npm init

**npm init**会询问一些基本问题,例如项目名称(express-rest-api)、入口点(server.js)等,如下所示:

This utility will walk you through creating a package.json file. 
It only covers the most common items, and tries to guess sensible defaults. 
See `npm help json` for definitive documentation on these fields and exactly what they do. 
Use `npm install <pkg>` afterwards to install a package and save it as a dependency in the package.json file. 
Press ^C at any time to quit. 
package name: (expense-rest-api) 
version: (1.0.0) 
description: Rest api for Expense Application 
entry point: (index.js) server.js 
test command:
git repository: 
keywords: 
author: 
license: (ISC) 
About to write to \path\to\workspace\expense-rest-api\package.json: { 
   "name": "expense-rest-api", 
   "version": "1.0.0", 
   "description": "Rest api for Expense Application", 
   "main": "server.js", 
   "scripts": { 
      "test": "echo \"Error: no test specified\" && exit 1" 
   }, 
   "author": "", 
   "license": "ISC" 
} 
Is this OK? (yes) yes

使用以下命令安装**express、sqlite**和**cors**模块:

npm install express sqlite3 cors

创建一个新文件**sqlitedb.js**并将以下代码放在其中:

var sqlite3 = require('sqlite3').verbose()
const DBSOURCE = "expensedb.sqlite"

let db = new sqlite3.Database(DBSOURCE, (err) => {
   if (err) {
      console.error(err.message)
      throw err
   }else{
      console.log('Connected to the SQLite database.')
      db.run(`CREATE TABLE expense (
         id INTEGER PRIMARY KEY AUTOINCREMENT,
         item text, 
         amount real, 
         category text, 
         location text, 
         spendOn text, 
         createdOn text 
         )`,
            (err) => {
               if (err) {
                  console.log(err);
               }else{
                  var insert = 'INSERT INTO expense (item, amount, category, location, spendOn, createdOn) VALUES (?,?,?,?,?,?)'

                  db.run(insert, ['Pizza', 10, 'Food', 'KFC', '2020-05-26 10:10', '2020-05-26 10:10'])
                  db.run(insert, ['Pizza', 9, 'Food', 'Mcdonald', '2020-05-28 11:10', '2020-05-28 11:10'])
                  db.run(insert, ['Pizza', 12, 'Food', 'Mcdonald', '2020-05-29 09:22', '2020-05-29 09:22'])
                  db.run(insert, ['Pizza', 15, 'Food', 'KFC', '2020-06-06 16:18', '2020-06-06 16:18'])
                  db.run(insert, ['Pizza', 14, 'Food', 'Mcdonald', '2020-06-01 18:14', '2020-05-01 18:14'])
               }
            }
      );  
   }
});

module.exports = db

在这里,我们创建一个新的sqlite数据库并加载一些示例数据。

打开server.js并将以下代码放在其中:

var express = require("express")
var cors = require('cors')
var db = require("./sqlitedb.js")

var app = express()
app.use(cors());

var bodyParser = require("body-parser");
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());

var HTTP_PORT = 8000 
app.listen(HTTP_PORT, () => {
   console.log("Server running on port %PORT%".replace("%PORT%",HTTP_PORT))
});

app.get("/", (req, res, next) => {
    res.json({"message":"Ok"})
});

app.get("/api/expense", (req, res, next) => {
   var sql = "select * from expense"
   var params = []
   db.all(sql, params, (err, rows) => {
      if (err) {
        res.status(400).json({"error":err.message});
        return;
      }
      res.json(rows)
     });

});

app.get("/api/expense/:id", (req, res, next) => {
   var sql = "select * from expense where id = ?"
   var params = [req.params.id]
   db.get(sql, params, (err, row) => {
      if (err) {
         res.status(400).json({"error":err.message});
         return;
      }
      res.json(row)
   });
});

app.post("/api/expense/", (req, res, next) => {
   var errors=[]
   if (!req.body.item){
      errors.push("No item specified");
   }
   var data = {
      item : req.body.item,
      amount: req.body.amount,
      category: req.body.category,
      location : req.body.location,
      spendOn: req.body.spendOn,
      createdOn: req.body.createdOn,
   }
   var sql = 'INSERT INTO expense (item, amount, category, location, spendOn, createdOn) VALUES (?,?,?,?,?,?)'
   var params =[data.item, data.amount, data.category, data.location, data.spendOn, data.createdOn]
   db.run(sql, params, function (err, result) {
      if (err){
         res.status(400).json({"error": err.message})
         return;
      }
      data.id = this.lastID;
      res.json(data);
   });
})

app.put("/api/expense/:id", (req, res, next) => {
   var data = {
      item : req.body.item,
      amount: req.body.amount,
      category: req.body.category,
      location : req.body.location,
      spendOn: req.body.spendOn
   }
   db.run(
      `UPDATE expense SET
         item = ?, 

         amount = ?,
         category = ?, 
         location = ?, 

         spendOn = ? 
         WHERE id = ?`,
            [data.item, data.amount, data.category, data.location,data.spendOn, req.params.id],
      function (err, result) {
         if (err){
            console.log(err);
            res.status(400).json({"error": res.message})
            return;
         }
         res.json(data)
   });
})

app.delete("/api/expense/:id", (req, res, next) => {
   db.run(
      'DELETE FROM expense WHERE id = ?',
      req.params.id,
      function (err, result) {
         if (err){
            res.status(400).json({"error": res.message})
            return;
         }
         res.json({"message":"deleted", changes: this.changes})
   });
})

app.use(function(req, res){
   res.status(404);
});

在这里,我们创建了一个基本的CURD rest api来选择、插入、更新和删除费用条目。

使用以下命令运行应用程序:

npm run start

打开浏览器,输入**https://:8000/**并按回车键。您将看到以下响应:

{ 
   "message": "Ok" 
}

它确认我们的应用程序运行良好。

将URL更改为**https://:8000/api/expense**,您将看到所有费用条目以JSON格式显示。

[
   {
      "id": 1,

      "item": "Pizza",
      "amount": 10,
      "category": "Food",
      "location": "KFC",
      "spendOn": "2020-05-26 10:10",
      "createdOn": "2020-05-26 10:10"
   },
   {
      "id": 2,
      "item": "Pizza",
      "amount": 14,
      "category": "Food",
      "location": "Mcdonald",
      "spendOn": "2020-06-01 18:14",
      "createdOn": "2020-05-01 18:14"
   },
   {
      "id": 3,
      "item": "Pizza",
      "amount": 15,
      "category": "Food",
      "location": "KFC",
      "spendOn": "2020-06-06 16:18",
      "createdOn": "2020-06-06 16:18"
   },
   {
      "id": 4,
      "item": "Pizza",
      "amount": 9,
      "category": "Food",
      "location": "Mcdonald",
      "spendOn": "2020-05-28 11:10",
      "createdOn": "2020-05-28 11:10"
   },
   {
      "id": 5,
      "item": "Pizza",
      "amount": 12,
      "category": "Food",
      "location": "Mcdonald",
      "spendOn": "2020-05-29 09:22",
      "createdOn": "2020-05-29 09:22"
   }
]

最后,我们为费用条目创建了一个简单的CURD REST API,我们可以从Angular应用程序访问该REST API来学习HttpClient模块。

配置Http客户端

让我们在本节中学习如何配置**HttpClient**服务。

**HttpClient**服务位于**HttpClientModule**模块内部,该模块位于@angular/common/http包内。

要注册**HttpClientModule**模块:

在**AppComponent**中导入HttpClientModule

import { HttpClientModule } from '@angular/common/http';

在AppComponent的imports元数据中包含HttpClientModule。

@NgModule({ 
   imports: [ 
      BrowserModule, 
      // import HttpClientModule after BrowserModule. 
      HttpClientModule, 
   ] 
}) 
export class AppModule {}

创建费用服务

让我们在我们的**ExpenseManager**应用程序中创建一个新的服务**ExpenseEntryService**来与**Expense REST API**交互。ExpenseEntryService将获取最新的费用条目、插入新的费用条目、修改现有的费用条目和删除不需要的费用条目。

打开命令提示符并转到项目根文件夹。

cd /go/to/expense-manager

启动应用程序。

ng serve

运行以下命令生成一个Angular服务**ExpenseService**。

ng generate service ExpenseEntry

这将创建两个Typescript文件(费用条目服务及其测试),如下所示:

CREATE src/app/expense-entry.service.spec.ts (364 bytes) 
CREATE src/app/expense-entry.service.ts (141 bytes)

打开**ExpenseEntryService**(src/app/expense-entry.service.ts)并从rxjs库导入**ExpenseEntry、throwError**和**catchError**,并从@angular/common/http包导入**HttpClient、HttpHeaders**和**HttpErrorResponse**。

import { Injectable } from '@angular/core'; 
import { ExpenseEntry } from './expense-entry'; import { throwError } from 'rxjs';
import { catchError } from 'rxjs/operators'; 
import { HttpClient, HttpHeaders, HttpErrorResponse } from 
'@angular/common/http';

将HttpClient服务注入到我们的服务中。

constructor(private httpClient : HttpClient) { }

创建一个变量**expenseRestUrl**来指定**Expense Rest API**端点。

private expenseRestUrl = 'https://:8000/api/expense';

创建一个变量**httpOptions**来设置Http Header选项。这将在Angular **HttpClient**服务通过Http Rest API调用期间使用。

private httpOptions = { 
   headers: new HttpHeaders( { 'Content-Type': 'application/json' }) 
};

完整代码如下:

import { Injectable } from '@angular/core';
import { ExpenseEntry } from './expense-entry';
import { Observable, throwError } from 'rxjs';
import { catchError, retry } from 'rxjs/operators';
import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http';

@Injectable({
   providedIn: 'root'
})
export class ExpenseEntryService {
   private expenseRestUrl = 'api/expense';
   private httpOptions = {
      headers: new HttpHeaders( { 'Content-Type': 'application/json' })
   };

   constructor(
      private httpClient : HttpClient) { }
}

HTTP GET

HttpClient提供get()方法从网页获取数据。主要参数是目标Web URL。另一个可选参数是具有以下格式的选项对象:

{
   headers?: HttpHeaders | {[header: string]: string | string[]},
   observe?: 'body' | 'events' | 'response',

   params?: HttpParams|{[param: string]: string | string[]},
   reportProgress?: boolean,
   responseType?: 'arraybuffer'|'blob'|'json'|'text',
   withCredentials?: boolean,
}

这里,

  • **headers** - 请求的HTTP标头,可以是字符串、字符串数组或HttpHeaders数组。

  • **observe** - 处理响应并返回响应的特定内容。可能的值为body、response和events。observer的默认选项是body。

  • **params** - 请求的HTTP参数,可以是字符串、字符串数组或HttpParams数组。

  • **reportProgress** - 是否报告进程的进度(true或false)。

  • **responseType** - 指的是响应的格式。可能的值为**arraybuffer、blob、json**和**text**。

  • **withCredentials** - 请求是否包含凭据(true或false)。

所有选项都是可选的。

**get()**方法将请求的响应作为**Observable**返回。当从服务器收到响应时,返回的Observable会发出数据。

使用**get()**方法的示例代码如下:

httpClient.get(url, options) 
.subscribe( (data) => console.log(data) );

类型化响应

**get()**方法可以选择返回observable,它也发出类型化响应。获取类型化响应(ExpenseEntry)的示例代码如下

httpClient.get<T>(url, options) .subscribe( (data: T) => console.log(data) );

处理错误

错误处理是HTTP编程中的一个重要方面。遇到错误是HTTP编程中的常见情况。

HTTP编程中的错误可以分为两类:

  • 客户端问题可能是由于网络故障、错误配置等造成的。如果发生客户端错误,则**get()**方法会抛出**ErrorEvent**对象。

  • 服务器端问题可能是由于错误的URL、服务器不可用、服务器编程错误等造成的。

让我们为我们的**ExpenseEntryService**服务编写一个简单的错误处理程序。

private httpErrorHandler (error: HttpErrorResponse) {
   if (error.error instanceof ErrorEvent) {
      console.error("A client side error occurs. The error message is " + error.message);
      } else {
         console.error(
            "An error happened in server. The HTTP status code is "  + error.status + " and the error returned is " + error.message);
      }

   return throwError("Error occurred. Pleas try again");
}

错误函数可以在**get()**中调用,如下所示:

httpClient.get(url, options)  
   .pipe(catchError(this.httpErrorHandler) 
   .subscribe( (data) => console.log(data) )

处理失败的请求

如前所述,错误可能会发生,一种方法是处理它。另一种选择是尝试一定次数。如果请求因网络问题或HTTP服务器暂时脱机而失败,则下一次请求可能会成功。

在这种情况下,我们可以使用**rxjs**库的**retry**运算符,如下所示

httpClient.get(url, options) 
   .pipe( 
      retry(5), 
      catchError(this.httpErrorHandler)) 
   .subscribe( (data) => console.log(data) )

获取费用条目

让我们在我们的ExpenseManager应用程序中执行实际编码以从**Expense Rest API**获取费用。

打开命令提示符并转到项目根文件夹。

cd /go/to/expense-manager

启动应用程序。

ng serve

在**ExpenseEntryService**(src/app/expense-entry.service.ts)服务中添加**getExpenseEntries()**和**httpErrorHandler()**方法。

getExpenseEntries() : Observable<ExpenseEntry[]> {
   return this.httpClient.get<ExpenseEntry[]>(this.expenseRestUrl, this.httpOptions)
   .pipe(retry(3),catchError(this.httpErrorHandler));
}

getExpenseEntry(id: number) : Observable<ExpenseEntry> {
   return this.httpClient.get<ExpenseEntry>(this.expenseRestUrl + "/" + id, this.httpOptions)
   .pipe(
      retry(3),
      catchError(this.httpErrorHandler)
   );
}

private httpErrorHandler (error: HttpErrorResponse) {
   if (error.error instanceof ErrorEvent) {
      console.error("A client side error occurs. The error message is " + error.message);
   } else {
      console.error(
         "An error happened in server. The HTTP status code is "  + error.status + " and the error returned is " + error.message);
   }

   return throwError("Error occurred. Pleas try again");
}

这里,

  • **getExpenseEntries()**使用费用端点调用**get()**方法,并配置错误处理程序。此外,它还配置**httpClient**在发生故障时最多尝试3次。最后,它将来自服务器的响应作为类型化的**(ExpenseEntry[])** Observable对象返回。

  • **getExpenseEntry**类似于getExpenseEntries(),只是它传递了ExpenseEntry对象的ID并获取ExpenseEntry Observable对象。

**ExpenseEntryService**的完整编码如下:

import { Injectable } from '@angular/core';
import { ExpenseEntry } from './expense-entry';

import { Observable, throwError } from 'rxjs';
import { catchError, retry } from 'rxjs/operators';
import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http';

@Injectable({

   providedIn: 'root'
})
export class ExpenseEntryService {
   private expenseRestUrl = 'https://:8000/api/expense';
   private httpOptions = {
      headers: new HttpHeaders( { 'Content-Type': 'application/json' })
   };

   constructor(private httpClient : HttpClient) { } 

   getExpenseEntries() : Observable {
      return this.httpClient.get(this.expenseRestUrl, this.httpOptions)
      .pipe(
         retry(3),
         catchError(this.httpErrorHandler)
      );
   }

   getExpenseEntry(id: number) : Observable {
      return this.httpClient.get(this.expenseRestUrl + "/" + id, this.httpOptions)
      .pipe(
         retry(3),
         catchError(this.httpErrorHandler)
      );
   }

   private httpErrorHandler (error: HttpErrorResponse) {
      if (error.error instanceof ErrorEvent) {
         console.error("A client side error occurs. The error message is " + error.message);
      } else {
         console.error(
            "An error happened in server. The HTTP status code is "  + error.status + " and the error returned is " + error.message);
      }

      return throwError("Error occurred. Pleas try again");
   }
}

打开**ExpenseEntryListComponent**(src-entry-list-entry-list.component.ts)并通过构造函数注入**ExpenseEntryService**,如下所示

constructor(private debugService: DebugService, private restService : 
ExpenseEntryService ) { }

更改**getExpenseEntries()**函数。调用**ExpenseEntryService**中的getExpenseEntries()方法,而不是返回模拟项。

getExpenseItems() {  
   this.restService.getExpenseEntries() 
      .subscribe( data =− this.expenseEntries = data ); 
}

**ExpenseEntryListComponent**的完整编码如下:

import { Component, OnInit } from '@angular/core';
import { ExpenseEntry } from '../expense-entry';
import { DebugService } from '../debug.service';
import { ExpenseEntryService } from '../expense-entry.service';

@Component({
   selector: 'app-expense-entry-list',
   templateUrl: './expense-entry-list.component.html',
   styleUrls: ['./expense-entry-list.component.css'],
   providers: [DebugService]
})
export class ExpenseEntryListComponent implements OnInit {
   title: string;
   expenseEntries: ExpenseEntry[];
   constructor(private debugService: DebugService, private restService : ExpenseEntryService ) { }

   ngOnInit() {
      this.debugService.info("Expense Entry List component initialized");
      this.title = "Expense Entry List";

      this.getExpenseItems();
   }

   getExpenseItems() {
      this.restService.getExpenseEntries()
      .subscribe( data => this.expenseEntries = data );
   }
}

最后,检查应用程序,您将看到以下响应。

failed request

HTTP POST

HTTP POST类似于HTTP GET,只是POST请求会将必要的数据作为POST内容与请求一起发送。HTTP POST用于将新记录插入系统。

**HttpClient**提供**post()**方法,它类似于**get()**,只是它支持额外的参数来将数据发送到服务器。

让我们在我们的**ExpenseEntryService**中添加一个新方法**addExpenseEntry()**来添加新的费用条目,如下所示:

addExpenseEntry(expenseEntry: ExpenseEntry): Observable<ExpenseEntry> {
   return this.httpClient.post<ExpenseEntry>(this.expenseRestUrl, expenseEntry, this.httpOptions)
   .pipe(
      retry(3),
      catchError(this.httpErrorHandler)
   );
}

HTTP PUT

HTTP PUT类似于HTTP POST请求。HTTP PUT用于更新系统中现有的记录。

**httpClient**提供**put()**方法,它类似于**post()**。

更新费用条目

让我们在我们的**ExpenseEntryService**中添加一个新方法**updateExpenseEntry()**来更新现有的费用条目,如下所示

updateExpenseEntry(expenseEntry: ExpenseEntry): Observable<ExpenseEntry> {
   return this.httpClient.put<ExpenseEntry>(this.expenseRestUrl + "/" + expenseEntry.id, expenseEntry, this.httpOptions)
   .pipe(
      retry(3),
      catchError(this.httpErrorHandler)
   );
}

HTTP DELETE

HTTP DELETE类似于http GET请求。HTTP DELETE用于删除系统中的条目。

**httpclient**提供**delete()**方法,它类似于**get()**。

删除费用条目

让我们在我们的**ExpenseEntryService**中添加一个新方法**deleteExpenseEntry()**来删除现有的费用条目,如下所示:

deleteExpenseEntry(expenseEntry: ExpenseEntry | number) : Observable<ExpenseEntry> {
   const id = typeof expenseEntry == 'number' ? expenseEntry : expenseEntry.id
   const url = `${this.expenseRestUrl}/${id}`;

   return this.httpClient.delete<ExpenseEntry>(url, this.httpOptions)
   .pipe(
      retry(3),
      catchError(this.httpErrorHandler)
   );
}

Angular - Angular Material

Angular Material提供大量基于Material Design的高质量、现成的Angular组件。让我们学习如何在Angular应用程序中包含Angular Material并使用其组件。

配置Angular Material

让我们看看如何在Angular应用程序中配置Angular Material。

打开命令提示符并转到项目根文件夹。

cd /go/to/expense-manager

使用以下命令添加Angular Material包:

ng add @angular/material

Angular CLI会询问一些关于主题、手势识别和浏览器动画的问题。选择您喜欢的任何主题,然后对手势识别和浏览器动画做出肯定的回答。

Installing packages for tooling via npm.
Installed packages for tooling via npm.
Choose a prebuilt theme name, or "custom" for a custom theme: Indigo/Pink        [ Preview: https://material.angular.i
o?theme=indigo-pink ]
Set up HammerJS for gesture recognition? Yes
Set up browser animations for Angular Material? Yes

Angular Material将每个UI组件打包到一个单独的模块中。通过根模块**(src/app/app.module.ts)**将所有必要的模块导入到应用程序中

import { MatTableModule } from '@angular/material/table';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';

@NgModule({
  imports: [
    MatTableModule,
    MatButtonModule,
    MatIconModule
  ]
})

使用ExpenseEntryListComponent模板(src/app/expense-entry-list/expense-entry-list.component.html)更改编辑按钮,如下所示:

<div class="col-sm" style="text-align: right;">
    <!-- <button type="button" class="btn btn-primary">Edit</button> -->
    <button mat-raised-button color="primary">Edit</button> 
</div>

运行应用程序并测试页面。

ng serve

应用程序的输出如下所示:

Configure Angular

在这里,应用程序清楚地显示了Angular Material按钮。

工作示例

Angular Material包提供的一些重要的UI元素。

  • 表单字段
  • 输入
  • 复选框
  • 单选按钮
  • 选择
  • 按钮
  • 日期选择器
  • 列表
  • 卡片
  • 网格列表
  • 表格
  • 分页器
  • 选项卡
  • 工具栏
  • 菜单
  • 对话框
  • Snackbar
  • 进度条
  • 图标
  • 分隔符

使用Material组件非常简单,我们将通过一个示例项目学习其中一个常用的Material组件**Material Table**。

打开命令提示符并转到项目根文件夹。

ng add @angular/material

让我们更改我们的**ExpenseEntryListComponent**(src/app/expense-entry-list/expense-entry-list.component.ts)并使用Material Table组件。

声明一个变量displayedColumns并为其赋值要显示的列列表。

displayedColumns: string[] = ['item', 'amount', 'category', 'location', 'spendOn' ];

在**ExpenseEntryListComponent**模板**(src/app/expense-entry-list/expense-entry-list.component.html)**中添加Material Table,如下所示,并删除我们现有的列表。

<div class="mat-elevation-z8">
   <table mat-table [dataSource]="expenseEntries"> 
      <ng-container matColumnDef="item">
         <th mat-header-cell *matHeaderCellDef> Item </th>
         <td mat-cell *matCellDef="let element" style="text-align: left"> {{element.item}} </td>
      </ng-container>

      <ng-container matColumnDef="amount">
         <th mat-header-cell *matHeaderCellDef > Amount </th>
         <td mat-cell *matCellDef="let element" style="text-align: left"> {{element.amount}} </td>
      </ng-container>

      <ng-container matColumnDef="category">
         <th mat-header-cell *matHeaderCellDef> Category </th>
         <td mat-cell *matCellDef="let element" style="text-align: left"> {{element.category}} </td>
      </ng-container>

      <ng-container matColumnDef="location">
         <th mat-header-cell *matHeaderCellDef> Location </th>
         <td mat-cell *matCellDef="let element" style="text-align:left"> {{element.location}} </td>
      </ng-container>

      <ng-container matColumnDef="spendOn">
         <th mat-header-cell *matHeaderCellDef> Spend On </th>
         <td mat-cell *matCellDef="let element" style="text-align: left"> {{element.spendOn}} </td>
      </ng-container>

      <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
      <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
   </table>
</div>

这里,

  • **mat-table**属性用于将普通表格转换为Material表格。

  • **[dataSource]**属性用于指定表格的数据源。

  • 物料表格是基于模板的,每一列可以使用单独的模板进行设计。ng-container 用于创建模板。

  • matColumnDef 用于指定应用于特定 ng-container 的数据源的列。

  • mat-header-cell 用于指定每列的标题文本。

  • mat-cell 用于指定每列的内容。

  • mat-header-row 和 mat-row 用于指定列在行中的顺序。

  • 我们只使用了 Material 表格的基本功能。Material 表格还有许多其他功能,例如排序、分页等。

运行应用程序。

ng serve

应用程序的输出如下所示:

Configure Angular

Angular - 路由和导航

导航是 Web 应用程序中一个重要的方面。即使单页应用程序 (SPA) 没有多个页面的概念,它也会从一个视图(例如费用列表)移动到另一个视图(例如费用详情)。提供清晰易懂的导航元素决定了应用程序的成功。

Angular 提供了广泛的导航功能集,以适应从简单场景到复杂场景的需求。定义导航元素和对应视图的过程称为路由。Angular 提供了一个单独的模块RouterModule来设置 Angular 应用程序中的导航。让我们在本节中学习如何在 Angular 应用程序中进行路由。

配置路由

Angular CLI 提供了完整的支持,可以在应用程序创建过程中以及在应用程序运行期间设置路由。让我们使用以下命令创建一个启用了路由的新应用程序:

ng new routing-app --routing

Angular CLI 生成一个新的模块 AppRoutingModuele 用于路由目的。生成的代码如下:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';


const routes: Routes = [];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

这里,

  • 从 @angular/router 包中导入 RouterModule 和 Routes。

  • RouterMoudle 提供了在应用程序中配置和执行路由的功能。

  • Routes 是用于设置导航规则的类型。

  • Routes 是用于配置应用程序实际导航规则的局部变量(类型为 Routes)。

  • RouterMoudle.forRoot() 方法将设置在 routes 变量中配置的导航规则。

Angular CLI 将生成的 AppRoutingModule 包含在 AppComponent 中,如下所示:

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

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';

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

这里,

AppComponent 使用 imports 元数据导入AppRoutingModule 模块。

Angular CLI 也提供了在现有应用程序中设置路由的选项。在现有应用程序中包含路由的通用命令如下:

ng generate module my-module --routing

这将生成一个启用了路由功能的新模块。要在现有模块(AppModule)中启用路由功能,我们需要包含如下所示的额外选项:

ng generate module app-routing --module app --flat

这里,

–module app 在 AppModule 模块中配置新创建的路由模块AppRoutingModule

让我们在ExpenseManager应用程序中配置路由模块。

打开命令提示符并转到项目根文件夹。

cd /go/to/expense-manager

使用以下命令生成路由模块:

ng generate module app-routing --module app --flat

输出

输出如下所示:

CREATE src/app/app-routing.module.ts (196 bytes) 
UPDATE src/app/app.module.ts (785 bytes)

这里,

CLI 生成AppRoutingModule,然后在AppModule中配置它。

创建路由

创建路由简单易行。创建路由的基本信息如下:

  • 要调用的目标组件。
  • 访问目标组件的路径。

创建简单路由的代码如下所示:

const routes: Routes = [
  { path: 'about', component: AboutComponent },
];

这里,

  • Routes 是 AppRoutingModule 中的变量。

  • about 是路径,AboutComponent 是目标/目的地组件。当用户请求 https://:4200/about URL 时,路径与 about 规则匹配,然后将调用 AboutComponent。

访问路由

让我们学习如何在应用程序中使用已配置的路由。

访问路由是一个两步过程。

在根组件模板中包含router-outlet标签。

<router-outlet></router-outlet>

在需要的地方使用routerLinkrouterLinkActive属性。

<a routerLink="/about" routerLinkActive="active">First Component</a>

这里,

  • routerLink 使用路径设置要调用的路由。

  • routerLinkActive 设置激活路由时要使用的 CSS 类。

有时,我们需要在组件内部而不是模板中访问路由。然后,我们需要遵循以下步骤:

在相应的组件中注入RouterActivatedRoute的实例。

import { Router, ActivatedRoute } from '@angular/router'; 
constructor(private router: Router, private route: ActivatedRoute)

这里,

  • Router 提供执行路由操作的功能。

  • Route 指的是当前的激活路由

使用路由器的 navigate 函数。

this.router.navigate(['about']);

这里,

navigate 函数期望一个包含必要路径信息的数组。

使用相对路径

路由路径类似于网页 URL,它也支持相对路径。要从另一个组件(例如HomePageComponent)访问AboutComponent,只需像在 Web URL 或文件夹路径中一样使用 .. 符号即可。

<a routerLink="../about">Relative Route to about component</a>

要在组件中访问相对路径:

import { NavigationExtras } from '@angular/router'; 
this.router.navigate(['about'], { relativeTo: this.route });

这里,

relativeToNavigationExtras类中可用。

路由排序

路由排序在路由配置中非常重要。如果同一路径配置多次,则将调用第一个匹配的路径。如果由于某种原因第一个匹配失败,则将调用第二个匹配。

重定向路由

Angular 路由允许一个路径重定向到另一个路径。redirectTo 是设置重定向路径的选项。示例路由如下:

const routes: Routes = [ 
   { path: '', redirectTo: '/about' }, 
];

这里,

  • redirectTo 如果实际路径与空字符串匹配,则将 about 设置为重定向路径。

通配符路由

通配符路由将匹配任何路径。它使用 ** 创建,并将用于处理应用程序中不存在的路径。将通配符路由放在配置的末尾,使其在其他路径不匹配时被调用。

示例代码如下:

const routes: Routes = [
  { path: 'about', component: AboutComponent },
  { path: '',   redirectTo: '/about', pathMatch: 'full' },
  { path: '**', component: PageNotFoundComponent },  // Wildcard route for a 404 page
];

这里,

如果调用不存在的页面,则前两个路由将失败。但是,最终的通配符路由将成功,并且将调用PageNotFoundComponent

访问路由参数

在 Angular 中,我们可以使用参数在路径中附加额外信息。可以通过使用 paramMap 接口在组件中访问参数。在路由中创建新参数的语法如下:

const routes: Routes = [
  { path: 'about', component: AboutComponent },
  { path: 'item/:id', component: ItemComponent },
  { path: '',   redirectTo: '/about', pathMatch: 'full' },
  { path: '**', component: PageNotFoundComponent },  // Wildcard route for a 404 page
];

在这里,我们在路径中附加了id。可以通过两种技术在ItemComponent中访问id

  • 使用 Observable。
  • 使用快照(非 Observable 选项)。

使用 Observable

Angular 提供了一个特殊的接口 paramMap 来访问路径的参数。parmaMap 具有以下方法:

  • has(name) - 如果路径(参数列表)中存在指定名称,则返回 true。

  • get(name) - 返回路径(参数列表)中指定名称的值。

  • getAll(name) - 返回路径中指定名称的多个值。当有多个值可用时,get() 方法仅返回第一个值。

  • keys - 返回路径中所有可用的参数。

使用paramMap访问参数的步骤如下:

  • 导入@angular/router包中可用的paramMap

  • ngOnInit()中使用paramMap访问参数并将其设置为局部变量。

ngOnInit() {
    this.route.paramMap.subscribe(params => {
        this.id = params.get('id);
    });
}

我们可以使用pipe方法直接在 rest 服务中使用它。

this.item$ = this.route.paramMap.pipe(
    switchMap(params => {
      this.selectedId = Number(params.get('id'));
      return this.service.getItem(this.selectedId);
    })
);

使用快照

snapshot类似于Observable,除了它不支持 Observable 并立即获取参数值。

let id = this.route.snapshot.paramMap.get('id');

嵌套路由

通常,router-outlet将放置在应用程序的根组件(AppComponent)中。但是,router-outlet可以在任何组件中使用。当router-outlet在根组件以外的组件中使用时,特定组件的路由必须配置为父组件的子组件。这称为嵌套路由

让我们考虑一个组件,例如ItemComponent配置了router-outlet并具有两个routerLink,如下所示:

<h2>Item Component</h2> 
<nav> 
   <ul> 
      <li><a routerLink="view">View</a></li> 
      <li><a routerLink="edit">Edit</a></li> 
   </ul>
</nav> 
<router-outlet></router-outlet>

ItemComponent 的路由必须配置为嵌套路由,如下所示:

const routes: Routes = [
{ 
   path: 'item',

   component: ItemComponent,
   children: [
   {
      path: 'view',
      component: ItemViewComponent
   },
   {
   path: 'edit',
   component: ItemEditComponent
   }
   ]
}]

工作示例

让我们在我们的ExpenseManager应用程序中应用本节中学到的路由概念。

打开命令提示符并转到项目根文件夹。

cd /go/to/expense-manager

使用以下命令生成路由模块,如果之前未执行此操作。

ng generate module app-routing --module app --flat

输出

输出如下:

CREATE src/app/app-routing.module.ts (196 bytes) UPDATE src/app/app.module.ts (785 bytes)

这里,

CLI 生成AppRoutingModule,然后在AppModule中配置它。

更新AppRoutingModule (src/app/app.module.ts),如下所示:

import { NgModule } from '@angular/core'; 
import { Routes, RouterModule } from '@angular/router'; import { ExpenseEntryComponent } from './expense-entry/expense-entry.component'; 
import { ExpenseEntryListComponent } from './expense-entry-list/expense-entry-list.component'; 
const routes: Routes = [ 
   { path: 'expenses', component: ExpenseEntryListComponent }, 
   { path: 'expenses/detail/:id', component: ExpenseEntryComponent }, 
   { path: '', redirectTo: 'expenses', pathMatch: 'full' }]; 
@NgModule({ 
   imports: [RouterModule.forRoot(routes)], 
   exports: [RouterModule] }) 
export class AppRoutingModule { }

在这里,我们为费用列表和费用详情组件添加了路由。

更新AppComponent模板(src/app/app.component.html)以包含router-outletrouterLink

<!-- Navigation --> 
<nav class="navbar navbar-expand-lg navbar-dark bg-dark static-top"> 
<div class="container"> 
   <a class="navbar-brand" href="#">{{ title }}</a> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarResponsive" aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation"> 
      <span class="navbar-toggler-icon"></span> 
   </button> 
   <div class="collapse navbar-collapse" id="navbarResponsive"> 
      <ul class="navbar-nav ml-auto"> 
         <li class="nav-item active"> 
            <a class="nav-link" href="#">Home 
               <span class="sr-only" routerLink="/">(current)</span> 
            </a> 
         </li> 
         <li class="nav-item"> 
            <a class="nav-link" routerLink="/expenses">Report</a> 
         </li> 
         <li class="nav-item"> 
            <a class="nav-link" href="#">Add Expense</a> 
         </li> 
         <li class="nav-item"> 
            <a class="nav-link" href="#">About</a> 
         </li> 
      </ul> 
   </div> 
</div> 
</nav> 
<router-outlet></router-outlet>

打开ExpenseEntryListComponent模板 (src/app/expense-entry-list/expense-entry-list.component.html) 并为每个费用条目包含视图选项。

<table class="table table-striped"> 
   <thead> 
      <tr> 
         <th>Item</th>
         <th>Amount</th> 
         <th>Category</th> 
         <th>Location</th> 
         <th>Spent On</th> 
         <th>View</th> 
      </tr> 
   </thead> 
   <tbody> 
      <tr *ngFor="let entry of expenseEntries"> 
         <th scope="row">{{ entry.item }}</th> 
         <th>{{ entry.amount }}</th> 
         <td>{{ entry.category }}</td> 
         <td>{{ entry.location }}</td> 
         <td>{{ entry.spendOn | date: 'medium' }}</td> 
         <td><a routerLink="../expenses/detail/{{ entry.id }}">View</a></td> 
      </tr> 
   </tbody> 
</table>

在这里,我们更新了费用列表表格并添加了一列以显示视图选项。

打开ExpenseEntryComponent (src/app/expense-entry/expense-entry.component.ts)并添加功能以获取当前选定的费用条目。这可以通过首先通过paramMap获取 id,然后使用ExpenseEntryService中的getExpenseEntry()方法来完成。

this.expenseEntry$ = this.route.paramMap.pipe(  
   switchMap(params => { 
      this.selectedId = Number(params.get('id')); 
      return 
this.restService.getExpenseEntry(this.selectedId); })); 
   this.expenseEntry$.subscribe( (data) => this.expenseEntry = data );

更新 ExpenseEntryComponent 并添加转到费用列表的选项。

goToList() { 
   this.router.navigate(['/expenses']); 
}

ExpenseEntryComponent 的完整代码如下:

import { Component, OnInit } from '@angular/core'; import { ExpenseEntry } from '../expense-entry'; import { ExpenseEntryService } from '../expense-entry.service'; 
import { Router, ActivatedRoute } from '@angular/router'; 
import { Observable } from 'rxjs';
import { switchMap } from 'rxjs/operators'; 
@Component({ 
   selector: 'app-expense-entry', 
   templateUrl: './expense-entry.component.html', 
   styleUrls: ['./expense-entry.component.css'] 
}) 
export class ExpenseEntryComponent implements OnInit { 
   title: string; 
   expenseEntry$ : Observable<ExpenseEntry>; 
   expenseEntry: ExpenseEntry = {} as ExpenseEntry; 
   selectedId: number; 
   constructor(private restService : ExpenseEntryService, private router : Router, private route : 
ActivatedRoute ) { } 
   ngOnInit() { 
      this.title = "Expense Entry"; 
   this.expenseEntry$ = this.route.paramMap.pipe( 
      switchMap(params => { 
         this.selectedId = Number(params.get('id')); 
         return 
this.restService.getExpenseEntry(this.selectedId); })); 
   this.expenseEntry$.subscribe( (data) => this.expenseEntry = data ); 
   } 
   goToList() { 
      this.router.navigate(['/expenses']); 
   } 
}

打开ExpenseEntryComponent (src/app/expense-entry/expense-entry.component.html)模板并添加一个新按钮以导航回费用列表页面。

<div class="col-sm" style="text-align: right;"> 
   <button type="button" class="btn btn-primary" (click)="goToList()">Go to List</button>  
   <button type="button" class="btn btn-primary">Edit</button> 
</div>

在这里,我们在Edit按钮之前添加了Go to List按钮。

使用以下命令运行应用程序:

ng serve

应用程序的最终输出如下所示:

Nested routing

单击第一个条目的视图选项将导航到详细信息页面并显示选定的费用条目,如下所示:

Nested routing

Angular - 动画

动画使 Web 应用程序焕然一新,并提供了丰富的用户交互。在 HTML 中,动画基本上是在特定时间段内将 HTML 元素从一种 CSS 样式转换为另一种 CSS 样式。例如,可以通过更改图像元素的宽度和高度来放大它。

如果图像的宽度和高度在一段时间内(例如 10 秒)分步从初始值更改为最终值,那么我们将获得动画效果。因此,动画的范围取决于 CSS 为设置 HTML 元素样式提供的功能/属性。

Angular 提供了一个单独的模块BrowserAnimationModule来执行动画。BrowserAnimationModule提供了一种简单明了的方法来执行动画。

配置动画模块

让我们在本节中学习如何在应用程序中配置动画模块。

按照以下步骤在应用程序中配置动画模块BrowserAnimationModule

在 AppModule 中导入BrowserAnimationModule

import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 
@NgModule({ 
   imports: [ 
      BrowserModule, 
      BrowserAnimationsModule 
   ], 
   declarations: [ ], 
   bootstrap: [ ] 
}) 
export class AppModule { }

在相关的组件中导入动画函数。

import { state, style, transition, animate, trigger } from '@angular/animations'

在相关的组件中添加animations元数据属性。

@Component({ 
   animations: [ 
      // animation functionality goes here 
   ] 
}) 
export class MyAnimationComponent

概念

在 Angular 中,我们需要理解五个核心概念及其与动画的关系。

状态

状态指的是组件的特定状态。一个组件可以有多个定义的状态。状态是使用 state() 方法创建的。state() 方法有两个参数。

  • 名称 - 状态的唯一名称。

  • 样式 - 使用 style() 方法定义的状态样式。

animations: [ 
   ... 
   state('start', style( { width: 200px; } )) 
   ... 
]

这里,start 是状态的名称。

样式

样式指的是在特定状态下应用的 CSS 样式。style() 方法用于为组件的特定状态设置样式。它使用 CSS 属性,并且可以有多个项目。

animations: [ 
   ... 
   state('start', style( { width: 200px; opacity: 1 } )) 
   ... 
]

这里,start 状态定义了两个 CSS 属性,width 的值为 200px,opacity 的值为 1。

过渡

过渡指的是从一个状态到另一个状态的转换。动画可以有多个过渡。每个过渡都是使用 transition() 函数定义的。transition() 接受两个参数。

  • 指定两个过渡状态之间的方向。例如,start => end 指的是初始状态为start,最终状态为end。实际上,它是一个功能丰富的表达式。

  • 使用animate() 函数指定动画细节。

animations: [ 
   ... 
   transition('start => end', [ 
      animate('1s') 
   ])
   ... 
]

这里,transition() 函数定义了从 start 状态到 end 状态的过渡,动画在animate() 方法中定义。

动画

动画定义了从一个状态到另一个状态的转换方式。animation() 函数用于设置动画细节。animate() 以如下表达式的形式接受单个参数:

duration delay easing
  • 持续时间 - 指的是过渡的持续时间。它表示为 1s、100ms 等。

  • 延迟 - 指的是开始过渡的延迟时间。它与持续时间类似。

  • 缓动 - 指的是如何在给定的持续时间内加速/减速过渡。

触发器

每个动画都需要一个触发器来启动动画。trigger() 方法用于在一个地方设置所有动画信息,例如状态、样式、过渡和动画,并为其提供一个唯一的名称。唯一的名称进一步用于触发动画。

animations: [ 
   trigger('enlarge', [ 
      state('start', style({ 
         height: '200px', 
      })), 
      state('end', style({ 
         height: '500px', 
      })), 
      transition('start => end', [ 
         animate('1s') 
      ]), 
      transition('end => start', [ 
         animate('0.5s') 
   ]) ]), 
]

这里,enlarge 是给定动画的唯一名称。它有两个状态和相关的样式。它有两个过渡,一个从 start 到 end,另一个从 end 到 start。End 到 start 状态执行动画的反向操作。

触发器可以附加到元素上,如下所示:

<div [@triggerName]="expression">...</div>;

例如,

<img [@enlarge]="isEnlarge ? 'end' : 'start'">...</img>;

这里,

  • @enlarge - 触发器设置为 image 标签并附加到表达式。

  • 如果isEnlarge 的值为 true,则将设置end 状态,并触发start => end 过渡。

  • 如果isEnlarge 的值为 false,则将设置start 状态,并触发end => start 过渡

简单动画示例

让我们编写一个新的 Angular 应用程序,通过使用动画效果放大图像来更好地理解动画概念。

打开命令提示符并创建新的 Angular 应用程序。

cd /go/to/workspace 
ng new animation-app 
cd animation-app

AppModule(src/app/app.module.ts)中配置BrowserAnimationModule

import { BrowserModule } from '@angular/platform-browser'; 
import { NgModule } from '@angular/core' 
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 
import { AppComponent } from './app.component'; @NgModule({ 
   declarations: [ 
      AppComponent 
   ], 
   imports: [ 
      BrowserModule, 
      BrowserAnimationsModule 
   ], 
   providers: [], 
   bootstrap: [AppComponent] 
}) 
export class AppModule { }

打开AppComponent(src/app/app.component.ts)并导入必要的动画函数。

import { state, style, transition, animate, trigger } from '@angular/animations';

添加动画功能,这将在图像放大/缩小时为图像设置动画。

@Component({
   selector: 'app-root',
   templateUrl: './app.component.html',
   styleUrls: ['./app.component.css'],
   animations: [
      trigger('enlarge', [
         state('start', style({
            height: '150px'
         })),
         state('end', style({
            height: '250px'
         })),
         transition('start => end', [
            animate('1s 2s')
         ]),
         transition('end => start', [
            animate('1s 2s')
         ])
      ])
   ]
})

打开AppComponent 模板,src/app/app.component.html 并删除示例代码。然后,包括一个带有应用程序标题、图像和一个放大/缩小图像按钮的标题。

<h1>{{ title }}</h1> 
<img src="assets/puppy.jpeg" style="height: 200px" /> <br /> 
<button>{{ this.buttonText }}</button>

编写一个函数来更改动画表达式。

export class AppComponent { 
   title = 'Animation Application'; 
   isEnlarge: boolean = false; 
   buttonText: string = "Enlarge"; 
   triggerAnimation() { 
      this.isEnlarge = !this.isEnlarge; 
      if(this.isEnlarge) 
         this.buttonText = "Shrink"; 
      else 
         this.buttonText = "Enlarge"; 
   } 
}

在图像标签中附加动画。此外,为按钮附加点击事件。

<h1>{{ title }}</h1>
<img [@enlarge]="isEnlarge ? 'end' : 'start'" src="assets/puppy.jpeg" style="height: 200px" />
<br />
<button (click)='triggerAnimation()'>{{ this.buttonText }}</button>

完整的AppComponent 代码如下:

import { Component } from '@angular/core';
import { state, style, transition, animate, trigger } from '@angular/animations';

@Component({
   selector: 'app-root',
   templateUrl: './app.component.html',
   styleUrls: ['./app.component.css'],
   animations: [
      trigger('enlarge', [
         state('start', style({
            height: '150px'
         })),
         state('end', style({
            height: '250px'
         })),
         transition('start => end', [
            animate('1s 2s')
         ]),
         transition('end => start', [
            animate('1s 2s')
         ])
      ])
   ]
})
export class AppComponent {
   title = 'Animation Application';
   isEnlarge: boolean = false;
   buttonText: string = "Enlarge";


   triggerAnimation() {
      this.isEnlarge = !this.isEnlarge;

      if(this.isEnlarge) 
         this.buttonText = "Shrink";
      else
         this.buttonText = "Enlarge";
   }
}

完整的 AppComponent 模板代码如下:

<h1>{{ title }}</h1>
<img [@enlarge]="isEnlarge ? 'end' : 'start'" src="assets/puppy.jpeg" style="height: 200px" />
<br />
<button (click)='triggerAnimation()'>{{ this.buttonText }}</button>

使用以下命令运行应用程序:

ng serve

单击放大按钮,它将使用动画放大图像。结果将如下所示:

appcomponents

再次单击按钮以缩小它。结果将如下所示:

shrink

Angular - 表单

表单用于处理用户输入数据。Angular 支持两种类型的表单。它们是模板驱动表单响应式表单。本节详细解释了 Angular 表单。

模板驱动表单

模板驱动表单是使用模板中的指令创建的。它主要用于创建简单的表单应用程序。让我们简要了解如何创建模板驱动表单。

配置表单

在了解表单之前,让我们学习如何在应用程序中配置表单。要启用模板驱动表单,首先需要在app.module.ts中导入FormsModule。它在下面给出:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';

//import FormsModule here
import { FormsModule } from '@angular/forms'; 

imports: [
   BrowserModule,
   AppRoutingModule,
   FormsModule   //Assign FormsModule
],

一旦导入FormsModule,应用程序将准备好进行表单编程。

创建简单表单

让我们在 Angular 中创建一个示例应用程序(template-form-app)来学习模板驱动表单。

打开命令提示符并使用以下命令创建新的 Angular 应用程序:

cd /go/to/workspace 
ng new template-form-app 
cd template-form-app

AppComponent中配置FormsModule,如下所示:

...

import { FormsModule } from '@angular/forms';

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

使用 Angular CLI 创建测试组件,如下所示:

ng generate component test

以上创建了一个新组件,输出如下所示:

CREATE src/app/test/test.component.scss (0 bytes)
CREATE src/app/test/test.component.html (19 bytes)
CREATE src/app/test/test.component.spec.ts (614 bytes)
CREATE src/app/test/test.component.ts (262 bytes)
UPDATE src/app/app.module.ts (545 bytes)

让我们创建一个简单的表单来显示用户输入的文本。

test.component.html文件中添加以下代码,如下所示:

<form #userName="ngForm" (ngSubmit)="onClickSubmit(userName.value)"> 
   <input type="text" name="username" placeholder="username" ngModel> 
   <br/> 
   <br/> 
   <input type="submit" value="submit"> 
</form>

这里,我们在input 文本字段中使用了ngModel 属性。

test.component.ts文件中创建onClickSubmit()方法,如下所示

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

@Component({
   selector: 'app-test',
   templateUrl: './test.component.html',
   styleUrls: ['./test.component.scss']
})

export class TestComponent implements OnInit {

   ngOnInit() {
   }
   onClickSubmit(result) {
      console.log("You have entered : " + result.username); 
   }
}

打开 app.component.html 并更改内容,如下所示:

<app-test></app-test>

最后,使用以下命令启动您的应用程序(如果尚未启动):

ng serve

现在,运行您的应用程序,您会看到以下响应:

Form

在输入文本字段中输入Peter 并回车提交。onClickSubmit 函数将被调用,用户输入的文本Peter 将作为参数发送。onClickSubmit 将在控制台中打印用户名,输出如下所示:

Forms

响应式表单

响应式表单是在组件类内部创建的,因此也称为模型驱动表单。每个表单控件在组件中都有一个对象,这在表单编程中提供了更大的控制和灵活性。响应式表单基于结构化数据模型。让我们了解如何在 Angular 中使用响应式表单。

配置响应式表单

要启用响应式表单,首先需要在app.module.ts中导入ReactiveFormsModule。它在下面定义

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

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { TestComponent } from './test/test.component';
import { FormsModule } from '@angular/forms';

//import ReactiveFormsModule here
import { ReactiveFormsModule } from '@angular/forms';

  imports: [
    BrowserModule,
    AppRoutingModule,
    FormsModule, 
    ReactiveFormsModule   //Assign here
  ]

创建响应式表单

在开始创建响应式表单之前,我们需要了解以下概念:

  • FormControl - 定义单个表单控件的基本功能

  • FormGroup - 用于聚合表单控件集合的值

  • FormArray - 用于将表单控件的值聚合成数组

  • ControlValueAccessor - 充当表单 API 与 HTML DOM 元素之间的接口。

让我们在 Angular 中创建一个示例应用程序(reactive-form-app)来学习模板驱动表单。

打开命令提示符并使用以下命令创建新的 Angular 应用程序:

cd /go/to/workspace
ng new reactive-form-app
cd reactive-form-app

AppComponent中配置ReactiveFormsModule,如下所示:

...
import { ReactiveFormsModule } from '@angular/forms';

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

使用 Angular CLI 创建一个 test 组件,如下所示:

ng generate component test

以上创建了一个新组件,输出如下所示:

CREATE src/app/test/test.component.scss (0 bytes)
CREATE src/app/test/test.component.html (19 bytes)
CREATE src/app/test/test.component.spec.ts (614 bytes)
CREATE src/app/test/test.component.ts (262 bytes)
UPDATE src/app/app.module.ts (545 bytes)

让我们创建一个简单的表单来显示用户输入的文本。

我们需要在TestComponent中导入FormGroup、FormControl 类。

import { FormGroup, FormControl } from '@angular/forms';

test.component.ts文件中创建onClickSubmit()方法,如下所示:

import { Component, OnInit } from '@angular/core';
import { FormGroup, FormControl } from '@angular/forms';

@Component({
   selector: 'app-test',
   templateUrl: './test.component.html',
   styleUrls: ['./test.component.css']
})
export class TestComponent implements OnInit {
   userName; 
   formdata;
   ngOnInit() { 
      this.formdata = new FormGroup({ 
         userName: new FormControl("Tutorialspoint")
      }); 
   } 
   onClickSubmit(data) {this.userName = data.userName;}
}

这里,

  • 创建了formGroup的实例并将其设置为局部变量 formdata。

  • 创建FormControl的实例并将其设置为 formdata 中的一个条目。

  • 创建了一个onClickSubmit()方法,它使用其参数设置局部变量userName

test.component.html文件中添加以下代码。

<div> 
   <form [formGroup]="formdata" (ngSubmit)="onClickSubmit(formdata.value)" > 
      <input type= text"  name="userName" placeholder="userName" 
         formControlName = "userName"> 
      <br/>
      <br/>
      <input type="submit"  value="Click here"> 
   </form>
</div> 
<p> Textbox result is: {{userName}} </p>

这里,

  • 创建了一个新表单并将其formGroup属性设置为 formdata。

  • 创建了一个新的输入文本字段并将其formControlName设置为 username。

  • ngSubmit 事件属性用于表单并将 onClickSubmit() 方法设置为其值。

  • onClickSubmit() 方法获取 formdata 值作为其参数。

打开app.component.html并更改内容,如下所示:

<app-test></app-test>

最后,使用以下命令启动您的应用程序(如果尚未启动):

ng serve

现在,运行您的应用程序,您会看到以下响应:

Nested response

在输入文本字段中输入Tutorialspoint 并回车提交。onClickSubmit 函数将被调用,用户输入的文本Peter 将作为参数发送。

responses

我们将在下一章中执行表单验证。

Angular - 表单验证

表单验证是 Web 应用程序的重要组成部分。它用于验证用户输入是否格式正确。

RequiredValidator

让我们在 Angular 中执行简单的必填字段验证。

打开命令提示符并转到reactive-form-app

cd /go/to/reactive-form-app

test.component.ts文件中替换以下代码。

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

//import validator and FormBuilder
import { FormGroup, FormControl, Validators, FormBuilder } from '@angular/forms';

@Component({
   selector: 'app-test',
   templateUrl: './test.component.html',
   styleUrls: ['./test.component.css']
})

export class TestComponent implements OnInit {
   //Create FormGroup
   requiredForm: FormGroup;
   constructor(private fb: FormBuilder) {
      this.myForm();
   }

   //Create required field validator for name
   myForm() {
      this.requiredForm = this.fb.group({
      name: ['', Validators.required ]
      });
   }
   ngOnInit()
   {

   }
}

这里,

我们使用表单构建器来处理所有验证。构造函数用于创建具有验证规则的表单。

test.component.html文件中添加以下代码。

<div>
   <h2>
     Required Field validation
   </h2>
   <form [formGroup]="requiredForm" novalidate>
         <div class="form-group">
           <label class="center-block">Name:
             <input class="form-control" formControlName="name">
           </label>
         </div>
         <div *ngIf="requiredForm.controls['name'].invalid && requiredForm.controls['name'].touched" class="alert alert-danger">
             <div *ngIf="requiredForm.controls['name'].errors.required">
             Name is required.
           </div>
         </div>
   </form>
 <p>Form value: {{ requiredForm.value | json }}</p>
 <p>Form status: {{ requiredForm.status | json }}</p>
 </div>

这里,

  • requiredForm 被称为全局表单组对象。它是一个父元素。表单控件是 requiredForm 的子元素。

  • 条件语句用于检查,如果用户触碰了输入字段但没有输入值,则显示错误消息。

最后,使用以下命令启动您的应用程序(如果尚未启动):

ng serve

现在运行您的应用程序并将焦点放在文本框上。然后,它将显示“名称是必填项”,如下所示:

Validation

如果您在文本框中输入文本,则它将被验证,结果如下所示:

Validations

PatternValidator

PatternValidator 用于验证正则表达式模式。让我们执行简单的电子邮件验证。

打开命令提示符并转到reactive-form-app

cd /go/to/reactive-form-app

在 test.component.ts 文件中替换以下代码:

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

import { FormGroup, FormControl, Validators, FormBuilder } from 
'@angular/forms';

@Component({
   selector: 'app-test',
   templateUrl: './test.component.html',
   styleUrls: ['./test.component.css']
})

export class TestComponent implements OnInit {
   requiredForm: FormGroup;
   constructor(private fb: FormBuilder) {
      this.myForm();
   }

   myForm() {
      this.requiredForm = this.fb.group({
      email: ['', [Validators.required, 
         Validators.pattern("^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$")] ]
      });
   }

   ngOnInit()
   {

   }
}

这里,

在 Validator 内部添加了电子邮件模式验证器。

更新 test.component.html 文件中的以下代码:

<div>
   <h2>
   Pattern validation
   </h2>
   <form [formGroup]="requiredForm" novalidate>
   <div class="form-group">
      <label class="center-block">Email:
      <input class="form-control" formControlName="email">
      </label>
   </div>
   <div *ngIf="requiredForm.controls['email'].invalid && requiredForm.controls['email'].touched" class="alert alert-danger">
       <div *ngIf="requiredForm.controls['email'].errors.required">
      Email is required.
      </div>
   </div>
   </form>
   <p>Form value: {{ requiredForm.value | json }}</p>
   <p>Form status: {{ requiredForm.status | json }}</p>
</div>

这里,我们创建了电子邮件控件并调用了电子邮件验证器。

运行您的应用程序,您将看到以下结果:

PatternValidator

PatternValidators

类似地,您可以尝试自己执行其他类型的验证器。

Angular - 身份验证和授权

身份验证是将 Web 应用程序的访问者与系统中预定义的用户身份集进行匹配的过程。换句话说,它是识别用户身份的过程。在安全性方面,身份验证是系统中非常重要的过程。

授权是授予用户访问系统中某些资源的权限的过程。只有经过身份验证的用户才能被授权访问资源。

让我们在本节中学习如何在 Angular 应用程序中进行身份验证和授权。

路由中的守卫

在 Web 应用程序中,资源由 URL 引用。系统中的每个用户都将被允许访问一组 URL。例如,管理员可以被分配管理部分下的所有 URL。

正如我们已经知道的,URL 由路由处理。Angular 路由使 URL 可以根据编程逻辑进行保护和限制。因此,URL 可能会被拒绝普通用户访问,而允许管理员访问。

Angular 提供了一个称为路由守卫的概念,可用于通过路由阻止对应用程序某些部分的未授权访问。Angular 提供多个守卫,如下所示

  • CanActivate - 用于阻止对路由的访问。

  • CanActivateChild - 用于阻止对子路由的访问。

  • CanDeactivate - 用于停止正在进行的流程并获取用户的反馈。例如,如果用户回复否定,则可以停止删除流程。

  • Resolve - 用于在导航到路由之前预取数据。

  • CanLoad - 用于加载资产。

工作示例

让我们尝试向我们的应用程序添加登录功能并使用 CanActivate 守卫对其进行保护。

打开命令提示符并转到项目根文件夹。

cd /go/to/expense-manager

启动应用程序。

ng serve

创建一个新的服务 AuthService 来对用户进行身份验证。

ng generate service auth
CREATE src/app/auth.service.spec.ts (323 bytes)
CREATE src/app/auth.service.ts (133 bytes)

打开AuthService并包含以下代码。

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

import { Observable, of } from 'rxjs';
import { tap, delay } from 'rxjs/operators';

@Injectable({
   providedIn: 'root'
})
export class AuthService {

   isUserLoggedIn: boolean = false;

   login(userName: string, password: string): Observable {
      console.log(userName);
      console.log(password);
      this.isUserLoggedIn = userName == 'admin' && password == 'admin';
      localStorage.setItem('isUserLoggedIn', this.isUserLoggedIn ? "true" : "false"); 

   return of(this.isUserLoggedIn).pipe(
      delay(1000),
      tap(val => { 
         console.log("Is User Authentication is successful: " + val); 
      })
   );
   }

   logout(): void {
   this.isUserLoggedIn = false;
      localStorage.removeItem('isUserLoggedIn'); 
   }

   constructor() { }
}

这里,

  • 我们编写了两个方法,loginlogout

  • login 方法的目的是验证用户,如果用户成功验证,则将其信息存储在localStorage中,然后返回 true。

  • 身份验证验证是用户名和密码应为admin

  • 我们没有使用任何后端。相反,我们使用 Observables 模拟了 1 秒的延迟。

  • logout 方法的目的是使用户无效并删除存储在localStorage中的信息。

使用以下命令创建login组件:

ng generate component login
CREATE src/app/login/login.component.html (20 bytes)
CREATE src/app/login/login.component.spec.ts (621 bytes)
CREATE src/app/login/login.component.ts (265 bytes)
CREATE src/app/login/login.component.css (0 bytes)
UPDATE src/app/app.module.ts (1207 bytes)

打开LoginComponent并包含以下代码:

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

import { FormGroup, FormControl } from '@angular/forms';
import { AuthService } from '../auth.service';
import { Router } from '@angular/router';

@Component({
   selector: 'app-login',
   templateUrl: './login.component.html',
   styleUrls: ['./login.component.css']
})
export class LoginComponent implements OnInit {

   userName: string;
   password: string;
   formData: FormGroup;

   constructor(private authService : AuthService, private router : Router) { }

   ngOnInit() {
      this.formData = new FormGroup({
         userName: new FormControl("admin"),
         password: new FormControl("admin"),
      });
   }

   onClickSubmit(data: any) {
      this.userName = data.userName;
      this.password = data.password;

      console.log("Login page: " + this.userName);
      console.log("Login page: " + this.password);

      this.authService.login(this.userName, this.password)
         .subscribe( data => { 
            console.log("Is Login Success: " + data); 
      
           if(data) this.router.navigate(['/expenses']); 
      });
   }
}

这里,

  • 使用响应式表单。

  • 导入 AuthService 和 Router 并将其配置在构造函数中。

  • 创建了 FormGroup 的实例,并包含了 FormControl 的两个实例,一个用于用户名,另一个用于密码。

  • 创建了一个 onClickSubmit 来使用 authService 验证用户,如果成功,则导航到费用列表。

打开LoginComponent模板并包含以下模板代码。

<!-- Page Content -->
<div class="container">
   <div class="row">
      <div class="col-lg-12 text-center" style="padding-top: 20px;">
         <div class="container box" style="margin-top: 10px; padding-left: 0px; padding-right: 0px;">
            <div class="row">
               <div class="col-12" style="text-align: center;">
                                    <form [formGroup]="formData" (ngSubmit)="onClickSubmit(formData.value)" 
                                          class="form-signin">
                                    <h2 class="form-signin-heading">Please sign in</h2>
                                    <label for="inputEmail" class="sr-only">Email address</label>
                                    <input type="text" id="username" class="form-control" 
                                          formControlName="userName" placeholder="Username" required autofocus>
                                    <label for="inputPassword" class="sr-only">Password</label>
                                    <input type="password" id="inputPassword" class="form-control" 
                                          formControlName="password" placeholder="Password" required>
                                    <button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
                                    </form>
               </div>
            </div>
         </div>
      </div>
   </div>
</div>

这里,

创建了一个响应式表单并设计了一个登录表单。

onClickSubmit方法附加到表单提交操作。

打开LoginComponent样式并包含以下 CSS 代码。

.form-signin {
   max-width: 330px;

   padding: 15px;
   margin: 0 auto;
}

input {
   margin-bottom: 20px;
}

这里,添加了一些样式来设计登录表单。

使用以下命令创建一个注销组件:

ng generate component logout
CREATE src/app/logout/logout.component.html (21 bytes)
CREATE src/app/logout/logout.component.spec.ts (628 bytes)
CREATE src/app/logout/logout.component.ts (269 bytes)
CREATE src/app/logout/logout.component.css (0 bytes)
UPDATE src/app/app.module.ts (1368 bytes)

打开LogoutComponent并包含以下代码。

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

import { AuthService } from '../auth.service';
import { Router } from '@angular/router';

@Component({
   selector: 'app-logout',
   templateUrl: './logout.component.html',
   styleUrls: ['./logout.component.css']
})
export class LogoutComponent implements OnInit {

   constructor(private authService : AuthService, private router: Router) { }

   ngOnInit() {
      this.authService.logout();
      this.router.navigate(['/']);
   }

}

这里,

  • 使用AuthService的注销方法。
  • 用户注销后,页面将重定向到主页(/)。

使用以下命令创建一个守卫:

ng generate guard expense
CREATE src/app/expense.guard.spec.ts (364 bytes)
CREATE src/app/expense.guard.ts (459 bytes)

打开ExpenseGuard并包含以下代码:

import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router, UrlTree } from '@angular/router';
import { Observable } from 'rxjs';

import { AuthService } from './auth.service';

@Injectable({
   providedIn: 'root'
})
export class ExpenseGuard implements CanActivate {

   constructor(private authService: AuthService, private router: Router) {}

   canActivate(
   next: ActivatedRouteSnapshot,
   state: RouterStateSnapshot): boolean | UrlTree {
      let url: string = state.url;

          return this.checkLogin(url);
      }

      checkLogin(url: string): true | UrlTree {
         console.log("Url: " + url)
         let val: string = localStorage.getItem('isUserLoggedIn');

         if(val != null && val == "true"){
            if(url == "/login")
               this.router.parseUrl('/expenses');
            else 
               return true;
         } else {
            return this.router.parseUrl('/login');
         }
      }
}

这里,

  • checkLogin将检查localStorage中是否包含用户信息,如果存在,则返回true。
  • 如果用户已登录并转到登录页面,则会将用户重定向到支出页面
  • 如果用户未登录,则用户将被重定向到登录页面。

打开AppRoutingModule (src/app/app-routing.module.ts)并更新以下代码:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { ExpenseEntryComponent } from './expense-entry/expense-entry.component';
import { ExpenseEntryListComponent } from './expense-entry-list/expense-entry-list.component';
import { LoginComponent } from './login/login.component';
import { LogoutComponent } from './logout/logout.component';

import { ExpenseGuard } from './expense.guard';

const routes: Routes = [
   { path: 'login', component: LoginComponent },
   { path: 'logout', component: LogoutComponent },
   { path: 'expenses', component: ExpenseEntryListComponent, canActivate: [ExpenseGuard]},
   { path: 'expenses/detail/:id', component: ExpenseEntryComponent, canActivate: [ExpenseGuard]},
   { path: '', redirectTo: 'expenses', pathMatch: 'full' }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

这里,

  • 导入了LoginComponent和LogoutComponent。
  • 导入了ExpenseGuard。
  • 创建了两个新的路由,login和logout,分别用于访问LoginComponent和LogoutComponent。
  • 为ExpenseEntryComponent和ExpenseEntryListComponent添加新的选项canActivate。

打开AppComponent模板并添加两个登录和注销链接。

<div class="collapse navbar-collapse" id="navbarResponsive">
   <ul class="navbar-nav ml-auto">
      <li class="nav-item active">
         <a class="nav-link" href="#">Home
            <span class="sr-only" routerLink="/">(current)</span>

         </a>
      </li>
      <li class="nav-item">
         <a class="nav-link" routerLink="/expenses">Report</a>
      </li>
      <li class="nav-item">
         <a class="nav-link" href="#">Add Expense</a>
      </li>
      <li class="nav-item">

         <a class="nav-link" href="#">About</a>
      </li>
      <li class="nav-item">
                  <div *ngIf="isUserLoggedIn; else isLogOut">
                        <a class="nav-link" routerLink="/logout">Logout</a>
                  </div>

                  <ng-template #isLogOut>
                              <a class="nav-link" routerLink="/login">Login</a>
                  </ng-template>
      </li>
   </ul>
</div>

打开AppComponent并更新以下代码:

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

import { AuthService } from './auth.service';

@Component({
   selector: 'app-root',
   templateUrl: './app.component.html',
   styleUrls: ['./app.component.css']
})
export class AppComponent {

   title = 'Expense Manager';
   isUserLoggedIn = false;

   constructor(private authService: AuthService) {}

   ngOnInit() {
      let storeData = localStorage.getItem("isUserLoggedIn");
      console.log("StoreData: " + storeData);

      if( storeData != null && storeData == "true")
         this.isUserLoggedIn = true;
      else


         this.isUserLoggedIn = false;
   }
}

在这里,我们添加了识别用户状态的逻辑,以便我们可以显示登录/注销功能。

打开AppModule (src/app/app.module.ts)并配置ReactiveFormsModule

import { ReactiveFormsModule } from '@angular/forms'; 
imports: [ 
   ReactiveFormsModule 
]

现在,运行应用程序,应用程序将打开登录页面。

ReactiveFormsModule

输入admin和admin作为用户名和密码,然后点击提交。应用程序处理登录并将用户重定向到支出列表页面,如下所示:

FormsModule

最后,您可以点击注销并退出应用程序。

Angular - Web Workers

Web Worker使JavaScript应用程序能够在后台运行CPU密集型操作,以便应用程序主线程专注于UI的流畅运行。Angular提供了在应用程序中包含Web Worker的支持。让我们编写一个简单的Angular应用程序并尝试使用Web Worker。

使用以下命令创建一个新的Angular应用程序:

cd /go/to/workspace
ng new web-worker-sample

使用以下命令运行应用程序:

cd web-worker-sample
npm run start

使用以下命令添加新的Web Worker:

ng generate web-worker app

上述命令的输出如下:

CREATE tsconfig.worker.json (212 bytes)
CREATE src/app/app.worker.ts (157 bytes)
UPDATE tsconfig.app.json (296 bytes)
UPDATE angular.json (3776 bytes)
UPDATE src/app/app.component.ts (605 bytes)

这里,

  • app指的是要创建的Web Worker的位置。
  • Angular CLI将生成两个新文件,tsconfig.worker.json和src/app/app.worker.ts,并更新三个文件,tsconfig.app.json、angular.json和src/app/app.component.ts文件。

让我们检查一下更改:

// tsconfig.worker.json
{
   "extends": "./tsconfig.json",
   "compilerOptions": {
      "outDir": "./out-tsc/worker",
      "lib": [
         "es2018",
         "webworker"
      ],


      "types": []
   },
   "include": [
      "src/**/*.worker.ts"
   ]
}

这里,

tsconfig.worker.json扩展了tsconfig.json,并包含编译Web Worker的选项。

// tsconfig.app.json [only a snippet]
"exclude": [
   "src/test.ts",
   "src/**/*.spec.ts",
   "src/**/*.worker.ts"
]

这里,

基本上,它排除了所有要编译的worker,因为它有单独的配置。

// angular.json (only a snippet) "webWorkerTsConfig": "tsconfig.worker.json"

这里,

angular.json包含Web Worker配置文件tsconfig.worker.json。

// src/app/app.worker.ts
addEventListener('message', ({ data }) => {
   const response = `worker response to ${data}`;
   postMessage(response);
});

这里,

创建了一个Web Worker。Web Worker基本上是一个函数,当触发消息事件时将被调用。Web Worker将接收调用方发送的数据,处理它,然后将响应发送回调用方。

// src/app/app.component.ts [only a snippet]
if (typeof Worker !== 'undefined') {
   // Create a new
   const worker = new Worker('./app.worker', { type: 'module' });
   worker.onmessage = ({ data }) => {
      console.log(`page got message: ${data}`);
   };
   worker.postMessage('hello');
} else {

   // Web Workers are not supported in this environment.
   // You should add a fallback so that your program still executes correctly.
}

这里,

  • AppComponent创建一个新的worker实例,创建一个回调函数来接收响应,然后将消息发布到worker。

重新启动应用程序。由于angular.json文件已更改,而Angular运行器未监视该文件,因此有必要重新启动应用程序。否则,Angular无法识别新的Web Worker并且不会编译它。

让我们创建一个TypeScript类,src/app/app.prime.ts,用于查找第n个素数。

export class PrimeCalculator
{
   static isPrimeNumber(num : number) : boolean {
      if(num == 1) return true;

      let idx : number = 2;
      for(idx = 2; idx < num / 2; idx++)
      {
         if(num % idx == 0)
            return false;
      }

      return true;
   }

   static findNthPrimeNumber(num : number) : number {
      let idx : number = 1;
      let count = 0;

      while(count < num) {
         if(this.isPrimeNumber(idx))
            count++;

         idx++;
         console.log(idx);
      }

      return idx - 1;
   }
}

这里,

  • isPrimeNumber检查给定数字是否为素数。
  • findNthPrimeNumber查找第n个素数。

将新创建的素数类导入到src/app/app.worker.ts中,并将Web Worker的逻辑更改为查找第n个素数。

/// <reference lib="webworker" />

import { PrimeCalculator } from './app.prime';

addEventListener('message', ({ data }) => {
   // const response = `worker response to ${data}`;
   const response = PrimeCalculator.findNthPrimeNumber(parseInt(data));
   postMessage(response);
});

更改AppComponent并包含两个函数,find10thPrimeNumberfind10000thPrimeNumber

import { Component } from '@angular/core';
import { PrimeCalculator } from './app.prime';

@Component({
   selector: 'app-root',
   templateUrl: './app.component.html',
   styleUrls: ['./app.component.css']
})
export class AppComponent {
   title = 'Web worker sample';
   prime10 : number = 0;
   prime10000 : number = 0;

   find10thPrimeNumber() {
      this.prime10 = PrimeCalculator.findNthPrimeNumber(10);
   }

   find10000thPrimeNumber() {
      if (typeof Worker !== 'undefined') {
         // Create a new
         const worker = new Worker('./app.worker', { type: 'module' });
         worker.onmessage = ({ data }) => {
         this.prime10000 = data;
         };
         worker.postMessage(10000);
      } else {
         // Web Workers are not supported in this environment.
         // You should add a fallback so that your program still executes correctly.
      }
   }
}

这里,

find10thPrimeNumber直接使用PrimeCalculator。但是,find10000thPrimeNumber将计算委托给Web Worker,Web Worker依次使用PrimeCalculator。

更改AppComponent模板,src/app/app.commands.html,并包含两个选项,一个用于查找第10个素数,另一个用于查找第10000个素数。

<h1>{{ title }}</h1>

<div>
   <a href="#" (click)="find10thPrimeNumber()">Click here</a> to find 10th prime number
   <div>The 10<sup>th</sup> prime number is {{ prime10 }}</div> <br/>
   <a href="#" (click)="find10000thPrimeNumber()">Click here</a> to find 10000th prime number
   <div>The 10000<sup>th</sup> prime number is {{ prime10000 }}</div>
</div>

这里,

查找第10000个素数将花费几秒钟,但不会影响其他进程,因为它使用Web Worker。只需先尝试查找第10000个素数,然后再查找第10个素数。

由于Web Worker正在计算第10000个素数,因此UI不会冻结。我们可以在此期间检查第10个素数。如果我们没有使用Web Worker,我们无法在浏览器中执行任何操作,因为它正在积极处理第10000个素数。

应用程序的结果如下:

应用程序的初始状态。

Workers

单击并尝试查找第10000个素数,然后尝试查找第10个素数。应用程序非常快地找到第10个素数并显示它。应用程序仍在后台处理以查找第10000个素数。

Web worker

两个进程都已完成。

Web workers

Web Worker通过在后台执行复杂操作来增强Web应用程序的用户体验,并且在Angular应用程序中也很容易做到这一点。

Angular - Service Worker和PWA

渐进式Web应用程序(PWA)是普通Web应用程序,具有一些增强功能,并且表现得像原生应用程序。PWA应用程序不依赖于网络工作。PWA缓存应用程序并从本地缓存中呈现它。它定期检查应用程序的实时版本,然后在后台缓存最新版本。

PWA可以像原生应用程序一样安装在系统中,并且可以在桌面上显示快捷方式。单击快捷方式将在浏览器中打开应用程序,并使用本地缓存,即使系统中没有任何网络可用。

Angular应用程序可以转换为PWA应用程序。要转换Angular应用程序,我们需要使用Service Worker API。Service Worker实际上是一个代理服务器,位于浏览器、应用程序和网络之间。

Service Worker与网页是分开的。它无法访问DOM对象。相反,Service Worker通过PostMessage接口与网页交互。

PWA应用程序有两个先决条件。它们如下:

  • 浏览器支持 - 尽管许多浏览器都支持PWA应用程序,但IE、Opera mini和一些其他浏览器不支持PWA。

  • HTTPS交付 - 应用程序需要通过HTTPS协议交付。https支持的一个例外是用于开发目的的localhost

让我们创建一个新的应用程序并将其转换为PWA应用程序。

使用以下命令创建一个新的Angular应用程序:

cd /go/to/workspace 
ng new pwa-sample

使用以下命令添加PWA支持:

cd pwa-sample
ng add @angular/pwa --project pwa-sample

构建应用程序的生产版本:

ng build --prod

PWA应用程序不会在Angular开发服务器下运行。使用以下命令安装一个简单的Web服务器:

npm install -g http-server

运行Web服务器并将应用程序的生产构建设置为根文件夹。

f the application as root folder.
http-server -p 8080 -c-1 dist/pwa-sample

打开浏览器并输入https://:8080。

现在,转到开发者工具 -> 网络并选择脱机选项。

如果网络设置为脱机,则普通应用程序将停止工作,但PWA应用程序可以正常工作,如下所示: PWA

Angular - 服务器端渲染

服务器端渲染(SSR)是一种现代技术,用于将浏览器中运行的单页应用程序(SPA)转换为基于服务器的应用程序。通常,在SPA中,服务器返回一个简单的index.html文件,其中包含对基于JavaScript的SPA应用程序的引用。SPA应用程序从那里接管,配置整个应用程序,处理请求,然后发送最终响应。

但在支持SSR的应用程序中,服务器也执行所有必要的配置,然后将最终响应发送到浏览器。浏览器呈现响应并启动SPA应用程序。SPA应用程序从那里接管,并将进一步的请求转移到SPA应用程序。SPA和SSR的流程如下图所示。

SSR

将SPA应用程序转换为SSR提供了一些优势,它们如下:

  • 速度 - 第一次请求相对较快。SPA的主要缺点之一是初始渲染速度慢。一旦应用程序呈现,SPA应用程序的速度就非常快。SSR解决了初始渲染问题。

  • SEO友好 - 使网站能够对搜索引擎友好。SPA的另一个主要缺点是Web爬虫无法为了SEO目的而对其进行抓取。SSR解决了这个问题。

Angular Universal

要在Angular中启用SSR,Angular应该能够在服务器中呈现。为了实现这一点,Angular提供了一种名为Angular Universal的特殊技术。这是一项非常新的技术,并且正在不断发展。Angular Universal知道如何在服务器中呈现Angular应用程序。我们可以将我们的应用程序升级到Angular Universal以支持SSR。

Angular - 国际化(i18n)

国际化(i18n)是任何现代Web应用程序都必须具备的功能。国际化使应用程序能够定位世界上任何语言。本地化是国际化的一部分,它使应用程序能够以目标本地语言呈现。Angular完全支持国际化和本地化功能。

让我们学习如何在不同的语言中创建一个简单的Hello World应用程序。

使用以下命令创建一个新的Angular应用程序:

cd /go/to/workspace
ng new i18n-sample

使用以下命令运行应用程序:

cd i18n-sample
npm run start

按如下所示更改AppComponent的模板:

<h1>{{ title }}</h1>

<div>Hello</div>
<div>The Current time is {{ currentDate | date : 'medium' }}</div>

使用以下命令添加本地化模块:

ng add @angular/localize

重新启动应用程序。

LOCALE_ID是Angular变量,用于引用当前区域设置。默认情况下,它设置为en_US。让我们通过在AppModule中的提供程序中使用它来更改区域设置。

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

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

@NgModule({
   declarations: [
      AppComponent
   ],
   imports: [
      BrowserModule
   ],
   providers: [ { provide: LOCALE_ID, useValue: 'hi' } ],

   bootstrap: [AppComponent]
})
export class AppModule { }

这里,

  • LOCALE_ID@angular/core导入。
  • LOCALE_ID通过提供程序设置为hi,以便LOCALE_ID可以在应用程序的任何地方使用。

从@angular/common/locales/hi导入区域设置数据,然后使用registerLocaleData方法注册它,如下所示

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

import { registerLocaleData } from '@angular/common';
import localeHi from '@angular/common/locales/hi';

registerLocaleData(localeHi);

@Component({
   selector: 'app-root',
   templateUrl: './app.component.html',
   styleUrls: ['./app.component.css'],
})
export class AppComponent {
   title = 'Internationzation Sample';
}

创建一个局部变量CurrentDate,并使用Date.now()设置当前时间。

export class AppComponent {
   title = 'Internationzation Sample';

   currentDate: number = Date.now();
}

更改AppComponent的模板内容,并包含currentDate,如下所示:

<h1>{{ title }}</h1>

<div>Hello</div>
<div>The Current time is {{ currentDate | date : 'medium' }}</div>

检查结果,您将看到日期使用hi区域设置指定。

routings

我们已将日期更改为当前区域设置。让我们也更改其他内容。为此,请在相关标签中包含i18n属性,格式为title|description@@id

<h1>{{ title }}</h1>

<h1 i18n="greeting|Greeting a person@@greeting">Hello</h1>
<div>
   <span i18n="time|Specifiy the current time@@currentTime">
      The Current time is {{ currentDate | date : 'medium' }}
   </span>
</div>

这里,

  • hello是简单的翻译格式,因为它包含要翻译的完整文本。
  • Time有点复杂,因为它也包含动态内容。文本的格式应遵循ICU消息格式进行翻译。

我们可以使用以下命令提取要翻译的数据:

ng xi18n --output-path src/locale

命令生成messages.xlf文件,内容如下:

<?xml version="1.0" encoding="UTF-8" ?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
   <file source-language="en" datatype="plaintext" original="ng2.template">
      <body>
         <trans-unit id="greeting" datatype="html">
            <source>Hello</source>
            <context-group purpose="location">
               <context context-type="sourcefile">src/app/app.component.html</context>
               <context context-type="linenumber">3</context>
            </context-group>
            <note priority="1" from="description">Greeting a person</note>
            <note priority="1" from="meaning">greeting</note>

         </trans-unit>
         <trans-unit id="currentTime" datatype="html">
            <source>
                        The Current time is <x id="INTERPOLATION" equiv-text="{{ currentDate | date : 'medium' }}"/>
            </source>
            <context-group purpose="location">
               <context context-type="sourcefile">src/app/app.component.html</context>
               <context context-type="linenumber">5</context>
            </context-group>
            <note priority="1" from="description">Specifiy the current time</note>
            <note priority="1" from="meaning">time</note>
         </trans-unit>
      </body>
   </file>
</xliff>

复制文件并将其重命名为messages.hi.xlf

使用支持 Unicode 的文本编辑器打开文件。找到source标签并复制为target标签,然后将内容更改为hi语言环境。使用谷歌翻译查找匹配的文本。更改后的内容如下所示:

Nested target

Nested targets

打开angular.json文件,并在build -> configuration下添加以下配置

"hi": { 
   "aot": true,
   "outputPath": "dist/hi/",
   "i18nFile": "src/locale/messages.hi.xlf",
   "i18nFormat": "xlf",
   "i18nLocale": "hi",
   "i18nMissingTranslation": "error",
   "baseHref": "/hi/"
},
"en": {
   "aot": true,
   "outputPath": "dist/en/",
   "i18nFile": "src/locale/messages.xlf",
   "i18nFormat": "xlf",
   "i18nLocale": "en",
   "i18nMissingTranslation": "error",
   "baseHref": "/en/"
}

这里,

我们为hien语言环境使用了单独的设置。

serve -> configuration下设置以下内容。

"hi": {
   "browserTarget": "i18n-sample:build:hi"
},
"en": {
   "browserTarget": "i18n-sample:build:en"
}

我们添加了必要的配置。停止应用程序并运行以下命令:

npm run start -- --configuration=hi

这里,

我们指定了需要使用hi配置。

导航到https://:4200/hi,您将看到印地语本地化的内容。

configuration

最后,我们在 Angular 中创建了一个本地化应用程序。

Angular - 可访问性

无障碍性支持是每个基于 UI 的应用程序的重要功能之一。无障碍性是设计应用程序的一种方式,以便那些有某些残疾的人也可以访问它。让我们学习 Angular 提供的支持,以开发具有良好无障碍性的应用程序。

  • 在使用属性绑定时,对 ARIA 属性使用attr.前缀。

  • 使用 Angular Material 组件实现无障碍性。一些有用的组件包括LiveAnnouncercdkTrapFocu

  • 尽可能使用原生 HTML 元素,因为原生 HTML 元素提供了最大的无障碍性功能。在创建组件时,选择与您的用例匹配的原生 HTML 元素,而不是重新开发原生功能。

  • 使用NavigationEnd跟踪和控制应用程序的焦点,因为它对无障碍性有很大帮助。

Angular - CLI 命令

Angular CLI 帮助开发人员轻松快速地创建项目。正如我们已经知道的,Angular CLI 工具用于开发,并且构建在 Node.js 之上,从 NPM 安装。本章详细解释了 Angular CLI 命令。

验证 CLI

在转到 Angular CLI 命令之前,我们必须确保 Angular CLI 已安装在您的机器上。如果已安装,您可以使用以下命令进行验证:

ng version

您将看到以下响应:

CLI

如果未安装 CLI,则使用以下命令安装它。

npm install -g @angular/cli@^8.0.0

让我们简要了解一下这些命令。

新命令

要在 Angular 中创建应用程序,请使用以下语法:

ng new <project-name>

示例

如果要创建 CustomerApp,请使用以下代码:

ng new CustomerApp

生成命令

它用于根据架构生成或修改文件。在您的 Angular 项目中输入以下命令:

ng generate

或者,您也可以简单地将 generate 键入为 g。您还可以使用以下语法:

ng g

它将列出可用的架构:

schematics

让我们在下节中了解一些重复使用的 ng generate 架构。

创建组件

组件是 Angular 的构建块。要在 Angular 中创建组件,请使用以下语法:

ng g c <component-name>

例如,如果用户想要创建一个Details组件,则使用以下代码:

ng g c Details

使用此命令后,您将看到以下响应:

CREATE src/app/details/details.component.scss (0 bytes)
CREATE src/app/details/details.component.html (22 bytes)
CREATE src/app/details/details.component.spec.ts (635 bytes)
CREATE src/app/details/details.component.ts (274 bytes)
UPDATE src/app/app.module.ts (1201 bytes)

创建类

它用于在 Angular 中创建新类。定义如下:

ng g class <class-name>

如果要创建 customer 类,请键入以下命令:

ng g class Customer

使用此命令后,您将看到以下响应:

CREATE src/app/customer.spec.ts (162 bytes)
CREATE src/app/customer.ts (26 bytes)

创建管道

管道用于过滤数据。它用于在 Angular 中创建自定义管道。定义如下:

ng g pipe <pipe-name>

如果要在管道中创建自定义数字计数,请键入以下命令:

ng g pipe DigitCount

使用此命令后,您将看到以下响应:

CREATE src/app/digit-count.pipe.spec.ts (204 bytes)
CREATE src/app/digit-count.pipe.ts (213 bytes)
UPDATE src/app/app.module.ts (1274 bytes)

创建指令

它用于在 Angular 中创建新指令。定义如下:

ng g directive <directive-name>

如果要创建 UnderlineText 指令,请键入以下命令:

ng g directive UnderlineText

使用此命令后,您将看到以下响应:

CREATE src/app/underline-text.directive.spec.ts (253 bytes)
CREATE src/app/underline-text.directive.ts (155 bytes)
UPDATE src/app/app.module.ts (1371 bytes)

创建模块

它用于在 Angular 中创建新模块。定义如下:

ng g module <module-name>

如果要创建用户信息模块,请键入以下命令:

ng g module Userinfo

使用此命令后,您将看到以下响应:

CREATE src/app/userinfo/userinfo.module.ts (194 bytes)

创建接口

它用于在 Angular 中创建接口。如下所示:

ng g interface <interface-name>

如果要创建 customer 类,请键入以下命令:

ng g interface CustomerData

使用此命令后,您将看到以下响应:

CREATE src/app/customer-data.ts (34 bytes)

创建 Web Worker

它用于在 Angular 中创建新的 Web Worker。如下所示:

ng g webWorker <webWorker-name>

如果要创建 customer 类,请键入以下命令:

ng g webWorker CustomerWebWorker

使用此命令后,您将看到以下响应:

CREATE tsconfig.worker.json (212 bytes)
CREATE src/app/customer-web-worker.worker.ts (157 bytes)
UPDATE tsconfig.app.json (296 bytes)
UPDATE angular.json (3863 bytes)

创建服务

它用于在 Angular 中创建服务。如下所示:

ng g service <service-name>

如果要创建 customer 类,请键入以下命令:

ng g service CustomerService

使用此命令后,您将看到以下响应:

CREATE src/app/customer-service.service.spec.ts (379 bytes)
CREATE src/app/customer-service.service.ts (144 bytes)

创建枚举

它用于在 Angular 中创建枚举。如下所示:

ng g enum <enum-name>

如果要创建 customer 类,请键入以下命令:

ng g enum CustomerRecords

使用此命令后,您将看到以下响应:

CREATE src/app/customer-records.enum.ts (32 bytes)

添加命令

它用于向您的项目添加对外部库的支持。由以下命令指定:

ng add [name]

构建命令

它用于编译或构建您的 Angular 应用程序。定义如下:

ng build

使用此命令后,您将看到以下响应:

Generating ES5 bundles for differential loading...
ES5 bundle generation complete.

配置命令

它用于在工作区的 angular.json 文件中检索或设置 Angular 配置值。定义如下:

ng config

使用此命令后,您将看到以下响应:

{
   "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
   "version": 1,
   "newProjectRoot": "projects",
   "projects": {
      "MyApp": {
         "projectType": "application",
         "schematics": {
            "@schematics/angular:component": {
               "style": "scss"
            }
         },
 .............................
 .............................

文档命令

它用于在浏览器中打开官方 Angular 文档 (angular.io),并搜索给定的关键字。

ng doc <keyword>

例如,如果您使用 component 作为 ng g component 进行搜索,它将打开文档。

端到端 (e2e) 命令

它用于构建和提供 Angular 应用程序,然后使用 Protractor 运行端到端测试。如下所示:

ng e2e <project> [options]

帮助命令

它列出可用的命令及其简短描述。如下所示:

ng help

服务命令

它用于构建和提供您的应用程序,并在文件更改时重新构建。如下所示:

ng serve

测试命令

在项目中运行单元测试。如下所示:

ng test

更新命令

更新您的应用程序及其依赖项。如下所示:

ng update

版本命令

显示 Angular CLI 版本。如下所示:

ng version

Angular - 测试

测试是应用程序开发生命周期中非常重要的一个阶段。它确保应用程序质量。它需要仔细的计划和执行。

单元测试

单元测试是测试应用程序最简单的方法。它基于确保代码片段或类方法的正确性。但是,它没有反映真实的运行环境,因此是查找错误的最低选择。

通常,Angular 使用 Jasmine 和 Karma 配置。要执行此操作,首先需要在您的项目中使用以下命令进行配置:

ng test

现在,您将看到以下响应:

Unit Test

现在,Chrome 浏览器也会打开并在“Jasmine HTML Reporter”中显示测试输出。它看起来类似于这样:

HTML Reporter

端到端 (E2E) 测试

单元测试是一个小型、简单且快速的流程,而 E2E 测试阶段则涉及多个组件并协同工作,涵盖了应用程序中的流程。要执行 e2e 测试,请键入以下命令:

ng e2e

您将看到以下响应:

E2E

Angular - Ivy 编译器

Ivy 编译器是 Angular 团队发布的最新 Angular 应用程序编译器。目前,Angular 使用View Engine编译器来编译 Angular 应用程序。

通常,Angular 编译器有两种编译应用程序的选项。

即时 (JIT) 编译器

即时 (JIT)编译中,编译器将与应用程序捆绑在一起并发送到浏览器。Angular 应用程序将在浏览器中编译并在应用程序执行之前运行。

即使JIT提供了一些高级功能,JIT也会减慢编译速度,并且应用程序包的大小也会是AOT编译器生成的两倍,因为它也包含编译器。

提前 (AOT) 编译器

AOT编译中,编译器将发出优化的代码,这些代码可以在浏览器中直接运行,无需任何额外步骤。它将减少捆绑包的大小,并减少应用程序的编译时间和启动时间。

Ivy 编译器的优势

Ivy 编译器是 Angular 的优化和高级编译器。就 Angular 而言,它还没有完成,即使它在这个阶段可以使用。Angular 团队建议开发人员在 Angular 中使用它。

Ivy 编译器的主要优势如下:

  • 优化的代码。
  • 更快的构建时间。
  • 减少的捆绑包大小。
  • 更好的性能。

如何使用 Ivy?

可以通过更改如下所示的项目设置在 Angular 应用程序中使用Ivy 编译器

打开 angular.json 并将项目的 aot 选项(projects -> -> architect -> build -> configurations -> production)设置为 true。

{
   "projects": {
      "my-existing-project": {
         "architect": {

            "build": {
               "options": {
                  ...
                  "aot": true,
               }
            }
         }
      }
   }
}

打开tsconfig.app.json并在angularCompilerOptions下将enableIvy设置为 true。

{ 
   ... 
   "angularCompilerOptions": { 
      "enableIvy": true 
}

编译并运行应用程序,并从Ivy 编译器中获益。

Angular - 使用 Bazel 构建

Bazel是一个高级构建和测试工具。它支持许多适用于大型项目的功能。

Bazel的一些功能如下

  • 支持多种语言。
  • 支持多个平台。
  • 支持多个存储库。
  • 支持高级构建语言。
  • 快速可靠。

Angular 支持使用 Bazel 构建应用程序。让我们看看如何使用 Bazel 编译 Angular 应用程序。

首先,安装@angular/bazel包。

npm install -g @angular/bazel 

对于现有应用程序,请按如下所示添加@angular/bazel

ng add @angular/bazel

对于新应用程序,请使用以下命令

ng new --collection=@angular/bazel 

要使用 Bazel 构建应用程序,请使用以下命令

ng build --leaveBazelFilesOnDisk

这里,

leaveBazelFilesOnDisk选项将保留构建过程中创建的 Bazel 文件,我们可以使用这些文件直接使用 Bazel 构建应用程序。

要直接使用 Bazel 构建应用程序,请安装@bazel/bazelisk,然后使用bazelisk构建命令。

npm install -g @bazel/bazelisk 
bazelisk build

Angular - 向后兼容性

Angular 框架提供了与先前版本的最大兼容性。如果 Angular 团队在一个版本中弃用了一个功能,它将等待另外三个版本才能完全删除该功能。Angular 团队每六个月发布一个主要版本。每个版本将有一个为期六个月的活跃维护期,然后是为期一年的长期支持 (LTS) 期。在这些 18 个月中,Angular 不会引入重大更改。如果 Angular 版本在版本 5 中弃用了一个功能,那么它可能会在版本 8 或后续版本中将其删除。

Angular 维护所有版本的文档和指南。例如,可以在 https://v7.angular.io 上查看 Angular 版本 7 的文档。Angular 还通过 https://update.angular.io/ 网站提供详细的升级路径。

要更新从先前版本编写的 Angular 应用程序,请在项目目录中使用以下命令

ng update @angular/cli@8 @angular/core@8

让我们看看 Angular 中引入的一些重要更改。

  • HttpModule模块及其关联的Http服务已被删除。请使用HttpClientModule模块中的HttpClient服务。

  • /deep/>>>:ng-deep组件选择器已被删除。

  • Angular 的 TypeScript 默认版本为 3.4。

  • Angular 支持的 Node 版本为 v10 及更高版本。

  • @ViewChild()ContentChild()装饰器的行为已从动态更改为静态。

路由器模块中的延迟加载字符串语法已被删除,仅支持基于函数的语法。

loadChildren: './lazy/lazy.module#LazyModule' 
loadChildren: () => import('./lazy/lazy.module' 

Angular - 工作示例

在这里,我们将学习关于 Angular 的完整的分步工作示例。

让我们创建一个 Angular 应用程序来检查我们每天的支出。让我们将ExpenseManager作为我们新应用程序的选择。

创建应用程序

使用以下命令创建新应用程序。

cd /path/to/workspace
ng new expense-manager

这里,

new 是 ng CLI 应用程序的命令之一。它将用于创建新的应用程序。它会提出一些基本问题以便创建新的应用程序。让应用程序选择默认选项即可。关于如下所述的路由问题,请指定

Would you like to add Angular routing? No

回答完基本问题后,ng CLI 应用程序会在 expense-manager 文件夹下创建一个新的 Angular 应用程序。

让我们进入我们新创建的应用程序文件夹。

cd expense-manager

让我们使用以下命令启动应用程序。

ng serve

让我们启动浏览器并打开 https://:4200。浏览器将显示如下所示的应用程序 -

applications

让我们将应用程序的标题更改为更好地反映我们的应用程序。打开 src/app/app.component.ts 并将代码更改为如下所示 -

export class AppComponent { 
   title = 'Expense Manager';
}

我们的最终应用程序将在浏览器中呈现,如下所示 -

applications

添加组件

使用以下指定的 ng generate component 命令创建一个新的组件:

ng generate component expense-entry

输出

输出如下:

CREATE src/app/expense-entry/expense-entry.component.html (28 bytes)
CREATE src/app/expense-entry/expense-entry.component.spec.ts (671 bytes)
CREATE src/app/expense-entry/expense-entry.component.ts (296 bytes)
CREATE src/app/expense-entry/expense-entry.component.css (0 bytes)
UPDATE src/app/app.module.ts (431 bytes)

这里,

  • ExpenseEntryComponent 创建在 src/app/expense-entry 文件夹下。
  • 创建了组件类、模板和样式表。
  • AppModule 使用新组件更新。

向 ExpenseEntryComponent (src/app/expense-entry/expense-entry.component.ts) 组件添加 title 属性。

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

@Component({
   selector: 'app-expense-entry',
   templateUrl: './expense-entry.component.html',
   styleUrls: ['./expense-entry.component.css']
})
export class ExpenseEntryComponent implements OnInit {
   title: string;
   constructor() { }

   ngOnInit() {
      this.title = "Expense Entry"
   }
}

使用以下内容更新模板,src/app/expense-entry/expense-entry.component.html

<p>{{ title }}</p>

打开

src/app/app.component.html

并包含新创建的组件。

<h1>{{ title }}</h1>
<app-expense-entry></app-expense-entry>

这里,

app-expense-entry 是选择器值,可以用作常规 HTML 标签。

应用程序的输出如下所示:

HTML Tag

包含 Bootstrap

让我们使用 styles 选项将 Bootstrap 包含到我们的 ExpenseManager 应用程序中,并将默认模板更改为使用 Bootstrap 组件。

打开命令提示符并转到 ExpenseManager 应用程序。

cd /go/to/expense-manager

使用以下命令安装 bootstrapJQuery

npm install --save bootstrap@4.5.0 jquery@3.5.1

这里,

我们安装了 JQuery,因为 Bootstrap 广泛使用 jquery 来实现高级组件。

选项 angular.json 并设置 bootstrap 和 jquery 库路径。

{ 
   "projects": { 
      "expense-manager": { 
         "architect": { 
            "build": {
               "builder":"@angular-devkit/build-angular:browser", "options": { 
                  "outputPath": "dist/expense-manager", 
                  "index": "src/index.html", 
                  "main": "src/main.ts", 
                  "polyfills": "src/polyfills.ts", 
                  "tsConfig": "tsconfig.app.json", 
                  "aot": false, 
                  "assets": [ 
                     "src/favicon.ico", 
                     "src/assets" 
                  ], 
                  "styles": [ 
                     "./node_modules/bootstrap/dist/css/bootstrap.css", "src/styles.css" 
                  ], 
                  "scripts": [ 
                     "./node_modules/jquery/dist/jquery.js", "./node_modules/bootstrap/dist/js/bootstrap.js" 
                  ] 
               }, 
            }, 
         } 
   }}, 
   "defaultProject": "expense-manager" 
}

这里,

scripts 选项用于包含 JavaScript 库。通过 scripts 注册的 JavaScript 将可用于应用程序中的所有 Angular 组件。

打开 app.component.html 并将内容更改为如下所示

<!-- Navigation --> 
<nav class="navbar navbar-expand-lg navbar-dark bg-dark static-top"> 
   <div class="container"> 
      <a class="navbar-brand" href="#">{{ title }}</a> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarResponsive" aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation"> 
         <span class="navbar-toggler-icon">
         </span> 
      </button> 
      <div class="collapse navbar-collapse" id="navbarResponsive"> 
         <ul class="navbar-nav ml-auto"> 
            <li class="nav-item active"> 
            <a class="nav-link" href="#">Home
               <span class="sr-only">(current)
               </span>
            </a> 
            </li> 
            <li class="nav-item"> 
            <a class="nav-link" href="#">Report</a> 
            </li> 
            <li class="nav-item"> 
            <a class="nav-link" href="#">Add Expense</a> 
            </li> 
            <li class="nav-item"> 
            <a class="nav-link" href="#">About</a> 
            </li> 
         </ul> 
      </div> 
   </div> 
</nav> 
<app-expense-entry></app-expense-entry>

这里,

使用 Bootstrap 导航和容器。

打开 src/app/expense-entry/expense-entry.component.html 并放置以下内容。

<!-- Page Content --> 
<div class="container"> 
   <div class="row"> 
      <div class="col-lg-12 text-center" style="padding-top: 20px;"> 
         <div class="container" style="padding-left: 0px; padding-right: 0px;"> 
            <div class="row"> 
            <div class="col-sm" style="text-align: left;"> {{ title }} 
            </div> 
            <div class="col-sm" style="text-align: right;"> 
               <button type="button" class="btn btn-primary">Edit</button> 
            </div> 
            </div> 
         </div> 
         <div class="container box" style="margin-top: 10px;"> 
         <div class="row"> 
         <div class="col-2" style="text-align: right;">  
            <strong><em>Item:</em></strong> 
         </div> 
         <div class="col" style="text-align: left;"> 
            Pizza 
         </div>
         </div> 
         <div class="row"> 
         <div class="col-2" style="text-align: right;">
            <strong><em>Amount:</em></strong> 
         </div> 
         <div class="col" style="text-align: left;"> 
            20 
         </div> 
         </div> 
         <div class="row"> 
         <div class="col-2" style="text-align: right;"> 
            <strong><em>Category:</em></strong> 
         </div> 
         <div class="col" style="text-align: left;"> 
            Food 
         </div> 
         </div> 
         <div class="row"> 
         <div class="col-2" style="text-align: right;"> 
            <strong><em>Location:</em></strong>
         </div> 
         <div class="col" style="text-align: left;"> 
            Zomato 
         </div> 
         </div> 
         <div class="row"> 
         <div class="col-2" style="text-align: right;"> 
            <strong><em>Spend On:</em></strong> 
         </div> 
         <div class="col" style="text-align: left;"> 
            June 20, 2020 
         </div> 
         </div> 
      </div> 
   </div> 
</div> 
</div>

重新启动应用程序。

应用程序的输出如下所示:

Restart Tag

我们将在下一章改进应用程序以处理动态费用条目。

添加一个接口

创建 ExpenseEntry 接口 (src/app/expense-entry.ts) 并添加 id、amount、category、Location、spendOn 和 createdOn。

export interface ExpenseEntry {
   id: number;
   item: string;
   amount: number;
   category: string;
   location: string;
   spendOn: Date;
   createdOn: Date;
}

ExpenseEntry 导入到 ExpenseEntryComponent 中。

import { ExpenseEntry } from '../expense-entry';

创建一个 ExpenseEntry 对象,expenseEntry 如下所示:

export class ExpenseEntryComponent implements OnInit {
   title: string;
   expenseEntry: ExpenseEntry;
   constructor() { }

   ngOnInit() {
      this.title = "Expense Entry";

      this.expenseEntry = {

         id: 1,
         item: "Pizza",
         amount: 21,
         category: "Food",
         location: "Zomato",
         spendOn: new Date(2020, 6, 1, 10, 10, 10),
         createdOn: new Date(2020, 6, 1, 10, 10, 10),
      };
   }
}

使用 expenseEntry 对象,src/app/expense-entry/expense-entry.component.html 更新组件模板,如下所示:

<!-- Page Content -->
<div class="container">
   <div class="row">
      <div class="col-lg-12 text-center" style="padding-top: 20px;">
         <div class="container" style="padding-left: 0px; padding-right: 0px;">
            <div class="row">
               <div class="col-sm" style="text-align: left;">
                  {{ title }}
               </div>
               <div class="col-sm" style="text-align: right;">
                  <button type="button" class="btn btn-primary">Edit</button>
               </div>
            </div>
         </div>
         <div class="container box" style="margin-top: 10px;">
            <div class="row">
               <div class="col-2" style="text-align: right;">
                  <strong><em>Item:</em></strong>
               </div>
               <div class="col" style="text-align: left;">
                  {{ expenseEntry.item }} 
               </div>
            </div>
            <div class="row">
               <div class="col-2" style="text-align: right;">
                  <strong><em>Amount:</em></strong>
               </div>
               <div class="col" style="text-align: left;">
                  {{ expenseEntry.amount }}   
               </div>
            </div>
            <div class="row">
               <div class="col-2" style="text-align: right;">
                  <strong><em>Category:</em></strong>
               </div>
               <div class="col" style="text-align: left;">

                  {{ expenseEntry.category }} 
               </div>
            </div>
            <div class="row">
               <div class="col-2" style="text-align: right;">
                  <strong><em>Location:</em></strong>
               </div>
               <div class="col" style="text-align: left;">
                  {{ expenseEntry.location }} 
               </div>
            </div>
            <div class="row">
               <div class="col-2" style="text-align: right;">
                  <strong><em>Spend On:</em></strong>
               </div>
               <div class="col" style="text-align: left;">
                  {{ expenseEntry.spendOn }}  
               </div>
            </div>
         </div>
      </div>
   </div>
</div>

应用程序的输出如下所示:

Interface

使用指令

让我们在ExpenseManager应用程序中添加一个新组件来列出费用条目。

打开命令提示符并转到项目根文件夹。

cd /go/to/expense-manager

启动应用程序。

ng serve

使用以下命令创建一个新组件ExpenseEntryListComponent

ng generate component ExpenseEntryList

输出

输出如下:

CREATE src/app/expense-entry-list/expense-entry-list.component.html (33 bytes) 
CREATE src/app/expense-entry-list/expense-entry-list.component.spec.ts (700 bytes) 
CREATE src/app/expense-entry-list/expense-entry-list.component.ts (315 bytes) 
CREATE src/app/expense-entry-list/expense-entry-list.component.css (0 bytes) 
UPDATE src/app/app.module.ts (548 bytes)

在这里,该命令创建了ExpenseEntryList组件并在AppModule中更新了必要的代码。

ExpenseEntry导入到ExpenseEntryListComponent组件(src/app/expense-entry-list/expense-entry-list.component)

import { ExpenseEntry } from '../expense-entry';

添加一个方法getExpenseEntries(),在ExpenseEntryListComponent (src/app/expense-entry-list/expense-entry-list.component)中返回费用条目的列表(模拟项目)。

getExpenseEntries() : ExpenseEntry[] { 
   let mockExpenseEntries : ExpenseEntry[] = [ 
      { id: 1, 
         item: "Pizza", 
         amount: Math.floor((Math.random() * 10) + 1), 
         category: "Food", 
         location: "Mcdonald", 
         spendOn: new Date(2020, 4, Math.floor((Math.random() * 30) + 1), 10, 10, 10), 
         createdOn: new Date(2020, 4, Math.floor((Math.random() * 30) + 1), 10, 10, 10) }, 
      { id: 1, 
         item: "Pizza", 
         amount: Math.floor((Math.random() * 10) + 1), 
         category: "Food", 
         location: "KFC", 
         spendOn: new Date(2020, 4, Math.floor((Math.random() * 30) + 1), 10, 10, 10), 
         createdOn: new Date(2020, 4, Math.floor((Math.random() * 30) + 1), 10, 10, 10) }, 
      { id: 1,
         item: "Pizza",
         amount: Math.floor((Math.random() * 10) + 1), 
         category: "Food", 
         location: "Mcdonald", 
         spendOn: new Date(2020, 4, Math.floor((Math.random() * 30) + 1), 10, 10, 10), 
         createdOn: new Date(2020, 4, Math.floor((Math.random() * 30) + 1), 10, 10, 10) }, 
      { id: 1, 
         item: "Pizza", 
         amount: Math.floor((Math.random() * 10) + 1), 
         category: "Food", 
         location: "KFC", 
         spendOn: new Date(2020, 4, Math.floor((Math.random() * 30) + 1), 10, 10, 10), 
         createdOn: new Date(2020, 4, Math.floor((Math.random() * 30) + 1), 10, 10, 10) }, 
      { id: 1, 
         item: "Pizza", 
         amount: Math.floor((Math.random() * 10) + 1), 
         category: "Food", 
         location: "KFC", 
         spendOn: new Date(2020, 4, Math.floor((Math.random() * 30) + 1), 10, 10, 10), 
         createdOn: new Date(2020, 4, Math.floor((Math.random() * 30) + 1), 10, 10, 10) 
      }, 
   ]; 
   return mockExpenseEntries; 
}

声明一个局部变量expenseEntries,并加载如下所示的费用条目的模拟列表:

title: string; 
expenseEntries: ExpenseEntry[]; 
constructor() { } 
ngOnInit() { 
   this.title = "Expense Entry List"; 
   this.expenseEntries = this.getExpenseEntries(); 
}

打开模板文件(src/app/expense-entry-list/expense-entry-list.component.html)并在表格中显示模拟条目。

<!-- Page Content -->
<div class="container"> 
   <div class="row"> 
      <div class="col-lg-12 text-center" style="padding-top: 20px;">
         <div class="container" style="padding-left: 0px; padding-right: 0px;"> 
            <div class="row"> 
               <div class="col-sm" style="text-align: left;"> 
                  {{ title }} 
               </div> 
               <div class="col-sm" style="text-align: right;"> 
                  <button type="button" class="btn btn-primary">Edit</button> 
               </div> 
            </div> 
         </div> 
         <div class="container box" style="margin-top: 10px;"> 
            <table class="table table-striped"> 
               <thead> 
                  <tr> 
                     <th>Item</th> 
                     <th>Amount</th> 
                     <th>Category</th> 
                     <th>Location</th> 
                     <th>Spent On</th> 
                  </tr> 
               </thead> 
               <tbody> 
                  <tr *ngFor="let entry of expenseEntries"> 
                     <th scope="row">{{ entry.item }}</th> 
                     <th>{{ entry.amount }}</th> 
                     <td>{{ entry.category }}</td> 
                     <td>{{ entry.location }}</td> 
                     <td>{{ entry.spendOn | date: 'short' }}</td> 
                  </tr> 
               </tbody> 
            </table> 
         </div> 
      </div> 
   </div> 
</div>

这里,

  • 使用bootstrap表格。tabletable-striped将根据Boostrap样式标准对表格进行样式设置。

  • 使用ngFor遍历expenseEntries并生成表格行。

打开AppComponent模板src/app/app.component.html并包含ExpenseEntryListComponent并删除ExpenseEntryComponent,如下所示:

... 
<app-expense-entry-list></app-expense-entry-list>

最后,应用程序的输出如下所示。

AppComponent

使用管道

让我们在我们的 ExpenseManager 应用程序中使用管道

打开ExpenseEntryListComponent的模板src/app/expense-entry-list/expense-entry-list.component.html并在entry.spendOn中包含管道,如下所示:

<td>{{ entry.spendOn | date: 'short' }}</td>

在这里,我们使用了日期管道以短格式显示花费的日期。

最后,应用程序的输出如下所示:

Pipes

添加调试服务

运行以下命令以生成一个 Angular 服务,DebugService

ng g service debug

这将创建两个 TypeScript 文件(调试服务及其测试),如下所示:

CREATE src/app/debug.service.spec.ts (328 bytes) 
CREATE src/app/debug.service.ts (134 bytes)

让我们分析DebugService 服务的内容。

import { Injectable } from '@angular/core'; @Injectable({ 
   providedIn: 'root' 
}) 
export class DebugService { 
   constructor() { } 
}

这里,

  • @Injectable 装饰器附加到 DebugService 类,这使得 DebugService 能够在应用程序的 Angular 组件中使用。

  • providerIn 选项及其值 root 使 DebugService 能够在应用程序的所有组件中使用。

让我们添加一个方法 Info,它将消息打印到浏览器控制台。

info(message : String) : void { 
   console.log(message); 
}

让我们在ExpenseEntryListComponent 中初始化服务并使用它来打印消息。

import { Component, OnInit } from '@angular/core'; import { ExpenseEntry } from '../expense-entry'; import { DebugService } from '../debug.service'; @Component({ 
   selector: 'app-expense-entry-list', 
   templateUrl: './expense-entry-list.component.html', styleUrls: ['./expense-entry-list.component.css'] 
}) 
export class ExpenseEntryListComponent implements OnInit { 
   title: string; 
   expenseEntries: ExpenseEntry[]; 
   constructor(private debugService: DebugService) { } 
   ngOnInit() { 
      this.debugService.info("Expense Entry List 
      component initialized"); 
      this.title = "Expense Entry List"; 
      this.expenseEntries = this.getExpenseEntries(); 
   } 
   // other coding 
}

这里,

  • DebugService 使用构造函数参数初始化。设置类型为 DebugService 的参数 (debugService) 将触发依赖注入以创建一个新的 DebugService 对象并将其设置为 ExpenseEntryListComponent 组件。

  • 在 ngOnInit 方法中调用 DebugService 的 info 方法将在浏览器控制台中打印消息。

可以使用开发者工具查看结果,它看起来类似于以下所示:

Debug service

让我们扩展应用程序以了解服务的范围。

让我们使用以下命令创建一个DebugComponent

ng generate component debug
CREATE src/app/debug/debug.component.html (20 bytes) CREATE src/app/debug/debug.component.spec.ts (621 bytes) 
CREATE src/app/debug/debug.component.ts (265 bytes) CREATE src/app/debug/debug.component.css (0 bytes) UPDATE src/app/app.module.ts (392 bytes)

让我们删除根模块中的 DebugService。

// src/app/debug.service.ts
import { Injectable } from '@angular/core'; @Injectable() 
export class DebugService { 
   constructor() { 
   }
   info(message : String) : void {     
      console.log(message); 
   } 
}

在 ExpenseEntryListComponent 组件下注册 DebugService。

// src/app/expense-entry-list/expense-entry-list.component.ts @Component({ 
   selector: 'app-expense-entry-list', 
   templateUrl: './expense-entry-list.component.html', 
   styleUrls: ['./expense-entry-list.component.css'] 
   providers: [DebugService] 
})

这里,我们使用了 providers 元数据(ElementInjector) 来注册服务。

打开DebugComponent (src/app/debug/debug.component.ts) 并导入DebugService,并在组件的构造函数中设置一个实例。

import { Component, OnInit } from '@angular/core'; import { DebugService } from '../debug.service'; 
@Component({ 
   selector: 'app-debug', 
   templateUrl: './debug.component.html', 
   styleUrls: ['./debug.component.css'] 
}) 
export class DebugComponent implements OnInit { 
   constructor(private debugService: DebugService) { } 
   ngOnInit() { 
      this.debugService.info("Debug component gets service from Parent"); 
   } 
}

这里,我们没有注册DebugService。因此,如果用作父组件,则 DebugService 将不可用。当在父组件内部使用时,如果父组件可以访问该服务,则该服务可能从父组件获得。

打开ExpenseEntryListComponent 模板 (src/app/expense-entry-list/expense-entry-list.component.html) 并包含一个内容部分,如下所示

// existing content 
<app-debug></app-debug>
<ng-content></ng-content>

这里,我们包含了一个内容部分和 DebugComponent 部分。

让我们在 AppComponent 模板中将调试组件作为内容包含在ExpenseEntryListComponent 组件中。打开AppComponent 模板并将app-expense-entry-list 更改为以下内容:

// navigation code
<app-expense-entry-list>
<app-debug></app-debug>
</app-expense-entry-list>

这里,我们包含了DebugComponent 作为内容。

让我们检查应用程序,它将在页面末尾显示DebugService 模板,如下所示:

Debug

此外,我们可以在控制台中看到调试组件输出的两条调试信息。这表明调试组件从其父组件获取了服务。

让我们更改服务在**ExpenseEntryListComponent**中注入的方式以及它如何影响服务的作用域。将providers注入器更改为viewProviders注入。**viewProviders**不会将服务注入到内容子元素中,因此它应该会失败。

viewProviders: [DebugService]

检查应用程序,您会看到其中一个调试组件(用作内容子元素)抛出如下所示的错误:

Application

让我们从模板中删除调试组件并恢复应用程序。

打开**ExpenseEntryListComponent**模板(src/app/expense-entry-list/expense-entry-list.component.html)并删除以下内容

 
<app-debug></app-debug>
<ng-content></ng-content>

打开**AppComponent**模板并将**app-expense-entry-list**更改为如下所示:

// navigation code
<app-expense-entry-list>
</app-expense-entry-list>

将**ExpenseEntryListComponent**中的**viewProviders**设置更改为**providers**。

providers: [DebugService]

重新运行应用程序并检查结果。

创建费用服务

让我们在我们的**ExpenseManager**应用程序中创建一个新的服务**ExpenseEntryService**来与**Expense REST API**交互。ExpenseEntryService将获取最新的费用条目、插入新的费用条目、修改现有的费用条目和删除不需要的费用条目。

打开命令提示符并转到项目根文件夹。

cd /go/to/expense-manager

启动应用程序。

ng serve

运行以下命令生成一个Angular服务**ExpenseService**。

ng generate service ExpenseEntry

这将创建两个Typescript文件(费用条目服务及其测试),如下所示:

CREATE src/app/expense-entry.service.spec.ts (364 bytes) 
CREATE src/app/expense-entry.service.ts (141 bytes)

打开**ExpenseEntryService**(src/app/expense-entry.service.ts)并从rxjs库导入**ExpenseEntry、throwError**和**catchError**,并从@angular/common/http包导入**HttpClient、HttpHeaders**和**HttpErrorResponse**。

import { Injectable } from '@angular/core'; 
import { ExpenseEntry } from './expense-entry'; import { throwError } from 'rxjs';
import { catchError } from 'rxjs/operators'; 
import { HttpClient, HttpHeaders, HttpErrorResponse } from 
'@angular/common/http';

将HttpClient服务注入到我们的服务中。

constructor(private httpClient : HttpClient) { }

创建一个变量**expenseRestUrl**来指定**Expense Rest API**端点。

private expenseRestUrl = 'https://:8000/api/expense';

创建一个变量**httpOptions**来设置Http Header选项。这将在Angular **HttpClient**服务通过Http Rest API调用期间使用。

private httpOptions = { 
   headers: new HttpHeaders( { 'Content-Type': 'application/json' }) 
};

完整代码如下:

import { Injectable } from '@angular/core';
import { ExpenseEntry } from './expense-entry';
import { Observable, throwError } from 'rxjs';
import { catchError, retry } from 'rxjs/operators';
import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http';

@Injectable({
   providedIn: 'root'
})
export class ExpenseEntryService {
      private expenseRestUrl = 'api/expense';
      private httpOptions = {
         headers: new HttpHeaders( { 'Content-Type': 'application/json' })
      };

   constructor(
      private httpClient : HttpClient) { }
}

使用 HttpClient 服务进行 Http 编程

启动 Expense REST API 应用程序,如下所示:

cd /go/to/expense-rest-api 
node .\server.js

在**ExpenseEntryService**(src/app/expense-entry.service.ts)服务中添加**getExpenseEntries()**和**httpErrorHandler()**方法。

getExpenseEntries() : Observable<ExpenseEntry[]> {
   return this.httpClient.get<ExpenseEntry[]>(this.expenseRestUrl, this.httpOptions)
   .pipe(retry(3),catchError(this.httpErrorHandler));
}

getExpenseEntry(id: number) : Observable<ExpenseEntry> {
   return this.httpClient.get<ExpenseEntry>(this.expenseRestUrl + "/" + id, this.httpOptions)
   .pipe(
      retry(3),
      catchError(this.httpErrorHandler)
   );
}

private httpErrorHandler (error: HttpErrorResponse) {
      if (error.error instanceof ErrorEvent) {
      console.error("A client side error occurs. The error message is " + error.message);
      } else {
      console.error(
            "An error happened in server. The HTTP status code is "  + error.status + " and the error returned is " + error.message);
      }

      return throwError("Error occurred. Pleas try again");
}

这里,

  • **getExpenseEntries()**使用费用端点调用**get()**方法,并配置错误处理程序。此外,它还配置**httpClient**在发生故障时最多尝试3次。最后,它将来自服务器的响应作为类型化的**(ExpenseEntry[])** Observable对象返回。

  • **getExpenseEntry**类似于getExpenseEntries(),只是它传递了ExpenseEntry对象的ID并获取ExpenseEntry Observable对象。

**ExpenseEntryService**的完整编码如下:

import { Injectable } from '@angular/core';
import { ExpenseEntry } from './expense-entry';

import { Observable, throwError } from 'rxjs';
import { catchError, retry } from 'rxjs/operators';
import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http';

@Injectable({

   providedIn: 'root'
})
export class ExpenseEntryService {
   private expenseRestUrl = 'https://:8000/api/expense';
   private httpOptions = {
      headers: new HttpHeaders( { 'Content-Type': 'application/json' })
   };

   constructor(private httpClient : HttpClient) { } 

   getExpenseEntries() : Observable {
      return this.httpClient.get(this.expenseRestUrl, this.httpOptions)
      .pipe(
         retry(3),
         catchError(this.httpErrorHandler)
      );
   }

   getExpenseEntry(id: number) : Observable {
      return this.httpClient.get(this.expenseRestUrl + "/" + id, this.httpOptions)
      .pipe(
         retry(3),
         catchError(this.httpErrorHandler)
      );
   }

   private httpErrorHandler (error: HttpErrorResponse) {
      if (error.error instanceof ErrorEvent) {
         console.error("A client side error occurs. The error message is " + error.message);
      } else {
         console.error(
            "An error happened in server. The HTTP status code is "  + error.status + " and the error returned is " + error.message);
      }

      return throwError("Error occurred. Pleas try again");
   }
}

打开**ExpenseEntryListComponent**(src-entry-list-entry-list.component.ts)并通过构造函数注入**ExpenseEntryService**,如下所示

constructor(private debugService: DebugService, private restService : 
ExpenseEntryService ) { }

更改**getExpenseEntries()**函数。调用**ExpenseEntryService**中的getExpenseEntries()方法,而不是返回模拟项。

getExpenseItems() {  
   this.restService.getExpenseEntries() 
      .subscribe( data =− this.expenseEntries = data ); 
}

**ExpenseEntryListComponent**的完整编码如下:

import { Component, OnInit } from '@angular/core';
import { ExpenseEntry } from '../expense-entry';
import { DebugService } from '../debug.service';
import { ExpenseEntryService } from '../expense-entry.service';

@Component({
   selector: 'app-expense-entry-list',
   templateUrl: './expense-entry-list.component.html',
   styleUrls: ['./expense-entry-list.component.css'],
   providers: [DebugService]
})
export class ExpenseEntryListComponent implements OnInit {
   title: string;
   expenseEntries: ExpenseEntry[];
   constructor(private debugService: DebugService, private restService : ExpenseEntryService ) { }

   ngOnInit() {
      this.debugService.info("Expense Entry List component initialized");
      this.title = "Expense Entry List";

      this.getExpenseItems();
   }

   getExpenseItems() {
      this.restService.getExpenseEntries()
      .subscribe( data => this.expenseEntries = data );
   }
}

最后,检查应用程序,您将看到以下响应。

failed request

添加费用功能

让我们在我们的**ExpenseEntryService**中添加一个新方法**addExpenseEntry()**来添加新的费用条目,如下所示:

addExpenseEntry(expenseEntry: ExpenseEntry): Observable<ExpenseEntry> {
   return this.httpClient.post<ExpenseEntry>(this.expenseRestUrl, expenseEntry, this.httpOptions)
   .pipe(
      retry(3),
      catchError(this.httpErrorHandler)
   );
}

更新费用条目功能

让我们在我们的**ExpenseEntryService**中添加一个新方法**updateExpenseEntry()**来更新现有的费用条目,如下所示

updateExpenseEntry(expenseEntry: ExpenseEntry): Observable<ExpenseEntry> {
   return this.httpClient.put<ExpenseEntry>(this.expenseRestUrl + "/" + expenseEntry.id, expenseEntry, this.httpOptions)
   .pipe(
      retry(3),
      catchError(this.httpErrorHandler)
   );
}

删除费用条目功能

让我们在我们的**ExpenseEntryService**中添加一个新方法**deleteExpenseEntry()**来删除现有的费用条目,如下所示:

deleteExpenseEntry(expenseEntry: ExpenseEntry | number) : Observable<ExpenseEntry> {
   const id = typeof expenseEntry == 'number' ? expenseEntry : expenseEntry.id
   const url = `${this.expenseRestUrl}/${id}`;

   return this.httpClient.delete<ExpenseEntry>(url, this.httpOptions)
   .pipe(
      retry(3),
      catchError(this.httpErrorHandler)
   );
}

添加路由

使用以下命令生成路由模块,如果之前未执行此操作。

ng generate module app-routing --module app --flat

输出

输出如下所示:

CREATE src/app/app-routing.module.ts (196 bytes) 
UPDATE src/app/app.module.ts (785 bytes)

这里,

CLI 生成AppRoutingModule,然后在AppModule中配置它。

更新AppRoutingModule (src/app/app.module.ts),如下所示:

import { NgModule } from '@angular/core'; 
import { Routes, RouterModule } from '@angular/router'; import { ExpenseEntryComponent } from './expense-entry/expense-entry.component'; 
import { ExpenseEntryListComponent } from './expense-entry-list/expense-entry-list.component'; 
const routes: Routes = [ 
   { path: 'expenses', component: ExpenseEntryListComponent }, 
   { path: 'expenses/detail/:id', component: ExpenseEntryComponent }, 
   { path: '', redirectTo: 'expenses', pathMatch: 'full' }]; 
@NgModule({ 
   imports: [RouterModule.forRoot(routes)], 
   exports: [RouterModule] }) 
export class AppRoutingModule { }

在这里,我们为费用列表和费用详情组件添加了路由。

更新AppComponent模板(src/app/app.component.html)以包含router-outletrouterLink

<!-- Navigation --> 
<nav class="navbar navbar-expand-lg navbar-dark bg-dark static-top"> 
<div class="container"> 
   <a class="navbar-brand" href="#">{{ title }}</a> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarResponsive" aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation"> 
      <span class="navbar-toggler-icon"></span> 
   </button> 
   <div class="collapse navbar-collapse" id="navbarResponsive"> 
      <ul class="navbar-nav ml-auto"> 
         <li class="nav-item active"> 
            <a class="nav-link" href="#">Home 
               <span class="sr-only" routerLink="/">(current)</span> 
            </a> 
         </li> 
         <li class="nav-item"> 
            <a class="nav-link" routerLink="/expenses">Report</a> 
         </li> 
         <li class="nav-item"> 
            <a class="nav-link" href="#">Add Expense</a> 
         </li> 
         <li class="nav-item"> 
            <a class="nav-link" href="#">About</a> 
         </li> 
      </ul> 
   </div> 
</div> 
</nav> 
<router-outlet></router-outlet>

打开ExpenseEntryListComponent模板 (src/app/expense-entry-list/expense-entry-list.component.html) 并为每个费用条目包含视图选项。

<table class="table table-striped"> 
   <thead> 
      <tr> 
         <th>Item</th>
         <th>Amount</th> 
         <th>Category</th> 
         <th>Location</th> 
         <th>Spent On</th> 
         <th>View</th> 
      </tr> 
   </thead> 
   <tbody> 
      <tr *ngFor="let entry of expenseEntries"> 
         <th scope="row">{{ entry.item }}</th> 
         <th>{{ entry.amount }}</th> 
         <td>{{ entry.category }}</td> 
         <td>{{ entry.location }}</td> 
         <td>{{ entry.spendOn | date: 'medium' }}</td> 
         <td><a routerLink="../expenses/detail/{{ entry.id }}">View</a></td> 
      </tr> 
   </tbody> 
</table>

在这里,我们更新了费用列表表格并添加了一列以显示视图选项。

打开ExpenseEntryComponent (src/app/expense-entry/expense-entry.component.ts)并添加功能以获取当前选定的费用条目。这可以通过首先通过paramMap获取 id,然后使用ExpenseEntryService中的getExpenseEntry()方法来完成。

this.expenseEntry$ = this.route.paramMap.pipe(  
   switchMap(params => { 
      this.selectedId = Number(params.get('id')); 
      return 
this.restService.getExpenseEntry(this.selectedId); })); 
   this.expenseEntry$.subscribe( (data) => this.expenseEntry = data );

更新 ExpenseEntryComponent 并添加转到费用列表的选项。

goToList() { 
   this.router.navigate(['/expenses']); 
}

ExpenseEntryComponent 的完整代码如下:

import { Component, OnInit } from '@angular/core'; import { ExpenseEntry } from '../expense-entry'; import { ExpenseEntryService } from '../expense-entry.service'; 
import { Router, ActivatedRoute } from '@angular/router'; 
import { Observable } from 'rxjs';
import { switchMap } from 'rxjs/operators'; 
@Component({ 
   selector: 'app-expense-entry', 
   templateUrl: './expense-entry.component.html', 
   styleUrls: ['./expense-entry.component.css'] 
}) 
export class ExpenseEntryComponent implements OnInit { 
   title: string; 
   expenseEntry$ : Observable<ExpenseEntry>; 
   expenseEntry: ExpenseEntry = {} as ExpenseEntry; 
   selectedId: number; 
   constructor(private restService : ExpenseEntryService, private router : Router, private route : 
ActivatedRoute ) { } 
   ngOnInit() { 
      this.title = "Expense Entry"; 
   this.expenseEntry$ = this.route.paramMap.pipe( 
      switchMap(params => { 
         this.selectedId = Number(params.get('id')); 
         return 
this.restService.getExpenseEntry(this.selectedId); })); 
   this.expenseEntry$.subscribe( (data) => this.expenseEntry = data ); 
   } 
   goToList() { 
      this.router.navigate(['/expenses']); 
   } 
}

打开ExpenseEntryComponent (src/app/expense-entry/expense-entry.component.html)模板并添加一个新按钮以导航回费用列表页面。

<div class="col-sm" style="text-align: right;"> 
   <button type="button" class="btn btn-primary" (click)="goToList()">Go to List</button>  
   <button type="button" class="btn btn-primary">Edit</button> 
</div>

在这里,我们在Edit按钮之前添加了Go to List按钮。

使用以下命令运行应用程序:

ng serve

应用程序的最终输出如下所示:

Nested routing

单击第一个条目的视图选项将导航到详细信息页面并显示选定的费用条目,如下所示:

Nested routing

启用登录和注销功能

创建一个新的服务 AuthService 来对用户进行身份验证。

ng generate service auth
CREATE src/app/auth.service.spec.ts (323 bytes)
CREATE src/app/auth.service.ts (133 bytes)

打开AuthService并包含以下代码。

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

import { Observable, of } from 'rxjs';
import { tap, delay } from 'rxjs/operators';

@Injectable({
   providedIn: 'root'
})
export class AuthService {

   isUserLoggedIn: boolean = false;

   login(userName: string, password: string): Observable {
      console.log(userName);
      console.log(password);
      this.isUserLoggedIn = userName == 'admin' && password == 'admin';
      localStorage.setItem('isUserLoggedIn', this.isUserLoggedIn ? "true" : "false"); 

   return of(this.isUserLoggedIn).pipe(
      delay(1000),
      tap(val => { 
         console.log("Is User Authentication is successful: " + val); 
      })
   );
   }

   logout(): void {
   this.isUserLoggedIn = false;
      localStorage.removeItem('isUserLoggedIn'); 
   }

   constructor() { }
}

这里,

  • 我们编写了两个方法,loginlogout

  • login 方法的目的是验证用户,如果用户成功验证,则将其信息存储在localStorage中,然后返回 true。

  • 身份验证验证是用户名和密码应为admin

  • 我们没有使用任何后端。相反,我们使用 Observables 模拟了 1 秒的延迟。

  • logout 方法的目的是使用户无效并删除存储在localStorage中的信息。

使用以下命令创建login组件:

ng generate component login
CREATE src/app/login/login.component.html (20 bytes)
CREATE src/app/login/login.component.spec.ts (621 bytes)
CREATE src/app/login/login.component.ts (265 bytes)
CREATE src/app/login/login.component.css (0 bytes)
UPDATE src/app/app.module.ts (1207 bytes)

打开LoginComponent并包含以下代码:

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

import { FormGroup, FormControl } from '@angular/forms';
import { AuthService } from '../auth.service';
import { Router } from '@angular/router';

@Component({
   selector: 'app-login',
   templateUrl: './login.component.html',
   styleUrls: ['./login.component.css']
})
export class LoginComponent implements OnInit {

   userName: string;
   password: string;
   formData: FormGroup;

   constructor(private authService : AuthService, private router : Router) { }

   ngOnInit() {
      this.formData = new FormGroup({
         userName: new FormControl("admin"),
         password: new FormControl("admin"),
      });
   }

   onClickSubmit(data: any) {
      this.userName = data.userName;
      this.password = data.password;

      console.log("Login page: " + this.userName);
      console.log("Login page: " + this.password);

      this.authService.login(this.userName, this.password)
         .subscribe( data => { 
            console.log("Is Login Success: " + data); 
      
           if(data) this.router.navigate(['/expenses']); 
      });
   }
}

这里,

  • 使用响应式表单。

  • 导入 AuthService 和 Router 并将其配置在构造函数中。

  • 创建了 FormGroup 的实例,并包含了 FormControl 的两个实例,一个用于用户名,另一个用于密码。

  • 创建了一个 onClickSubmit 来使用 authService 验证用户,如果成功,则导航到费用列表。

打开LoginComponent模板并包含以下模板代码。

<!-- Page Content -->
<div class="container">
   <div class="row">
      <div class="col-lg-12 text-center" style="padding-top: 20px;">
         <div class="container box" style="margin-top: 10px; padding-left: 0px; padding-right: 0px;">
            <div class="row">
               <div class="col-12" style="text-align: center;">
                                    <form [formGroup]="formData" (ngSubmit)="onClickSubmit(formData.value)" 
                                          class="form-signin">
                                    <h2 class="form-signin-heading">Please sign in</h2>
                                    <label for="inputEmail" class="sr-only">Email address</label>
                                    <input type="text" id="username" class="form-control" 
                                          formControlName="userName" placeholder="Username" required autofocus>
                                    <label for="inputPassword" class="sr-only">Password</label>
                                    <input type="password" id="inputPassword" class="form-control" 
                                          formControlName="password" placeholder="Password" required>
                                    <button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
                                    </form>
               </div>
            </div>
         </div>
      </div>
   </div>
</div>

这里,

创建了一个响应式表单并设计了一个登录表单。

onClickSubmit方法附加到表单提交操作。

打开LoginComponent样式并包含以下 CSS 代码。

.form-signin {
   max-width: 330px;

   padding: 15px;
   margin: 0 auto;
}

input {
   margin-bottom: 20px;
}

这里,添加了一些样式来设计登录表单。

使用以下命令创建一个注销组件:

ng generate component logout
CREATE src/app/logout/logout.component.html (21 bytes)
CREATE src/app/logout/logout.component.spec.ts (628 bytes)
CREATE src/app/logout/logout.component.ts (269 bytes)
CREATE src/app/logout/logout.component.css (0 bytes)
UPDATE src/app/app.module.ts (1368 bytes)

打开LogoutComponent并包含以下代码。

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

import { AuthService } from '../auth.service';
import { Router } from '@angular/router';

@Component({
   selector: 'app-logout',
   templateUrl: './logout.component.html',
   styleUrls: ['./logout.component.css']
})
export class LogoutComponent implements OnInit {

   constructor(private authService : AuthService, private router: Router) { }

   ngOnInit() {
      this.authService.logout();
      this.router.navigate(['/']);
   }

}

这里,

  • 使用AuthService的注销方法。
  • 用户注销后,页面将重定向到主页(/)。

使用以下命令创建一个守卫:

ng generate guard expense
CREATE src/app/expense.guard.spec.ts (364 bytes)
CREATE src/app/expense.guard.ts (459 bytes)

打开ExpenseGuard并包含以下代码:

import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router, UrlTree } from '@angular/router';
import { Observable } from 'rxjs';

import { AuthService } from './auth.service';

@Injectable({
   providedIn: 'root'
})
export class ExpenseGuard implements CanActivate {

   constructor(private authService: AuthService, private router: Router) {}

   canActivate(
   next: ActivatedRouteSnapshot,
   state: RouterStateSnapshot): boolean | UrlTree {
      let url: string = state.url;

          return this.checkLogin(url);
      }

      checkLogin(url: string): true | UrlTree {
         console.log("Url: " + url)
         let val: string = localStorage.getItem('isUserLoggedIn');

         if(val != null && val == "true"){
            if(url == "/login")
               this.router.parseUrl('/expenses');
            else 
               return true;
         } else {
            return this.router.parseUrl('/login');
         }
      }
}

这里,

  • checkLogin将检查localStorage中是否包含用户信息,如果存在,则返回true。
  • 如果用户已登录并转到登录页面,则会将用户重定向到支出页面
  • 如果用户未登录,则用户将被重定向到登录页面。

打开AppRoutingModule (src/app/app-routing.module.ts)并更新以下代码:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { ExpenseEntryComponent } from './expense-entry/expense-entry.component';
import { ExpenseEntryListComponent } from './expense-entry-list/expense-entry-list.component';
import { LoginComponent } from './login/login.component';
import { LogoutComponent } from './logout/logout.component';

import { ExpenseGuard } from './expense.guard';

const routes: Routes = [
   { path: 'login', component: LoginComponent },
   { path: 'logout', component: LogoutComponent },
   { path: 'expenses', component: ExpenseEntryListComponent, canActivate: [ExpenseGuard]},
   { path: 'expenses/detail/:id', component: ExpenseEntryComponent, canActivate: [ExpenseGuard]},
   { path: '', redirectTo: 'expenses', pathMatch: 'full' }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

这里,

  • 导入了LoginComponent和LogoutComponent。
  • 导入了ExpenseGuard。
  • 创建了两个新的路由,login和logout,分别用于访问LoginComponent和LogoutComponent。
  • 为ExpenseEntryComponent和ExpenseEntryListComponent添加新的选项canActivate。

打开AppComponent模板并添加两个登录和注销链接。

<div class="collapse navbar-collapse" id="navbarResponsive">
   <ul class="navbar-nav ml-auto">
      <li class="nav-item active">
         <a class="nav-link" href="#">Home
            <span class="sr-only" routerLink="/">(current)</span>

         </a>
      </li>
      <li class="nav-item">
         <a class="nav-link" routerLink="/expenses">Report</a>
      </li>
      <li class="nav-item">
         <a class="nav-link" href="#">Add Expense</a>
      </li>
      <li class="nav-item">

         <a class="nav-link" href="#">About</a>
      </li>
      <li class="nav-item">
                  <div *ngIf="isUserLoggedIn; else isLogOut">
                        <a class="nav-link" routerLink="/logout">Logout</a>
                  </div>

                  <ng-template #isLogOut>
                              <a class="nav-link" routerLink="/login">Login</a>
                  </ng-template>
      </li>
   </ul>
</div>

打开AppComponent并更新以下代码:

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

import { AuthService } from './auth.service';

@Component({
   selector: 'app-root',
   templateUrl: './app.component.html',
   styleUrls: ['./app.component.css']
})
export class AppComponent {

   title = 'Expense Manager';
   isUserLoggedIn = false;

   constructor(private authService: AuthService) {}

   ngOnInit() {
      let storeData = localStorage.getItem("isUserLoggedIn");
      console.log("StoreData: " + storeData);

      if( storeData != null && storeData == "true")
         this.isUserLoggedIn = true;
      else


         this.isUserLoggedIn = false;
   }
}

在这里,我们添加了识别用户状态的逻辑,以便我们可以显示登录/注销功能。

打开AppModule (src/app/app.module.ts)并配置ReactiveFormsModule

import { ReactiveFormsModule } from '@angular/forms'; 
imports: [ 
   ReactiveFormsModule 
]

现在,运行应用程序,应用程序将打开登录页面。

ReactiveFormsModule

输入admin和admin作为用户名和密码,然后点击提交。应用程序处理登录并将用户重定向到支出列表页面,如下所示:

FormsModule

最后,您可以点击注销并退出应用程序。

添加/编辑/删除费用

添加新的组件,EditEntryComponent,使用以下命令添加新的费用条目并编辑现有的费用条目

ng generate component EditEntry
CREATE src/app/edit-entry/edit-entry.component.html (25 bytes)
CREATE src/app/edit-entry/edit-entry.component.spec.ts (650 bytes)
CREATE src/app/edit-entry/edit-entry.component.ts (284 bytes)
CREATE src/app/edit-entry/edit-entry.component.css (0 bytes)
UPDATE src/app/app.module.ts (1146 bytes)

使用以下代码更新 EditEntryComponent

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

import { FormGroup, FormControl, Validators } from '@angular/forms';

import { ExpenseEntry } from '../expense-entry';
import { ExpenseEntryService } from '../expense-entry.service';

import { Router, ActivatedRoute } from '@angular/router';



@Component({
   selector: 'app-edit-entry',
   templateUrl: './edit-entry.component.html',
   styleUrls: ['./edit-entry.component.css']
})
export class EditEntryComponent implements OnInit {
   id: number;
   item: string;
   amount: number;
   category: string;
   location: string;
   spendOn: Date;

   formData: FormGroup;
   selectedId: number;
   expenseEntry: ExpenseEntry;

   constructor(private expenseEntryService : ExpenseEntryService, private router: Router, private route: ActivatedRoute) { }

   ngOnInit() {
      this.formData = new FormGroup({
         id: new FormControl(),
         item: new FormControl('', [Validators.required]),
         amount: new FormControl('', [Validators.required]),
         category: new FormControl(),
         location: new FormControl(),
         spendOn: new FormControl()
      });

      this.selectedId = Number(this.route.snapshot.paramMap.get('id'));

      if(this.selectedId != null && this.selectedId != 0) {
         this.expenseEntryService.getExpenseEntry(this.selectedId)
            .subscribe( (data) => 
               {
                  this.expenseEntry = data;
                  this.formData.controls['id'].setValue(this.expenseEntry.id);
                  this.formData.controls['item'].setValue(this.expenseEntry.item);
                  this.formData.controls['amount'].setValue(this.expenseEntry.amount);
                  this.formData.controls['category'].setValue(this.expenseEntry.category);
                  this.formData.controls['location'].setValue(this.expenseEntry.location);


                  this.formData.controls['spendOn'].setValue(this.expenseEntry.spendOn);
               })
      }


   }

   get itemValue() {
   return this.formData.get('item');
   }

   get amountValue() {
   return this.formData.get('amount');
   }

    onClickSubmit(data: any) {
   console.log('onClickSubmit fired');
   this.id = data.id;
   this.item = data.item;
   this.amount = data.amount;
   this.category = data.category;
   this.location = data.location;
   this.spendOn = data.spendOn;

   let expenseEntry : ExpenseEntry = {
      id: this.id,
       item: this.item,
       amount: this.amount,
       category: this.category,
       location: this.location,
       spendOn: this.spendOn,
       createdOn: new Date(2020, 5, 20)
   }
   console.log(expenseEntry);

      if(expenseEntry.id == null || expenseEntry.id == 0) {
         console.log('add fn fired');
      this.expenseEntryService.addExpenseEntry(expenseEntry)
         .subscribe( data => { console.log(data); this.router.navigate(['/expenses']); });
   } else {
         console.log('edit fn fired');
      this.expenseEntryService.updateExpenseEntry(expenseEntry)
         .subscribe( data => { console.log(data); this.router.navigate(['/expenses']); });
   }
    }
}

这里,

  • 使用 FormControlFormGroup 类以及适当的验证规则在 ngOnInit 方法中创建了一个表单,formData

  • ngOnInit 方法中加载要编辑的费用条目。

  • 创建了两个方法,itemValueamountValue,分别获取用户为验证目的输入的项目和金额值。

  • 创建了方法 onClickSubmit 以保存(添加/更新)费用条目。

  • 使用 Expense 服务添加和更新费用条目。

使用费用表单更新 EditEntryComponent 模板,如下所示:

<!-- Page Content -->
<div class="container">
   <div class="row">
   <div class="col-lg-12 text-center" style="padding-top: 20px;">
       <div class="container" style="padding-left: 0px; padding-right: 0px;">
       </div>
       <div class="container box" style="margin-top: 10px;">
<form [formGroup]="formData" (ngSubmit)="onClickSubmit(formData.value)" class="form" novalidate> 
  <div class="form-group">
    <label for="item">Item</label>
    <input type="hidden" class="form-control" id="id" formControlName="id">
    <input type="text" class="form-control" id="item" formControlName="item">
    <div
   *ngIf="!itemValue?.valid && (itemValue?.dirty ||itemValue?.touched)">
   <div [hidden]="!itemValue.errors.required">
      Item is required
   </div>
   </div>
  </div>
  <div class="form-group">
    <label for="amount">Amount</label>
    <input type="text" class="form-control" id="amount" formControlName="amount">
    <div
   *ngIf="!amountValue?.valid && (amountValue?.dirty ||amountValue?.touched)">
   <div [hidden]="!amountValue.errors.required">
      Amount is required
   </div>
   </div>
  </div>
  <div class="form-group">
    <label for="category">Category</label>
    <select class="form-control" id="category" formControlName="category">
      <option>Food</option>
      <option>Vegetables</option>
      <option>Fruit</option>
      <option>Electronic Item</option>

      <option>Bill</option>
    </select>
  </div>
  <div class="form-group">
    <label for="location">location</label>
    <input type="text" class="form-control" id="location" formControlName="location">
  </div>
  <div class="form-group">
    <label for="spendOn">spendOn</label>
    <input type="text" class="form-control" id="spendOn" formControlName="spendOn">
  </div>
<button class="btn btn-lg btn-primary btn-block" type="submit" [disabled]="!formData.valid">Submit</button>
</form>
       </div>
   </div>
    </div>
</div>

这里,

  • 创建了一个表单并将其绑定到类中创建的表单 formData

  • 验证 itemamount 为必填值。

  • 验证成功后调用 onClickSubmit 函数。

打开 EditEntryComponent 样式表并更新以下代码:

.form {
   max-width: 330px;
   padding: 15px;
   margin: 0 auto;
}

.form label {
   text-align: left;
   width: 100%;
}

input {
   margin-bottom: 20px;
}

在这里,我们对费用条目表单进行了样式设置。

使用以下命令添加 AboutComponent

ng generate component About
CREATE src/app/about/about.component.html (20 bytes)

CREATE src/app/about/about.component.spec.ts (621 bytes)
CREATE src/app/about/about.component.ts (265 bytes)
CREATE src/app/about/about.component.css (0 bytes)
UPDATE src/app/app.module.ts (1120 bytes)

打开 AboutComponent 并添加标题,如下所示:

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

@Component({
   selector: 'app-about',
   templateUrl: './about.component.html',
   styleUrls: ['./about.component.css']
})
export class AboutComponent implements OnInit {
   title = "About";
   constructor() { }

   ngOnInit() {
   }

}

打开 AboutComponent 模板并更新内容,如下所示:

<!-- Page Content -->
<div class="container">
   <div class="row">
   <div class="col-lg-12 text-center" style="padding-top: 20px;">
       <div class="container" style="padding-left: 0px; padding-right: 0px;">
      <div class="row">
          <div class="col-sm" style="text-align: left;">
         <h1>{{ title }}</h1>
          </div>
      </div>
       </div>
       <div class="container box" style="margin-top: 10px;">
      <div class="row">
          <div class="col" style="text-align: left;">
         <p>Expense management Application</p>
          </div>
      </div>
       </div>
   </div>
    </div>
</div>

添加添加和编辑费用条目的路由,如下所示

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

import { Routes, RouterModule } from '@angular/router';
import { ExpenseEntryComponent } from './expense-entry/expense-entry.component';
import { ExpenseEntryListComponent } from './expense-entry-list/expense-entry-list.component';
import { LoginComponent } from './login/login.component';
import { LogoutComponent } from './logout/logout.component';
import { EditEntryComponent } from './edit-entry/edit-entry.component';
import { AboutComponent } from './about/about.component';

import { ExpenseGuard } from './expense.guard';

const routes: Routes = [
   { path: 'about', component: AboutComponent },
   { path: 'login', component: LoginComponent },
   { path: 'logout', component: LogoutComponent },
   { path: 'expenses', component: ExpenseEntryListComponent, canActivate: [ExpenseGuard]},
   { path: 'expenses/detail/:id', component: ExpenseEntryComponent, canActivate: [ExpenseGuard]},
   { path: 'expenses/add', component: EditEntryComponent, canActivate: [ExpenseGuard]},
   { path: 'expenses/edit/:id', component: EditEntryComponent, canActivate: [ExpenseGuard]},
   { path: '', redirectTo: 'expenses', pathMatch: 'full' }
];

@NgModule({
      imports: [RouterModule.forRoot(routes)],
      exports: [RouterModule]
})
export class AppRoutingModule { }

在这里,我们添加了 about、add expenseedit expense 路由。

ExpenseEntryListComponent 模板中添加 EditDelete 链接。

<table class="table table-striped">
   <thead>
         <tr>
         <th>Item</th>
         <th>Amount</th>
         <th>Category</th>
         <th>Location</th>
         <th>Spent On</th>
         <th>View</th>
               <th>Edit</th>
               <th>Delete</th>
         </tr>
   </thead>
   <tbody>
      <tr *ngFor="let entry of expenseEntries">

      <th scope="row">{{ entry.item }}</th>
      <th>{{ entry.amount }}</th>
      <td>{{ entry.category }}</td>
      <td>{{ entry.location }}</td>
      <td>{{ entry.spendOn | date: 'medium' }}</td>
      <td><a routerLink="../expenses/detail/{{ entry.id }}">View</a></td>
      <td><a routerLink="../expenses/edit/{{ entry.id }}">Edit</a></td>
      <td><a href="#" (click)="deleteExpenseEntry($event, entry.id)">Delete</a></td>
      </tr>
   </tbody>
</table>

在这里,我们添加了两列。一列用于显示编辑链接,另一列用于显示删除链接。

如以下所示更新 ExpenseEntryListComponent 中的 deleteExpenseEntry 方法

deleteExpenseEntry(evt, id) {
   evt.preventDefault();
   if(confirm("Are you sure to delete the entry?")) {
      this.restService.deleteExpenseEntry(id)
         .subscribe( data => console.log(data) );

      this.getExpenseItems();
   }
}

在这里,我们要求确认删除,如果用户确认,则调用费用服务中的 deleteExpenseEntry 方法删除选定的费用项目。

ExpenseEntryListComponent 模板顶部的 Edit 链接更改为 Add 链接,如下所示:

<div class="col-sm" style="text-align: right;">
   <button class="btn btn-primary" routerLink="/expenses/add">ADD</button> 
   <!-- <button type="button" class="btn btn-primary">Edit</button> -->
</div>

ExpenseEntryComponent 模板中添加 Edit 链接。

<div class="col-sm" style="text-align: right;">
   <button type="button" class="btn btn-primary" (click)="goToList()">Go to List</button>
    <button type="button" class="btn btn-primary" (click)="goToEdit()">Edit</button>
</div>

打开 ExpenseEntryComponent 并添加 goToEdit() 方法,如下所示:

goToEdit() {      
   this.router.navigate(['/expenses/edit', this.selectedId]); 
}

更新 AppComponent 模板中的导航链接。

<!-- Navigation -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark static-top">
   <div class="container">
      <a class="navbar-brand" href="#">{{ title }}</a>
      <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarResponsive" aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation">
         <span class="navbar-toggler-icon"></span>
      </button>
      <div class="collapse navbar-collapse" id="navbarResponsive">
         <ul class="navbar-nav ml-auto">
            <li class="nav-item active">
               <a class="nav-link" href="#">Home
                  <span class="sr-only" routerLink="/">(current)</span>
               </a>
            </li>
            <li class="nav-item">
               <a class="nav-link" routerLink="/expenses/add">Add Expense</a>
            </li>
            <li class="nav-item">
               <a class="nav-link" routerLink="/about">About</a>
            </li>
            <li class="nav-item">
                        <div *ngIf="isUserLoggedIn; else isLogOut">
                              <a class="nav-link" routerLink="/logout">Logout</a>
                        </div>

                        <ng-template #isLogOut>
                                    <a class="nav-link" routerLink="/login">Login</a>
                        </ng-template>
            </li>
         </ul>
      </div>
   </div>
</nav>

<router-outlet></router-outlet>

在这里,我们更新了 add expense 链接和 about 链接。

运行应用程序,输出将类似于以下所示:

expense

尝试使用费用列表页面中的 Add 链接添加新的费用。输出将类似于以下所示

Add

填写表单,如下所示:

Submit

如果数据填写不正确,验证代码将发出警报,如下所示:

alert

点击 Submit。它将触发提交事件,数据将保存到后端并重定向到列表页面,如下所示:

backend

尝试使用费用列表页面中的编辑链接编辑现有费用。输出将类似于以下所示:

existing

点击 Submit。它将触发提交事件,数据将保存到后端并重定向到列表页面。

要删除项目,请点击删除链接。它将确认删除,如下所示:

trigger

最后,我们实现了应用程序管理费用所需的所有功能。

Angular - 新功能?

Angular 社区一直在不断更新其版本。本章说明了 Angular 9 版本更新。

安装 Angular 9

如果您想使用 Angular 9,首先需要使用以下命令设置 Angular 9 CLI

npm install -g @angular/cli@^9.0.0 

执行此命令后,您可以使用以下命令检查版本

ng version 

Angular 9 更新

让我们简要了解 Angular 9 更新。

Ivy 编译器

Ivy 编译器成为 Angular 9 中的默认编译器。这使得应用程序将更快且更高效。而 Angular Ivy 是可选的。我们必须在 tsconfig.json 文件中启用它。

Ivy 编译器支持以下功能

  • 执行更快的测试 - TestBed 实现有助于更有效地进行测试。

  • 改进的 CSS 类和样式 - Ivy 样式易于合并并设计为可预测的。

  • 改进的类型检查 - 此功能有助于在开发过程的早期发现错误。

  • 增强的调试 - Ivy 附带更多工具以启用更好的调试功能。这将有助于显示有用的堆栈跟踪,以便我们可以轻松跳转到指令。

  • 提前编译器 - 这是编译器性能的重要改进之一。AOT 构建速度非常快。 改进了国际化 - i18n 替换有助于比以前的版本快十倍以上地构建。

可靠的 ng update

ng 更新非常可靠。它包含清晰的进度更新并运行所有迁移。这可以使用以下命令完成

ng update --create-commits

这里,

–create-commits 标志用于在每次迁移后提交代码。

改进的依赖注入

@Injectable 服务有助于在您的应用程序中添加注入器。providedIn 元数据提供了一个新选项 platform 以确保对象可以被所有应用程序使用和共享。它定义如下

@Injectable({
   providedIn: 'platform'
}) 
class MyService {...} 

TypeScript 3.8

Angular 9 旨在支持 3.8 版本。TypeScript 3.8 为以下功能提供了支持

  • 仅类型导入和导出。
  • ECMAScript 私有字段。
  • 顶级 await。
  • JSDoc 属性修饰符。
  • export * as ns 语法。

Angular 9.0.0-next.5

Angular 9.0.0-next.5 构建的 main.js 文件大小较小,与以前的 Angular 版本相比,性能更好。

IDE 增强

Angular 9 提供了改进的 IDE 支持。TextMate 语法允许在内联和外部模板中进行语法突出显示。

结论

Angular 是一个灵活、不断改进、持续更新和可靠的框架。Angular 大大简化了 SPA 开发的过程。通过在每个版本中提供新的功能,如 Angular Universal、Progressive Web App、Web workers、Bazel build、Ivy Compiler 等,Angular 将拥有长久的生命周期并获得前端开发人员的全面支持。

广告
© . All rights reserved.