1. 程式人生 > >ReactiveX 學習筆記(26)使用 RxJS + React.js 呼叫 REST API

ReactiveX 學習筆記(26)使用 RxJS + React.js 呼叫 REST API

JSON : Placeholder

JSON : Placeholder (https://jsonplaceholder.typicode.com/) 是一個用於測試的 REST API 網站。
以下使用 RxJS6 + React.js 呼叫該網站的 REST API,獲取字串以及 JSON 資料。

  • GET /posts/1
  • GET /posts
  • POST /posts
  • PUT /posts/1
  • DELETE /posts/1

所有 GET API 都返回JSON資料,格式(JSON-Schema)如下:

{
  "type":"object",
  "properties": {
    "userId": {"type" : "integer"},
    "id": {"type" : "integer"},
    "title": {"type" : "string"},
    "body": {"type" : "string"}
  }
}

建立工程

# 安裝 CLI
$ npm install -g create-react-app
# 建立新的應用程式 RxExample
$ create-react-app rx-example --scripts-version=react-scripts-ts
$ cd rx-example
$ npm start

開啟 Intellij IDEA, File / New / Project From Existing Sources...,然後選中工程所在資料夾
在嚮導的第1頁選擇 Create project from existing sources
完成嚮導後在點選 Finish 建立工程。

點選 Add Configurations, 點選 +npm
Name: React CLI Server
Scripts: start
點選 OK 完成配置。
點選 React CLI Server 啟動程式。
http://localhost:3000/ 可開啟網頁。

TSLint

開啟 tslint.json
新增 rules

,
  "rules": {
    "interface-name": false,
    "ordered-imports": false,
    "no-console": false,
    "object-literal-sort-keys": false,
    "member-access": false,
    "variable-name": false,
    "member-ordering": false,
    "class-name": false
  }

Post

在 src 資料夾下新增 post.ts,內容如下

export class Post {
  userId!: number;
  id!: number;
  title!: string;
  body!: string;
  toString(): string {
    return `Post {userId = ${this.userId}, id = ${this.id}, title = "${this.title}", body = "${this.body.replace(/\n/g, '\\n')}"}`;
  }
}

安裝 react.di

react.di 是一個在 React.js 中實現 DI(依賴注入) 的包。
使用這個包需要安裝 react.di 和 reflect-metadata。

$ npm install [email protected] reflect-metadata --save

開啟 tsconfig.json 在 compilerOptions 中
新增關於 emitDecoratorMetadata 的設定。

    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,

安裝 Rxios

訪問 API 採用將 RxJS 和 axios 合為一體的 Rxios 元件,但是該元件尚未升級到 RxJS6。
這裡我們只安裝 RxJS 和 axios,然後直接以原始碼形式引入 Rxios,並將其升級到 RxJS6。

$ npm install axios rxjs

在 src 資料夾下新增 rxios.ts,內容如下

// https://github.com/davguij/rxios/blob/master/src/index.ts
// ported to rxjs6

import axios, { AxiosInstance, AxiosRequestConfig, AxiosPromise } from 'axios';
// import { Observable } from 'rxjs/Observable';
import { Observable } from 'rxjs';

export interface rxiosConfig extends AxiosRequestConfig {
  localCache?: boolean;
}

class rxios {
  private _httpClient: AxiosInstance;

  constructor(options: rxiosConfig = {}) {
    this._httpClient = axios.create(options);
  }

  private _makeRequest<T>(method: string, url: string, queryParams?: object, body?: object) {
    let request: AxiosPromise<T>;
    switch (method) {
      case 'GET':
        request = this._httpClient.get<T>(url, {params: queryParams});
        break;
      case 'POST':
        request = this._httpClient.post<T>(url, body, {params: queryParams});
        break;
      case 'PUT':
        request = this._httpClient.put<T>(url, body, {params: queryParams});
        break;
      case 'PATCH':
        request = this._httpClient.patch<T>(url, body, {params: queryParams});
        break;
      case 'DELETE':
        request = this._httpClient.delete(url, {params: queryParams});
        break;

      default:
        throw new Error('Method not supported');
    }
    return new Observable<T>(subscriber => {
      request.then(response => {
        subscriber.next(response.data);
        subscriber.complete();
      }).catch((err: Error) => {
        subscriber.error(err);
        subscriber.complete();
      });
    });
  }

  public get<T>(url: string, queryParams?: object) {
    return this._makeRequest<T>('GET', url, queryParams);
  }

  public post<T>(url: string, body: object, queryParams?: object) {
    return this._makeRequest<T>('POST', url, queryParams, body);
  }

  public put<T>(url: string, body: object, queryParams?: object) {
    return this._makeRequest<T>('PUT', url, queryParams, body);
  }

  public patch<T>(url: string, body: object, queryParams?: object) {
    return this._makeRequest<T>('PATCH', url, queryParams, body);
  }

  public delete(url: string, queryParams?: object) {
    return this._makeRequest('DELETE', url, queryParams);
  }
}

export {rxios, rxios as Rxios};

post 服務

在 src 資料夾下新增 post.service.ts,內容如下

import { Injectable } from 'react.di';
import { Observable, from } from 'rxjs';
import { map, mergeAll, take, tap } from 'rxjs/operators';
import { Post } from './post';
import { Rxios } from './rxios';

@Injectable
export class PostService {
  private readonly http = new Rxios();
  private readonly baseUrl = 'http://jsonplaceholder.typicode.com/';

  constructor() {
    this.getPostAsString().subscribe();
    this.getPostAsJson().subscribe();
    this.getPosts(2).subscribe();
    this.createPost().subscribe();
    this.updatePost().subscribe();
    this.deletePost().subscribe();
  }

  private getPostAsString(): Observable<string> {
    const url = `${this.baseUrl}posts/1`;
    return this.http.get(url, { responseType: 'text' })
      .pipe(
        tap((result: any) => console.log(result)),
      );
  }

  private getPostAsJson(): Observable<Post> {
    const url = `${this.baseUrl}posts/1`;
    return this.http.get<Post>(url)
      .pipe(
        map((result: any) => Object.assign(new Post(), result)),
        tap((result: any) => console.log('' + result)),
      );
  }

  private getPosts(n: number): Observable<Post> {
    const url = `${this.baseUrl}posts`;
    return from(this.http.get<Post[]>(url))
      .pipe(
        mergeAll(),
        map((result: any) => Object.assign(new Post(), result)),
        take(n),
        tap((result: any) => console.log('' + result)),
      );
  }

  private createPost(): Observable<string> {
    const url = `${this.baseUrl}posts`;
    return this.http.post(url, {
      params: {
        userId: 101,
        title: 'test title',
        body: 'test body',
      }
    })
      .pipe(
        map((result: any) => JSON.stringify(result)),
        tap((result: any) => console.log(result)),
      );
  }

  private updatePost(): Observable<string> {
    const url = `${this.baseUrl}posts/1`;
    return this.http.put(url, {
      params: {
        userId: 101,
        title: 'test title',
        body: 'test body',
      }
    })
      .pipe(
        map((result: any) => JSON.stringify(result)),
        tap((result: any) => console.log(result)),
      );
  }

  private deletePost(): Observable<string> {
    const url = `${this.baseUrl}posts/1`;
    return this.http.delete(url)
      .pipe(
        map((result: any) => JSON.stringify(result)),
        tap((result: any) => console.log(result)),
      );
  }
}
  • getPostAsString 方法取出第1個Post,返回字串
  • getPostAsJson 方法取出第1個Post,返回Post物件
  • getPosts 方法取出前n個Post,返回n個Post物件
  • createPost 方法建立1個Post,返回字串
  • updatePost 方法更新第1個Post,返回字串
  • deletePost 方法刪除第1個Post,返回字串

刪除 HelloWorld 元件,即 src/components/HelloWorld.vue 檔案。

開啟 App.tsx,將其改為

import * as React from 'react';
import './App.css';

import logo from './logo.svg';
import { PostService } from './post.service';
import { Inject, Module } from 'react.di';

@Module({
  providers: [
    PostService,
  ],
})
class App extends React.Component {
  @Inject postService!: PostService;

  componentDidMount() {
    console.log(this.postService);
  }

  public render() {
    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <h1 className="App-title">Welcome to React</h1>
        </header>
        <p className="App-intro">
          To get started, edit <code>src/App.tsx</code> and save to reload.
        </p>
      </div>
    );
  }
}

export default App;

輸出結果

Post {userId = 1, id = 1, title = "sunt aut facere repellat provident occaecati excepturi optio reprehenderit", body = "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"}
Post {userId = 1, id = 1, title = "sunt aut facere repellat provident occaecati excepturi optio reprehenderit", body = "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"}
Post {userId = 1, id = 2, title = "qui est esse", body = "est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla"}
{userId: 1, id: 1, title: "sunt aut facere repellat provident occaecati excepturi optio reprehenderit", body: "quia et suscipit↵suscipit recusandae consequuntur …strum rerum est autem sunt rem eveniet architecto"}
{"params":{"userId":101,"title":"test title","body":"test body"},"id":101}
{"params":{"userId":101,"title":"test title","body":"test body"},"id":1}
{}