[译] 如何使用 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/

相关文章
相关标签/搜索