Angular 8 - Http 客户端编程


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

本章我们来学习如何使用HttpClient服务。开发人员应该具备 Http 编程的基本知识才能理解本章。

费用 REST API

进行Http编程的前提是具备Http协议和REST API技术的基础知识。Http编程涉及两部分,服务器和客户端。Angular 提供创建客户端应用程序的支持。Express是一个流行的 Web 框架,提供创建服务器端应用程序的支持。

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

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

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

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

npm init

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

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

使用以下命令安装express、sqlitecors模块 -

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

打开浏览器,输入http://localhost:8000/并按 Enter。您将看到以下回复 -

{ 
   "message": "Ok" 
}

它证实我们的应用程序运行良好。

将 url 更改为http://localhost: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';

将 HttpClientModule 包含在 AppComponent 的导入元数据中。

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

创建费用服务

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

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

cd /go/to/expense-manager

启动应用程序。

ng serve

运行以下命令生成 Angular 服务ExpenseService

ng generate service ExpenseEntry

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

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

打开ExpenseEntryService (src/app/expense-entry.service.ts) 并从 rxjs 库导入ExpenseEntry、throwErrorcatchError ,并从 @angular/common/http 包导入HttpClient、HttpHeadersHttpErrorResponse 。

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) { }
}

HTTP 获取

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

{
   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 数组。

  • 观察- 处理响应并返回响应的具体内容。可能的值为正文、响应和事件。观察者的默认选项是body。

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

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

  • responseType - 指响应的格式。可能的值为arraybuffer、blob、jsontext

  • 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库的重试运算符,如下所示

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

获取费用条目

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

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

cd /go/to/expense-manager

启动应用程序。

ng serve

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

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

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

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

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

这里,

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

  • getExpenseEntry与 getExpenseEntries() 类似,只不过它传递 ExpenseEntry 对象的 id 并获取 ExpenseEntry Observable 对象。

ExpenseEntryService的完整代码如下 -

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

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

@Injectable({

   providedIn: 'root'
})
export class ExpenseEntryService {
   private expenseRestUrl = '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 );
   }
}

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

请求失败

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 放置

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 删除

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)
   );
}