从生活中跌落

大家好


您是否还有一个熟悉的反应开发人员,他讲述了关于redux副作用的惊人故事? 不? 我可以成为这个人吗



作者可以随意编写关于redux saga库的全部内容的介绍性部分。 他希望在数据不足的情况下,慷慨的读者可以使用Habr搜索或官方教程 这些示例已大大简化,以传达其实质。


所以,为什么我要你们在一起。 这将涉及在战斗客户的开放空间中使用redux saga。 更具体地说,与“接受操作=>发送API请求=>创建新操作”相比,情况更加复杂和有趣。


我希望激发同胞们对该图书馆的更深入研究,并希望分享复杂的异步事物变得更加易于理解和表达的乐趣。


网络套接字


用例:使用推送模型实时从服务器接收可用空缺列表的更新。


当然,这与Web套接字的使用有关。 例如,以socket.io为例,但实际上套接字API在这里并不重要。


在Sagas中,存在诸如通道之类的东西。 这是一条消息总线,事件源可通过该消息总线与其使用者进行通信。 通道的主要目的是在Sagas之间进行通信,并将异步事件流转换为便于工作的结构。


默认情况下,store是redux saga的主要事件渠道。 事件以行动的形式出现。 通道用于处理非存储事件。


事实证明,通道正是处理来自套接字的异步消息流所需要的。 让我们尽快创建频道!


但首先,创建一个套接字:


import io from 'socket.io-client'; export const socket = io.connect('/'); 

现在声明一个适度的事件列表:


 export const SocketEvents = { jobsFresh: 'jobs+fresh', }; 

接下来是用于创建通道的工厂方法。 该代码创建了一种用于从套接字订阅我们感兴趣的事件的方法,一种用于取消订阅的方法以及直接取消事件通道本身的方法:


 import { eventChannel } from 'redux-saga'; import { socket } from '../apis/socket'; import { SocketEvents } from '../constants/socket-events'; export function createFreshJobsChannel() { const subscribe = emitter => { socket.on(SocketEvents.jobsFresh, emitter); return () => socket.removeListener(SocketEvents.jobsFresh, emitter); }; return eventChannel(subscribe); } 

让我们编写一个相当简单的传奇,等待套接字中的更新并将其转换为相应的动作:


 import { take, call, put } from 'redux-saga/effects'; import { createFreshJobsChannel } from '../channels/fresh-jobs'; import { JobsActions } from '../actions/jobs'; export function * freshJobsSaga() { const channel = yield call(createFreshJobsChannel); while (true) { const jobs = yield take(channel); const action = JobsActions.fresh(jobs); yield put(action); } } 

它仅保留将其绑定到根传奇:


 import { fork } from 'redux-saga/effects'; import { freshJobsSaga } from './fresh-jobs'; export function * sagas() { yield fork(freshJobsSaga); } 

Google Places自动完成


用例:当用户输入地理位置以供随后搜索附近的房地产时显示提示。


实际上,我们需要坐标,而用户则需要人类可读的所需区域名称。


看来此任务与无聊的“ action => API => action”有什么不同? 在自动完成的情况下,我们希望对外部资源进行尽可能少的无用的调用,并且只向用户显示相关的提示。


首先,我们将利用Google地方信息自动填充服务编写一个API方法。 有趣的是,给定国家/地区内提示的局限性:


 export function getPlaceSuggestions(autocompleteService, countryCode, query) { return new Promise(resolve => { autocompleteService.getPlacePredictions({ componentRestrictions: { country: countryCode }, input: query, }, resolve); }); } 

有一个我们将提取的API方法,您可以开始编写传奇。 现在该澄清无用的请求了。


在用户额头上实现时,当用户键入内容时,我们对每个更改进行读取(对于每个字符,向API发送请求)都无济于事。 用户输入时,他不需要提示。 但是当他停下来时,该为他服务了。


另外,我们不希望出现以下情况:用户键入了一些内容,然后停止了,API请求消失了,用户键入了一些东西,另一个请求消失了。


因此,我们在两个请求之间创建了竞争。 活动可以通过两种方式发展,但都不太令人满意。


例如,不相关的请求将比当前请求更早结束,并且用户一会儿会看到不相关的提示。 令人不愉快,但并不重要。


或者,当前请求早于不相关的请求结束,并且在闪烁之后,用户将保持相同的不相关的提示。 这已经很关键。


当然,我们并不是第一个遇到这种问题的人,称为反跳的技术将帮助我们-仅在自收到最后一个事件以来的N个时间单位后才执行任务。 这是有关此的一些材料


