使用Angular和RxJS创建一个与REST后端应用进行交互的API服务
栏目: JavaScript · 发布时间: 7年前
内容简介:使用Angular和RxJS创建一个与REST后端应用进行交互的API服务
我们在 第1部分 中学习了如何创建 Todo 应用、运行它以及将其发布在 Github Pages 上。这很好,但不爽的是整个应用都被塞在一个组件中。
我们在 第2部分 体验了模块化组件架构并学习了如何把一个组件拆分成树形结构的多个小组件,这样更容易理解、重用和维护。
在这一部分中,我们会更新这个应用程序,以便与 REST API 后端通信。
你 不必 先去看第1部分和第2部分,第3部分是可以独立阅读的。你可以从 我们的库 取出第2部分产生的代码,以此作为起点。下面对此进行详细说明。
快速回顾
第2部分结束的时候我们的应用结构如下所示:
这时候 TodoDataService
把所有数据保存在内存中。在这第三部分中,我们会更新应用与后端的 REST API 通信,通过这种方式来代替保存数据的方案。
我们会:
- 创建一个模拟的后端 REST API
- 把 API URL 保存为环境变量
- 创建
ApiService
来与 REST API 通信 - 更新
TodoDataService
来使用新的ApiService
- 更新
AppComponent
处理异步 API 调用 - 创建
ApiMockService
来避免在单元测试中使用真实的 HTTP
文章的最后你将理解:
- 如何使用环境变量来存储应用设置
- 如何使用 Angular HTTP 客户端来执行 HTTP 请求
- 如何处理 Angular HTTP 客户端返回的 Observables
- 如何对模拟 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
之后,您将需要从第二部分中获得代码的副本。。 这可以在 https://github.com/sitepoint-editors/angular-todo-app 获得。 该系列中的每篇文章都在存储库中具有相应的标签,因此您可以在应用程序的不同状态之间来回切换。
我们在 第二部分 中结束的代码,以及在本文中我们开始的代码被标记为 第2部分 。 我们结束这篇文章的代码被标记为 第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 } ] }
最后,添加一个script到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
就是这样! 现在我们有了一个监听3000端口的REST API。
为了验证后端是否按预期运行,可以将浏览器导航到 http://localhost:3000。
支持以下端点:
-
GET /todos
: 获取所有存在的todo -
GET /todos/:id
: 获取一个存在的todo -
POST /todos
: 创建一个新的todo -
PUT /todos/:id
: 更新一个存在的todo -
DELETE /todos/:id
: 删除一个存在的todo
如果你将浏览器导航到 http://localhost:3000/todos
, 你将会从db.json看到一个包含所有todo的JSON响应。
要了解有关json-server的更多信息,请务必查看 使用json-server模拟REST API 。
存储API URL
现在我们的后端已经到位,我们必须将其URL存储在我们的Angular应用程序中。
理想情况下,我们应该能够:
- 将URL存储在一个地方,以便当我们需要更改其值时只需要更改一次
- 使我们的应用程序在开发过程中连接到开发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 serve
或ng build时,Angular CLI使用开发环境中指定的值(src/environments/environment.ts)。
但是,当我们运行ng serve --environment prod或ng build --environment prod时,Angular CLI使用src/environments/environment.prod.ts中指定的值。
为开发和生产使用不同的API URL而无需更改代码,这正是我们需要做的。
本系列中的应用程序不托管在生产环境中,因此我们在开发和生产环境中指定相同的API URL。 这使我们能够在本地运行 ng serve --environment prod
或 ng build --environment prod
,以查看一切是否符合预期。
您可以在.angle-cli.json中找到dev和prod之间的映射及其对应的环境文件:
"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模块中的provider。
打开 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服务。
如果您不熟悉语法,为什么不去购买我们的高级课程 引入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。
与AngularJS 1.x HTTP服务方法(返回promises)相反,Angular HTTP服务方法返回了Observables。
如果您还不熟悉RxJS Observables,请不要担心。 我们只需要使我们的应用程序启动并运行的基础知识。 当您的应用程序需要它们时,您可以逐步了解更多可用的操作符,并且 ReactiveX网站 提供了绝妙的文档。
如果您想了解有关Observables的更多信息,SitePoin上的文章 使用RxJS介绍函数响应式编程 也值得一看。
实现apiservice方法
如果我们回想一下我们的REST API后端暴露的端点:
-
GET /todos
: 获取所有存在的todo -
GET /todos/:id
: 获取存在的todo -
POST /todos
: 创建一个新的todo -
PUT /todos/:id
: 更新存在的todo -
DELETE /todos/:id
: 删除存在的todo
我们已经可以粗略地勾勒出我们需要的方法以及与他们相应的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获取所有todos:
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); }
首先,我们从API发起一个获取所有todo的GET请求:
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,并返回一个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';
并导入我们的代码需要的三个operator:
import 'rxjs/add/operator/map'; import 'rxjs/add/operator/catch'; import 'rxjs/add/observable/throw';
导入operator确保我们的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()方法。
我们只需要导入一次operator,以便在应用程序中全局启用相应的Observable方法。 然而,多次导入也没有问题,不会增加生成的包的大小。
getTodoById()
getTodoById()方法允许我们获得一个todo:
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()
方法可以使我们创建一个新的todo:
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()
方法可以更新单个的todo对象:
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()
The deleteTodoById()
method allows us to delete a single todo:
public deleteTodoById(todoId: number): Observable<null> { return this.http .delete(API_URL + '/todos/' + todoId) .map(response => null) .catch(this.handleError); }
We first perform a DELETE request to our API:
delete(API_URL + '/todos/' + todoId)
and then transform the response into null
:
map(response => null)
We don’t really need to transform the response here and could leave out this line. It is just included to give you an idea of how you could process the response if you API would return data when you perform a DELETE request.
以下是我们的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 id's lastId: number = 0; // Placeholder for todo's 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); } }
Our new method implementations look a lot simpler because the data logic is now handled by the REST API.
However, there is an important difference. The old methods contained synchronous code and immediately returned a value. The updated methods contain asynchronous code and return an Observable.
This means we also have to update the code that is calling the TodoDataService
methods to handle Observables correctly.
Updating AppComponent
Currently, the AppComponent
expects the TodoDataService
to directly return JavaScript objects and arrays:
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(); } }
but our new ApiService
methods return Observables.
Similar to Promises, Observables are asynchronous in nature, so we have to update the code to handle the Observable responses accordingly:
If we currently call the TodoDataService.getAllTodos()
method in get todos()
:
// AppComponent get todos() { return this.todoDataService.getAllTodos(); }
the TodoDataService.getAllTodos()
method calls the corresponding ApiService.getAllTodos()
method:
// TodoDataService getAllTodos(): Observable<Todo[]> { return this.api.getAllTodos(); }
which, in turn, instructs the Angular HTTP service to perform an HTTP GET request:
// 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); }
However, there is one important thing we have to remember!
As long as we don’t subscribe to the Observable returned by:
this.todoDataService.getAllTodos()
no actual HTTP request is made.
To subscribe to an Observable, we can use the subscribe()
method, which takes 3 arguments:
-
onNext
: function that is called when the Observable emits a new value -
onError
: function that is called when the Observable throws an error -
onCompleted
: function that is called when the Observable has gracefully terminated
让我们重写当前代码:
// AppComponent get todos() { return this.todoDataService.getAllTodos(); }
在AppComponent初始化时异步加载todo:
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.todos,覆盖其空数组的初始值。
现在我们来更新onAddTodo(todo)方法来处理一个Observable响应:
// previously: // onAddTodo(todo) { // this.todoDataService.addTodo(todo); // } onAddTodo(todo) { this.todoDataService .addTodo(todo) .subscribe( (newTodo) => { this.todos = this.todos.concat(newTodo); } ); }
再次,我们使用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); } ); } }
That’s it, all methods are now capable of handling Observables returned by the TodoDataService
methods.
Notice that there is no need to unsubscribe manually when you subscribe to an Observable that is returned by the Angular HTTP service. Angular will clean up everything for you to prevent memory leaks.
Let’s see if everything is working as expected.
Trying it Out
Open a terminal window.
From the root of our application directory, start the REST API back-end:
npm run json-server
Open a second terminal window.
Again, from the root of our application directory, serve the Angular application:
ng serve
现在,将您的浏览器导航到 http://localhost:4200
.
如果一切顺利,你应该会看到:
如果您看到错误,您可以将代码与 GitHub上的工作版本 进行比较。
真棒! 我们的应用程序正在与REST API通信!
小提示:如果你想要在同一终端中运行 npm run json-server
和ng serve,您可以 并发 运行这两个命令,而不用打开多个终端窗口或选项卡。
我们来运行我们的单元测试,以验证一切都按预期工作。
运行我们的测试
打开第三个终端窗口。
再一次,从您的应用程序的根目录,运行单元测试:
ng test
看来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,我们必须通过在传递给TestBed.configureTestingModule()的配置对象中列出ApiService作为provider来注册临时模块:
/* 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,它连接到我们的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请求:
注意每个方法如何返回新的模拟数据。 这似乎有点重复,但确实是一个很好的做法。 如果一个单元测试会更改模拟数据,该更改将永远不会影响另一个单元测试中的数据。
现在我们有一个ApiMockService服务,我们可以在ApiMockService的单元测试中替换ApiService。
我们再次打开src/todo-data.service.spec.ts。
在providers数组中,每当请求ApiService时,我们告诉注入器提供ApiMockService:
/* 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!
这些错误与我们刚刚修复的错误类似。
要修复第一个错误,让我们打开 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添加一个provider。
再次,我们不希望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(); })); });
Hurray! All our tests are passing:
We have successfully connected our Angular application to our REST API back-end.
To deploy our application to a production environment, we can now run:
ng build --aot --environment prod
and upload the generated dist
directory to our hosting server. How sweet is that?
Let’s recap what we have learned.
Summary
In the first article , we learned how to:
- initialize our Todo application using Angular CLI
- create a
Todo
class to represent individual todo’s - create a
TodoDataService
service to create, update and remove todo’s - use the
AppComponent
component to display the user interface - deploy our application to GitHub pages
In the second article , we refactored AppComponent
to delegate most of its work to:
- a
TodoListComponent
to display a list of todo’s - a
TodoListItemComponent
to display a single todo - a
TodoListHeaderComponent
to create a new todo - a
TodoListFooterComponent
to show how many todo’s are left
In this third article, we:
- created a mock REST API back-end
- stored the API URL as an environment variable
- created an
ApiService
to communicate with the REST API - updated the
TodoDataService
to use the newApiService
- updated the
AppComponent
to handle asynchronous API calls - created an
ApiMockService
to avoid real HTTP calls when running unit tests
In the process, we learned:
- how to use environment variables to store application settings
- how to use the Angular HTTP client to perform HTTP requests
- how to deal with Observables that are returned by the Angular HTTP client
- how to mock HTTP calls to avoid real HTTP requests when running unit tests
All code from this article is available at https://github.com/sitepoint-editors/angular-todo-app/tree/part-3 .
In part four, we will introduce the router and refactor AppComponent
to use the router to fetch the todo’s from the back-end.
In part five, we will implement authentication to prevent unauthorized access to our application.
So stay tuned for more!
以上所述就是小编给大家介绍的《使用Angular和RxJS创建一个与REST后端应用进行交互的API服务》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 如何与以太坊区块链交互并用Python和SQL创建数据库
- iOS 12 人机交互指南:交互(User Interaction)
- 生活NLP云服务“玩秘”站稳人机交互2.0语音交互场景
- asyncio之子进程交互
- 以太坊交互工具
- 学习 PixiJS — 交互工具
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。