大家好
您是否还有一个熟悉的反应开发人员,他讲述了关于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中,使用两种效果-delay和takeLatest实施此技术。 第一个将传奇的执行延迟指定的毫秒数。 当新事件到达时,第二个命令终止已运行的传奇事件的执行。
知道了所有这些之后,我们将写一个传奇:
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();
与前面的示例一样,仅保留将其绑定到根传奇:
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事件在任何内部部件上的分发,并将关闭事件自行发送到商店。 例如,当您单击打开时:
否则,该列表将根本无法打开。 选择选项时,单击也是如此。
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开放空间见!