Angular和RxJS:添加REST API后端

2023-11-13

本文是SitePoint Angular 2+教程的第3部分,该教程有关如何使用Angular CLI创建CRUD应用程序。 在本文中,我们将更新我们的应用程序以与REST API后端进行通信。

更喜欢使用分步视频课程学习Angular? 退房 了解角5 上SitePoint保费。

在第一部分中,我们学习了如何启动和运行Todo应用程序并将其部署到GitHub页面。 这样做很好,但是不幸的是,整个应用程序都挤在一个组件中。

在第二部分中,我们研究了模块化程度更高的组件体系结构,并学习了如何将单个组件分解为较小的组件的结构化树,这些树更易于理解,重用和维护。

  1. 第0部分— Ultimate Angular CLI参考指南
  2. 第1部分-启动并运行我们的Todo应用程序的第一个版本
  3. 第2部分-创建单独的组件以显示待办事项列表和一个待办事项
  4. 第3部分-更新Todo服务以与REST API后端进行通信
  5. 第4部分-使用Angular路由器解析数据
  6. 第5部分-添加身份验证以保护私有内容
  7. 第6部分-如何将Angular项目更新到最新版本。

并不需要遵循第一和第二部分本教程为三来一补感。 您可以简单地获取我们的仓库的副本,从第二部分中检出代码,然后以此为起点。 下面将对此进行详细说明。

REST API后端:两个人拿着无线电天线,与小山上的巨型Angular徽标通信。

快速回顾

这是第2部分结尾处的应用程序体系结构:

REST API后端:应用程序体系结构

当前, TodoDataService将所有数据存储在内存中。 在第三篇文章中,我们将更新应用程序以与REST API后端进行通信。

我们会:

  • 创建一个模拟REST API后端
  • 将API URL存储为环境变量
  • 创建一个ApiService与REST API后端进行通信
  • 更新TodoDataService以使用新的ApiService
  • 更新AppComponent以处理异步API调用
  • 创建一个ApiMockService以避免在运行单元测试时进行真正的HTTP调用。

REST API后端:应用程序体系结构

到本文结尾,您将了解:

  • 如何使用环境变量存储应用程序设置
  • 如何使用Angular HTTP客户端执行HTTP请求
  • 如何处理Angular HTTP客户端返回的Observable
  • 如何在运行单元测试时模拟HTTP调用以避免发出真实的HTTP请求。

所以,让我们开始吧!

启动并运行

确保已安装最新版本的Angular CLI。 如果不这样做,则可以使用以下命令进行安装:

npm install -g @angular/cli@latest

如果您需要删除以前版本的Angular CLI,则可以:

npm uninstall -g @angular/cli angular-cli
npm cache clean
npm install -g @angular/cli@latest

之后,您将需要第二部分的代码副本。 这在GitHub可用 。 本系列中的每篇文章在存储库中都有一个相应的标记,因此您可以在应用程序的不同状态之间来回切换。

我们在第二部分结尾并且在本文开始的代码被标记为part-2 。 本文结尾处的代码被标记为part-3

您可以将标签视为特定提交ID的别名。 您可以使用git checkout在它们之间切换。 您可以在此处阅读更多内容

因此,要启动并运行(安装了最新版本的Angular CLI),我们可以这样做:

git clone git@github.com:sitepoint-editors/angular-todo-app.git
cd angular-todo-app
git checkout part-2
npm install
ng serve

然后访问http:// localhost:4200 / 。 如果一切顺利,您应该会看到正在运行的Todo应用程序。

设置REST API后端

让我们使用json-server快速设置模拟后端。

从应用程序的根目录运行:

npm install json-server --save

接下来,在应用程序的根目录中,创建一个名为db.json的文件,其内容如下:

{
  "todos": [
    {
      "id": 1,
      "title": "Read SitePoint article",
      "complete": false
    },
    {
      "id": 2,
      "title": "Clean inbox",
      "complete": false
    },
    {
      "id": 3,
      "title": "Make restaurant reservation",
      "complete": false
    }
  ]
}

最后,将脚本添加到package.json以启动我们的后端:

"scripts": {
  ...
  "json-server": "json-server --watch db.json"
}

现在,我们可以使用以下命令启动REST API后端:

npm run json-server

这应该显示以下内容:

  \{^_^}/ hi!

  Loading db.json
  Done

  Resources
  http://localhost:3000/todos

  Home
  http://localhost:3000

而已! 现在,我们有一个REST API后端在端口3000上侦听。

要验证后端是否按预期运行,可以将浏览器导航到http://localhost:3000

支持以下端点:

  • GET /todos :获取所有现有的待办事项
  • GET /todos/:id :获取现有的待办事项
  • POST /todos :创建一个新的待办事项
  • PUT /todos/:id :更新现有的待办事项
  • DELETE /todos/:id :删除现有的待办事项

因此,如果将浏览器导航到http://localhost:3000/todos ,您应该会看到一个JSON响应,其中包含来自db.json所有db.json

