- 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 8 - 动画
- Angular 8 - 表单
- Angular 8 - 表单验证
- 认证与授权
- Angular 8 - 网络工作者
- Service Worker 和 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 应用程序来检查我们的日常开支。让我们选择ExpenseManager作为新应用程序的选择。
创建应用程序
使用以下命令创建新应用程序。
cd /path/to/workspace ng new expense-manager
这里,
new是 ng CLI 应用程序的命令之一。它将用于创建新的应用程序。它将询问一些基本问题以创建新的应用程序。让应用程序选择默认选项就足够了。对于下面提到的路由问题,请指定No。
Would you like to add Angular routing? No
回答了基本问题后,ng CLI 应用程序将在费用管理器文件夹下创建一个新的 Angular 应用程序。
让我们进入新创建的应用程序文件夹。
cd expense-manager
让我们使用以下命令启动应用程序。
ng serve
让我们启动浏览器并打开 http://localhost:4200。浏览器将显示应用程序,如下所示 -
让我们更改应用程序的标题以更好地反映我们的应用程序。打开src/app/app.component.ts并更改代码,如下所示 -
export class AppComponent {
title = 'Expense Manager';
}
我们的最终应用程序将在浏览器中呈现,如下所示 -
添加组件
使用 ng生成组件命令创建一个新组件,如下所示 -
ng generate component expense-entry
输出
输出如下 -
CREATE src/app/expense-entry/expense-entry.component.html (28 bytes) CREATE src/app/expense-entry/expense-entry.component.spec.ts (671 bytes) CREATE src/app/expense-entry/expense-entry.component.ts (296 bytes) CREATE src/app/expense-entry/expense-entry.component.css (0 bytes) UPDATE src/app/app.module.ts (431 bytes)
这里,
- ExpenseEntryComponent 在 src/app/expense-entry 文件夹下创建。
- 创建组件类、模板和样式表。
- AppModule 已更新为新组件。
将 title 属性添加到 ExpenseEntryComponent (src/app/expense-entry/expense-entry.component.ts)组件。
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-expense-entry',
templateUrl: './expense-entry.component.html',
styleUrls: ['./expense-entry.component.css']
})
export class ExpenseEntryComponent implements OnInit {
title: string;
constructor() { }
ngOnInit() {
this.title = "Expense Entry"
}
}
使用以下内容更新模板src/app/expense-entry/expense-entry.component.html 。
<p>{{ title }}</p>
打开
src/app/app.component.html
并包括新创建的组件。<h1>{{ title }}</h1>
<app-expense-entry></app-expense-entry>
这里,
app-expense-entry 是选择器值,它可以用作常规 HTML 标签。
应用程序的输出如下所示 -
包括引导程序
让我们使用styles选项将 bootstrap 添加到ExpenseManager应用程序中,并更改默认模板以使用 bootstrap 组件。
打开命令提示符并转到 ExpenseManager 应用程序。
cd /go/to/expense-manager
使用以下命令安装bootstrap和JQuery库
npm install --save bootstrap@4.5.0 jquery@3.5.1
这里,
我们安装了 JQuery,因为 bootstrap 广泛使用 jquery 来实现高级组件。
选项angular.json并设置 bootstrap 和 jquery 库路径。
{
"projects": {
"expense-manager": {
"architect": {
"build": {
"builder":"@angular-devkit/build-angular:browser", "options": {
"outputPath": "dist/expense-manager",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json",
"aot": false,
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"./node_modules/bootstrap/dist/css/bootstrap.css", "src/styles.css"
],
"scripts": [
"./node_modules/jquery/dist/jquery.js", "./node_modules/bootstrap/dist/js/bootstrap.js"
]
},
},
}
}},
"defaultProject": "expense-manager"
}
这里,
scripts选项用于包含 JavaScript 库。通过脚本注册的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>
这里,
使用引导导航和容器。
打开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>
这里,
使用引导表。table和table-striped将根据 Boostrap 样式标准对桌子进行样式设置。
使用ngFor循环费用条目并生成表行。
打开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 组件中使用。
providerIn选项及其值,root 使 DebugService 能够在应用程序的所有组件中使用。
让我们添加一个方法 Info,它将把消息打印到浏览器控制台中。
info(message : String) : void {
console.log(message);
}
让我们在ExpenseEntryListComponent中初始化服务并使用它来打印消息。
import { Component, OnInit } from '@angular/core'; import { ExpenseEntry } from '../expense-entry'; import { DebugService } from '../debug.service'; @Component({
selector: 'app-expense-entry-list',
templateUrl: './expense-entry-list.component.html', styleUrls: ['./expense-entry-list.component.css']
})
export class ExpenseEntryListComponent implements OnInit {
title: string;
expenseEntries: ExpenseEntry[];
constructor(private debugService: DebugService) { }
ngOnInit() {
this.debugService.info("Expense Entry List
component initialized");
this.title = "Expense Entry List";
this.expenseEntries = this.getExpenseEntries();
}
// other coding
}
这里,
DebugService 使用构造函数参数进行初始化。设置 DebugService 类型的参数 (debugService) 将触发依赖项注入来创建新的 DebugService 对象并将其设置到 ExpenseEntryListComponent 组件中。
在 ngOnInit 方法中调用 DebugService 的 info 方法会在浏览器控制台中打印消息。
可以使用开发人员工具查看结果,如下所示 -
让我们扩展应用程序来了解服务的范围。
让我们使用下面提到的命令创建一个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]
})
在这里,我们使用提供者元数据(ElementInjector)来注册服务。
打开DebugComponent (src/app/debug/debug.component.ts) 并导入DebugService并在组件的构造函数中设置一个实例。
import { Component, OnInit } from '@angular/core'; import { DebugService } from '../debug.service';
@Component({
selector: 'app-debug',
templateUrl: './debug.component.html',
styleUrls: ['./debug.component.css']
})
export class DebugComponent implements OnInit {
constructor(private debugService: DebugService) { }
ngOnInit() {
this.debugService.info("Debug component gets service from Parent");
}
}
这里,我们还没有注册DebugService。因此,如果用作父组件,则 DebugService 将不可用。当在父组件内部使用时,如果父组件有权访问该服务,则可以从父组件获得该服务。
打开ExpenseEntryListComponent模板 (src/app/expense-entry-list/expense-entry-list.component.html) 并包含内容部分,如下所示:
// existing content <app-debug></app-debug> <ng-content></ng-content>
在这里,我们包含了内容部分和 DebugComponent 部分。
让我们将调试组件作为内容包含在 AppComponent 模板的ExpenseEntryListComponent组件内。打开AppComponent模板并更改app-expense-entry-list如下 -
// navigation code <app-expense-entry-list> <app-debug></app-debug> </app-expense-entry-list>
在这里,我们将DebugComponent作为内容包含在内。
让我们检查应用程序,它将在页面末尾显示DebugService模板,如下所示 -
此外,我们还可以在控制台中的调试组件中看到两条调试信息。这表明调试组件从其父组件获取服务。
让我们更改将服务注入ExpenseEntryListComponent的方式以及它如何影响服务的范围。将提供程序注入器更改为 viewProviders 注入。viewProviders不会将服务注入到内容子项中,因此它应该会失败。
viewProviders: [DebugService]
检查应用程序,您将看到调试组件之一(用作内容子组件)抛出错误,如下所示 -
让我们删除模板中的调试组件并恢复应用程序。
打开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>
将viewProviders设置更改为ExpenseEntryListComponent中的提供者。
providers: [DebugService]
重新运行应用程序并检查结果。
创建费用服务
让我们在ExpenseManager应用程序中创建一个新服务ExpenseEntryService来与Expense REST API进行交互。ExpenseEntryService 将获取最新的费用条目、插入新的费用条目、修改现有的费用条目以及删除不需要的费用条目。
打开命令提示符并转到项目根文件夹。
cd /go/to/expense-manager
启动应用程序。
ng serve
运行以下命令生成 Angular 服务ExpenseService。
ng generate service ExpenseEntry
这将创建两个 Typescript 文件(费用输入服务及其测试),如下所示 -
CREATE src/app/expense-entry.service.spec.ts (364 bytes) CREATE src/app/expense-entry.service.ts (141 bytes)
打开ExpenseEntryService (src/app/expense-entry.service.ts) 并从 rxjs 库导入ExpenseEntry、throwError和catchError ,并从 @angular/common/http 包导入HttpClient、HttpHeaders和HttpErrorResponse 。
import { Injectable } from '@angular/core';
import { ExpenseEntry } from './expense-entry'; import { throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { HttpClient, HttpHeaders, HttpErrorResponse } from
'@angular/common/http';
将 HttpClient 服务注入到我们的服务中。
constructor(private httpClient : HttpClient) { }
创建变量ExpenseRestUrl来指定Expense Rest API端点。
private expenseRestUrl = 'http://localhost:8000/api/expense';
创建变量httpOptions以设置 Http 标头选项。这将在 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 编程
启动费用 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 = 'http://localhost: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方法的目的是验证用户,如果用户验证成功,它将信息存储在localStorage中,然后返回true。
认证验证是用户名和密码应该是admin。
我们没有使用任何后端。相反,我们使用 Observables 模拟了 1 秒的延迟。
logout方法的目的是使用户失效并删除localStorage中存储的信息。
使用以下命令创建登录组件 -
ng generate component login CREATE src/app/login/login.component.html (20 bytes) CREATE src/app/login/login.component.spec.ts (621 bytes) CREATE src/app/login/login.component.ts (265 bytes) CREATE src/app/login/login.component.css (0 bytes) UPDATE src/app/app.module.ts (1207 bytes)
打开LoginComponent并包含以下代码 -
import { Component, OnInit } from '@angular/core';
import { FormGroup, FormControl } from '@angular/forms';
import { AuthService } from '../auth.service';
import { Router } from '@angular/router';
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.css']
})
export class LoginComponent implements OnInit {
userName: string;
password: string;
formData: FormGroup;
constructor(private authService : AuthService, private router : Router) { }
ngOnInit() {
this.formData = new FormGroup({
userName: new FormControl("admin"),
password: new FormControl("admin"),
});
}
onClickSubmit(data: any) {
this.userName = data.userName;
this.password = data.password;
console.log("Login page: " + this.userName);
console.log("Login page: " + this.password);
this.authService.login(this.userName, this.password)
.subscribe( data => {
console.log("Is Login Success: " + data);
if(data) this.router.navigate(['/expenses']);
});
}
}
这里,
使用反应形式。
导入AuthService和Router并在构造函数中进行配置。
创建了 FormGroup 的一个实例,并包含两个 FormControl 实例,一个用于用户名,另一个用于密码。
创建了一个 onClickSubmit 以使用 authService 验证用户,如果成功,则导航到费用列表。
打开LoginComponent模板并包含以下模板代码。
<!-- Page Content -->
<div class="container">
<div class="row">
<div class="col-lg-12 text-center" style="padding-top: 20px;">
<div class="container box" style="margin-top: 10px; padding-left: 0px; padding-right: 0px;">
<div class="row">
<div class="col-12" style="text-align: center;">
<form [formGroup]="formData" (ngSubmit)="onClickSubmit(formData.value)"
class="form-signin">
<h2 class="form-signin-heading">Please sign in</h2>
<label for="inputEmail" class="sr-only">Email address</label>
<input type="text" id="username" class="form-control"
formControlName="userName" placeholder="Username" required autofocus>
<label for="inputPassword" class="sr-only">Password</label>
<input type="password" id="inputPassword" class="form-control"
formControlName="password" placeholder="Password" required>
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
这里,
创建了一个反应式表单并设计了一个登录表单。
将onClickSubmit方法附加到表单提交操作。
打开LoginComponent样式并包含以下 CSS 代码。
.form-signin {
max-width: 330px;
padding: 15px;
margin: 0 auto;
}
input {
margin-bottom: 20px;
}
这里添加了一些样式来设计登录表单。
使用以下命令创建注销组件 -
ng generate component logout CREATE src/app/logout/logout.component.html (21 bytes) CREATE src/app/logout/logout.component.spec.ts (628 bytes) CREATE src/app/logout/logout.component.ts (269 bytes) CREATE src/app/logout/logout.component.css (0 bytes) UPDATE src/app/app.module.ts (1368 bytes)
打开LogoutComponent并包含以下代码。
import { Component, OnInit } from '@angular/core';
import { AuthService } from '../auth.service';
import { Router } from '@angular/router';
@Component({
selector: 'app-logout',
templateUrl: './logout.component.html',
styleUrls: ['./logout.component.css']
})
export class LogoutComponent implements OnInit {
constructor(private authService : AuthService, private router: Router) { }
ngOnInit() {
this.authService.logout();
this.router.navigate(['/']);
}
}
这里,
- 使用AuthService的注销方法。
- 用户注销后,页面将重定向到主页(/)。
使用以下命令创建一个守卫 -
ng generate guard expense CREATE src/app/expense.guard.spec.ts (364 bytes) CREATE src/app/expense.guard.ts (459 bytes)
打开 ExpenseGuard 并包含以下代码 -
import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router, UrlTree } from '@angular/router';
import { Observable } from 'rxjs';
import { AuthService } from './auth.service';
@Injectable({
providedIn: 'root'
})
export class ExpenseGuard implements CanActivate {
constructor(private authService: AuthService, private router: Router) {}
canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot): boolean | UrlTree {
let url: string = state.url;
return this.checkLogin(url);
}
checkLogin(url: string): true | UrlTree {
console.log("Url: " + url)
let val: string = localStorage.getItem('isUserLoggedIn');
if(val != null && val == "true"){
if(url == "/login")
this.router.parseUrl('/expenses');
else
return true;
} else {
return this.router.parseUrl('/login');
}
}
}
这里,
- checkLogin会检查localStorage是否有用户信息,如果有则返回true。
- 如果用户登录并进入登录页面,它会将用户重定向到费用页面
- 如果用户未登录,则用户将被重定向到登录页面。
打开AppRoutingModule (src/app/app-routing.module.ts)并更新以下代码 -
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { ExpenseEntryComponent } from './expense-entry/expense-entry.component';
import { ExpenseEntryListComponent } from './expense-entry-list/expense-entry-list.component';
import { LoginComponent } from './login/login.component';
import { LogoutComponent } from './logout/logout.component';
import { ExpenseGuard } from './expense.guard';
const routes: Routes = [
{ path: 'login', component: LoginComponent },
{ path: 'logout', component: LogoutComponent },
{ path: 'expenses', component: ExpenseEntryListComponent, canActivate: [ExpenseGuard]},
{ path: 'expenses/detail/:id', component: ExpenseEntryComponent, canActivate: [ExpenseGuard]},
{ path: '', redirectTo: 'expenses', pathMatch: 'full' }
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
这里,
- 导入LoginComponent和LogoutComponent。
- 进口ExpenseGuard。
- 创建了两个新路由,登录和注销,分别访问 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']); });
}
}
}
这里,
使用具有适当验证规则的FormControl和FormGroup类在ngOnInit方法中创建表单 formData 。
加载了要在ngOnInit方法中编辑的费用条目。
创建了两个方法itemValue和amountValue来获取用户分别输入的项目值和金额值以进行验证。
创建方法onClickSubmit来保存(添加/更新)费用条目。
使用费用服务来添加和更新费用条目。
使用费用表单更新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 ty