- Angular 8 教程
- Angular 8 - 首页
- Angular 8 - 简介
- Angular 8 - 安装
- 创建第一个应用程序
- Angular 8 - 架构
- Angular 组件和模板
- Angular 8 - 数据绑定
- Angular 8 - 指令
- Angular 8 - 管道
- Angular 8 - 响应式编程
- 服务和依赖注入
- Angular 8 - Http 客户端编程
- Angular 8 - Angular Material
- 路由和导航
- Angular 8 - 动画
- Angular 8 - 表单
- Angular 8 - 表单验证
- 身份验证和授权
- Angular 8 - Web Workers
- Service Workers 和 PWA
- Angular 8 - 服务器端渲染
- Angular 8 - 国际化 (i18n)
- Angular 8 - 可访问性
- Angular 8 - CLI 命令
- Angular 8 - 测试
- Angular 8 - Ivy 编译器
- Angular 8 - 使用 Bazel 构建
- Angular 8 - 向后兼容性
- Angular 8 - 实例
- Angular 9 - 新特性?
- Angular 8 有用资源
- Angular 8 快速指南
- Angular 8 - 有用资源
- Angular 8 - 讨论
Angular 8 快速指南
Angular 8 - 简介
Angular 8 是一个基于 TypeScript 的全栈 Web 框架,用于构建 Web 和移动应用程序。其主要优势之一是 Angular 8 支持适应任何屏幕分辨率的 Web 应用程序。Angular 应用程序与手机、平板电脑、笔记本电脑或台式机完全兼容。Angular 8 为 Web 开发人员提供了一个优秀的用户界面库,其中包含可重用的 UI 组件。
此功能帮助我们创建单页面应用程序 (SPA)。SPA 是响应迅速且快速的应用程序。例如,如果您在单页面中有一个按钮,并且单击该按钮,则操作会在当前页面中动态执行,而无需从服务器加载新页面。Angular 8 基于 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 8 新特性
Angular 8 带来了以下新的吸引人的特性:
Bazel 支持 - 如果您的应用程序使用多个模块和库,Bazel 并发构建有助于加快应用程序加载速度。
延迟加载 - Angular 8 将AppRoutingModule 分割成更小的包并在 DOM 中加载数据。
差异加载 - 创建应用程序时,Angular CLI 会生成模块,这些模块将自动加载,然后浏览器将呈现数据。
Web worker - 它在后台运行,不会影响页面的性能。
改进的 CLI 工作流程 - Angular 8 CLI 命令 ng-build、ng-test 和 ng-run 扩展到第三方库。
路由器向后兼容性 - Angular 路由器向后兼容性功能有助于为大型项目创建路径,以便用户可以轻松地借助懒加载添加代码。
选择加入的使用情况共享 - 用户可以选择加入共享 Angular CLI 使用数据。
应用案例
下面列出了一些使用 Angular 框架的流行网站:
Weather.com - 它是领先的天气预报网站之一。
Youtube - 它是Google 托管的视频共享网站。
Netflix - 它是一家技术和媒体服务提供商。
PayPal - 它是一个在线支付系统。
Angular 8 - 安装
本章介绍如何在您的机器上安装Angular 8。在进行安装之前,让我们先验证先决条件。
先决条件
众所周知,Angular 是用TypeScript 编写的。我们需要Node 和npm 将文件编译成JavaScript,然后才能部署我们的应用程序。为此,必须在您的系统中安装Node.js。希望您已经在您的机器上安装了Node.js。
我们可以使用以下命令检查它:
node --version
您将看到 node 的版本,如下所示:
v14.2.0
如果未安装Node,您可以访问以下链接下载并安装:
https://node.org.cn/en/download/。Angular 8 安装
Angular 8 CLI 安装基于非常简单的步骤。安装时间不超过五分钟。
npm 用于安装Angular 8 CLI。安装Node.js 后,npm 也已安装。如果要验证它,请键入以下命令
npm -v
您将看到以下版本:
6.14.4
让我们使用npm 安装Angular 8 CLI,如下所示:
npm install -g @angular/cli@^8.0.0
要验证Angular 8 是否已正确安装在您的机器上,请键入以下命令:
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 8 - 创建第一个应用程序
让我们创建一个简单的 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
这里:
new 是ng CLI 应用程序的命令之一。它将用于创建新应用程序。为了创建新应用程序,它会提出一些基本问题。让应用程序选择默认选项就足够了。关于如下所述的路由问题,请指定否。我们将在路由章节中学习如何创建路由。
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.ico 和assets 是应用程序的图标和应用程序的根资产文件夹。
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://127.0.0.1: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://127.0.0.1:4200/ ** i wdm: Compiled successfully.
这里,serve 是用于使用本地开发 Web 服务器编译和运行 Angular 应用程序的子命令。ng server 将启动一个开发 Web 服务器,并在端口 4200 下提供服务。
让我们启动浏览器并打开 https://127.0.0.1:4200。浏览器将显示如下所示的应用程序:
让我们更改应用程序的标题以更好地反映我们的应用程序。打开src/app/app.component.ts 并按如下所示更改代码:
export class AppComponent { title = 'Expense Manager'; }
我们的最终应用程序将在浏览器中呈现,如下所示:
我们将在接下来的章节中更改应用程序并学习如何编写 Angular 应用程序。
Angular 8 - 架构
让我们在本节中了解 Angular 框架的架构。
Angular 框架基于四个核心概念,它们是:
- 组件。
- 带有数据绑定和指令的模板。
- 模块。
- 服务和依赖注入。
组件
Angular 框架架构的核心是Angular 组件。Angular 组件是每个 Angular 应用程序的构建块。每个 Angular 应用程序都由一个或多个Angular 组件组成。它基本上是一个普通的 JavaScript/TypeScript 类,以及一个 HTML 模板和一个关联的名称。
HTML 模板可以访问其对应的 JavaScript/TypeScript 类中的数据。组件的 HTML 模板可以使用其选择器值(名称)包含其他组件。Angular 组件可能具有与其关联的可选 CSS 样式,并且 HTML 模板也可以访问 CSS 样式。
让我们分析一下我们的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 }}
这里,title 是 AppComponent 中的一个属性,它使用插值绑定到模板。
指令
用于包含逻辑以及启用复杂 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' />
这里,ngIf 和 showToolTip(只是一个例子)是指令。只有当 canShow 为 true 时,ngIf 才创建段落 DOM 元素。类似地,showToolTip 是属性指令,它向段落元素添加工具提示功能。
当用户鼠标悬停在段落上时,将显示一个工具提示。工具提示的内容来自其对应组件的 tips 属性。
模块
Angular 模块基本上是相关特性/功能的集合。Angular 模块将多个组件和服务分组在一个上下文中。
例如,动画相关功能可以分组到单个模块中,Angular 已经为动画相关功能提供了一个模块,即 BrowserAnimationModule 模块。
一个 Angular 应用程序可以包含任意数量的模块,但只有一个模块可以设置为根模块,它将引导应用程序,然后根据需要调用其他模块。一个模块也可以配置为访问其他模块的功能。简而言之,任何模块中的组件都可以访问任何其他模块中的组件和服务。
下图描述了模块及其组件之间的交互。
让我们检查一下我们的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 应用程序的完整流程。
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 8 - 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 已使用新组件更新。
向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 标签。
最后,应用程序的输出如下所示:
在学习更多关于模板的过程中,我们将更新组件的内容。
模板
Angular 组件的组成部分是模板。它用于生成 HTML 内容。模板是具有附加功能的普通 HTML。
附加模板
模板可以使用@component装饰器的元数据附加到 Angular 组件。Angular 提供两种元数据将模板附加到组件。
templateUrl
我们已经知道如何使用 templateUrl。它期望模板文件的相对路径。例如,AppComponent 将其模板设置为 app.component.html。
templateUrl: './app.component.html',
template
template 允许将 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) 来自应用程序配置。
组件配置
Component 装饰器提供两个选项,styles 和 styleUrls,用于向其模板提供 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
使用以下命令安装bootstrap和JQuery库:
npm install --save [email protected] [email protected]
这里:
我们安装了 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>
重启应用程序。
应用程序的输出如下:
我们将在下一章中改进应用程序以处理动态费用条目。
Angular 8 - 数据绑定
数据绑定处理如何将数据从组件绑定到 HTML DOM 元素(模板)。我们可以轻松地与应用程序交互,而无需担心如何插入数据。我们可以通过单向和双向绑定两种不同的方式建立连接。
在进入本主题之前,让我们在 Angular 8 中创建一个组件。
打开命令提示符并使用以下命令创建新的 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 8"; }
转到 test.component.html 文件并添加以下代码:
<h1>{{appName}}</h1>
通过替换现有内容,将 test 组件添加到app.component.html文件中,如下所示:
<app-test></app-test>
最后,使用以下命令启动应用程序(如果尚未启动):
ng serve
您可以在屏幕上看到以下输出:
事件绑定
事件是诸如鼠标单击、双击、悬停或任何键盘和鼠标操作之类的操作。如果用户与应用程序交互并执行某些操作,则会引发事件。它由括号()或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
现在,运行您的应用程序,您将看到以下响应:
在这里,当用户单击按钮时,事件绑定会理解按钮单击操作并调用组件 showData() 方法,因此我们可以得出结论,它是单向绑定。
属性绑定
属性绑定用于将数据从组件的属性绑定到 DOM 元素。它由[]表示。
让我们通过一个简单的例子来理解。
在 **test.component.ts** 文件中添加以下代码。
export class TestComponent { userName:string = "Peter"; }
在视图 test.component.html 中添加以下更改。
<input type="text" [value]="userName">
这里:
**userName** 属性绑定到 DOM 元素 **<input>** 标签的一个属性。
最后,使用以下命令启动应用程序(如果尚未启动):
ng serve
属性绑定
**属性绑定** 用于将组件中的数据绑定到 HTML 属性。语法如下:
<HTMLTag [attr.ATTR]="Component data">
例如:
<td [attr.colspan]="columnSpan"> ... </td>
让我们通过一个简单的例子来理解。
在 **test.component.ts** 文件中添加以下代码。
export class TestComponent { userName:string = "Peter"; }
在视图 **test.component.html** 中添加以下更改。
<input type="text" [value]="userName">
这里:
userName 属性绑定到 DOM 元素 <input> 标签的一个属性。
最后,使用以下命令启动应用程序(如果尚未启动):
ng serve
类绑定
**类绑定** 用于将组件中的数据绑定到 HTML 类属性。语法如下:
<HTMLTag [class]="component variable holding class name">
**类绑定** 提供额外的功能。如果组件数据是布尔值,则只有在值为 true 时才会绑定类。可以通过字符串(“foo bar”)和字符串数组来提供多个类。还有许多其他选项可用。
例如:
<p [class]="myClasses">
让我们通过一个简单的例子来理解。
在 test.component.ts 文件中添加以下代码。
export class TestComponent { myCSSClass = "red"; applyCSSClass = false; }
在视图 **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
最终输出将如下所示:
样式绑定
**样式绑定** 用于将组件中的数据绑定到 HTML style 属性。语法如下:
<HTMLTag [style.STYLE]="component data">
例如:
<p [style.color]="myParaColor"> ... </p>
让我们通过一个简单的例子来理解。
在 **test.component.ts** 文件中添加以下代码。
myColor = 'brown';
在视图 **test.component.html** 中添加以下更改。
<p [style.color]="myColor">Text color is styled using style binding</p>
最后,使用以下命令启动应用程序(如果尚未启动):
ng serve
最终输出将如下所示:
双向数据绑定
**双向数据绑定** 是一种双向交互,数据双向流动(从组件到视图,以及从视图到组件)。简单的例子是 **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
现在,运行您的应用程序,您将看到以下响应:
现在,尝试将输入值更改为 **Jack**。当您键入时,输入框下方的文本会发生更改,最终输出将如下所示:
我们将在接下来的章节中学习更多关于表单控件的内容。
工作示例
让我们在 **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>
Angular 8 - 指令
Angular 8 指令是与您的应用程序交互的 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 提供许多内置指令,我们将在后面的章节中学习。
基于组件的指令
组件可以用作指令。每个组件都有 **Input** 和 **Output** 选项,用于在组件及其父 HTML 元素之间传递数据。
<component-selector-name [input-reference]="input-value"> ... </component-selector-name>
例如:
<list-item [items]="fruits"> ... </list-item>
这里,**list-item** 是一个组件,**items** 是输入选项。我们将在后面的章节中学习如何创建组件以及高级用法。
在进入本主题之前,让我们在 Angular 8 中创建一个示例应用程序 **(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 8 中使用 DOM 模型:
- 我们可以轻松地使用 DOM 元素导航文档结构。
- 我们可以轻松地添加 html 元素。
- 我们可以轻松地更新元素及其内容。
结构指令
结构指令通过添加或删除元素来更改 **DOM** 的结构。它用 * 符号表示,并具有三个预定义指令 **NgIf、NgFor** 和 **NgSwitch**。让我们简要了解一下。
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
现在,运行您的应用程序,您将看到以下响应:
如果您将条件设置为 **ngIf=“false”**,则内容将被隐藏。
ngIfElse 指令
**ngIfElse** 与 **ngIf** 类似,只是它提供了在失败情况下呈现内容的选项。
让我们通过一个示例了解 **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
现在,运行您的应用程序,您将看到以下响应:
这里:
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
现在,运行您的应用程序,您将看到以下响应:
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
现在,运行您的应用程序,您将看到以下响应:
这里,应用程序将打印学生姓名。现在,应用程序使用学生 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
现在,运行您的应用程序,您将看到以下响应:
这里,我们将 **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
现在,运行您的应用程序,您将看到以下响应:
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** 类中创建了两个属性 **userId** 和 **userName**。
打开 **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
现在,运行您的应用程序,您将看到以下响应:
自定义指令
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
现在,运行您的应用程序,您将看到以下响应:
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、ngFor** 或 **ngSwitch** 指令内以呈现结果。
让我们假设一个简单的代码。
<ng-template [ngIf]=true> <div><h2>ng-template works!</h2></div> </ng-template>
这里,如果 **ngIf** 条件为真,它将打印 div 元素内的信息。同样,您也可以使用 **ngFor** 和 **ngSwitch** 指令。
NgForOf 指令
**ngForOf** 也是一个结构指令,用于呈现集合中的项目。以下示例用于显示 **ngForOf** 指令在 **ng-template** 内。
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>
在这里,我们将AppComponent作为指令,并使用输入属性,在TestComponent中使用。
最后,使用以下命令启动应用程序(如果尚未启动):
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表格。table和table-striped将根据Bootstrap样式标准对表格进行样式设置。
使用ngFor循环遍历expenseEntries并生成表格行。
打开AppComponent模板src/app/app.component.html,包含ExpenseEntryListComponent并移除ExpenseEntryComponent,如下所示:
... <app-expense-entry-list></app-expense-entry-list>
最后,应用程序的输出如下所示。
Angular 8 - 管道
管道被称为过滤器。它有助于转换数据并在插值中管理数据,由{{ | }}表示。它接受数据、数组、整数和字符串作为输入,这些输入由'|'符号分隔。本章详细解释了管道。
添加参数
在你的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
链式管道
我们可以将多个管道组合在一起。当一个场景与多个必须应用于数据转换的管道相关联时,这将非常有用。
在上面的示例中,如果要以大写字母显示日期,则可以同时应用Date和Uppercase管道。
<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 8支持以下内置管道。我们将逐一简要讨论。
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 8中有很多预定义的管道可用,但有时我们可能希望以自定义格式转换值。本节解释了创建自定义管道。
使用以下命令创建一个自定义管道:
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>
在这里,我们使用了日期管道以短格式显示支出日期。
最后,应用程序的输出如下所示:
Angular 8 - 响应式编程
响应式编程是一种处理数据流和变化传播的编程范式。数据流可以是静态的或动态的。静态数据流的一个例子是数组或数据集合。它将具有初始数量,并且不会改变。动态数据流的一个例子是事件发射器。事件发射器在事件发生时发出数据。最初可能没有事件,但随着时间的推移,事件发生,它将被发出。
响应式编程使数据流能够从称为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 - 方法也向控制台写入完成消息。
我们可以跳过error和complete方法,只编写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++ ); } }
这里:
- 使用of、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://127.0.0.1:4200。
点击“点击此处”链接五次。对于每次事件,事件将被发射并转发到 **Observer**。Observer 回调函数将被调用。回调函数为每次点击递增计数器,最终结果如下所示:
Angular 8 - 服务和依赖注入
如前所述,**服务**在 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 @ 平台级别
- 使用 providers 元数据的 ElementInjector
- 使用 viewProviders 元数据的 ElementInjector
- NullInjector
ModuleInjector @ 根级别
**ModuleInjector** 仅强制在特定模块内使用服务。**@Injectable** 中可用的 **providedIn** 元数据用于指定可以使用该服务的模块。
该值应引用已注册的 Angular 模块之一(用 **@NgModule** 装饰)。**root** 是一个特殊选项,它指的是应用程序的根模块。示例代码如下:
import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root', }) export class DebugService { constructor() { } }
ModuleInjector @ 平台级别
**Platform Injector** 比 **ModuleInjector** 高一级,仅在高级和罕见情况下使用。每个 Angular 应用程序都通过执行 **platformBrowserDynamic().bootstrap** 方法(参见 **main.js**)启动,该方法负责引导 Angular 应用程序的根模块。
**platformBrowserDynamic()** 方法创建一个由 **PlatformModule** 配置的注入器。我们可以使用 **PlatformModule** 提供的 **platformBrowser()** 方法配置平台级服务。
NullInjector
**NullInjector** 比平台级 **ModuleInjector** 高一级,位于层次结构的顶层。我们无法在 **NullInjector** 中注册任何服务。当在层次结构中任何地方都找不到所需的服务时,它会解析并简单地抛出错误。
使用 providers 的 ElementInjector
**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** 装饰器。
使用 viewProviders 的 ElementInjector
**viewProviders** 与 **providers** 类似,不同之处在于它不允许在使用 **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 服务
让我们看看组件如何使用下面的流程图解析服务。
这里:
- 首先,组件尝试查找使用 **viewProviders** 元数据注册的服务。
- 如果未找到,组件尝试查找使用 **providers** 元数据注册的服务。
- 如果未找到,组件尝试查找使用 **ModuleInjector** 注册的服务。
- 如果未找到,组件尝试查找使用 **PlatformInjector** 注册的服务。
- 如果未找到,组件尝试查找使用 **NullInjector** 注册的服务,这始终会抛出错误。
注入器的层次结构以及服务解析的工作流程如下:
解析修饰符
正如我们在上一节中了解到的,服务的解析从组件开始,并在找到服务或到达 **NullInjector** 时停止。这是默认解析,可以使用 **解析修饰符**更改它。它们如下所示:
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** 服务的实例。
值提供程序
值提供程序的目的是提供值本身,而不是要求 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) {
工厂提供程序
工厂提供程序允许复杂的的创建。它将对象的创建委托给外部函数。工厂提供程序还可以选择为工厂对象设置依赖项。
{ 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 组件中使用。
**providedIn** 选项及其值 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 方法会在浏览器控制台中打印消息。
可以使用开发者工具查看结果,它看起来类似于以下所示:
让我们扩展应用程序以了解服务的范围。
让我们使用下面提到的命令创建一个 **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模板中将debug组件作为内容包含在**ExpenseEntryListComponent**组件内。打开**AppComponent**模板,并将**app-expense-entry-list**修改如下:
// navigation code <app-expense-entry-list> <app-debug></app-debug> </app-expense-entry-list>
这里,我们添加了**DebugComponent**作为内容。
让我们检查一下应用程序,它会在页面末尾显示**DebugService**模板,如下所示:
此外,我们还可以在控制台中看到来自debug组件的两个调试信息。这表明debug组件从其父组件获取服务。
让我们更改在**ExpenseEntryListComponent**中注入服务的方式以及它如何影响服务的范围。将providers注入器更改为viewProviders注入。**viewProviders**不会将服务注入到内容子级,因此它应该会失败。
viewProviders: [DebugService]
检查应用程序,您会看到一个debug组件(用作内容子级)抛出错误,如下所示:
让我们从模板中删除debug组件并恢复应用程序。
打开**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 8 - 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
使用以下命令初始化一个新的Node应用程序:
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://127.0.0.1:8000/**并按回车键。您将看到以下响应:
{ "message": "Ok" }
这证实了我们的应用程序运行良好。
将url更改为**https://127.0.0.1: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**来与**费用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**来指定**费用Rest API**端点。
private expenseRestUrl = 'https://127.0.0.1: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()方法从网页获取数据。主要参数是目标网络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()**方法有一个选项可以返回可观察对象,它也会发出类型化响应。获取类型化响应(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应用程序中编写实际代码以从**费用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://127.0.0.1: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 ); } }
最后,检查应用程序,您将看到以下响应。
HTTP POST
HTTP POST类似于HTTP GET,只是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 8 - 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
应用程序的输出如下:
在这里,应用程序清楚地显示了Angular Material按钮。
工作示例
Angular Material包提供的一些重要的UI元素。
- 表单字段
- 输入框
- 复选框
- 单选按钮
- 选择框
- 按钮
- 日期选择器
- 列表
- 卡片
- 网格列表
- 表格
- 分页器
- 标签页
- 工具栏
- 菜单
- 对话框
- Snackbar(小吃栏)
- 进度条
- 图标
- 分隔线
使用 Material 组件非常简单,我们将通过一个示例项目学习其中一个常用的 Material 组件:Material 表格。
打开命令提示符并转到项目根文件夹。
ng add @angular/material
让我们修改ExpenseEntryListComponent (src/app/expense-entry-list/expense-entry-list.component.ts) 并使用 Material 表格组件。
声明一个变量 `displayedColumns` 并赋值为要显示的列列表。
displayedColumns: string[] = ['item', 'amount', 'category', 'location', 'spendOn' ];
在ExpenseEntryListComponent 模板(src/app/expense-entry-list/expense-entry-list.component.html)中添加 Material 表格,如下所示,并删除现有的列表。
<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] 属性用于指定表格的数据源。
Material 表格是基于模板的,每一列可以使用单独的模板进行设计。ng-container 用于创建模板。
matColumnDef 用于指定应用于特定 ng-container 的数据源列。
mat-header-cell 用于指定每一列的标题文本。
mat-cell 用于指定每一列的内容。
mat-header-row 和 mat-row 用于指定行中列的顺序。
我们只使用了 Material 表格的基本功能。Material 表格还有许多其他功能,例如排序、分页等。
运行应用程序。
ng serve
应用程序的输出如下:
Angular 8 - 路由和导航
导航是 Web 应用中的一个重要方面。即使单页应用 (SPA) 没有多个页面的概念,它也会从一个视图(例如支出列表)移动到另一个视图(例如支出详情)。提供清晰易懂的导航元素决定了应用的成功。
Angular 提供了广泛的导航功能集,以适应从简单场景到复杂场景的需求。定义导航元素和对应视图的过程称为路由。Angular 提供了一个单独的模块RouterModule来设置 Angular 应用中的导航。本章我们将学习如何在 Angular 应用中进行路由。
配置路由
Angular CLI 完全支持在应用创建过程中以及在应用运行过程中设置路由。让我们使用以下命令创建一个启用路由的新应用:
ng new routing-app --routing
Angular CLI 生成一个新的模块 AppRoutingModule 用于路由目的。生成的代码如下:
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。
RouterModule 提供了在应用中配置和执行路由的功能。
Routes 是用于设置导航规则的类型。
Routes 是用于配置应用实际导航规则的局部变量(Routes 类型)。
RouterModule.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 将新创建的路由模块AppRoutingModule配置到 AppModule 模块中。
让我们在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://127.0.0.1:4200/about` URL 时,路径与 `about` 规则匹配,然后将调用 AboutComponent。
访问路由
让我们学习如何在应用中使用已配置的路由。
访问路由是一个两步过程。
在根组件模板中包含router-outlet标签。
<router-outlet></router-outlet>
在需要的地方使用routerLink和routerLinkActive属性。
<a routerLink="/about" routerLinkActive="active">First Component</a>
这里:
routerLink 使用路径设置要调用的路由。
routerLinkActive 设置在激活路由时要使用的 CSS 类。
有时,我们需要在组件内部而不是模板中访问路由。然后,我们需要遵循以下步骤:
在相应的组件中注入Router和ActivatedRoute的实例。
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 });
这里:
relativeTo 可在NavigationExtras类中使用。
路由顺序
路由顺序在路由配置中非常重要。如果多次配置相同的路径,则将调用第一个匹配的路径。如果由于某种原因第一个匹配失败,则将调用第二个匹配。
重定向路由
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` 来访问路径的参数。`paramMap` 具有以下方法:
has(name) - 如果路径(参数列表)中存在指定的名称,则返回 true。
get(name) - 返回路径(参数列表)中指定名称的值。
getAll(name) - 返回路径中指定名称的多个值。当有多个值可用时,`get()` 方法只返回第一个值。
keys - 返回路径中所有可用的参数。
使用paramMap访问参数的步骤如下:
导入paramMap,它位于@angular/router包中。
在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-outlet和routerLink。
<!-- 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>
在这里,我们在编辑按钮之前添加了转到列表按钮。
使用以下命令运行应用程序:
ng serve
应用的最终输出如下所示:
单击第一个条目的查看选项将导航到详细信息页面并显示所选支出条目,如下所示:
Angular 8 - 动画
动画使 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()` 方法有两个参数。
name - 状态的唯一名称。
style - 使用 `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
duration – 指的是过渡的持续时间。表示方式为 1s、100ms 等。
delay – 指的是过渡开始之前的延迟时间。表示方式与duration类似。
easing – 指的是在给定的持续时间内如何加速/减速过渡。
触发器
每个动画都需要一个触发器来启动。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"; } }
将动画附加到 image 标签。此外,还为按钮附加点击事件。
<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
点击放大按钮,它将以动画效果放大图像。结果将如下所示:
再次点击按钮将其缩小。结果将如下所示:
Angular 8 - 表单
表单用于处理用户输入数据。Angular 8 支持两种类型的表单。它们是模板驱动表单和响应式表单。本节详细解释了 Angular 8 表单。
模板驱动表单
模板驱动表单是使用模板中的指令创建的。它主要用于创建简单的表单应用程序。让我们简要了解如何创建模板驱动表单。
配置表单
在了解表单之前,让我们学习如何在应用程序中配置表单。要启用模板驱动表单,首先需要在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 8 中创建一个示例应用程序(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
现在,运行您的应用程序,您将看到以下响应:
在输入文本字段中输入Peter 并提交。onClickSubmit 函数将被调用,用户输入的文本Peter 将作为参数发送。onClickSubmit 将在控制台中打印用户名,输出如下:
响应式表单
响应式表单是在组件类中创建的,因此也称为模型驱动表单。每个表单控件都将在组件中拥有一个对象,这在表单编程中提供了更大的控制和灵活性。响应式表单基于结构化数据模型。让我们了解如何在 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 8 中创建一个示例应用程序(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
现在,运行您的应用程序,您将看到以下响应:
在输入文本字段中输入Tutorialspoint 并提交。onClickSubmit 函数将被调用,用户输入的文本Peter 将作为参数发送。
我们将在下一章中执行表单验证。
Angular 8 - 表单验证
表单验证是 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
现在运行您的应用程序并将焦点放在文本框上。然后,它将显示“Name is required”,如下所示:
如果您在文本框中输入文本,则它将被验证,并显示以下输出:
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>
在这里,我们创建了电子邮件控件并调用了电子邮件验证器。
运行您的应用程序,您将看到以下结果:
同样,您可以自己尝试执行其他类型的验证器。
Angular 8 - 身份验证和授权
身份验证是将 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() { } }
这里:
我们编写了两个方法,login 和logout。
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 的 logout 方法。
- 用户注销后,页面将重定向到主页 (/)。
使用以下命令创建一个守卫:
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 ]
现在,运行应用程序,应用程序将打开登录页面。
输入 admin 和 admin 作为用户名和密码,然后单击提交。应用程序处理登录并如以下所示将用户重定向到支出列表页面:
最后,您可以单击注销并退出应用程序。
Angular 8 - Web Workers
Web Workers 使 JavaScript 应用程序能够在后台运行 CPU 密集型任务,以便应用程序主线程专注于 UI 的平滑运行。Angular 支持在应用程序中包含 Web Workers。让我们编写一个简单的 Angular 应用程序并尝试使用 Web Workers。
使用以下命令创建一个新的 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 Workers 的选项。
// 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 基本上是一个函数,当触发 message 事件时将调用该函数。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 并包含两个函数 find10thPrimeNumber 和 find10000thPrimeNumber。
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,后者又使用 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 Workers。先尝试查找第 10000 个素数,然后再查找第 10 个素数。
由于 Web Worker 正在计算第 10000 个素数,因此 UI 不会冻结。与此同时,我们可以检查第 10 个素数。如果我们没有使用 Web Worker,我们无法在浏览器中执行任何操作,因为它正在积极处理第 10000 个素数。
应用程序的结果如下:
应用程序的初始状态。
单击并尝试查找第 10000 个素数,然后尝试查找第 10 个素数。应用程序很快找到第 10 个素数并显示它。应用程序仍在后台处理以查找第 10000 个素数。
两个进程都已完成。
Web Worker 通过在后台执行复杂操作来增强 Web 应用程序的用户体验,并且在 Angular 应用程序中也很容易实现。
Angular 8 - Service Workers 和 PWA
渐进式 Web 应用 (PWA) 是具有少量增强的普通 Web 应用程序,其行为类似于原生应用程序。PWA 应用不依赖网络即可工作。PWA 缓存应用程序并从本地缓存中呈现它。它定期检查应用程序的实时版本,然后在后台缓存最新版本。
PWA 可以像原生应用程序一样安装在系统中,并且可以在桌面上显示快捷方式。单击快捷方式将在浏览器中打开应用程序,即使系统中没有任何可用的网络,它也会使用本地缓存。
Angular 应用程序可以转换为 PWA 应用程序。要转换 Angular 应用程序,我们需要使用 Service Worker API。Service Worker 实际上是一个代理服务器,位于浏览器、应用程序和网络之间。
Service Workers 与网页是分开的。它无法访问 DOM 对象。相反,Service Workers 通过 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://127.0.0.1:8080。
现在,转到 开发者工具 -> 网络 并选择 脱机 选项。
如果网络设置为脱机,则普通应用程序将停止工作,但 PWA 应用程序可以正常工作,如下所示:Angular 8 - 服务器端渲染
服务器端渲染 (SSR) 是一种现代技术,用于将运行在浏览器中的单页应用程序 (SPA) 转换为基于服务器的应用程序。通常,在 SPA 中,服务器返回一个简单的 index.html 文件,其中包含对基于 JavaScript 的 SPA 应用的引用。SPA 应用从此处接管,配置整个应用程序,处理请求,然后发送最终响应。
但在支持 SSR 的应用程序中,服务器也会执行所有必要的配置,然后将最终响应发送到浏览器。浏览器呈现响应并启动 SPA 应用。SPA 应用从此处接管,进一步的请求将被导向到 SPA 应用。SPA 和 SSR 的流程如下图所示。
将 SPA 应用程序转换为 SSR 提供了一些优势,如下所示:
速度 - 第一个请求相对较快。SPA 的主要缺点之一是初始渲染速度慢。一旦应用程序呈现完毕,SPA 应用的速度就相当快。SSR 解决了初始渲染问题。
SEO 友好 - 使站点更利于 SEO。SPA 的另一个主要缺点是 Web 抓取工具无法为了 SEO 的目的而对其进行抓取。SSR 解决了这个问题。
Angular Universal
要在 Angular 中启用 SSR,Angular 应该能够在服务器中呈现。为此,Angular 提供了一种名为 Angular Universal 的特殊技术。这是一项相当新的技术,并且仍在不断发展。Angular Universal 知道如何在服务器中呈现 Angular 应用程序。我们可以将我们的应用程序升级到 Angular Universal 以支持 SSR。
Angular 8 - 国际化 (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 区域设置指定。
我们已将日期更改为当前区域设置。让我们也更改其他内容。为此,请在相关标签中包含 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语言环境。使用谷歌翻译查找匹配的文本。更改后的内容如下:
打开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/" }
这里:
我们为hi和en语言环境使用了单独的设置。
在serve -> configuration下设置以下内容。
"hi": { "browserTarget": "i18n-sample:build:hi" }, "en": { "browserTarget": "i18n-sample:build:en" }
我们添加了必要的配置。停止应用程序并运行以下命令:
npm run start -- --configuration=hi
这里:
我们指定了要使用 hi 配置。
导航到 https://127.0.0.1:4200/hi,您将看到印地语本地化内容。
最后,我们创建了一个 Angular 本地化应用程序。
Angular 8 - 可访问性
辅助功能支持是每个基于 UI 的应用程序的重要功能之一。辅助功能是一种应用程序设计方式,使其也能被某些残疾人士访问。让我们学习 Angular 提供的支持,以开发具有良好辅助功能的应用程序。
使用属性绑定时,对 ARIA 属性使用attr.前缀。
使用 Angular Material 组件实现辅助功能。一些有用的组件包括LiveAnnouncer和cdkTrapFocus。
尽可能使用原生 HTML 元素,因为原生 HTML 元素提供了最大的辅助功能。创建组件时,选择与您的用例匹配的原生 html 元素,而不是重新开发原生功能。
使用NavigationEnd来跟踪和控制应用程序的焦点,因为它对辅助功能有很大帮助。
Angular 8 - CLI 命令
Angular CLI 帮助开发人员轻松快速地创建项目。正如我们已经知道的那样,Angular CLI 工具用于开发,并且构建在 Node.js 之上,从 NPM 安装。本章将详细解释 Angular 8 CLI 命令。
验证 CLI
在继续使用 Angular CLI 命令之前,我们必须确保 Angular CLI 已安装在您的机器上。如果已安装,您可以使用以下命令进行验证:
ng version
您将看到以下响应:
如果未安装 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
它将列出可用的架构:
让我们在下一节中了解一些重复使用的 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 8 - 测试
测试是应用程序开发生命周期中非常重要的一个阶段。它确保应用程序的质量。它需要仔细的计划和执行。
单元测试
单元测试是测试应用程序最简单的方法。它基于确保一段代码或类的某个方法的正确性。但是,它并不反映真实的运行环境,因此它是查找错误的最低效选项。
通常,Angular 8 使用 Jasmine 和 Karma 配置。要执行此操作,首先需要使用以下命令在您的项目中进行配置:
ng test
现在,您将看到以下响应:
现在,Chrome 浏览器也会打开并在“Jasmine HTML Reporter”中显示测试输出。它看起来类似于:
端到端 (E2E) 测试
单元测试是一个小型、简单且快速的流程,而 E2E 测试阶段则涉及多个组件协同工作,涵盖应用程序中的流程。要执行 e2e 测试,请键入以下命令:
ng e2e
您将看到以下响应:
Angular 8 - Ivy 编译器
Ivy 编译器是 Angular 团队发布的 Angular 应用程序的最新编译器。目前,Angular 使用View Engine编译器来编译 Angular 应用程序。
一般来说,Angular 编译器有两种编译应用程序的选项。
即时 (JIT) 编译器
在即时 (JIT)编译中,编译器将与应用程序捆绑在一起并发送到浏览器。Angular 应用程序将在浏览器中编译并在应用程序执行之前运行。
即使JIT提供了一些高级功能,JIT也会减慢编译速度,并且应用程序包的大小将是AOT编译器生成的两倍,因为它也包含编译器。
提前 (AOT) 编译器
在AOT编译中,编译器将发出优化的代码,准备在浏览器中运行而无需任何额外步骤。它将减小包的大小,并减少应用程序的编译时间和启动时间。
Ivy 编译器的优点
Ivy 编译器是 Angular 的优化和高级编译器。截至 Angular 8,它虽然可用,但尚未完全完成。Angular 团队建议开发人员在 Angular 8 中使用它。
Ivy 编译器的主要优点如下:
- 优化的代码。
- 更快的构建时间。
- 减小的包大小。
- 更好的性能。
如何使用 Ivy?
通过更改如下指定的项目设置,可以在 Angular 8 应用程序中使用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 8 - 使用 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 build 命令。
npm install -g @bazel/bazelisk bazelisk build
Angular 8 - 向后兼容性
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 8 中引入的一些重要更改。
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 8 - 实例
在这里,我们将学习关于 Angular 8 的完整分步工作示例。
让我们创建一个Angular应用程序来检查我们日常的开支。让我们将ExpenseManager作为我们新应用程序的名称。
创建应用程序
使用以下命令创建新的应用程序。
cd /path/to/workspace ng new expense-manager
这里:
ng new 是ng CLI应用程序的一个命令。它将用于创建新的应用程序。它会提出一些基本问题以便创建新的应用程序。让应用程序选择默认选项即可。关于如下提到的路由问题,请指定否。
Would you like to add Angular routing? No
回答完基本问题后,ng CLI应用程序将在expense-manager文件夹下创建一个新的Angular应用程序。
让我们进入我们新创建的应用程序文件夹。
cd expense-manager
让我们使用以下命令启动应用程序。
ng serve
让我们启动浏览器并打开 https://127.0.0.1:4200。浏览器将显示如下所示的应用程序:
让我们更改应用程序的标题以更好地反映我们的应用程序。打开src/app/app.component.ts 并按如下所示更改代码:
export class AppComponent { title = '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 已使用新组件更新。
向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标签。
应用程序的输出如下所示:
包含 Bootstrap
让我们使用styles选项将 Bootstrap 包含到我们的ExpenseManager应用程序中,并更改默认模板以使用 Bootstrap 组件。
打开命令提示符并转到 ExpenseManager 应用程序。
cd /go/to/expense-manager
使用以下命令安装bootstrap和JQuery库:
npm install --save [email protected] [email protected]
这里:
我们安装了 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>
重启应用程序。
应用程序的输出如下:
我们将在下一章中改进应用程序以处理动态费用条目。
添加接口
创建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>
应用程序的输出如下:
使用指令
让我们在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表格。table和table-striped将根据Bootstrap样式标准对表格进行样式设置。
使用ngFor循环遍历expenseEntries并生成表格行。
打开AppComponent模板src/app/app.component.html,包含ExpenseEntryListComponent并移除ExpenseEntryComponent,如下所示:
... <app-expense-entry-list></app-expense-entry-list>
最后,应用程序的输出如下所示。
使用管道
让我们在我们的ExpenseManager应用程序中使用管道
打开ExpenseEntryListComponent的模板src/app/expense-entry-list/expense-entry-list.component.html,并在entry.spendOn中包含管道,如下所示:
<td>{{ entry.spendOn | date: 'short' }}</td>
在这里,我们使用了日期管道以短格式显示支出日期。
最后,应用程序的输出如下所示:
添加调试服务
运行以下命令生成 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 组件中使用。
**providedIn** 选项及其值 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 方法会在浏览器控制台中打印消息。
可以使用开发者工具查看结果,它看起来类似于以下所示:
让我们扩展应用程序以了解服务的范围。
让我们使用下面提到的命令创建一个 **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模板中将debug组件作为内容包含在**ExpenseEntryListComponent**组件内。打开**AppComponent**模板,并将**app-expense-entry-list**修改如下:
// navigation code <app-expense-entry-list> <app-debug></app-debug> </app-expense-entry-list>
这里,我们添加了**DebugComponent**作为内容。
让我们检查一下应用程序,它会在页面末尾显示**DebugService**模板,如下所示:
此外,我们还可以在控制台中看到来自debug组件的两个调试信息。这表明debug组件从其父组件获取服务。
让我们更改在**ExpenseEntryListComponent**中注入服务的方式以及它如何影响服务的范围。将providers注入器更改为viewProviders注入。**viewProviders**不会将服务注入到内容子级,因此它应该会失败。
viewProviders: [DebugService]
检查应用程序,您会看到一个debug组件(用作内容子级)抛出错误,如下所示:
让我们从模板中删除debug组件并恢复应用程序。
打开**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**来与**费用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**来指定**费用Rest API**端点。
private expenseRestUrl = 'https://127.0.0.1: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://127.0.0.1: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 ); } }
最后,检查应用程序,您将看到以下响应。
添加开支功能
让我们在我们的**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-outlet和routerLink。
<!-- 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>
在这里,我们在编辑按钮之前添加了转到列表按钮。
使用以下命令运行应用程序:
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() { } }
这里:
我们编写了两个方法,login 和logout。
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 的 logout 方法。
- 用户注销后,页面将重定向到主页 (/)。
使用以下命令创建一个守卫:
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 ]
现在,运行应用程序,应用程序将打开登录页面。
输入 admin 和 admin 作为用户名和密码,然后单击提交。应用程序处理登录并如以下所示将用户重定向到支出列表页面:
最后,您可以单击注销并退出应用程序。
添加/编辑/删除开支
使用以下命令添加新组件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']); }); } } }
这里:
在ngOnInit方法中使用FormControl和FormGroup类以及适当的验证规则创建了一个表单formData。
在ngOnInit方法中加载要编辑的开支条目。
创建了两个方法itemValue和amountValue,分别用于获取用户输入的项目和金额值,以进行验证。
创建了方法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。
验证item和amount为必填值。
验证成功后调用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 { }
在这里,我们添加了关于、添加开支和编辑开支路由。
在ExpenseEntryListComponent模板中添加编辑和删除链接。
<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模板顶部的编辑链接更改为添加链接,如下所示:
<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模板中添加编辑链接。
<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>
在这里,我们更新了添加开支链接和关于链接。
运行应用程序,输出将类似于以下所示:
尝试使用开支列表页面中的添加链接添加新的开支。输出将类似于以下所示:
填写表单,如下所示:
如果数据填写不正确,验证代码将发出如下所示的警报:
点击提交。它将触发提交事件,数据将保存到后端并重定向到列表页面,如下所示:
尝试使用开支列表页面中的编辑链接编辑现有开支。输出将类似于以下所示:
点击提交。它将触发提交事件,数据将保存到后端并重定向到列表页面。
要删除项目,请单击删除链接。它将确认删除,如下所示:
最后,我们实现了应用程序中管理开支所需的所有功能。
Angular 8 - 新功能?
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 8中Ivy是可选的。我们必须在tsconfig.json文件中启用它。
Ivy编译器支持以下功能:
执行更快的测试 - TestBed实现有助于更有效地进行测试。
改进的CSS类和样式 - Ivy样式易于合并和设计,并且可预测。
改进的类型检查 - 此功能有助于在开发过程的早期发现错误。
增强的调试 - Ivy附带更多工具以启用更好的调试功能。这将有助于显示有用的堆栈跟踪,以便我们可以轻松跳转到指令。
提前编译器(Ahead-of-Time compiler) - 这是编译器性能的重要改进之一。AOT构建速度非常快。改进的国际化 - i18n替换有助于比以前的版本快十倍以上地构建。
可靠的ng update
ng update非常可靠。它包含清晰的进度更新并运行所有迁移。这可以使用以下命令完成:
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私有字段。
- 顶级等待。
- JSDoc属性修饰符。
- export * as ns语法。
Angular 9.0.0-next.5
Angular 9.0.0-next.5构建的main.js文件大小较小,与之前的Angular 8版本相比,性能更好。
IDE增强
Angular 9提供了改进的IDE支持。TextMate语法支持内联和外部模板中的语法高亮显示。
结论
Angular是一个灵活的、不断改进的、持续更新的可靠框架。Angular极大地简化了SPA开发的过程。通过在每个版本中提供新功能,例如Angular Universal、渐进式Web应用程序、Web工作线程、Bazel构建、Ivy编译器等,Angular将拥有长久的生命周期并获得前端开发人员的全面支持。