要了解有关json-server的更多信息,请确保使用json-server签出模拟REST API

存储API URL

现在我们已经有了后端,我们必须将其URL存储在Angular应用程序中。

理想情况下,我们应该能够做到这一点:

  1. 将网址存储在一个位置,以便我们只需要在需要更改其值时更改一次
  2. 使我们的应用程序在开发期间连接到开发API,并在生产中连接到生产API。

幸运的是,Angular CLI支持环境。 默认情况下,有两种环境:开发和生产,都有相应的环境文件: src/environments/environment.ts和' src/environments/environment.prod.ts

让我们将API URL添加到两个文件中:

// src/environments/environment.ts
// used when we run `ng serve` or `ng build`
export const environment = {
  production: false,

  // URL of development API
  apiUrl: 'http://localhost:3000'
};
// src/environments/environment.prod.ts
// used when we run `ng serve --environment prod` or `ng build --environment prod`
export const environment = {
  production: true,

  // URL of production API
  apiUrl: 'http://localhost:3000'
};

稍后,这将允许我们通过执行以下操作从Angular应用程序的环境中获取API URL:

import { environment } from 'environments/environment';

// we can now access environment.apiUrl
const API_URL = environment.apiUrl;

当我们运行ng serveng build ,Angular CLI使用开发环境中指定的值( src/environments/environment.ts )。

但是当我们运行ng serve --environment prodng build --environment prod ,Angular CLI使用src/environments/environment.prod.ts指定的值。

这正是我们需要使用其他API URL进行开发和生产而无需更改代码的需求。

本系列文章中的应用程序未托管在生产环境中,因此我们在开发和生产环境中指定了相同的API URL。 这使我们可以在本地运行ng serve --environment prodng build --environment prod来查看是否一切正常。

您可以在.angular-cli.json找到devprod及其对应的环境文件之间的映射:

"environments": {
  "dev": "environments/environment.ts",
  "prod": "environments/environment.prod.ts"
}

您还可以通过添加密钥来创建其他环境,例如staging

"environments": {
  "dev": "environments/environment.ts",
  "staging": "environments/environment.staging.ts",
  "prod": "environments/environment.prod.ts"
}

并创建相应的环境文件。

要了解有关Angular CLI环境的更多信息,请确保查看《最终Angular CLI参考指南》

现在我们已经在环境中存储了API URL,我们可以创建一个Angular服务来与REST API后端进行通信。

创建服务以与REST API后端通信

让我们使用Angular CLI创建一个ApiService与我们的REST API后端进行通信:

ng generate service Api --module app.module.ts

这给出以下输出:

installing service
  create src/app/api.service.spec.ts
  create src/app/api.service.ts
  update src/app/app.module.ts

--module app.module.ts选项告诉Angular CLI不仅创建服务,而且还将其注册为app.module.ts定义的Angular模块中的提供者。

让我们打开src/app/api.service.ts

import { Injectable } from '@angular/core';

@Injectable()
export class ApiService {

  constructor() { }

}

接下来,我们注入我们的环境和Angular的内置HTTP服务:

import { Injectable } from '@angular/core';
import { environment } from 'environments/environment';
import { Http } from '@angular/http';

const API_URL = environment.apiUrl;

@Injectable()
export class ApiService {

  constructor(
    private http: Http
  ) {
  }

}

在实现所需的方法之前,让我们看一下Angular的HTTP服务。

如果您不熟悉语法,为什么不购买我们的高级课程Introducing TypeScript

Angular HTTP服务

Angular HTTP服务可以从@angular/http作为可注入类使用。

建立在XHR / JSONP之上,并为我们提供了HTTP客户端,可用于从Angular应用程序中发出HTTP请求。

以下方法可用于执行HTTP请求:

  • delete(url, options) :执行DELETE请求
  • get(url, options) :执行GET请求
  • head(url, options) :执行HEAD请求
  • options(url, options) :执行一个OPTIONS请求
  • patch(url, body, options) :执行PATCH请求
  • post(url, body, options) :执行POST请求
  • put(url, body, options) :执行PUT请求。

这些方法中的每一个都返回一个RxJS Observable。

与返回Promise的AngularJS 1.x HTTP服务方法相反,Angular HTTP服务方法返回Observables。

如果您还不熟悉RxJS Observables,请不要担心。 我们只需要基础知识即可启动并运行我们的应用程序。 当您的应用程序需要可用的运算符时,您可以逐渐了解更多信息。ReactiveX网站提供了出色的文档。

如果您想了解有关Observables的更多信息,那么还可以查看一下SitePoint的RxJS 函数式反应性编程简介

实施ApiService方法

如果我们回想一下端点,那么我们的REST API后端就会暴露出:

  • GET /todos :获取所有现有的待办事项

  • GET /todos/:id :获取现有的待办事项

  • POST /todos :创建一个新的待办事项

  • PUT /todos/:id :更新现有的待办事项

  • DELETE /todos/:id :删除现有的待办事项

