rxjs angular
本文是SitePoint Angular 2+教程的第3部分,该教程介绍如何使用Angular CLI创建CRUD应用程序。 在本文中,我们将更新我们的应用程序以与REST API后端进行通信。
更喜欢使用分步视频课程学习Angular? 退房 了解角5 上SitePoint保费。
在第一部分中,我们学习了如何启动和运行Todo应用程序并将其部署到GitHub页面。 这样做很好,但不幸的是,整个应用程序都挤在一个组件中。
在第二部分中,我们研究了模块化程度更高的组件体系结构,并学习了如何将单个组件分解为较小的组件的结构化树,这些树更易于理解,重用和维护。
- 第0部分— Ultimate Angular CLI参考指南
- 第1部分-启动并运行我们的Todo应用程序的第一个版本
- 第2部分-创建单独的组件以显示待办事项列表和一个待办事项
- 第3部分-更新Todo服务以与REST API后端进行通信
- 第4部分-使用Angular路由器解析数据
- 第5部分-添加身份验证以保护私有内容
- 第6部分—如何将Angular项目更新到最新版本。
你并不需要遵循第一和第二部分本教程为三来一补感。 您可以简单地获取我们的repo的副本,从第二部分中检出代码,并以此作为起点。 下面将对此进行详细说明。
快速回顾
这是第2部分结尾处的应用程序体系结构:
当前, TodoDataService
将所有数据存储在内存中。 在第三篇文章中,我们将更新应用程序以与REST API后端进行通信。
我们会:
- 创建一个模拟REST API后端
- 将API URL存储为环境变量
- 创建一个
ApiService
与REST API后端进行通信 - 更新
TodoDataService
以使用新的ApiService
- 更新
AppComponent
以处理异步API调用 - 创建一个
ApiMockService
以避免在运行单元测试时进行真正的HTTP调用。
到本文结尾,您将了解:
- 如何使用环境变量存储应用程序设置
- 如何使用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
免费学习PHP!
全面介绍PHP和MySQL,从而实现服务器端编程的飞跃。
原价$ 11.95 您的完全免费
然后访问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应用程序中。
理想情况下,我们应该能够做到这一点:
- 将网址存储在一个位置,以便我们只需要在需要更改其值时更改一次
- 使我们的应用程序在开发期间连接到开发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
,以查看一切是否按预期工作。
您可以在.angular-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模块中的提供者。
让我们打开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 函数式React性编程简介 。
实施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
。
如果一切顺利,您应该看到以下内容:
如果看到错误,可以将代码与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
,我们必须通过将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!
错误类似于我们刚刚解决的错误。
要解决第一个错误,我们打开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();
}));
});
欢呼! 我们所有的测试都通过了:
我们已成功将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请求。
在第四部分中,我们将介绍路由器并重构AppComponent
以使用路由器从后端获取AppComponent
。
在第五部分中,我们将实现身份验证以防止未经授权访问我们的应用程序。
本文由Vildan Softic同行评审。 感谢所有SitePoint的同行评审员使SitePoint内容达到最佳状态!
翻译自: https://www.sitepoint.com/angular-rxjs-create-api-service-rest-backend/
rxjs angular