wake-up-neo.com

Vor- / Nachteile der Verwendung von Redux-Saga mit ES6-Generatoren im Vergleich zu Redux-Thunk mit ES2017 async / await

Es wird viel über das neueste Kind in Redux Town gesprochen, Redux-Saga/Redux-Saga . Es verwendet Generatorfunktionen zum Abhören/Verteilen von Aktionen.

Bevor ich mich damit beschäftige, möchte ich die Vor- und Nachteile der Verwendung von redux-saga kennen, anstelle der folgenden Vorgehensweise, bei der ich redux-thunk mit async/await verwende.

Eine Komponente könnte so aussehen und Aktionen wie gewohnt auslösen.

import { login } from 'redux/auth';

class LoginForm extends Component {

  onClick(e) {
    e.preventDefault();
    const { user, pass } = this.refs;
    this.props.dispatch(login(user.value, pass.value));
  }

  render() {
    return (<div>
        <input type="text" ref="user" />
        <input type="password" ref="pass" />
        <button onClick={::this.onClick}>Sign In</button>
    </div>);
  } 
}

export default connect((state) => ({}))(LoginForm);

Dann sehen meine Aktionen ungefähr so ​​aus:

// auth.js

import request from 'axios';
import { loadUserData } from './user';

// define constants
// define initial state
// export default reducer

export const login = (user, pass) => async (dispatch) => {
    try {
        dispatch({ type: LOGIN_REQUEST });
        let { data } = await request.post('/login', { user, pass });
        await dispatch(loadUserData(data.uid));
        dispatch({ type: LOGIN_SUCCESS, data });
    } catch(error) {
        dispatch({ type: LOGIN_ERROR, error });
    }
}

// more actions...

// user.js

import request from 'axios';

// define constants
// define initial state
// export default reducer

export const loadUserData = (uid) => async (dispatch) => {
    try {
        dispatch({ type: USERDATA_REQUEST });
        let { data } = await request.get(`/users/${uid}`);
        dispatch({ type: USERDATA_SUCCESS, data });
    } catch(error) {
        dispatch({ type: USERDATA_ERROR, error });
    }
}

// more actions...
437
hampusohlsson

In der Redux-Saga wäre das Äquivalent des obigen Beispiels

export function* loginSaga() {
  while(true) {
    const { user, pass } = yield take(LOGIN_REQUEST)
    try {
      let { data } = yield call(request.post, '/login', { user, pass });
      yield fork(loadUserData, data.uid);
      yield put({ type: LOGIN_SUCCESS, data });
    } catch(error) {
      yield put({ type: LOGIN_ERROR, error });
    }  
  }
}

export function* loadUserData(uid) {
  try {
    yield put({ type: USERDATA_REQUEST });
    let { data } = yield call(request.get, `/users/${uid}`);
    yield put({ type: USERDATA_SUCCESS, data });
  } catch(error) {
    yield put({ type: USERDATA_ERROR, error });
  }
}

Als Erstes müssen wir feststellen, dass wir die API-Funktionen mit der Form yield call(func, ...args) aufrufen. call führt den Effekt nicht aus, sondern erstellt nur ein einfaches Objekt wie {type: 'CALL', func, args}. Die Ausführung wird an die Redux-Saga-Middleware delegiert, die sich um die Ausführung der Funktion und die Wiederaufnahme des Generators mit dem Ergebnis kümmert.

Der Hauptvorteil ist, dass Sie den Generator außerhalb von Redux mit einfachen Gleichheitsprüfungen testen können

const iterator = loginSaga()

assert.deepEqual(iterator.next().value, take(LOGIN_REQUEST))

// resume the generator with some dummy action
const mockAction = {user: '...', pass: '...'}
assert.deepEqual(
  iterator.next(mockAction).value, 
  call(request.post, '/login', mockAction)
)

// simulate an error result
const mockError = 'invalid user/password'
assert.deepEqual(
  iterator.throw(mockError).value, 
  put({ type: LOGIN_ERROR, error: mockError })
)

Beachten Sie, dass wir das Ergebnis des API-Aufrufs verspotten, indem wir die verspotteten Daten einfach in die next -Methode des Iterators einfügen. Das Verspotten von Daten ist viel einfacher als das Verspotten von Funktionen.