我们已经可以创建所需方法及其对应的Angular HTTP方法的粗略概述:

import { Injectable } from '@angular/core';
import { environment } from 'environments/environment';

import { Http, Response } from '@angular/http';
import { Todo } from './todo';
import { Observable } from 'rxjs/Observable';

const API_URL = environment.apiUrl;

@Injectable()
export class ApiService {

  constructor(
    private http: Http
  ) {
  }

  // API: GET /todos
  public getAllTodos() {
    // will use this.http.get()
  }

  // API: POST /todos
  public createTodo(todo: Todo) {
    // will use this.http.post()
  }

  // API: GET /todos/:id
  public getTodoById(todoId: number) {
    // will use this.http.get()
  }

  // API: PUT /todos/:id
  public updateTodo(todo: Todo) {
    // will use this.http.put()
  }

  // DELETE /todos/:id
  public deleteTodoById(todoId: number) {
    // will use this.http.delete()
  }
}

让我们仔细看看每种方法。

getAllTodos()

使用getAllTodos()方法可以从API获取所有getAllTodos()

public getAllTodos(): Observable<Todo[]> {
  return this.http
    .get(API_URL + '/todos')
    .map(response => {
      const todos = response.json();
      return todos.map((todo) => new Todo(todo));
    })
    .catch(this.handleError);
}

首先,我们发出GET请求以从我们的API获取所有待办事项:

this.http
  .get(API_URL + '/todos')

这将返回一个Observable。

然后,我们在Observable上调用map()方法,将来自API的响应转换为Todo对象数组:

.map(response => {
  const todos = response.json();
  return todos.map((todo) => new Todo(todo));
})

传入的HTTP响应是一个字符串,因此我们首先调用response.json()将JSON字符串解析为其对应的JavaScript值。

然后,我们遍历API响应的待办事项,并返回一个Todo实例数组。 请注意, map()第二次使用是使用Array.prototype.map() ,而不是RxJS运算符。

最后,我们附加一个错误处理程序以将潜在错误记录到控制台:

.catch(this.handleError);

我们在单独的方法中定义错误处理程序,因此可以在其他方法中重用它:

private handleError (error: Response | any) {
  console.error('ApiService::handleError', error);
  return Observable.throw(error);
}

在运行此代码之前,我们必须从RxJS库导入必要的依赖项:

import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';
import 'rxjs/add/observable/throw';

请注意,RxJS库非常庞大。 建议不要仅使用所需的片段,而不要使用import * as Rx from 'rxjs/Rx'导入整个RxJS库。 这将大大减少最终代码包的大小。

在我们的应用程序中,我们导入Observable类:

import { Observable } from 'rxjs/Observable';

我们导入代码需要的三个运算符:

import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';
import 'rxjs/add/observable/throw';

导入运算符可确保我们的Observable实例具有附加的相应方法。

如果我们的代码中没有import 'rxjs/add/operator/map' ,则以下操作将无效:

this.http
  .get(API_URL + '/todos')
  .map(response => {
    const todos = response.json();
    return todos.map((todo) => new Todo(todo));
  })

这是因为this.http.get返回的Observable将没有map()方法。

我们只需导入一次运算符即可在您的应用程序中全局启用相应的Observable方法。 但是,多次导入它们不是问题,也不会增加结果包的大小。

getTodoById()

getTodoById()方法允许我们获得一个待办事项:

public getTodoById(todoId: number): Observable<Todo> {
  return this.http
    .get(API_URL + '/todos/' + todoId)
    .map(response => {
      return new Todo(response.json());
    })
    .catch(this.handleError);
}

我们的应用程序中不需要此方法,但其中包含的内容使您对它的外观有所了解。

createTodo()

createTodo()方法允许我们创建一个新的待办事项:

public createTodo(todo: Todo): Observable<Todo> {
  return this.http
    .post(API_URL + '/todos', todo)
    .map(response => {
      return new Todo(response.json());
    })
    .catch(this.handleError);
}

我们首先对我们的API执行POST请求,然后将数据作为第二个参数传递:

this.http.post(API_URL + '/todos', todo)

然后,我们将响应转换为Todo对象:

map(response => {
  return new Todo(response.json());
})

updateTodo()

updateTodo()方法允许我们更新单个待办事项:

public updateTodo(todo: Todo): Observable<Todo> {
  return this.http
    .put(API_URL + '/todos/' + todo.id, todo)
    .map(response => {
      return new Todo(response.json());
    })
    .catch(this.handleError);
}

我们首先对我们的API执行PUT请求,然后将数据作为第二个参数传递:

put(API_URL + '/todos/' + todo.id, todo)

然后,我们将响应转换为Todo对象:

map(response => {
  return new Todo(response.json());
})

deleteTodoById()

deleteTodoById()方法允许我们删除单个待办事项:

public deleteTodoById(todoId: number): Observable<null> {
  return this.http
    .delete(API_URL + '/todos/' + todoId)
    .map(response => null)
    .catch(this.handleError);
}

我们首先对我们的API执行DELETE请求:

delete(API_URL + '/todos/' + todoId)

然后,我们将响应转换为null

map(response => null)

我们真的不需要在这里转换响应,也可以省去这一行。 它只是为了让您了解如何在执行DELETE请求时如果API返回数据而如何处理响应。

这是我们的ApiService的完整代码:

import { Injectable } from '@angular/core';
import { environment } from 'environments/environment';

import { Http, Response } from '@angular/http';
import { Todo } from './todo';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';
import 'rxjs/add/observable/throw';

const API_URL = environment.apiUrl;

@Injectable()
export class ApiService {

  constructor(
    private http: Http
  ) {
  }

  public getAllTodos(): Observable<Todo[]> {
    return this.http
      .get(API_URL + '/todos')
      .map(response => {
        const todos = response.json();
        return todos.map((todo) => new Todo(todo));
      })
      .catch(this.handleError);
  }

  public createTodo(todo: Todo): Observable<Todo> {
    return this.http
      .post(API_URL + '/todos', todo)
      .map(response => {
        return new Todo(response.json());
      })
      .catch(this.handleError);
  }

  public getTodoById(todoId: number): Observable<Todo> {
    return this.http
      .get(API_URL + '/todos/' + todoId)
      .map(response => {
        return new Todo(response.json());
      })
      .catch(this.handleError);
  }

  public updateTodo(todo: Todo): Observable<Todo> {
    return this.http
      .put(API_URL + '/todos/' + todo.id, todo)
      .map(response => {
        return new Todo(response.json());
      })
      .catch(this.handleError);
  }

  public deleteTodoById(todoId: number): Observable<null> {
    return this.http
      .delete(API_URL + '/todos/' + todoId)
      .map(response => null)
      .catch(this.handleError);
  }

  private handleError (error: Response | any) {
    console.error('ApiService::handleError', error);
    return Observable.throw(error);
  }
}

现在我们已经有了ApiService ,我们可以使用它来让TodoDataService与REST API后端进行通信。

更新TodoDataService

当前,我们的TodoDataService将所有数据存储在内存中:

import {Injectable} from '@angular/core';
import {Todo} from './todo';

@Injectable()
export class TodoDataService {

  // Placeholder for last id so we can simulate
  // automatic incrementing of ids
  lastId: number = 0;

  // Placeholder for todos
  todos: Todo[] = [];

  constructor() {
  }

  // Simulate POST /todos
  addTodo(todo: Todo): TodoDataService {
    if (!todo.id) {
      todo.id = ++this.lastId;
    }
    this.todos.push(todo);
    return this;
  }

  // Simulate DELETE /todos/:id
  deleteTodoById(id: number): TodoDataService {
    this.todos = this.todos
      .filter(todo => todo.id !== id);
    return this;
  }

  // Simulate PUT /todos/:id
  updateTodoById(id: number, values: Object = {}): Todo {
    let todo = this.getTodoById(id);
    if (!todo) {
      return null;
    }
    Object.assign(todo, values);
    return todo;
  }

  // Simulate GET /todos
  getAllTodos(): Todo[] {
    return this.todos;
  }

  // Simulate GET /todos/:id
  getTodoById(id: number): Todo {
    return this.todos
      .filter(todo => todo.id === id)
      .pop();
  }

  // Toggle todo complete
  toggleTodoComplete(todo: Todo) {
    let updatedTodo = this.updateTodoById(todo.id, {
      complete: !todo.complete
    });
    return updatedTodo;
  }

}

为了让TodoDataService与REST API后端通信,我们必须注入新的ApiService

import { Injectable } from '@angular/core';
import { Todo } from './todo';
import { ApiService } from './api.service';
import { Observable } from 'rxjs/Observable';

@Injectable()
export class TodoDataService {

  constructor(
    private api: ApiService
  ) {
  }
}

我们还更新了其方法,以将所有工作委托给ApiService的相应方法:

import { Injectable } from '@angular/core';
import { Todo } from './todo';
import { ApiService } from './api.service';
import { Observable } from 'rxjs/Observable';

@Injectable()
export class TodoDataService {

  constructor(
    private api: ApiService
  ) {
  }

  // Simulate POST /todos
  addTodo(todo: Todo): Observable<Todo> {
    return this.api.createTodo(todo);
  }

  // Simulate DELETE /todos/:id
  deleteTodoById(todoId: number): Observable<Todo> {
    return this.api.deleteTodoById(todoId);
  }

  // Simulate PUT /todos/:id
  updateTodo(todo: Todo): Observable<Todo> {
    return this.api.updateTodo(todo);
  }

  // Simulate GET /todos
  getAllTodos(): Observable<Todo[]> {
    return this.api.getAllTodos();
  }

