天天看點

rxjs angular_Angular和RxJS:添加REST API後端

rxjs angular

本文是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項目更新到最新版本。

你并不需要遵循第一和第二部分本教程為三來一補感。 您可以簡單地擷取我們的repo的副本,從第二部分中檢出代碼,并以此作為起點。 下面将對此進行詳細說明。

rxjs angular_Angular和RxJS:添加REST API後端

快速回顧

這是第2部分結尾處的應用程式體系結構:

rxjs angular_Angular和RxJS:添加REST API後端

目前,

TodoDataService

将所有資料存儲在記憶體中。 在第三篇文章中,我們将更新應用程式以與REST API後端進行通信。

我們會:

  • 建立一個模拟REST API後端
  • 将API URL存儲為環境變量
  • 建立一個

    ApiService

    與REST API後端進行通信
  • 更新

    TodoDataService

    以使用新的

    ApiService

  • 更新

    AppComponent

    以處理異步API調用
  • 建立一個

    ApiMockService

    以避免在運作單元測試時進行真正的HTTP調用。
rxjs angular_Angular和RxJS:添加REST API後端

到本文結尾,您将了解:

  • 如何使用環境變量存儲應用程式設定
  • 如何使用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
           
rxjs angular_Angular和RxJS:添加REST API後端
rxjs angular_Angular和RxJS:添加REST API後端

免費學習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應用程式中。

理想情況下,我們應該能夠做到這一點:

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

如果一切順利,您應該看到以下内容:

rxjs angular_Angular和RxJS:添加REST API後端

如果看到錯誤,可以将代碼與GitHub上的工作版本進行比較。

太棒了! 我們的應用程式現在正在與REST API後端通信!

提示:如果要在同一終端上運作

npm run json-server

ng serve

,則可以同時使用兩個指令同時運作兩個指令,而無需打開多個終端視窗或頁籤。

讓我們運作我們的單元測試以驗證一切是否按預期進行。

運作我們的測試

打開第三個終端視窗。

同樣,從應用程式目錄的根目錄運作單元測試:

ng test
           

看來11個單元測試失敗了:

rxjs angular_Angular和RxJS:添加REST API後端

讓我們看看為什麼測試失敗,以及如何修複它們。

修複我們的單元測試

首先,讓我們打開

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!
           
rxjs angular_Angular和RxJS:添加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();
  }));
});
           

歡呼! 我們所有的測試都通過了:

rxjs angular_Angular和RxJS:添加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内容達到最佳狀态!

翻譯自: https://www.sitepoint.com/angular-rxjs-create-api-service-rest-backend/

rxjs angular