Das zweite, was zu bemerken ist, ist der Aufruf von yield take(ACTION). Thunks werden vom Aktionsersteller bei jeder neuen Aktion aufgerufen (z. B. LOGIN_REQUEST). d.h. Aktionen werden fortlaufend auf Thunks gedrückt , und Thunks haben keine Kontrolle darüber, wann sie mit der Bearbeitung dieser Aktionen aufhören sollen.

In der Redux-Saga ziehen Generatoren die nächste Aktion . d.h. sie haben die Kontrolle, wann auf eine Aktion zu hören ist und wann nicht. Im obigen Beispiel befinden sich die Flussanweisungen in einer while(true) -Schleife, sodass sie auf jede eingehende Aktion warten, was das Push-Verhalten des Thunks etwas nachahmt.

Der Pull-Ansatz ermöglicht die Implementierung komplexer Kontrollabläufe. Angenommen, wir möchten zum Beispiel die folgenden Anforderungen hinzufügen

  • Handle LOGOUT Benutzeraktion

  • bei der ersten erfolgreichen Anmeldung gibt der Server ein Token zurück, das nach einer gewissen Verzögerung abläuft und in einem expires_in -Feld gespeichert ist. Die Autorisierung muss alle expires_in Millisekunden im Hintergrund aktualisiert werden

  • Beachten Sie, dass sich der Benutzer beim Warten auf das Ergebnis von API-Aufrufen (entweder Erstanmeldung oder Aktualisierung) zwischenzeitlich abmelden kann.

Wie würden Sie das mit Thunks umsetzen? und gleichzeitig eine vollständige Testabdeckung für den gesamten Durchfluss bieten? So könnte es mit Sagas aussehen:

function* authorize(credentials) {
  const token = yield call(api.authorize, credentials)
  yield put( login.success(token) )
  return token
}

function* authAndRefreshTokenOnExpiry(name, password) {
  let token = yield call(authorize, {name, password})
  while(true) {
    yield call(delay, token.expires_in)
    token = yield call(authorize, {token})
  }
}

function* watchAuth() {
  while(true) {
    try {
      const {name, password} = yield take(LOGIN_REQUEST)

      yield race([
        take(LOGOUT),
        call(authAndRefreshTokenOnExpiry, name, password)
      ])

      // user logged out, next while iteration will wait for the
      // next LOGIN_REQUEST action

    } catch(error) {
      yield put( login.error(error) )
    }
  }
}

Im obigen Beispiel drücken wir unsere Nebenläufigkeitsanforderung mit race aus. Wenn take(LOGOUT) das Rennen gewinnt (d. H. Der Benutzer hat auf einen Logout-Button geklickt). Das Rennen bricht die Hintergrundaufgabe authAndRefreshTokenOnExpiry automatisch ab. Und wenn das authAndRefreshTokenOnExpiry während eines call(authorize, {token}) -Aufrufs blockiert wurde, wird es ebenfalls abgebrochen. Die Stornierung erfolgt automatisch nach unten.

Sie finden eine lauffähige Demo des obigen Ablaufs

430
Yassine Elouafi

Ich werde meine Erfahrung mit der Verwendung von Saga im Produktionssystem zusätzlich zu der ziemlich gründlichen Antwort des Autors der Bibliothek hinzufügen.

Pro (mit Saga):

  • Testbarkeit. Es ist sehr einfach, Sagen zu testen, da call () ein reines Objekt zurückgibt. Um Thunks zu testen, müssen Sie normalerweise einen MockStore in Ihren Test einbinden.

  • redux-saga enthält viele nützliche Hilfsfunktionen für Aufgaben. Es scheint mir, dass das Konzept der Saga darin besteht, eine Art Hintergrund-Worker/Thread für Ihre App zu erstellen, der als fehlendes Element in der Redux-Reaktionsarchitektur fungiert (actionCreators und Reducer müssen reine Funktionen sein.) Was zum nächsten Punkt führt.

  • Sagas bieten einen unabhängigen Ort, um alle Nebenwirkungen zu behandeln. Nach meiner Erfahrung ist es in der Regel einfacher, Aktionen zu ändern und zu verwalten als Thunk-Aktionen.

