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存儲為環境變量
- 建立一個
與REST API後端進行通信ApiService
- 更新
以使用新的TodoDataService
ApiService
- 更新
以處理異步API調用AppComponent
- 建立一個
以避免在運作單元測試時進行真正的HTTP調用。ApiMockService
到本文結尾,您将了解:
- 如何使用環境變量存儲應用程式設定
- 如何使用Angular HTTP用戶端執行HTTP請求
- 如何處理Angular HTTP用戶端傳回的Observable
- 如何在運作單元測試時模拟HTTP調用以避免發出真實的HTTP請求。
是以,讓我們開始吧!
啟動并運作
確定您已安裝最新版本的Angular CLI。 如果沒有安裝,則可以使用以下指令進行安裝:
npm install -g @angular/[email protected]
如果需要删除以前版本的Angular CLI,則可以:
npm uninstall -g @angular/cli angular-cli
npm cache clean
npm install -g @angular/[email protected]
之後,您将需要第二部分的代碼副本。 這在GitHub上可用 。 本系列中的每篇文章在存儲庫中都有一個相應的标記,是以您可以在應用程式的不同狀态之間來回切換。
我們在第二部分結尾并且在本文開始的代碼被标記為part-2 。 本文結尾處的代碼被标記為part-3 。
您可以将标簽視為特定送出ID的别名。 您可以使用
git checkout
在它們之間切換。 您可以在此處閱讀更多内容 。
是以,要啟動并運作(安裝了最新版本的Angular CLI),我們可以這樣做:
git clone [email protected]: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請求delete(url, options)
-
:執行GET請求get(url, options)
-
:執行一個HEAD請求head(url, options)
-
:執行一個OPTIONS請求options(url, options)
-
:執行PATCH請求patch(url, body, options)
-
:執行POST請求post(url, body, options)
-
:執行一個PUT請求。put(url, body, options)
這些方法中的每一個都傳回一個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()
方法,該方法帶有三個參數:
-
:當Observable發出新值時調用的函數onNext
-
:當Observable抛出錯誤時調用的函數onError
-
:當Observable正常終止時調用的函數。onCompleted
讓我們重寫目前代碼:
// 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存儲為環境變量
- 建立了一個
與REST API後端進行通信ApiService
- 更新了
以使用新的TodoDataService
ApiService
- 更新了
以處理異步API調用AppComponent
- 建立
以避免在運作單元測試時進行真正的HTTP調用。ApiMockService
在此過程中,我們了解到:
- 如何使用環境變量存儲應用程式設定
- 如何使用Angular HTTP用戶端執行HTTP請求
- 如何處理Angular HTTP用戶端傳回的Observable
- 如何在運作單元測試時模拟HTTP調用以避免真實的HTTP請求。
這篇文章中的所有代碼都可以在GitHub上找到 。
在第四部分中,我們将介紹路由器并重構
AppComponent
以使用路由器從後端擷取
AppComponent
。
在第五部分中,我們将實作身份驗證以防止未經授權通路我們的應用程式。
本文由Vildan Softic同行評審。 感謝所有SitePoint的同行評審員使SitePoint内容達到最佳狀态!
翻譯自: https://www.sitepoint.com/angular-rxjs-create-api-service-rest-backend/
rxjs angular