  // Simulate GET /todos/:id
  getTodoById(todoId: number): Observable<Todo> {
    return this.api.getTodoById(todoId);
  }

  // Toggle complete
  toggleTodoComplete(todo: Todo) {
    todo.complete = !todo.complete;
    return this.api.updateTodo(todo);
  }

}

我们的新方法实现看起来简单得多,因为数据逻辑现在由REST API后端处理。

但是,有一个重要的区别。 旧方法包含同步代码,并立即返回一个值。 更新的方法包含异步代码,并返回一个Observable。

这意味着我们还必须更新调用TodoDataService方法的代码以正确处理Observable。

更新AppComponent

当前, AppComponent希望TodoDataService直接返回JavaScript对象和数组:

import {Component} from '@angular/core';
import {TodoDataService} from './todo-data.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
  providers: [TodoDataService]
})
export class AppComponent {

  constructor(
    private todoDataService: TodoDataService
  ) {
  }

  onAddTodo(todo) {
    this.todoDataService.addTodo(todo);
  }

  onToggleTodoComplete(todo) {
    this.todoDataService.toggleTodoComplete(todo);
  }

  onRemoveTodo(todo) {
    this.todoDataService.deleteTodoById(todo.id);
  }

  get todos() {
    return this.todoDataService.getAllTodos();
  }

}

但是我们新的ApiService方法返回Observables。

与Promises类似,Observables本质上是异步的,因此我们必须更新代码以相应地处理Observable响应:

如果当前我们在get todos()调用TodoDataService.getAllTodos()方法:

// AppComponent

get todos() {
  return this.todoDataService.getAllTodos();
}

TodoDataService.getAllTodos()方法调用相应的ApiService.getAllTodos()方法:

// TodoDataService

getAllTodos(): Observable<Todo[]> {
  return this.api.getAllTodos();
}

这又指示Angular HTTP服务执行HTTP GET请求:

// ApiService

public getAllTodos(): Observable<Todo[]> {
  return this.http
    .get(API_URL + '/todos')
    .map(response => {
      const todos = response.json();
      return todos.map((todo) => new Todo(todo));
    })
    .catch(this.handleError);
}

但是,我们必须记住一件事!

只要我们不订阅以下对象返回的Observable:

this.todoDataService.getAllTodos()

没有实际的HTTP请求。

要订阅一个Observable,我们可以使用subscribe()方法,该方法带有三个参数:

  • onNext :当Observable发出新值时调用的函数
  • onError :当Observable抛出错误时调用的函数
  • onCompleted :当Observable正常终止时调用的函数。

让我们重写当前代码:

// AppComponent

get todos() {
  return this.todoDataService.getAllTodos();
}

在初始化AppComponent时,这将异步加载AppComponent

import { Component, OnInit } from '@angular/core';
import { TodoDataService } from './todo-data.service';
import { Todo } from './todo';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
  providers: [TodoDataService]
})
export class AppComponent implements OnInit {

  todos: Todo[] = [];

  constructor(
    private todoDataService: TodoDataService
  ) {
  }

  public ngOnInit() {
    this.todoDataService
      .getAllTodos()
      .subscribe(
        (todos) => {
          this.todos = todos;
        }
      );
  }
}

首先,我们定义一个公共属性todos ,并将其初始值设置为一个空数组。

然后,我们使用ngOnInit()方法订阅this.todoDataService.getAllTodos() ,当值this.todoDataService.getAllTodos()时,我们将其分配给this.todos ,覆盖其空数组的初始值。

现在,让我们更新onAddTodo(todo)方法以处理可观察到的响应:

// previously:
// onAddTodo(todo) {
//  this.todoDataService.addTodo(todo);
// }

onAddTodo(todo) {
  this.todoDataService
    .addTodo(todo)
    .subscribe(
      (newTodo) => {
        this.todos = this.todos.concat(newTodo);
      }
    );
}

再次,我们使用this.todoDataService.addTodo(todo) subscribe()方法订阅this.todoDataService.addTodo(todo)返回的Observable,并且当响应进入时,我们将新创建的todo添加到当前的todo列表中。

我们对其他方法重复相同的练习,直到AppComponent看起来像这样:

import { Component, OnInit } from '@angular/core';
import { TodoDataService } from './todo-data.service';
import { Todo } from './todo';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
  providers: [TodoDataService]
})
export class AppComponent implements OnInit {

  todos: Todo[] = [];

  constructor(
    private todoDataService: TodoDataService
  ) {
  }

  public ngOnInit() {
    this.todoDataService
      .getAllTodos()
      .subscribe(
        (todos) => {
          this.todos = todos;
        }
      );
  }

  onAddTodo(todo) {
    this.todoDataService
      .addTodo(todo)
      .subscribe(
        (newTodo) => {
          this.todos = this.todos.concat(newTodo);
        }
      );
  }