Con:

  • Generatorsyntax.

  • Viele Konzepte zu lernen.

  • API-Stabilität. Es scheint, dass Redux-Saga immer noch Funktionen hinzufügt (z. B. Kanäle?) Und die Community nicht so groß ist. Es gibt Bedenken, wenn die Bibliothek eines Tages ein nicht abwärtskompatibles Update vornimmt.

88
yjcxy12

Ich möchte nur einige Kommentare aus meiner persönlichen Erfahrung hinzufügen (sowohl mit Sagen als auch mit Thunk):

Sagen sind großartig zu testen:

  • Sie müssen keine mit Effekten umhüllten Funktionen verspotten
  • Daher sind die Tests sauber, lesbar und leicht zu schreiben
  • Bei der Verwendung von Sagen geben Aktionsersteller meist einfache Objektliterale zurück. Es ist auch einfacher zu testen und zu behaupten, anders als Thunks Versprechen.

Sagen sind mächtiger. Alles, was Sie mit dem Action Creator eines Thunks tun können, können Sie auch in einer Saga tun, aber nicht umgekehrt (oder zumindest nicht einfach). Zum Beispiel:

  • warten Sie, bis eine oder mehrere Aktionen ausgelöst wurden (take).
  • bestehende Routine abbrechen (cancel, takeLatest, race)
  • mehrere Routinen können dieselbe Aktion abhören (take, takeEvery, ...)

Sagas bietet auch andere nützliche Funktionen, die einige gängige Anwendungsmuster verallgemeinern:

  • channels zum Abhören externer Ereignisquellen (z. B. Websockets)
  • gabelmodell (fork, spawn)
  • throttle
  • ...

Sagas sind ein großartiges und mächtiges Werkzeug. Mit der Macht geht jedoch auch die Verantwortung einher. Wenn Ihre Anwendung wächst, können Sie leicht verloren gehen, indem Sie herausfinden, wer auf die Auslösung der Aktion wartet oder was alles passiert, wenn eine Aktion ausgelöst wird. Auf der anderen Seite ist Thunk einfacher und leichter zu überlegen. Die Wahl des einen oder anderen hängt von vielen Aspekten ab, wie Art und Größe des Projekts, welche Arten von Nebeneffekten Ihr Projekt handhaben muss oder welche Einstellungen das Entwicklerteam hat. Halten Sie in jedem Fall Ihre Bewerbung einfach und vorhersehbar.

25
madox2

Nur ein paar persönliche Erfahrungen:

  1. Für den Codierungsstil und die Lesbarkeit besteht einer der wichtigsten Vorteile der Verwendung von Redux-Saga in der Vergangenheit darin, die Rückrufhölle in Redux-Thunk zu vermeiden - man muss dann nicht mehr viele Verschachtelungen verwenden. Angesichts der Popularität von Async/Warten-Thunk könnte man bei Verwendung von Redux-Thunk auch Async-Code im Sync-Stil schreiben, was als Verbesserung des Redux-Denkens angesehen werden kann.

  2. Wenn Sie Redux-Saga verwenden, müssen Sie möglicherweise viel mehr Boilerplate-Code schreiben, insbesondere in TypeScript. Wenn man beispielsweise eine asynchrone Abruffunktion implementieren möchte, könnte die Daten- und Fehlerbehandlung mit einer einzigen FETCH-Aktion direkt in einer Thunk-Einheit in action.js ausgeführt werden. In der Redux-Saga muss man möglicherweise die Aktionen FETCH_START, FETCH_SUCCESS und FETCH_FAILURE sowie alle zugehörigen Typprüfungen definieren, da eines der Merkmale der Redux-Saga darin besteht, diese Art von umfangreichem "Token" -Mechanismus zum Erstellen von Effekten und zum Anweisen von Anweisungen zu verwenden Redux Store für einfaches Testen. Natürlich könnte man eine Saga schreiben, ohne diese Aktionen zu verwenden, aber das würde es einem Thunk ähnlich machen.

  3. In Bezug auf die Dateistruktur scheint Redux-Saga in vielen Fällen expliziter zu sein. Man könnte leicht einen asynchronen Code in jeder sagas.ts finden, aber in redux-thunk müsste man ihn in Actions sehen.

  4. Einfaches Testen kann ein weiteres wichtiges Merkmal in der Redux-Saga sein. Das ist wirklich praktisch. Eine Sache, die geklärt werden muss, ist, dass der Redux-Saga-Aufruf-Test keinen tatsächlichen API-Aufruf beim Testen ausführen würde, daher müsste das Beispielergebnis für die Schritte angegeben werden, die ihn nach dem API-Aufruf verwenden könnten. Vor dem Schreiben in Redux-Saga ist es daher besser, eine Saga und die dazugehörigen Saga-Angaben im Detail zu planen.

  5. Redux-saga bietet auch viele erweiterte Funktionen, wie das parallele Ausführen von Aufgaben und Hilfsprogrammen wie takeLatest/takeEvery, fork/spawn, die weitaus leistungsfähiger sind als Thunks.

