Angular 8 - Http 客户端编程

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

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

Expense 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';

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

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

创建ExpenseEntryService服务

让我们在 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 GET

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

  • observe − 处理响应并返回响应的具体内容。 可能的值为正文、响应和事件。 观察者默认选项是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 库的 retry 运算符,如下所示

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

获取费用条目

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

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

cd /go/to/expense-manager

启动应用程序。

ng serve

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

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

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

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

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

这里,

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

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

ExpenseEntryService的完整代码如下 −

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

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

@Injectable({

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