- 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 - 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); });
在这里,我们创建了一个基本的 CRUD 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" } ]
最后,我们为支出条目创建了一个简单的 CRUD 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() 方法来从网页获取数据。主要参数是目标 Web URL。另一个可选参数是具有以下格式的选项对象:
{ headers?: HttpHeaders | {[header: string]: string | string[]}, observe?: 'body' | 'events' | 'response', params?: HttpParams|{[param: string]: string | string[]}, reportProgress?: boolean, responseType?: 'arraybuffer'|'blob'|'json'|'text', withCredentials?: boolean, }
这里:
headers - 请求的 HTTP 头,可以是字符串、字符串数组或 HttpHeaders 数组。
observe - 处理响应并返回响应的特定内容。可能的值为 body、response 和 events。observer 的默认选项是 body。
params - 请求的 HTTP 参数,可以是字符串、字符串数组或 HttpParams 数组。
reportProgress - 是否报告进程的进度(true 或 false)。
responseType - 指的是响应的格式。可能的值为arraybuffer、blob、json 和text。
withCredentials - 请求是否包含凭据(true 或 false)。
所有选项都是可选的。
get() 方法将请求的响应作为Observable 返回。当从服务器接收到响应时,返回的 Observable 会发出数据。
使用get() 方法的示例代码如下:
httpClient.get(url, options) .subscribe( (data) => console.log(data) );
类型化响应
get() 方法有一个选项可以返回 observable,它也会发出类型化响应。获取类型化响应 (ExpenseEntry) 的示例代码如下:
httpClient.get<T>(url, options) .subscribe( (data: T) => console.log(data) );
处理错误
错误处理是 HTTP 编程中的一个重要方面。遇到错误是 HTTP 编程中的常见情况。
HTTP 编程中的错误可以分为两类:
客户端问题可能是由于网络故障、错误配置等造成的。如果发生客户端错误,则get() 方法将抛出ErrorEvent 对象。
服务器端问题可能是由于错误的 URL、服务器不可用、服务器编程错误等造成的。
让我们为我们的ExpenseEntryService 服务编写一个简单的错误处理程序。
private httpErrorHandler (error: HttpErrorResponse) { if (error.error instanceof ErrorEvent) { console.error("A client side error occurs. The error message is " + error.message); } else { console.error( "An error happened in server. The HTTP status code is " + error.status + " and the error returned is " + error.message); } return throwError("Error occurred. Pleas try again"); }
错误函数可以在get() 中调用,如下所示:
httpClient.get(url, options) .pipe(catchError(this.httpErrorHandler) .subscribe( (data) => console.log(data) )
处理失败的请求
正如我们前面提到的,错误可能会发生,一种方法是处理它。另一种选择是尝试一定次数。如果请求由于网络问题或 HTTP 服务器暂时脱机而失败,则下一个请求可能会成功。
在这种情况下,我们可以使用rxjs 库的retry 运算符,如下所示:
httpClient.get(url, options) .pipe( retry(5), catchError(this.httpErrorHandler)) .subscribe( (data) => console.log(data) )
获取支出条目
让我们在 ExpenseManager 应用程序中编写实际代码来从支出 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) ); }