原文连接: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/