Abschließend möchte ich persönlich sagen: In vielen normalen Fällen und bei kleinen bis mittelgroßen Apps sollten Sie sich für Async/Warten-Redux-Thunk entscheiden. Es würde Ihnen viele Boilerplate-Codes/Aktionen/Typedefs ersparen, und Sie müssten nicht um viele verschiedene sagas.ts wechseln und einen bestimmten Saga-Baum pflegen. Wenn Sie jedoch eine große App mit einer sehr komplexen asynchronen Logik entwickeln und Funktionen wie Parallelität/Parallelität benötigen oder einen hohen Bedarf an Tests und Wartung haben (insbesondere bei der testgetriebenen Entwicklung), können Redux-Sagen möglicherweise Ihr Leben retten .

Wie auch immer, Redux-Saga ist nicht schwieriger und komplexer als Redux selbst, und es gibt keine so genannte steile Lernkurve, da es nur begrenzte Kernkonzepte und APIs gibt. Wenn Sie ein wenig Zeit damit verbringen, Redux-Saga zu lernen, können Sie eines Tages davon profitieren.

1
Jonathan

Eine einfachere Möglichkeit ist die Verwendung von redux-auto .

aus der Dokumentation

redux-auto hat dieses asynchrone Problem einfach dadurch behoben, dass Sie eine "Aktions" -Funktion erstellen konnten, die ein Versprechen zurückgibt. Um Ihre "Standard" -Funktions-Aktionslogik zu begleiten.

  1. Keine Notwendigkeit für andere asynchrone Redux-Middleware. z.B. Thunk, Versprechen-Middleware, Saga
  2. Mit Leichtigkeit können Sie ein Versprechen an Redux übergeben und es für Sie verwalten lassen
  3. Ermöglicht es Ihnen, externe Serviceabrufe an dem Ort zu lokalisieren, an dem sie umgewandelt werden
  4. Wenn Sie die Datei "init.js" benennen, wird sie beim Start der App einmal aufgerufen. Dies ist gut, um beim Start Daten vom Server zu laden

Die Idee ist, jedes Aktion in einer bestimmten Datei zu haben. Lokalisieren des Serveraufrufs in der Datei mit Reduzierungsfunktionen für "ausstehend", "erfüllt" und "zurückgewiesen". Dies macht die Abwicklung von Versprechungen sehr einfach.

Außerdem wird automatisch ein Hilfsobjekt ("async" genannt) an den Prototyp Ihres Bundesstaates angehängt, sodass Sie die von Ihnen angeforderten Übergänge in Ihrer Benutzeroberfläche nachverfolgen können.

0
codemeasandwich

Thunks versus Sagas

Redux-Thunk und Redux-Saga unterscheiden sich in einigen wichtigen Punkten. Beide sind Middleware-Bibliotheken für Redux (Redux-Middleware ist Code, der Aktionen abfängt, die über die dispatch () -Methode in den Store gelangen).

Eine Aktion kann buchstäblich alles sein. Wenn Sie jedoch Best Practices befolgen, handelt es sich bei einer Aktion um ein einfaches Javascript-Objekt mit einem Typfeld und optionalen Nutzdaten-, Meta- und Fehlerfeldern. z.B.

const loginRequest = {
    type: 'LOGIN_REQUEST',
    payload: {
        name: 'admin',
        password: '123',
    }, };

Redux-Thunk

Zusätzlich zum Versenden von Standardaktionen können Sie mit der Middleware Redux-Thunk spezielle Funktionen versenden, die thunks heißen.