在redux saga中,使用两种效果-delaytakeLatest实施此技术。 第一个将传奇的执行延迟指定的毫秒数。 当新事件到达时,第二个命令终止已运行的传奇事件的执行。


知道了所有这些之后,我们将写一个传奇:


 import { delay } from 'redux-saga'; import { put, call, select } from 'redux-saga/effects'; import { PlaceActions } from '../actions/place'; import { MapsActions } from '../actions/maps'; import { getPlaceSuggestions } from '../api/get-place-suggestions'; export function placeSuggestionsSaga * ({ payload: query }) { const { maps: { isApiLoaded } } = yield select(); //  API    , //      if (!isApiLoaded) { yield take(MapsActions.apiLoaded); } //     Google Places Autocomplete  store const { maps: { autocompleteService }, countryCode } = yield select(); //    , //        if (query) { yield put(PlaceActions.suggestions([])); yield put(PlaceActions.select(null)); return; } //  250    yield call(delay, 250); //  API  const suggestions = yield call( getPlaceSuggestions, autocompleteService, countryCode, query, ); //  action   const action = PlacesActions.suggestions(suggestions || []); //     store yield put(action); }; 

与前面的示例一样,仅保留将其绑定到根传奇:


 import { takeLatest } from 'redux-saga/effects'; import { PlaceActions } from '../actions/place'; import { placeSuggestionsSaga } from './place-suggestions'; export function * sagas() { yield takeLatest( PlaceActions.changeQuery, placeSuggestionsSaga, ); } 


使用案例:在控件的区域之外单击时,关闭自写的下拉列表。


实际上,这是对浏览器选择内置行为的模拟。 您可能需要在div上写一个下拉列表的原因将留给读者的想象力。


要解决的任务的关键功能是将事件传递到控件外部,例如,在列表外部单击时。


猜到了吗 是的,渠道也会在这里为我们提供帮助。 使用它们,我们会将弹出到最顶部的点击事件变成相应的动作。


最好有一个工厂方法为任意窗口事件创建通道。 他在这里:


 import { eventChannel } from 'redux-saga'; export function createWindowEventChannel(eventName) { const subscribe = emitter => { window.addEventListener(eventName, emitter); return () => window.removeEventListener(eventName, emitter); }; return eventChannel(subscribe); } 

我们创建一个与第一个示例非常相似的传奇(如果需要,您可以为其创建一个工厂方法):


 import { take, put, call } from 'redux-saga/effects'; import { createWindowEventChannel } from '../channels/window-event'; import { DropdownActions } from '../actions/dropdown'; export function * closeDropdownsSaga() { const channel = yield call(createWindowEventChannel, 'onClick'); while (true) { const event = yield take(channel); const action = DropdownActions.closeAll(event); yield put(action(event)); } } 

有兴趣的减速器会将控件置于关闭状态:


 import { handleActions } from 'redux-actions'; import { DropdownActions } from '../actions/dropdown'; export const priceReducer = handleActions({ ..., [DropdownActions.closeAll]: state => ({ ...state, isOpen: false}), }, {}); 

下拉列表本身必须停止click事件在任何内部部件上的分发,并将关闭事件自行发送到商店。 例如,当您单击打开时:


 // components/dropdown.js import React from 'react'; export class Dropdown extends React.Component { ... __open(event) { event.stopPropagation(); this.props.open(); } } // dispatchers/open-price-dropdown.js import { DropdownActions } from '../actions/dropdown'; import { PriceActions } from '../actions/price'; export const openPriceDropdownDispatcher = dispatch => () => { dispatch( DropdownActions.closeAll() ); dispatch( PriceActions.open() ); }; 

否则,该列表将根本无法打开。 选择选项时,单击也是如此。


El clasico,登上传奇:


 import { fork } from 'redux-saga/effects'; import { closeDropdownsSaga } from './close-dropdowns'; export function * sagas() { yield fork(closeDropdownsSaga); } 

通知事项


用例:如果选项卡在后台,则显示有关新职位空缺的浏览器通知。


在活动选项卡中,用户将在特殊控件中看到更改,因此通知不适当。 但是对于背景选项卡可能会派上用场。 当然,要得到用户的许可!


我还想单击通知以转到选项卡并显示新的空缺。 如果用户未响应,则关闭通知。 为此,我们需要另一个有用的效果-race 。 它允许您安排其他几种效果之间的竞赛。 在大多数情况下,竞赛用于为某些操作提供超时。


由于与上一示例的点击拦截代码相同,因此我们省略了用于跟踪选项卡活动的代码。


我们将编写一个工厂方法,该方法将创建一个渠道来请求用户批准以接收通知:


 import { eventChannel, END } from 'redux-saga'; export function createRequestNotificationPermissionChannel() { const subscribe = emitter => { Notification.requestPermission(permission => { emitter(permission); emitter(END); }); return () => {}; }; return eventChannel(subscribe); } 

为了寻求另一种工厂方法,但具有接收通知的渠道,请单击:


 import { eventChannel, END } from 'redux-saga'; export function createNotificationClickChannel(notification) { const subscribe = emitter => { notification.onclick = event => { emitter(event); emitter(END); }; return () => notification.onclick = null; }; return eventChannel(subscribe); } 

两个通道均为一次性使用,一次最多可拍摄最多,然后关闭。


关键仍然是-具有逻辑的传奇。 检查选项卡是否处于活动状态,请求许可,创建通知,等待单击或超时,显示新的职位空缺,使选项卡处于活动状态,然后关闭通知:


 import { delay } from 'redux-saga'; import { call, select, race, take } from 'redux-saga/effects'; import { createRequestNotificationPermissionChannel } from '../channels/request-notification-permission'; import { createNotificationClickChannel } from '../channels/notification-click'; import { JobsActions } from '../actions/jobs'; export function * notificationsSaga(action) { const { inFocus } = yield select(); if (inFocus) return; const permissionChannel = yield call(createRequestNotificationPermissionChannel); const permission = yield take(permissionChannel); if (permission !== 'granted') return; const notification = new Notification( `You have ${action.payload.jobs.length} new job posts`, { icon: 'assets/new-jobs.png' } ); const clickChannel = yield call(createNotificationClickChannel, notification); const { click, timeout } = yield race({ click: take(clickChannel), timeout: call(delay, 5000), }); if (click) { yield put(JobsActions.show()); window.focus(); window.scrollTo(0, 0); } notification.close(); } 

在执行此功能检测之前,请先安装传奇:


 import { takeEvery } from 'redux-saga/effects'; import { JobsActions } from '../actions/jobs'; import { notificationsSaga } from './notifications'; export default function * sagas() { if ( 'Notification' in window && Notification.permission !== 'denied' ) { yield takeEvery(JobsActions.fresh, notificationsSaga); } } 

全球活动巴士


用例:在Redux商店之间转移指定类别的事件。


如果页面上存在多个具有公共数据的应用程序,则需要这种总线。 同时,应用程序可以彼此独立地实现。


例如,带有过滤器和搜索结果的搜索字符串作为单独的react应用程序。 更改过滤器时,我希望结果应用程序知道它是否也在页面上。


我们使用标准的事件发射器:


 import EventEmmiter from 'events'; if (!window.GlobalEventBus) { window.GlobalEventBus = new EventEmmiter(); } export const globalEventBus = window.GlobalEventBus; 

备受喜爱的eventChannel将标准发射器变成一个通道:


 import { eventChannel } from 'redux-saga'; import { globalEventBus as bus } from '../utils/global-event-bus'; exports function createGlobalEventBusChannel() { const subscribe = emitter => { const handler = event => emitter({ ...event, external: true }); bus.on('global.event', handler); return bus.removeListener('global.event', handler); }; return eventChannel(subscribe); } 

传奇很简单-我们创建了一个频道并无休止地接受内部或外部事件。 如果我们收到内部事件,则将其发送到总线,如果外部事件则在商店中发送:


 import { take, put, race, call } from 'redux-saga/effects'; import { globalEventBus as bus } from '../utils/global-event-bus'; import { createGlobalEventBusChannel } from '../channels/global-event-bus'; export function * globalEventBusSaga(allowedActions) { allowedActions = allowedActions.map(x => x.toString()); const channel = yield call(createGlobalEventBusChannel); while (true) { const { local, external } = yield race({ local: take(), external: take(channel), }); if ( external && allowedActions.some(action => action === external.type) ) { yield put(external); } if ( local && !local.external && allowedActions.some(action => action === local.type) ) { bus.emit('global.event', local); } } }; 

最后一个-用必要的事件安装传奇:


 import { fork } from 'redux-saga/effects'; import { globalEventBusSaga } from './global-event-bus'; import { DropdownsActions } from '../actions/dropdowns'; import { AreasActions } from '../actions/areas'; export function * sagas() { yield fork(globalEventBusSaga, [ DropdownsActions.closeAll, AreasActions.add, AreasActions.remove, ... ]); } 



我希望能够证明Sagas使描述复杂的副作用更加容易。 探索库API,转移到您的案例中,编写复杂的事件期望模式并感到高兴。 在JS开放空间见!

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


All Articles