Angular 8 Http 客户端编程


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

让我们学习如何使用 HttpClient 本章服务。开发者需要具备 Http 编程的基本知识才能理解本章。

费用 REST API


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

让我们创建一个 费用休息 API 使用 express 框架,然后从我们的 费用经理 使用 Angular HttpClient 服务的应用程序。

打开命令提示符并创建一个新文件夹, 快速休息 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

Install 快递,sqlite and 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

打开浏览器,输入 http://localhost:8000/ 然后按回车。你将看到以下回复:

{ 
    "message": "Ok"
}

它确认我们的应用程序运行良好。

将网址更改为 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 服务可在里面 HttpClient模块 模块,在 @angular/common/http 包中可用。

注册 HttpClient模块 module:

将 HttpClientModule 导入 应用组件

import { HttpClientModule } from '@angular/common/http';

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

@NgModule({ 
    imports: [
        BrowserModule,
        // 在 BrowserModule 之后导入 HttpClientModule。
        HttpClientModule,
    ]
}) 
export class AppModule {}

创建费用服务

让我们创建一个新服务 费用录入服务 in our 费用经理 与之交互的应用程序 费用 REST API . ExpenseEntryService 将获取最新的费用条目,插入新的费用条目,修改现有的费用条目并删除不需要的费用条目。

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

cd /go/to/expense-manager

启动应用程序。

ng serve

运行以下命令以生成 Angular 服务, 费用服务 .

ng generate service ExpenseEntry

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

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

Open 费用录入服务 (src/app/expense-entry.service.ts) 并导入 费用条目,抛出错误 and 捕获错误 从 rxjs 库和导入 HttpClient、HttpHeaders and HttpErrorResponse 来自@angular/common/http 包。

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

创建一个变量, 费用休息网址 指定 费用休息 API 端点。

private expenseRestUrl = 'http:// localhost:8000/api/expense';

创建一个变量, http选项 设置 Http Header 选项。这将在 Angular 调用 Http Rest API 期间使用 HttpClient service.

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,
}

Here,

  • headers :请求的HTTP Headers,可以是字符串、字符串数组或HttpHeaders数组。

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

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

  • 报告进度 : 是否报告进程的进度(true or false)。

  • 响应类型 : 指响应的格式。可能的值为 数组缓冲区,blob,json and text .

  • 有凭据 :请求是否有凭据(true or false)。

所有选项都是可选的。

get() 方法将请求的响应返回为 可观察的 .当从服务器接收到响应时,返回的 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() 方法抛出 错误事件 object.

  • 由于错误的 url、服务器不可用、服务器编程错误等,可能会出现服务器端问题,

让我们为我们的代码编写一个简单的错误处理 费用录入服务 service.

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

获取费用条目

让我们进行实际编码以从中获取费用 费用休息 API 在我们的 ExpenseManager 应用程序中。

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

cd /go/to/expense-manager

启动应用程序。

ng serve

Add 获取费用条目() and httpErrorHandler() 方法在 费用录入服务 (src/app/expense-entry.service.ts) 服务。

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

Here,

  • 获取费用条目() 调用 get() 方法使用费用端点,还配置错误处理程序。此外,它配置 httpClient 在失败的情况下最多尝试 3 次。最后,它以输入的形式返回来自服务器的响应 (费用条目[]) 可观察对象。

  • 获取费用条目 与 getExpenseEntries() 类似,只是它传递 ExpenseEntry 对象的 id 并获取 ExpenseEntry Observable 对象。

完整的编码 费用录入服务 如下:

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

Open 费用条目列表组件 (src-entry-list-entry-list.component.ts) 并注入 费用录入服务 通过如下指定的构造函数:

constructor(private debugService: DebugService, private restService : 
ExpenseEntryService ) { }

更改 获取费用条目() 功能。调用 getExpenseEntries() 方法 费用录入服务 而不是返回模拟项目。

getExpenseItems() {  
    this.restService.getExpenseEntries()
        .subscribe( data =− this.expenseEntries = data );
}

完整的 费用条目列表组件 编码如下:

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

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

failed request

HTTP POST


HTTP POST 与 HTTP GET 类似,只是 post 请求会将必要的数据作为已发布的内容与请求一起发送。 HTTP POST 用于将新记录插入系统。

HttpClient provides post() 方法,类似于 get() 除了它支持将数据发送到服务器的额外参数。

让我们添加一个新方法, addExpenseEntry() in our 费用录入服务 添加新的费用条目如下:

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 provides put() 方法,类似于 post() .

更新费用条目

让我们添加一个新方法, updateExpenseEntry() in our 费用录入服务 更新现有费用条目,如下所述:

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 provides delete() 方法,类似于 get() .

删除费用条目

让我们添加一个新方法, 删除费用条目() in our 费用录入服务 删除现有的费用条目,如下所述:

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