  onToggleTodoComplete(todo) {
    this.todoDataService
      .toggleTodoComplete(todo)
      .subscribe(
        (updatedTodo) => {
          todo = updatedTodo;
        }
      );
  }

  onRemoveTodo(todo) {
    this.todoDataService
      .deleteTodoById(todo.id)
      .subscribe(
        (_) => {
          this.todos = this.todos.filter((t) => t.id !== todo.id);
        }
      );
  }
}

而已; 现在,所有方法都能够处理TodoDataService方法返回的Observable。

请注意,当您订阅由Angular HTTP服务返回的Observable时,无需手动取消订阅。 Angular将为您清理所有内容以防止内存泄漏。

让我们看看一切是否按预期进行。

尝试一下

打开一个终端窗口。

从我们应用程序目录的根目录,启动REST API后端:

npm run json-server

打开第二个终端窗口。

同样,从我们应用程序目录的根目录中,服务Angular应用程序:

ng serve

现在,将浏览器导航到http://localhost:4200

如果一切顺利,您应该看到以下内容:

REST API后端:我们现在应该看到的

如果看到错误,可以将代码与GitHub上工作版本进行比较。

太棒了! 我们的应用程序现在正在与REST API后端通信!

提示:如果要在同一终端上运行npm run json-serverng serve ,则可以同时使用两个命令同时运行两个命令,而无需打开多个终端窗口或选项卡。

让我们运行我们的单元测试,以验证一切是否按预期工作。

运行我们的测试

打开第三个终端窗口。

同样,从应用程序目录的根目录运行单元测试:

ng test

看来11个单元测试失败了:

REST API后端:11个测试失败

让我们看看为什么我们的测试失败,以及如何修复它们。

修复我们的单元测试

首先,让我们打开src/todo-data.service.spec.ts

/* tslint:disable:no-unused-variable */

import {TestBed, async, inject} from '@angular/core/testing';
import {Todo} from './todo';
import {TodoDataService} from './todo-data.service';

describe('TodoDataService', () => {
  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [TodoDataService]
    });
  });

  it('should ...', inject([TodoDataService], (service: TodoDataService) => {
    expect(service).toBeTruthy();
  }));

  describe('#getAllTodos()', () => {

    it('should return an empty array by default', inject([TodoDataService], (service: TodoDataService) => {
      expect(service.getAllTodos()).toEqual([]);
    }));

    it('should return all todos', inject([TodoDataService], (service: TodoDataService) => {
      let todo1 = new Todo({title: 'Hello 1', complete: false});
      let todo2 = new Todo({title: 'Hello 2', complete: true});
      service.addTodo(todo1);
      service.addTodo(todo2);
      expect(service.getAllTodos()).toEqual([todo1, todo2]);
    }));

  });

  describe('#save(todo)', () => {

    it('should automatically assign an incrementing id', inject([TodoDataService], (service: TodoDataService) => {
      let todo1 = new Todo({title: 'Hello 1', complete: false});
      let todo2 = new Todo({title: 'Hello 2', complete: true});
      service.addTodo(todo1);
      service.addTodo(todo2);
      expect(service.getTodoById(1)).toEqual(todo1);
      expect(service.getTodoById(2)).toEqual(todo2);
    }));

  });

  describe('#deleteTodoById(id)', () => {

    it('should remove todo with the corresponding id', inject([TodoDataService], (service: TodoDataService) => {
      let todo1 = new Todo({title: 'Hello 1', complete: false});
      let todo2 = new Todo({title: 'Hello 2', complete: true});
      service.addTodo(todo1);
      service.addTodo(todo2);
      expect(service.getAllTodos()).toEqual([todo1, todo2]);
      service.deleteTodoById(1);
      expect(service.getAllTodos()).toEqual([todo2]);
      service.deleteTodoById(2);
      expect(service.getAllTodos()).toEqual([]);
    }));

    it('should not removing anything if todo with corresponding id is not found', inject([TodoDataService], (service: TodoDataService) => {
      let todo1 = new Todo({title: 'Hello 1', complete: false});
      let todo2 = new Todo({title: 'Hello 2', complete: true});
      service.addTodo(todo1);
      service.addTodo(todo2);
      expect(service.getAllTodos()).toEqual([todo1, todo2]);
      service.deleteTodoById(3);
      expect(service.getAllTodos()).toEqual([todo1, todo2]);
    }));

  });

  describe('#updateTodoById(id, values)', () => {

    it('should return todo with the corresponding id and updated data', inject([TodoDataService], (service: TodoDataService) => {
      let todo = new Todo({title: 'Hello 1', complete: false});
      service.addTodo(todo);
      let updatedTodo = service.updateTodoById(1, {
        title: 'new title'
      });
      expect(updatedTodo.title).toEqual('new title');
    }));

    it('should return null if todo is not found', inject([TodoDataService], (service: TodoDataService) => {
      let todo = new Todo({title: 'Hello 1', complete: false});
      service.addTodo(todo);
      let updatedTodo = service.updateTodoById(2, {
        title: 'new title'
      });
      expect(updatedTodo).toEqual(null);
    }));

  });

  describe('#toggleTodoComplete(todo)', () => {

    it('should return the updated todo with inverse complete status', inject([TodoDataService], (service: TodoDataService) => {
      let todo = new Todo({title: 'Hello 1', complete: false});
      service.addTodo(todo);
      let updatedTodo = service.toggleTodoComplete(todo);
      expect(updatedTodo.complete).toEqual(true);
      service.toggleTodoComplete(todo);
      expect(updatedTodo.complete).toEqual(false);
    }));

  });

});

