Guten Tag.
Haben Sie auch einen vertrauten Reaktionsentwickler, der erstaunliche Geschichten über Nebenwirkungen von Redux erzählt? Nein?! Kann ich diese Person werden?

Der Autor hat sich erlaubt, keinen einleitenden Teil darüber zu schreiben, worum es in der Redux-Saga-Bibliothek geht. Er hofft, dass ein großzügiger Leser bei unzureichenden Daten die Habr-Suche oder das offizielle Tutorial verwenden wird . Diese Beispiele sind stark vereinfacht, um die Essenz zu vermitteln.
Also, warum habe ich euch alle zusammengebracht? Es geht um die Verwendung der Redux-Saga in den offenen Räumen von Kampfklienten. Insbesondere über Fälle, die komplexer und interessanter sind als "Aktion akzeptieren => API-Anfrage senden => Neue Aktion erstellen".
Ich hoffe, die Mitbürger zu einem tieferen Studium dieser Bibliothek anzuregen und die Freude daran zu teilen, wie anspruchsvolle asynchrone Dinge verständlicher und ausdrucksvoller werden.
Websockets
Anwendungsfall: Erhalten Sie mithilfe des Push-Modells in Echtzeit Aktualisierungen der Liste der verfügbaren Stellen vom Server.
Wir sprechen natürlich über die Verwendung von Web-Sockets. Nehmen Sie zum Beispiel socket.io, aber tatsächlich spielt die Socket-API hier keine Rolle.
In Sagen gibt es so etwas wie einen Kanal. Dies ist ein Nachrichtenbus, über den die Ereignisquelle mit ihrem Verbraucher kommunizieren kann. Der Hauptzweck der Kanäle ist die Kommunikation zwischen Sagen und die Umwandlung des Stroms asynchroner Ereignisse in eine für die Arbeit geeignete Struktur.
Standardmäßig ist store der Hauptereigniskanal für die Redux-Saga. Ereignisse kommen in Form von Handlungen. Kanäle werden verwendet, um mit Ereignissen zu arbeiten, die nicht aus dem Geschäft stammen.
Es stellt sich heraus, dass der Kanal genau das ist, was Sie benötigen, um mit dem asynchronen Nachrichtenstrom vom Socket zu arbeiten. Lassen Sie uns den Kanal so schnell wie möglich erstellen!
Aber zuerst erstellen Sie einen Socket:
import io from 'socket.io-client'; export const socket = io.connect('/');
Deklarieren Sie nun eine bescheidene Liste von Ereignissen:
export const SocketEvents = { jobsFresh: 'jobs+fresh', };
Als nächstes folgt eine Factory-Methode zum Erstellen eines Kanals. Der Code erstellt eine Methode zum Abonnieren von Ereignissen, die für uns von Interesse sind, aus dem Socket, eine Methode zum Abbestellen und direkt den Ereigniskanal selbst:
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); }
Schreiben wir eine ziemlich einfache Saga, warten auf Aktualisierungen aus dem Socket und konvertieren sie in die entsprechende Aktion:
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); } }
Es bleibt nur, um es an die Wurzelsaga zu binden:
import { fork } from 'redux-saga/effects'; import { freshJobsSaga } from './fresh-jobs'; export function * sagas() { yield fork(freshJobsSaga); }
Google Places Autocomplete
Anwendungsfall: Zeigen Sie Tipps an, wenn der Benutzer einen geografischen Standort für die anschließende Suche nach Immobilien in der Nähe eingibt.
Tatsächlich benötigen wir die Koordinaten, und der Benutzer benötigt den für Menschen lesbaren Namen des gewünschten Bereichs.
Es scheint, wie sich diese Aufgabe von der langweiligen "Aktion => API => Aktion" unterscheidet. Im Falle der automatischen Vervollständigung möchten wir so wenig nutzlose Aufrufe wie möglich an externe Ressourcen tätigen und dem Benutzer nur relevante Hinweise anzeigen.
Zunächst schreiben wir eine API-Methode unter Verwendung des Google Places Autocomplete-Dienstes. Von den interessanten Dingen hier ist die Begrenzung der Eingabeaufforderungen innerhalb eines bestimmten Landes:
export function getPlaceSuggestions(autocompleteService, countryCode, query) { return new Promise(resolve => { autocompleteService.getPlacePredictions({ componentRestrictions: { country: countryCode }, input: query, }, resolve); }); }
Es gibt eine API-Methode, die wir abrufen werden. Sie können mit dem Schreiben einer Saga beginnen. Es ist Zeit, nutzlose Anfragen zu klären.
Die Implementierung in der Stirn führt, wenn der Benutzer tippt und für jede Änderung liest - für jedes Zeichen eine Anfrage an die API senden - nirgendwo hin. Während der Eingabe des Benutzers benötigt er keine Hinweise. Aber wenn er aufhört, ist es Zeit, ihm zu dienen.
Außerdem möchten wir nicht in einer Situation sein, in der der Benutzer etwas eingegeben, gestoppt, die API-Anforderung weg, der Benutzer etwas eingegeben und eine andere Anforderung weg war.
So haben wir einen Wettlauf zwischen zwei Anfragen erstellt. Ereignisse können sich auf zwei Arten entwickeln und beide sind nicht sehr angenehm.
Beispielsweise endet eine irrelevante Anforderung früher als die aktuelle und für einen Moment sieht der Benutzer irrelevante Eingabeaufforderungen. Unangenehm, aber nicht kritisch.
Oder die aktuelle Anforderung endet früher als die irrelevante und nach dem Blinken bleibt der Benutzer bei denselben irrelevanten Eingabeaufforderungen. Dies ist bereits kritisch.
Natürlich sind wir nicht die Ersten, die auf ein solches Problem stoßen, und die als Debounce bekannte Technik wird uns helfen - die Ausführung von Aufgaben erst nach N Zeiteinheiten seit dem Empfang des letzten Ereignisses. Hier ist ein kleines Material dazu.
In der Redux-Saga wird diese Technik mit zwei Effekten implementiert - Delay und TakeLatest . Der erste verschiebt die Ausführung der Saga um die angegebene Anzahl von Millisekunden. Die zweite unterbricht die Ausführung einer bereits funktionierenden Saga, wenn ein neues Ereignis eintrifft.
Wenn wir das alles wissen, werden wir eine Saga schreiben:
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();
Wie im vorherigen Beispiel bleibt es nur, um es an die Wurzelsaga zu binden:
import { takeLatest } from 'redux-saga/effects'; import { PlaceActions } from '../actions/place'; import { placeSuggestionsSaga } from './place-suggestions'; export function * sagas() { yield takeLatest( PlaceActions.changeQuery, placeSuggestionsSaga, ); }
Dropdowns näher
Anwendungsfall: Schließen Sie selbst geschriebene Dropdown-Listen, wenn Sie außerhalb des Gebiets des Steuerelements klicken.
Tatsächlich ist dies eine Emulation des in die Browserauswahl integrierten Verhaltens. Die Gründe, warum Sie möglicherweise eine Dropdown-Liste benötigen, die auf divs geschrieben ist, bleiben der Vorstellungskraft des Lesers überlassen.
Ein wesentliches Merkmal der zu lösenden Aufgabe ist das Übergeben eines Ereignisses außerhalb des Steuerelements, beispielsweise wenn Sie außerhalb der Liste klicken.
Vermutlich? Ja, Kanäle helfen uns auch hier. Mit ihnen werden Klickereignisse, die ganz oben angezeigt werden, in die entsprechende Aktion umgewandelt.
Es wäre schön, eine Factory-Methode zu haben, die Kanäle für ein beliebiges Fensterereignis erstellt. Und hier ist er:
import { eventChannel } from 'redux-saga'; export function createWindowEventChannel(eventName) { const subscribe = emitter => { window.addEventListener(eventName, emitter); return () => window.removeEventListener(eventName, emitter); }; return eventChannel(subscribe); }
Wir erstellen eine Saga, die dem ersten Beispiel sehr ähnlich ist (Sie können eine Factory-Methode für sie erstellen, wenn Sie dies wünschen):
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)); } }
Interessierte Reduzierer versetzen die Steuerung in einen geschlossenen Zustand:
import { handleActions } from 'redux-actions'; import { DropdownActions } from '../actions/dropdown'; export const priceReducer = handleActions({ ..., [DropdownActions.closeAll]: state => ({ ...state, isOpen: false}), }, {});
Die Dropdown-Liste selbst muss die Weitergabe des Klickereignisses auf interne Teile stoppen und das Abschlussereignis selbst an das Geschäft senden. Zum Beispiel, wenn Sie zum Öffnen klicken:
Andernfalls wird die Liste einfach nicht geöffnet. Gleiches gilt für einen Klick bei Auswahl einer Option.
El clasico, montiere die Saga:
import { fork } from 'redux-saga/effects'; import { closeDropdownsSaga } from './close-dropdowns'; export function * sagas() { yield fork(closeDropdownsSaga); }
Benachrichtigungen
Anwendungsfall: Zeigen Sie Browserbenachrichtigungen über die Verfügbarkeit neuer Stellenangebote an, wenn sich die Registerkarte im Hintergrund befindet.
Auf der Registerkarte "Aktiv" sieht der Benutzer die Änderung in einem speziellen Steuerelement. Daher sind Benachrichtigungen nicht geeignet. Aber für den Hintergrund kann die Registerkarte nützlich sein. Natürlich mit Genehmigung des Benutzers!
Ich möchte auch auf die Benachrichtigung klicken, um zur Registerkarte zu gelangen und neue Stellenangebote anzuzeigen. Wenn der Benutzer nicht antwortet, schließen Sie die Benachrichtigung. Dazu brauchen wir einen weiteren nützlichen Effekt - Race . Sie können ein Rennen zwischen mehreren anderen Effekten arrangieren. In den meisten Fällen wird Race verwendet, um eine Zeitüberschreitung für eine Operation bereitzustellen.
Wir lassen den Code zum Verfolgen der Aktivität der Registerkarte aufgrund der Identität mit dem Klick-Abfangcode aus dem vorherigen Beispiel weg.
Wir schreiben eine Factory-Methode, mit der ein Kanal erstellt wird, in dem die Genehmigung eines Benutzers zum Empfang von Benachrichtigungen angefordert wird:
import { eventChannel, END } from 'redux-saga'; export function createRequestNotificationPermissionChannel() { const subscribe = emitter => { Notification.requestPermission(permission => { emitter(permission); emitter(END); }); return () => {}; }; return eventChannel(subscribe); }
Bei der Verfolgung einer anderen Factory-Methode, jedoch mit einem Kanal zum Empfangen einer Benachrichtigung, klicken Sie auf:
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); }
Beide Kanäle sind Einweg- und Maximalaufnahmen bei einer Veranstaltung, danach werden sie geschlossen.
Der Schlüssel bleibt - die Saga mit Logik. Überprüfen Sie, ob die Registerkarte aktiv ist, fordern Sie die Berechtigung an, erstellen Sie eine Benachrichtigung, warten Sie auf einen Klick oder eine Zeitüberschreitung, zeigen Sie neue Stellen an, aktivieren Sie die Registerkarte und schließen Sie die Benachrichtigung:
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(); }
Hängen Sie die Saga ein, bevor Sie diese Funktionserkennung durchführen:
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); } }
Globaler Eventbus
Anwendungsfall: Übertragen Sie die angegebene Kategorie von Ereignissen zwischen Redux-Stores.
Ein solcher Bus ist erforderlich, wenn auf der Seite mehrere Anwendungen mit gemeinsamen Daten vorhanden sind. Gleichzeitig können Anwendungen unabhängig voneinander implementiert werden.
Zum Beispiel eine Suchzeichenfolge mit Filtern und Suchergebnissen als separate Reaktionsanwendungen. Wenn Sie Filter ändern, möchte ich, dass die Ergebnisanwendung darüber informiert wird, ob sie sich auch auf der Seite befindet.
Wir verwenden den Standard-Event-Emitter:
import EventEmmiter from 'events'; if (!window.GlobalEventBus) { window.GlobalEventBus = new EventEmmiter(); } export const globalEventBus = window.GlobalEventBus;
Der bereits beliebte eventChannel verwandelt einen Standard-Emitter in einen Kanal:
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); }
Die Saga ist ganz einfach: Wir erstellen einen Kanal und akzeptieren endlose interne oder externe Ereignisse. Wenn wir ein internes Ereignis erhalten haben, senden Sie es an den Bus, wenn das externe - im Geschäft:
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); } } };
Und die letzte - die Saga mit den notwendigen Ereignissen zu montieren:
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, ... ]); }
Ich hoffe, zeigen zu können, dass Sagen die Beschreibung komplexer Nebenwirkungen erleichtern. Entdecken Sie Bibliotheks-APIs, übertragen Sie sie auf Ihre Fälle, erstellen Sie komplexe Muster der Ereigniserwartung und seien Sie glücklich. Wir sehen uns bei JS Open Spaces!