Thunks (in Redux) haben im Allgemeinen die folgende Struktur:

export const thunkName =
   parameters =>
        (dispatch, getState) => {
            // Your application logic goes here
        };

Das heißt, thunk ist eine Funktion, die (optional) einige Parameter übernimmt und eine andere Funktion zurückgibt. Die innere Funktion hat eine dispatch function und eine getState Funktion - beide werden von der Middleware Redux-Thunk geliefert.

Redux-Saga

Redux-Saga Mit der Middleware können Sie komplexe Anwendungslogik als reine Funktionen, sogenannte Sagas, ausdrücken. Reine Funktionen sind vom Standpunkt des Testens aus wünschenswert, da sie vorhersehbar und wiederholbar sind, wodurch sie relativ einfach zu testen sind.

Sagas werden durch spezielle Funktionen implementiert, die als Generatorfunktionen bezeichnet werden. Dies ist eine neue Funktion von ES6 JavaScript. Grundsätzlich springt die Ausführung immer dann in einen Generator ein und aus, wenn Sie eine Ertragsaussage sehen. Stellen Sie sich eine yield -Anweisung vor, die den Generator anhält und den ermittelten Wert zurückgibt. Später kann der Anrufer den Generator mit der Anweisung fortsetzen, die auf yield folgt.

Eine Generatorfunktion ist so definiert. Beachten Sie das Sternchen nach dem Funktionsschlüsselwort.

function* mySaga() {
    // ...
}

Sobald die Login-Saga mit Redux-Saga registriert ist. Aber dann pausiert der yield in der ersten Zeile die Saga, bis eine Aktion mit dem Typ 'LOGIN_REQUEST' in den Laden geschickt wird. Sobald dies geschieht, wird die Ausführung fortgesetzt.

Weitere Einzelheiten finden Sie in diesem Artikel .

0
Mselmi Ali

Eine kurze Notiz. Generatoren sind kündbar, asynchron/warten - nicht. Für ein Beispiel aus der Frage ergibt es also keinen Sinn, was zu wählen ist. Aber für kompliziertere Abläufe gibt es manchmal keine bessere Lösung als die Verwendung von Generatoren.

Eine andere Idee könnte sein, Generatoren mit Redux-Thunk zu verwenden, aber für mich scheint es, als würde man versuchen, ein Fahrrad mit quadratischen Rädern zu erfinden.

Generatoren sind natürlich einfacher zu testen.

0
Dmitriy

Hier ist ein Projekt, das die besten Teile (Profis) von sowohl redux-saga als auch redux-thunk kombiniert: Sie können alle Nebenwirkungen auf Sagen behandeln, während Sie von dispatching die entsprechende Aktion erhalten: https://github.com/diegohaz/redux-saga-thunk

class MyComponent extends React.Component {
  componentWillMount() {
    // `doSomething` dispatches an action which is handled by some saga
    this.props.doSomething().then((detail) => {
      console.log('Yaay!', detail)
    }).catch((error) => {
      console.log('Oops!', error)
    })
  }
}
0
Diego Haz

Nachdem ich meiner Erfahrung nach einige verschiedene React/Redux-Projekte in großem Maßstab durchgesehen habe, bieten Sagas Entwicklern eine strukturiertere Art, Code zu schreiben, die viel einfacher zu testen und schwerer zu verwechseln ist.

Ja, es ist anfangs etwas seltsam, aber die meisten Entwickler haben an einem Tag genug Verständnis dafür. Ich sage den Leuten immer, dass sie sich keine Sorgen machen sollen, was yield zu Beginn tut, und dass es zu Ihnen kommen wird, wenn Sie ein paar Tests geschrieben haben.

Ich habe ein paar Projekte gesehen, bei denen Thunks so behandelt wurden, als wären sie Controller aus dem MVC-Patten, und dies wird schnell zu einem unheilbaren Durcheinander.

Mein Rat ist, Sagas dort zu verwenden, wo Sie A benötigen, um Dinge vom Typ B in Bezug auf ein einzelnes Ereignis auszulösen. Für alles, was sich über mehrere Aktionen erstrecken könnte, ist es meiner Meinung nach einfacher, Kunden-Middleware zu schreiben und diese über die Meta-Eigenschaft einer FSA-Aktion auszulösen.

0
David Bradshaw