大多数失败的单元测试都与检查数据处理有关。 不再需要这些测试,因为数据处理现在由我们的REST API后端而不是TodoDataService ,所以让我们删除过时的测试:

/* tslint:disable:no-unused-variable */

import {TestBed, inject} from '@angular/core/testing';
import {TodoDataService} from './todo-data.service';

describe('TodoDataService', () => {
  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        TodoDataService,
      ]
    });
  });

  it('should ...', inject([TodoDataService], (service: TodoDataService) => {
    expect(service).toBeTruthy();
  }));

});

如果现在运行单元测试,则会收到错误消息:

TodoDataService should ...
Error: No provider for ApiService!

引发错误是因为TestBed.configureTestingModule()创建了一个用于测试的临时模块,并且该临时模块的注入程序不知道任何ApiService

为了使注入器知道ApiService ,我们必须通过将ApiService作为提供程序中的提供程序列出来,将其注册到临时模块中,该配置对象将传递给TestBed.configureTestingModule()

/* tslint:disable:no-unused-variable */

import {TestBed, inject} from '@angular/core/testing';
import {TodoDataService} from './todo-data.service';
import { ApiService } from './api.service';

describe('TodoDataService', () => {
  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        TodoDataService,
        ApiService
      ]
    });
  });

  it('should ...', inject([TodoDataService], (service: TodoDataService) => {
    expect(service).toBeTruthy();
  }));

});

但是,如果执行此操作,则单元测试将使用真正的ApiService ,该ApiService连接到我们的REST API后端。

我们不希望测试运行程序在运行单元测试时连接到真实的API,因此让我们创建一个ApiMockService来模拟单元测试中的真实ApiService

创建一个ApiMockService

让我们使用Angular CLI生成一个新的ApiMockService

ng g service ApiMock --spec false

显示以下内容:

installing service
  create src/app/api-mock.service.ts
  WARNING Service is generated but not provided, it must be provided to be used

接下来,我们实现与ApiService相同的方法,但是我们让这些方法返回模拟数据,而不是发出HTTP请求:

import { Injectable } from '@angular/core';
import { Todo } from './todo';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';

@Injectable()
export class ApiMockService {

  constructor(
  ) {
  }

  public getAllTodos(): Observable<Todo[]> {
    return Observable.of([
      new Todo({id: 1, title: 'Read article', complete: false})
    ]);
  }

  public createTodo(todo: Todo): Observable<Todo> {
    return Observable.of(
      new Todo({id: 1, title: 'Read article', complete: false})
    );
  }

  public getTodoById(todoId: number): Observable<Todo> {
    return Observable.of(
      new Todo({id: 1, title: 'Read article', complete: false})
    );
  }

  public updateTodo(todo: Todo): Observable<Todo> {
    return Observable.of(
      new Todo({id: 1, title: 'Read article', complete: false})
    );
  }

  public deleteTodoById(todoId: number): Observable<null> {
    return null;
  }
}

注意每个方法如何返回新的新模拟数据。 这似乎有些重复,但这是一个好习惯。 如果一个单元测试将更改模拟数据,则更改将永远不会影响另一单元测试中的数据。

现在我们有了ApiMockService服务,我们可以用ApiService代替单元测试中的ApiMockService

让我们再次打开src/todo-data.service.spec.ts

providers阵列,我们告诉喷油器提供ApiMockService每当ApiService要求:

/* tslint:disable:no-unused-variable */

import {TestBed, inject} from '@angular/core/testing';
import {TodoDataService} from './todo-data.service';
import { ApiService } from './api.service';
import { ApiMockService } from './api-mock.service';

describe('TodoDataService', () => {
  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        TodoDataService,
        {
          provide: ApiService,
          useClass: ApiMockService
        }
      ]
    });
  });

  it('should ...', inject([TodoDataService], (service: TodoDataService) => {
    expect(service).toBeTruthy();
  }));

});

如果现在重新运行单元测试,则错误消失了。 大!

不过,我们还有另外两个失败的测试:

ApiService should ...
Error: No provider for Http!

AppComponent should create the app
Failed: No provider for ApiService!

REST API后端:无提供者

错误类似于我们刚刚解决的错误。

要解决第一个错误,我们打开src/api.service.spec.ts

import { TestBed, inject } from '@angular/core/testing';

