如何使用React + RxJS 6 + Recompose在GitHub上进行用户搜索

图片引起关注


本文适用于具有React和RxJS经验的人。 我只是分享我认为对创建这样的UI有用的模板。


这是我们的工作:



没有类,则使用生命周期或setState


准备工作


您只需要在GitHub上的我的存储库中即可。


 git clone https://github.com/yazeedb/recompose-github-ui cd recompose-github-ui yarn install 

master分支中,有一个完成的项目。 如果要逐步进行,请切换到start分支。


 git checkout start 

并运行该项目。


 npm start 

该应用程序应从localhost:3000开始,这是我们的初始UI。



启动您喜欢的编辑器,然后打开src/index.js



重新组合


如果您不熟悉Recompose ,那么这是一个很棒的库,它允许您以功能样式创建React组件。 它包含大量功能。 这是我最喜欢的。


就像Lodash / Ramda,仅是React。


我也很高兴它支持Observer模式。 引用文档


事实证明,大多数React Component API都可以通过Observer模式来表达。

今天我们将实践这个概念!


内联组件


到目前为止,我们拥有App最常见的React组件。 使用Recompose库中的componentFromStream函数,我们可以通过一个可观察对象来获取它。


componentFromStream函数从我们的observable开始对每个新值进行渲染。 如果尚无值,则 null


构型


Recompose中的流遵循ECMAScript Observable Proposal文档。 它描述了可观察对象在现代浏览器中实现时应如何工作。


同时,我们将使用RxJS,xstream,most,Flyd等库。


Recompose不知道我们使用哪个库,因此它提供了setObservableConfig函数。 有了它,您可以将我们需要的所有内容转换为ES Observable。


src文件夹中创建一个新文件,并将其命名为observableConfig.js


要将RxJS 6连接到Recompose,请在其中编写以下代码:


 import { from } from 'rxjs'; import { setObservableConfig } from 'recompose'; setObservableConfig({ fromESObservable: from }); 

将此文件导入index.js


 import './observableConfig'; 

仅此而已!


重组+ RxJS


componentFromStream导入添加到index.js


 import { componentFromStream } from 'recompose'; 

让我们开始覆盖App组件:


 const App = componentFromStream(prop$ => { ... }); 

请注意, componentFromStream将带有prop$参数的函数作为参数,这是props的可观察版本。 这个想法是使用map将常规props变成React组件。


如果您使用RxJS,则应该熟悉map运算符。


地图


顾名思义, mapObservable(something)变为Observable(somethingElse) 。 在我们的情况下Observable(component) 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而不是语句链。


保存文件并检查结果。 一切都没有改变!



添加事件处理程序


现在,我们将使输入字段变得有些反应。


添加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是将值传递给订阅者的可观察对象。


我们将链接streamprop$以获取输入字段的当前值。


在我们的情况下,一个不错的选择是使用combineLatest函数。


鸡蛋和鸡肉问题


要使用combineLateststreamprop$必须产生值。 但是, stream不会释放任何内容,除非某些值释放了prop$ ,反之亦然。


您可以通过将stream设置stream初始值来解决此问题。


从RxJS导入startWith语句:


 import { map, startWith } from 'rxjs/operators'; 

创建一个新变量以从更新的stream获取值:


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

我们知道,当输入字段更改时, stream将引发事件,因此让我们立即将其转换为文本。


并且由于输入字段的默认值是一个空字符串,因此请使用该value$初始化value$对象value$


编织在一起


现在我们准备连接两个流。 导入combineLatest作为创建Observable对象的方法, 而不是作为operator


 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请求。


jsx / css


所有这些都基于出色的GitHub Cards项目。 大多数代码(尤其是样式)均已复制或改编。


创建src/User文件夹。 在其中创建一个User.css文件,并将此代码复制到其中。


并将此代码复制到src/User/Component.js文件。


该组件仅使用来自GitHub API调用的数据填充模板。


货柜


现在这个组件是“哑巴”,我们不在路上,让我们制作一个“智能”组件。


