原文連接: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,沒有生命週期鉤子,也沒有 setState
。java
全部代碼均可以在我 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.js
。shell
若是你以前沒見過 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';
複製代碼
準備完畢!
導入 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)。
顧名思義,該操做符用於將 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
對象有兩個頗有意思的屬性:handler
和 stream
。
在底層實現方面,handler
其實就是一個將數據推送給 stream
的事件發射器,而 stream
則是把這些數據廣播給其訂閱者的一個 observable 對象。
在這裏使用 combineLatest
會是一個很好的選擇。
但要使用 combineLatest
,stream
和 prop$
都必須被髮射(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]
就會被記錄下來。
該組件將負責獲取並顯示咱們輸入的用戶名。它會收到來自 App
的 value
,並將其映射爲 AJAX 請求。
這部分徹底是基於一個叫 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>
標籤。
雖然 User
會收到來自鍵盤的 props,可是咱們並不但願監聽用戶全部的輸入操做。
當用戶開始輸入時,debounceTime(1000)
會每隔一秒接收一次輸入。這種模式在處理用戶輸入上是很是經常使用的。
該組件在這裏只須要用到 prop.user
屬性。經過使用 pluck
來提取 user
,咱們就能夠不用每次都解構 props
了。
確保 user
存在且不爲空。
到這裏,只須要將 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 後渲染到屏幕上。
這是個很好的開始,但咱們仍須要獲取真正的用戶信息。
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 請求發出去!如今就該讓 switchMap
和 ajax
登場了。
switchMap
很是適合將一個 observable 對象切換爲另外一個,這對於處理用戶輸入上仍是頗有用的。
假設用戶輸入了一個用戶名,咱們在 switchMap
中獲取其用戶信息。
但在結果返回以前,用戶又輸入了新的東西,結果會是如何?咱們還會在乎以前的 API 響應嗎?
並不會。
switchMap
會取消掉先前的請求,從而專一於處理當前最新的。
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
操做符,咱們能夠顯示一個合理的錯誤提示,而非直接卡死。
導入之:
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))
)
)
複製代碼
如今至少有一些回饋了,但還能夠更完善一些。
在 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 請求中的 response
和 status
。
讓咱們把它導入進 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/