import { ApiService } from './api.service';

describe('ApiService', () => {
  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [ApiService]
    });
  });

  it('should ...', inject([ApiService], (service: ApiService) => {
    expect(service).toBeTruthy();
  }));
});

测试失败,并显示一条消息No provider for Http! ,表明我们需要为Http添加提供程序。

同样,我们不希望Http服务发送实际的HTTP请求,因此我们实例化了使用Angular的MockBackend的模拟Http服务:

import { TestBed, inject } from '@angular/core/testing';

import { ApiService } from './api.service';
import { BaseRequestOptions, Http, XHRBackend } from '@angular/http';
import { MockBackend } from '@angular/http/testing';

describe('ApiService', () => {
  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        {
          provide: Http,
          useFactory: (backend, options) => {
            return new Http(backend, options);
          },
          deps: [MockBackend, BaseRequestOptions]
        },
        MockBackend,
        BaseRequestOptions,
        ApiService
      ]
    });
  });

  it('should ...', inject([ApiService], (service: ApiService) => {
    expect(service).toBeTruthy();
  }));
});

如果配置测试模块看起来有些繁琐,请不要担心。

您可以在用于测试Angular应用程序官方文档中了解有关设置单元测试的更多信息。

要修复最终错误:

AppComponent should create the app
Failed: No provider for ApiService!

让我们打开src/app.component.spec.ts

import { TestBed, async } from '@angular/core/testing';

import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { TodoDataService } from './todo-data.service';

describe('AppComponent', () => {
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [
        FormsModule
      ],
      declarations: [
        AppComponent
      ],
      providers: [
        TodoDataService
      ],
      schemas: [
        NO_ERRORS_SCHEMA
      ]
    }).compileComponents();
  }));

  it('should create the app', async(() => {
    const fixture = TestBed.createComponent(AppComponent);
    const app = fixture.debugElement.componentInstance;
    expect(app).toBeTruthy();
  }));
});

然后为注入器提供我们的模拟ApiService

import { TestBed, async } from '@angular/core/testing';

import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { TodoDataService } from './todo-data.service';
import { ApiService } from './api.service';
import { ApiMockService } from './api-mock.service';

describe('AppComponent', () => {
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [
        FormsModule
      ],
      declarations: [
        AppComponent
      ],
      providers: [
        TodoDataService,
        {
          provide: ApiService,
          useClass: ApiMockService
        }
      ],
      schemas: [
        NO_ERRORS_SCHEMA
      ]
    }).compileComponents();
  }));

  it('should create the app', async(() => {
    const fixture = TestBed.createComponent(AppComponent);
    const app = fixture.debugElement.componentInstance;
    expect(app).toBeTruthy();
  }));
});

欢呼! 我们所有的测试都通过了:

REST API后端:所有测试通过

我们已成功将Angular应用程序连接到REST API后端。

要将我们的应用程序部署到生产环境中,我们现在可以运行:

ng build --aot --environment prod

我们还将生成的dist目录上传到我们的托管服务器。 那有多甜?

让我们回顾一下我们学到的东西。

摘要

第一篇文章中 ,我们学习了如何:

  • 使用Angular CLI初始化我们的Todo应用程序
  • 创建一个Todo类来代表单个Todo
  • 创建TodoDataService服务以创建,更新和删除待办事项
  • 使用AppComponent组件显示用户界面
  • 将我们的应用程序部署到GitHub页面。

第二篇文章中 ,我们将AppComponent重构为将其大部分工作委托给:

  • TodoListComponent以显示TodoListComponent列表
  • TodoListItemComponent以显示单个待办事项
  • 一个TodoListHeaderComponent来创建一个新的待办事项
  • TodoListFooterComponent来显示还剩下多少个TodoListFooterComponent

在第三篇文章中,我们:

  • 创建了一个模拟REST API后端
  • 将API URL存储为环境变量
  • 创建了一个ApiService与REST API后端进行通信
  • 更新了TodoDataService以使用新的ApiService
  • 更新了AppComponent以处理异步API调用
  • 创建ApiMockService以避免在运行单元测试时进行真正的HTTP调用。

在此过程中,我们了解到:

  • 如何使用环境变量存储应用程序设置
  • 如何使用Angular HTTP客户端执行HTTP请求
  • 如何处理Angular HTTP客户端返回的Observable
  • 如何在运行单元测试时模拟HTTP调用以避免真实的HTTP请求。

这篇文章中的所有代码都可以在GitHub找到

在第四部分中,我们将介绍路由器并重构AppComponent以使用路由器从后端获取AppComponent

在第五部分中,我们将实现身份验证,以防止未经授权访问我们的应用程序。

本文由Vildan Softic进行同行评审。 感谢所有SitePoint的同行评审人员使SitePoint内容达到最佳状态!

From: https://www.sitepoint.com/angular-rxjs-create-api-service-rest-backend/

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Angular和RxJS:添加REST API后端 的相关文章