这是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 ,它返回一个Observable prop$对象,该对象将传入的属性转换为<h3>


去抖时间


每次在键盘上按下一个键,我们的User都会收到新值,但是我们不需要这种行为。


当用户开始键入时, debounceTime(1000)将跳过所有持续时间少于一秒的事件。



我们希望将user对象作为props.user传递。 pluck运算符从对象中获取指定的字段并返回其值。


过滤器


在这里,我们确保user通过,并且不是空字符串。


地图


我们从user制作<h3>标签。


连接


返回src/index.js并导入User组件:


 import User from './User'; 

我们将值value作为user参数传递:


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

现在,我们的值将延迟一秒钟显示。



还不错,现在我们需要获取有关用户的信息。


资料要求


GitHub提供了一个用于获取用户信息的API: https : //api.github.com/users/${ user } 。 我们可以轻松地编写一个辅助函数:


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

现在我们可以在filter之后添加map(formatUrl)


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

现在,URL代替用户名显示在屏幕上。


我们需要提出要求! switchMapajax可以switchMap


switchMap


该运算符是在多个可观察值之间切换的理想选择。


假设用户键入了名称,我们将在switchMap进行请求。


如果用户在API的响应到达之前输入了内容,将会发生什么? 我们应该担心以前的要求吗?


不行


switchMap将取消旧请求并切换到新请求。


阿贾克斯


RxJS提供了自己的ajax实现,可与switchMap一起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$; }); 

switchMap从我们的输入字段切换到AJAX请求。 当答案到达时,它将答案传递给我们的哑组件。


结果就是这里!



错误处理


尝试输入不存在的用户名。



我们的应用程序已损坏。


catchError


使用catchError运算符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)) ) ) 


已经不错,但是您当然可以做得更好。


组件错误


使用内容创建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 ,同时将其从RxJS导入operator:


 import Error from '../Error'; import { of } from 'rxjs'; 

请记住,传递给componentFromStream的函数应返回observable。 我们可以使用of运算符来实现:



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

现在我们的用户界面看起来好多了:



加载指示器


现在该介绍状态管理了。 您还可以如何实现加载指标?


如果放置setState我们将使用BehaviorSubject怎么办?


重组文档建议以下内容:


代替setState(),组合多个线程

好的,您需要两个新的导入:


 import { BehaviorSubject, merge, of } from 'rxjs'; 

BehaviorSubject将包含下载状态,并且merge会将其与组件相关联。


内部componentFromStream


 const User = componentFromStream(prop$ => { const loading$ = new BehaviorSubject(false); const getUser$ = ... 

BehaviorSubject用初始值或“状态”初始化。 由于在用户开始输入文本之前我们不会做任何事情,因此请将其初始化为false


我们将使用tap运算符更改loading$状态:


 import { catchError, debounceTime, filter, map, pluck, switchMap, tap // <--- } from 'rxjs/operators'; 

我们将像这样使用它:


 const loading$ = new BehaviorSubject(false); const getUser$ = prop$.pipe( debounceTime(1000), pluck('user'), filter(user => user && user.length), map(formatUrl), tap(() => loading$.next(true)), // <--- switchMap(url => ajax(url).pipe( pluck('response'), map(Component), tap(() => loading$.next(false)), // <--- catchError(error => of(<Error {...error} />)) ) ) ); 

就在switchMap和AJAX请求之前,我们将true传递给loading$值,并将false传递给成功响应。


现在,我们只需连接loading$getUser$


 return merge(loading$, getUser$).pipe( map(result => (result === true ? <h3>Loading...</h3> : result)) ); 

在开始工作之前,我们可以导入delay语句,以使过渡不会太快。


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

map(Component)之前添加delay


 ajax(url).pipe( pluck('response'), delay(1500), map(Component), tap(() => loading$.next(false)), catchError(error => of(<Error {...error} />)) ) 

结果呢



全部:)

Source: https://habr.com/ru/post/zh-CN419559/


All Articles