[譯] 如何使用 RxJS 6 + Recompose 在 React 中構建 Github 搜索功能

原文連接:How to build GitHub search functionality in React with RxJS 6 and Recompose
原文做者:Yazeed Bzadough;發表於2018年8月7日
譯者:yk;如需轉載,請註明出處,謝謝合做!javascript

本篇文章適合有 React 和 RxJS 使用經驗的讀者。如下僅僅是我我的在設計下面這個 UI 時以爲有用的模式,在此分享給你們。css

這將會是咱們的成果:html

沒有 Class,沒有生命週期鉤子,也沒有 setStatejava

安裝

全部代碼均可以在我 Github 上找到。react

git clone [https://github.com/yazeedb/recompose-github-ui](https://github.com/yazeedb/recompose-github-ui)
cd recompose-github-ui
yarn install
複製代碼

master 分支是一個已完成的項目,若是你想要獨自繼續開發的話,能夠新建一個 start 分支。git

git checkout start
複製代碼

而後運行程序。github

npm start
複製代碼

應用會運行在 localhost:3000,這是最初的效果。ajax

用你最喜歡的編輯器打開項目,進入 src/index.jsshell

Recompose

若是你以前沒見過 Recompose,我會告訴你這玩意兒是一個很是棒的 React 工具集,可讓你以函數式編程的風格來編寫組件。該工具集提供了很是多的功能,在其中作出選擇真不是件容易事兒。npm

它就至關於應用在 React 裏的 Lodash/Ramda。

另外,令我驚喜的是,他們還支持 observables(可觀察對象)。引用文檔裏的一句話:

事實證實,大部分 React 組件的 API 均可以用 observable 來替代。

今天咱們就來實踐這個概念!😁

讓咱們的組件「流」起來

假如如今有一個普通的 React 組件 App,咱們能夠經過使用 Recompose 的 componentFromStream 函數來以 observable 的方式這個從新定義這個組件。

這個函數最初會渲染一個值爲 null 的組件,一旦咱們的 observable 返回了一個新的值,該組件就會被從新渲染。

快速配置

Recompose 的流遵循了 ECMAScript 的 Observable 提案。該提案指出了 observables 在最終交付給現代瀏覽器時應該如何運做。

在提案的內容被徹底實現以前,咱們只能依賴於相似 RxJS,xstream,most,Flyd 等等庫。

Recompose 並不知道咱們使用的具體是哪一個庫,所以它提供了 setObservableConfig 來將 ES Observable 轉換爲任何咱們須要的形式。

首先,在 src 中建立一個名爲 observableConfig.js 的文件。

而後添加以下代碼,使 Recompose 兼容 RxJS 6:

import { from } from 'rxjs';
import { setObservableConfig } from 'recompose';

setObservableConfig({
  fromESObservable: from
});
複製代碼

將其導入至 index.js

import './observableConfig';
複製代碼

準備完畢!

Recompose + RxJS

導入 componentFromStream

import React from 'react';
import ReactDOM from 'react-dom';
import { componentFromStream } from 'recompose';
import './styles.css';
import './observableConfig';
複製代碼

開始從新定義 app

const App = componentFromStream(prop$ => {
  ...
});
複製代碼

注意,componentFromStream 須要一個回調函數做爲參數,該回調函數訂閱了一個 prop$ 數據流。想法是將咱們的 props 轉變爲一個 observable,而後再將它們映射到 React 組件裏。

若是你用過 RxJS,那麼你應該知道哪一種操做符最適合拿來作 映射(map)。

Map

顧名思義,該操做符用於將 Observable(something) 轉變爲 Observable(somethingElse)。在咱們的例子中,則是將 Observable(props) 轉變爲 Observable(component)

導入 map 操做符:

import { map } from 'rxjs/operators';
複製代碼

而後從新定義 App:

const App = componentFromStream(prop$ => {
  return prop$.pipe(
    map(() => (
      <div> <input placeholder="GitHub username" /> </div> )) ) }); 複製代碼

自 RxJS 5 以來,都應當使用 pipe 來代替鏈接操做符。

保存並查看效果,果不其然!

添加一個事件處理器

如今,讓咱們把 input 變得更 reactive (響應式)一些。

從 Recompose 導入 createEventHandler

import { componentFromStream, createEventHandler } from 'recompose';
複製代碼

代碼以下:

const App = componentFromStream(prop$ => {
  const { handler, stream } = createEventHandler();

  return prop$.pipe(
    map(() => (
      <div> <input onChange={handler} placeholder="GitHub username" /> </div> )) ) }); 複製代碼

createEventHandler 對象有兩個頗有意思的屬性:handlerstream

底層實現方面,handler 其實就是一個將數據推送給 stream 的事件發射器,而 stream 則是把這些數據廣播給其訂閱者的一個 observable 對象。

在這裏使用 combineLatest 會是一個很好的選擇。

先有雞仍是先有蛋?

但要使用 combineLateststreamprop$ 都必須被髮射(emit)。而在 prop$ 發射以前,stream 是不會被髮射的,反之亦然。

咱們能夠經過給 stream 一個初始值來解決這個問題。

導入 RxJS 的 startWith 操做符:

import { map, startWith } from 'rxjs/operators';
複製代碼

而後建立一個新的變量來捕獲變動後的 stream

const { handler, stream } = createEventHandler();

const value$ = stream.pipe(
  map(e => e.target.value),
  startWith('')
);
複製代碼

咱們知道 stream 會在 input 的文本值發生改變時發射事件,因此咱們能夠將每一個事件都映射爲其改變後的文本值。

最重要的是,咱們將 value$ 初始化爲一個空字符串,以便於在 input 爲空時獲得一個合理的默認值。

合二爲一

如今咱們準備將這兩個數據流組合到一塊兒,並導入 combineLatest 做爲建立方法,而非做爲操做符

import { combineLatest } from 'rxjs';
複製代碼

你也能夠導入 tap 用於實時檢查數據:

import { map, startWith, tap } from 'rxjs/operators';
複製代碼

具體寫法以下:

const App = componentFromStream(prop$ => {
  const { handler, stream } = createEventHandler();
  const value$ = stream.pipe(
    map(e => e.target.value),
    startWith('')
  );

  return combineLatest(prop$, value$).pipe(
    tap(console.warn),
    map(() => (
      <div> <input onChange={handler} placeholder="GitHub username" /> </div> )) ) }); 複製代碼

如今,每當你輸入一個字符時,[props, value] 就會被記錄下來。

用戶組件

該組件將負責獲取並顯示咱們輸入的用戶名。它會收到來自 Appvalue,並將其映射爲 AJAX 請求。

JSX/CSS

這部分徹底是基於一個叫 Github Cards 的項目,該項目很是之優秀。本教程大部分代碼,尤爲是編碼風格都是照搬過來並用 React 和 props 重寫的。

首先,新建一個文件夾 src/User,並將這段代碼放進 User.css

而後將這段代碼放進 src/User/Component.js

可見,該組件只包含了一個 Github API 的標準 JSON 響應模板。

容器

譯者注:這裏的「容器」指容器組件(Container Component)

如今,能夠把這個「單調」的組件放一邊了,讓咱們來實現一個更爲「智能」的組件:

新建 src/User/index.js,代碼以下:

import React from 'react';
import { componentFromStream } from 'recompose';
import {
  debounceTime,
  filter,
  map,
  pluck
} from 'rxjs/operators';
import Component from './Component';
import './User.css';

const User = componentFromStream(prop$ => {
  const getUser$ = prop$.pipe(
    debounceTime(1000),
    pluck('user'),
    filter(user => user && user.length),
    map(user => (
      <h3>{user}</h3>
    ))
  );

  return getUser$;
});

export default User;
複製代碼

咱們將 User 定義爲了一個 componentFromStream,組件中會將數據流 prop$ 映射爲包含用戶名的 <h3> 標籤。

debounceTime

雖然 User 會收到來自鍵盤的 props,可是咱們並不但願監聽用戶全部的輸入操做。

當用戶開始輸入時,debounceTime(1000) 會每隔一秒接收一次輸入。這種模式在處理用戶輸入上是很是經常使用的。

pluck

該組件在這裏只須要用到 prop.user 屬性。經過使用 pluck 來提取 user,咱們就能夠不用每次都解構 props 了。

filter

確保 user 存在且不爲空。

map

到這裏,只須要將 user 放到 <h3> 標籤裏就好了。

聯動

譯者注:標題原文爲「Hooking It Up」,含義比較多(如:行動起來、創建聯繫、**、組裝等等),我的以爲在這裏譯爲「聯動」會比較合適。

回到 src/index.js,導入 User 組件:

import User from './User';
複製代碼

並提供 value 做爲 user prop:

return combineLatest(prop$, value$).pipe(
  tap(console.warn),
  map(([props, value]) => (
    <div>
      <input
        onChange={handler}
        placeholder="GitHub username"
      />

      <User user={value} />
    </div>
  ))
);
複製代碼

如今,你輸入的值將會在 1s 後渲染到屏幕上。

這是個很好的開始,但咱們仍須要獲取真正的用戶信息。

獲取 User

Github 的 User API 接口爲 https://api.github.com/users/${user}。咱們能夠輕易地將其放到 User/index.js 的一個輔助函數裏:

const formatUrl = user => `https://api.github.com/users/${user}`;
複製代碼

如今,咱們能夠在 filter 後面添加 map(formatUrl)

輸入完成後,屏幕上很快就會出現預期的 API endpoint。

但咱們須要的是把這個 API 請求發出去!如今就該讓 switchMapajax 登場了。

switchMap

switchMap 很是適合將一個 observable 對象切換爲另外一個,這對於處理用戶輸入上仍是頗有用的。

假設用戶輸入了一個用戶名,咱們在 switchMap 中獲取其用戶信息。

但在結果返回以前,用戶又輸入了新的東西,結果會是如何?咱們還會在乎以前的 API 響應嗎?

並不會。

switchMap 會取消掉先前的請求,從而專一於處理當前最新的。

ajax

RxJS 提供了本身的 ajax 實現,且和 switchMap 配合得很是棒!

實際應用

讓咱們先導入這兩樣東西。代碼以下:

import { ajax } from 'rxjs/ajax';
import {
  debounceTime,
  filter,
  map,
  pluck,
  switchMap
} from 'rxjs/operators';
複製代碼

而後像這樣使用它們:

const User = componentFromStream(prop$ => {
  const getUser$ = prop$.pipe(
    debounceTime(1000),
    pluck('user'),
    filter(user => user && user.length),
    map(formatUrl),
    switchMap(url =>
      ajax(url).pipe(
        pluck('response'),
        map(Component)
      )
    )
  );

  return getUser$;
});
複製代碼

將咱們的 input切換ajax 請求流。一旦請求完成,response 就會被提取出來,並 map 到咱們的 User 組件中去。

搞定!

錯誤處理

試着輸入一個不存在的用戶名。

即使你改對了,咱們的程序依舊是崩潰的。你必須刷新頁面來從新獲取用戶信息。

是否是很是蛋疼?

catchError

有了 catchError 操做符,咱們能夠顯示一個合理的錯誤提示,而非直接卡死。

導入之:

import {
  catchError,
  debounceTime,
  filter,
  map,
  pluck,
  switchMap
} from 'rxjs/operators';
複製代碼

並將其複製到 ajax 鏈的尾部。

switchMap(url =>
  ajax(url).pipe(
    pluck('response'),
    map(Component),
    catchError(({ response }) => alert(response.message))
  )
)
複製代碼

如今至少有一些回饋了,但還能夠更完善一些。

Error 組件

src/Error/index.js 建立一個新組件:

import React from 'react';

const Error = ({ response, status }) => (
  <div className="error"> <h2>Oops!</h2> <b> {status}: {response.message} </b> <p>Please try searching again.</p> </div>
);

export default Error;
複製代碼

它會友好地顯示咱們 AJAX 請求中的 responsestatus

讓咱們把它導入進 User/index.js

import Error from '../Error';
複製代碼

同時,從 RxJS 中導入 of

import { of } from 'rxjs';
複製代碼

記住,咱們 componentFromStream 的回調函數必須返回一個 observable 對象。咱們能夠用 of 來實現。

更新代碼:

ajax(url).pipe(
  pluck('response'),
  map(Component),
  catchError(error => of(<Error {...error} />)) ) 複製代碼

其實就是簡單地將 error 對象做爲 props 傳遞給咱們的組件。

如今,再看一下效果:

啊~好多了!

加載指示器

通常來講,咱們如今須要某種形式的狀態管理。那麼如何構建一個加載指示器呢?

但在請 setState 出馬以前,讓咱們看看用 RxJS 該怎麼解決。

Recompose 的文檔讓我有了這方面的想法:

組合多條數據流來代替 setState()。

:我一開始用的是 BehaviorSubject,但後來 Matti Lankinen 回覆了我,告訴了我一個絕妙的方法來簡化代碼。謝謝你,Matti!

導入 merge 操做符。

import { merge, of } from 'rxjs';
複製代碼

當請求準備好時,咱們會將 ajax 流和 Loading 組件流合併到一塊兒。

componentFromStream 中這樣寫:

const User = componentFromStream(prop$ => {
  const loading$ = of(<h3>Loading...</h3>);
  const getUser$ = ...
複製代碼

一個簡單的 <h3> 加載指示器轉變成了一個 observable 對象!接着就能夠合併了:

const loading$ = of(<h3>Loading...</h3>);

const getUser$ = prop$.pipe(
  debounceTime(1000),
  pluck('user'),
  filter(user => user && user.length),
  map(formatUrl),
  switchMap(url =>
    merge(
      loading$,
      ajax(url).pipe(
        pluck('response'),
        map(Component),
        catchError(error => of(<Error {...error} />)) ) ) ) ); 複製代碼

我很喜歡如此簡潔的寫法。在進入 switchMap 後,合併 loading$ajax 這兩個 observable。

由於 loading$ 是一個靜態值,因此會率先呈現。一旦異步 ajax 完成,其結果就會代替 Loading,顯示到屏幕上。

在測試以前,咱們能夠導入一個 delay 操做符來放緩執行過程。

import {
  catchError,
  debounceTime,
  delay,
  filter,
  map,
  pluck,
  switchMap,
  tap
} from 'rxjs/operators';
複製代碼

並在 map(Component) 以前調用:

ajax(url).pipe(
  pluck('response'),
  delay(1500),
  map(Component),
  catchError(error => of(<Error {...error} />)) ) 複製代碼

最終效果如何?

我很想知道該模式在將來會如何發展,以及是否能夠走的更遠。歡迎在下面評論並分享你對此的見解!

記得 ClapClap 喲。(最多能夠 Clap 50 次!)

那咱們下次見咯。

Take care,
雅澤·巴扎多 Yazeed Bzadough
yazeedb.com/

相關文章
相關標籤/搜索