Erfahrung mit Redux ohne Reduzierstücke



Ich möchte meine Erfahrungen mit der Verwendung von Redux in einer Unternehmensanwendung teilen. Wenn ich als Teil des Artikels über Unternehmenssoftware spreche, konzentriere ich mich auf die folgenden Funktionen:

  • Erstens ist dies das Volumen der Funktionalität. Dies sind Systeme, die seit vielen Jahren entwickelt wurden, weiterhin neue Module bauen oder das, was bereits vorhanden ist, auf unbestimmte Zeit komplizieren.
  • Zweitens kann häufig, wenn wir nicht einen Präsentationsbildschirm in Betracht ziehen, sondern den Arbeitsplatz einer anderen Person, eine große Anzahl angeschlossener Komponenten auf einer Seite bereitgestellt werden.
  • Drittens die Komplexität der Geschäftslogik. Wenn wir eine reaktionsschnelle und benutzerfreundliche Anwendung erhalten möchten, muss ein wesentlicher Teil der Logik vom Client ausgeführt werden.

Die ersten beiden Punkte beschränken die Produktivitätsspanne. Dazu später mehr. Und jetzt schlage ich vor, die Probleme zu diskutieren, die bei der Verwendung des klassischen Redux-Workflows auftreten, und etwas Komplizierteres als die TODO-Liste zu entwickeln.

Klassischer Redux


Betrachten Sie als Beispiel die folgende Anwendung:

Bild

Der Benutzer treibt einen Reim - bekommt eine Einschätzung seines Talents. Die Kontrolle mit der Einführung des Verses wird kontrolliert und die Neubewertung der Bewertung erfolgt für jede Änderung. Es gibt auch eine Schaltfläche, mit der der Text mit dem Ergebnis zurückgesetzt wird, und dem Benutzer wird eine Meldung angezeigt, dass er von vorne beginnen kann. Quellcode in diesem Thread .

Code Organisation:

Bild

Es gibt zwei Module. Genauer gesagt ist ein Modul direkt das Gedicht-Scoring. Und die Wurzel der Anwendung mit gemeinsamen Funktionen für das gesamte System ist die App. Dort haben wir Informationen über den Benutzer und zeigen dem Benutzer Nachrichten an. Jedes Modul verfügt über eigene Reduzierungen, Aktionen, Steuerelemente usw. Mit zunehmender Anwendung vermehren sich neue Module.

Eine Kaskade von Reduzierern, die Redux-unveränderlich verwenden, bildet den folgenden vollständig unveränderlichen Zustand:

Bild

Wie es funktioniert:

1. Kontrollieren Sie den Ersteller der Versandaktion:

import at from '../constants/actionTypes'; export function poemTextChange(text) { return function (dispatch, getstate) { dispatch({ type: at.POEM_TYPE, payload: text }); }; } 

Konstanten von Aktionstypen werden in eine separate Datei verschoben. Erstens sind wir vor Tippfehlern so sicher. Zweitens wird uns Intellisense zur Verfügung stehen.

2. Dann kommt es zum Reduzierstück.

 import logic from '../logic/poem'; export default function poemScoringReducer(state = Immutable.Map(), action) { switch (action.type) { case at.POEM_TYPE: return logic.onType(state, action.payload); default: return state; } } 

Die Logikverarbeitung wird in eine separate Fallfunktion verschoben. Andernfalls wird der Reduzierungscode schnell unlesbar.

3. Die Logik der Verarbeitung von Klicks mithilfe von lexikalischer Analyse und künstlicher Intelligenz:

 export default { onType(state, text) { return state .set('poemText', text) .set('score', this.calcScore(text)); }, calcScore(text) { const score = Math.floor(text.length / 10); return score > 5 ? 5 : score; } }; 

Im Fall der Schaltfläche "Neues Gedicht" haben wir den folgenden Aktionsersteller:

 export function newPoem() { return function (dispatch, getstate) { dispatch({ type: at.POEM_TYPE, payload: '' }); dispatch({ type: appAt.SHOW_MESSAGE, payload: 'You can begin a new poem now!' }); }; } 

Senden Sie zunächst dieselbe Aktion aus, mit der unser Text und unsere Punktzahl zurückgesetzt werden. Senden Sie dann die Aktion, die von einem anderen Reduzierer abgefangen wird, und zeigen Sie dem Benutzer eine Nachricht an.
Alles ist schön. Lassen Sie uns Probleme für uns selbst schaffen:

Die Probleme:


Wir haben unsere Bewerbung veröffentlicht. Aber unsere Benutzer, die sahen, dass sie gebeten wurden, Gedichte zu schreiben, begannen natürlich, ihre Arbeiten zu veröffentlichen, was mit den Unternehmensstandards der poetischen Sprache unvereinbar ist. Mit anderen Worten, wir müssen obszöne Worte moderieren.

Was machen wir:

  • Im Eingabetext müssen alle nicht kultivierten Wörter durch * zensiert * ersetzt werden.
  • Wenn der Benutzer ein Schimpfwort geschrieben hat, müssen Sie ihn außerdem mit einer Meldung warnen, dass er etwas falsch macht.

Gut. Wir müssen nur den Text analysieren und zusätzlich die Punktzahl berechnen, um die schlechten Wörter zu ersetzen. Kein Problem. Um den Benutzer zu informieren, benötigen Sie außerdem eine Liste der von uns gelöschten Daten. Der Quellcode ist hier .

Wir überarbeiten die Funktion der Logik so, dass sie zusätzlich zum neuen Status die für die Nachricht erforderlichen Informationen an den Benutzer zurückgibt (ersetzte Wörter):

 export default { onType(state, text) { const { reductedText, censoredWords } = this.redactText(text); const newState = state .set('poemText', reductedText) .set('score', this.calcScore(reductedText)); return { newState, censoredWords }; }, calcScore(text) { const score = Math.floor(text.length / 10); return score > 5 ? 5 : score; }, redactText(text) { const result = { reductedText:text }; const censoredWords = []; obscenseWords.forEach((badWord) => { if (result.reductedText.indexOf(badWord) >= 0) { result.reductedText = result.reductedText.replace(badWord, '*censored*'); censoredWords.push(badWord); } }); if (censoredWords.length > 0) { result.censoredWords = censoredWords.join(' ,'); } return result; } }; 

Wenden wir es jetzt an. Aber wie? Im Reduzierer macht es für uns keinen Sinn mehr, es zu nennen, da wir den Text und die Bewertung in den Zustand versetzen werden, aber was sollen wir mit der Nachricht tun? Um eine Nachricht zu senden, müssen wir in jedem Fall die entsprechende Aktion auslösen. Also finalisieren wir den Action-Creator.

 export function poemTextChange(text) { return function (dispatch, getState) { const globalState = getState(); const scoringStateOld = globalState.get('poemScoring'); //        const { newState, censoredWords } = logic.onType(scoringStateOld, text); dispatch({ //        type: at.POEM_TYPE, payload: newState }); if (censoredWords) { //    ,    const userName = globalState.getIn(['app', 'account', 'name']); const message = `${userName}, avoid of using word ${censoredWords}, please!`; dispatch({ type: appAt.SHOW_MESSAGE, payload: message }); } }; } 

Es ist auch erforderlich, den Reduzierer zu ändern, da er die Logikfunktion nicht mehr aufruft:

  switch (action.type) { case at.POEM_TYPE: return action.payload; default: return state; 

Was ist passiert:

Bild

Und jetzt ist die Frage. Warum brauchen wir einen Reduzierer, der zum größten Teil einfach Nutzlast anstelle eines neuen Zustands zurückgibt? Wenn andere Aktionen angezeigt werden, die die Logik in der Aktion verarbeiten, muss dann ein neuer Aktionstyp registriert werden? Oder vielleicht ein gemeinsames SET_STATE erstellen? Wahrscheinlich nicht, denn dann wird der Inspektor ein Chaos sein. Also werden wir die gleiche Art von Fall produzieren?

Das Wesentliche des Problems ist wie folgt. Wenn die Verarbeitung von Logik das Arbeiten mit einem Zustand beinhaltet, für den mehrere Reduzierer verantwortlich sind, müssen Sie alle Arten von Perversionen schreiben. Zum Beispiel die Zwischenergebnisse von Fallfunktionen, die dann mit mehreren Aktionen auf verschiedene Reduzierungen verteilt werden müssen.

In einer ähnlichen Situation müssen Sie, wenn die Fallfunktion mehr Informationen benötigt als in Ihrem Reduzierer enthalten, die Aktion aufrufen, bei der Zugriff auf den globalen Status besteht, und anschließend den neuen Status als Nutzlast senden. Ein Reduzierer muss in jedem Fall aufgeteilt werden, wenn das Modul viel Logik enthält. Und das schafft große Unannehmlichkeiten.

Schauen wir uns die Situation auf einer Seite an. In unserer Aktion erhalten wir ein Stück Staat von der Welt. Dies ist notwendig, um es zu mutieren ( globalState.get ('gedichtScoring'); ). Es stellt sich heraus, dass wir bereits in Aktion wissen, mit welchem ​​Zustand die Arbeit läuft. Wir haben ein neues Stück Staat. Wir wissen, wo wir es hinstellen sollen. Aber anstatt es in ein globales zu setzen, führen wir es mit einer Art Textkonstante durch die Kaskade der Reduzierungen, so dass es jeden Schalterfall durchläuft und ihn nur einmal ersetzt. Ich von der Erkenntnis, Falten. Ich verstehe, dass dies getan wird, um die Entwicklung zu vereinfachen und die Konnektivität zu verringern. In unserem Fall spielt es jedoch keine Rolle mehr.

Jetzt werde ich alle Punkte auflisten, die mir in der aktuellen Implementierung nicht gefallen, wenn sie für eine unbegrenzte Zeit in der Breite und Tiefe skaliert werden müssen :

  1. Erhebliche Unannehmlichkeiten bei der Arbeit mit einem Zustand außerhalb des Reduzierers.
  2. Das Problem der Codetrennung. Jedes Mal, wenn wir eine Aktion auslösen, durchläuft sie jeden Reduzierer und jeden Fall. Es ist praktisch, sich nicht darum zu kümmern, wenn Sie eine kleine Anwendung haben. Aber wenn Sie ein Monster haben, das mehrere Jahre lang mit Dutzenden von Reduzierern und Hunderten von Fällen gebaut wurde, dann beginne ich über die Machbarkeit eines solchen Ansatzes nachzudenken. Selbst in Tausenden von Fällen hat dies möglicherweise keinen wesentlichen Einfluss auf die Leistung. Da ich jedoch verstehe, dass beim Drucken von Text jede Druckmaschine Hunderte von Fällen durchläuft, kann ich es nicht so lassen, wie es ist. Jede kleinste Verzögerung, multipliziert mit unendlich, tendiert zu unendlich. Mit anderen Worten, wenn Sie früher oder später nicht über solche Dinge nachdenken, treten Probleme auf.

    Welche Möglichkeiten gibt es?

    a. Isolierte Anwendungen mit eigenen Anbietern . In jedem Modul (Unteranwendung) müssen Sie die allgemeinen Teile des Status (Konto, Nachrichten usw.) duplizieren.

    b. Verwenden Sie steckbare asynchrone Reduzierstücke . Dies wird von Dan selbst nicht empfohlen.

    c. Verwenden Sie Aktionsfilter in Reduzierstücken. Das heißt, jeder Versand sollte Informationen darüber enthalten, an welches Modul er gesendet wird. Schreiben Sie in die Root-Reduzierer der Module die entsprechenden Bedingungen. Ich habe es versucht Es gab weder vorher noch nachher so viele unfreiwillige Fehler. Es gibt ständige Verwirrung darüber, wohin die Aktion geht.
  3. Jedes Mal, wenn eine Aktion ausgelöst wird, gibt es nicht nur einen Lauf für jeden Reduzierer, sondern auch die Erfassung des umgekehrten Zustands. Es spielt keine Rolle, ob sich der Status im Reduzierer geändert hat - er wird in Mähdreschern ersetzt.
  4. Bei jedem Versand wird die Verarbeitung von mapStateToProps für jede angehängte Komponente erzwungen, die auf der Seite bereitgestellt wird. Wenn wir Reduzierstücke aufteilen, müssen wir die Sendungen aufteilen. Ist es wichtig, dass wir eine Schaltfläche haben, die den Text überschreibt und die Nachricht mit verschiedenen Versendungen anzeigt? Wahrscheinlich nicht. Ich habe jedoch Optimierungserfahrung, wenn die Anzahl der Versendungen von 15 auf 3 reduziert wurde, um die Reaktionsfähigkeit des Systems bei gleicher verarbeiteter Geschäftslogik erheblich zu verbessern. Ich weiß, dass es Bibliotheken gibt, die mehrere Sendungen zu einer Charge kombinieren können, aber dies ist ein Kampf bei der Untersuchung mit Krücken.
  5. Beim Zerkleinern von Sendungen ist es manchmal sehr schwierig zu sehen, was passiert. Es gibt keinen Ort, alles ist auf verschiedene Dateien verteilt. Es muss gesucht werden, wo die Verarbeitung implementiert ist, indem in allen Quellcodes nach Konstanten gesucht wird.
  6. Im obigen Code greifen Komponenten und Aktionen direkt auf den globalen Status zu:

     const userName = globalState.getIn(['app', 'account', 'name']); … const text = state.getIn(['poemScoring', 'poemText']); 

    Dies ist aus mehreren Gründen nicht gut:

    a. Module sollten idealerweise isoliert werden. Sie müssen nicht wissen, wo in dem Staat sie leben.

    b. Das Erwähnen derselben Pfade an verschiedenen Stellen ist häufig nicht nur mit Fehlern / Tippfehlern behaftet, sondern erschwert auch das Refactoring, wenn die Konfiguration des globalen Status oder die Art und Weise seiner Speicherung geändert wird.
  7. Während ich eine neue Aktion schrieb, hatte ich zunehmend den Eindruck, dass ich Code für Code schreibe. Angenommen, wir möchten der Seite ein Kontrollkästchen hinzufügen und ihren booleschen Status in der Story widerspiegeln. Wenn wir eine einheitliche Organisation von Maßnahmen / Reduzierungen wollen, müssen wir:

    - Registrieren Sie die Konstante vom Aktionstyp
    - Schreiben Sie einen Aktionskrater
    - Importieren Sie es im Steuerelement und registrieren Sie es in mapDispatchToProps
    - Registrieren Sie sich in PropTypes
    - Erstellen Sie ein handleCheckBoxClick im Steuerelement und geben Sie es im Kontrollkästchen an
    - Fügen Sie einen Schalter im Reduzierstück mit einem Fallfunktionsaufruf hinzu
    - Schreiben Sie eine Fallfunktion in die Logik

    Für einen Boxcheck!
  8. Der mit combinReducers generierte Status ist statisch. Es spielt keine Rolle, ob Sie bereits Modul B eingegeben haben oder nicht, dieses Stück wird in der Geschichte enthalten sein. Leer, wird aber sein. Es ist nicht bequem, den Inspektor zu verwenden, wenn sich viele nicht verwendete leere Knoten in der Straße befinden.

Wie wir versuchen, einige der oben beschriebenen Probleme zu lösen


Also haben wir dumme Reduzierungen und in Action-Kratern / Logik schreiben wir Code-Teile für die Arbeit mit tief eingebetteten unveränderlichen Strukturen. Um dies zu beseitigen, verwende ich den Mechanismus hierarchischer Selektoren, die den Zugriff auf den gewünschten Status nicht nur ermöglichen, sondern ihn auch ersetzen (praktisches setIn). Ich habe dies in einem Paket mit unveränderlichen Selektoren veröffentlicht .

Schauen wir uns unser Beispiel an, wie es funktioniert ( Repository ):
Im GedichtScoring-Modul beschreiben wir das Selektorobjekt. Wir beschreiben die Felder aus dem Zustand, auf den wir direkten Lese- / Schreibzugriff haben möchten. Alle Verschachtelungen und Parameter für den Zugriff auf die Elemente von Sammlungen sind zulässig. Es ist nicht notwendig, alle möglichen Felder in unserem Artikel zu beschreiben.

 import extendSelectors from 'immutable-selectors'; const selectors = { poemText:{}, score:{} }; extendSelectors(selectors, [ 'poemScoring' ]); export default selectors; 

Darüber hinaus verwandelt die ExtendSelectors-Methode jedes Feld in unserem Objekt in eine Selektorfunktion. Der zweite Parameter gibt den Pfad zu dem Teil des Zustands an, den der Selektor steuert. Wir erstellen kein neues Objekt, sondern ändern das aktuelle. Dies gibt uns einen Bonus in Form von Arbeitsintelligenz:

Bild

Was ist unser Objekt - ein Selektor nach seiner Erweiterung:

Bild

Die Funktion selectors.poemText (state) führt einfach state.getIn (['gedichtScoring', 'gedichtText']) aus .

Funktionswurzel (Zustand) - erhält 'GedichtScoring'.

Jeder Selektor verfügt über eine eigene Ersetzungsfunktion (globalState, newPart) , die über setIn einen neuen globalen Status zurückgibt, wobei der entsprechende Teil ersetzt wird.

Außerdem wird ein flaches Objekt hinzugefügt, zu dem alle eindeutigen Auswahltasten dupliziert werden. Das heißt, wenn wir einen tiefen Zustand der Form verwenden

 selectors = { dive:{ in:{ to:{ the:{ deep:{} } } } }} 

Sie können als Selektoren tief in den Tiefpunkt (Zustand) oder als Selektoren flach ( tief ) eintauchen .

Mach weiter. Wir müssen die Datenerfassung in den Steuerelementen aktualisieren:

Gedicht:
 function mapStateToProps(state, ownprops) { return { text:selectors.poemText(state) || '' }; } 


Punktzahl:
 function mapStateToProps(state, ownprops) { const score = selectors.score(state); return { score }; } 

Ändern Sie als Nächstes den Wurzelreduzierer:

 import initialState from './initialState'; function setStateReducer(state = initialState, action) { if (action.setState) { return action.setState; } else { return state; // return combinedReducers(state, action); // } } export default setStateReducer; 

Auf Wunsch können wir mit combinReducers kombinieren.

Aktionskrater, zum Beispiel GedichtTextChange:

 export function poemTextChange(text) { return function (dispatch, getState) { dispatch({ type: 'Poem typing', setState: logic.onType(getState(), text), payload: text }); }; } 

Wir können keine Konstanten vom Aktionstyp mehr verwenden, da der Typ jetzt nur zur Visualisierung im Inspektor verwendet wird. Wir im Projekt schreiben Volltextbeschreibungen der Aktion in russischer Sprache. Sie können auch die Nutzlast entfernen, aber ich versuche, sie zu speichern, damit ich im Inspektor bei Bedarf verstehe, mit welchen Parametern die Aktion aufgerufen wurde.

Und in der Tat die Logik selbst:

  onType(gState, text) { const { reductedText, censoredWords } = this.redactText(text); const poemState = selectors.root(gState) || Immutable.Map(); //     const newPoemState = poemState //  .set('poemText', reductedText) .set('score', this.calcScore(reductedText)); let newGState = selectors.root.replace(gState, newPoemState); //    if (censoredWords) { //  ,    const userName = appSelectors.flat.userName(gState); const messageText = `${userName}, avoid of using word ${censoredWords}, please!`; newGState = message.showMessage(newGState, messageText); } return newGState; }, 

Gleichzeitig wird message.showMessage aus der Logik des benachbarten Moduls importiert, das dessen Selektoren beschreibt:

  showMessage(gState, text) { return selectors.message.text.replace(gState, text); }. 

Was sich herausstellt:

Bild

Beachten Sie, dass wir einen Versand hatten, die Daten in zwei Modulen geändert.
All dies ermöglichte es uns, Reduzierungen und Konstanten vom Aktionstyp zu beseitigen sowie die meisten der oben beschriebenen Engpässe zu lösen oder zu umgehen.

Wie kann dies sonst angewendet werden?


Dieser Ansatz ist praktisch, wenn sichergestellt werden muss, dass Ihre Steuerelemente oder Module die Arbeit mit unterschiedlichen Statuselementen ermöglichen. Nehmen wir an, ein Gedicht reicht uns nicht. Wir möchten, dass der Benutzer Gedichte auf zwei verschiedenen Registerkarten in verschiedenen Disziplinen (Kinder, Romantiker) verfassen kann. In diesem Fall können wir die Selektoren nicht in die Logik / Steuerelemente importieren, sondern sie als Parameter in der externen Steuerung angeben:

  <Poem selectors = {selectors.hildPoem}/> <Poem selectors = {selectors.romanticPoem}/> 

Übergeben Sie diesen Parameter außerdem an Aktionskrater. Dies reicht aus, um eine komplexe Kombination aus Komponenten und Logik vollständig einzuschließen und die Wiederverwendung zu vereinfachen.

Einschränkungen bei der Verwendung von unveränderlichen Selektoren:

Es funktioniert nicht, den Schlüssel im Status "Name" zu verwenden, da für die übergeordnete Funktion versucht wird, die reservierte Eigenschaft zu überschreiben.

Was ist das Ergebnis?


Als Ergebnis wurde ein ziemlich flexibler Ansatz erhalten, implizite Codebeziehungen durch Textkonstanten wurden eliminiert, der Overhead wurde reduziert, während der Entwicklungskomfort beibehalten wurde. Es gibt auch einen voll funktionsfähigen Redux-Inspektor mit der Möglichkeit einer Zeitreise. Ich habe keine Lust, zu Standardreduzierern zurückzukehren.

Im Allgemeinen ist das alles. Vielen Dank für Ihre Zeit. Vielleicht wird jemand daran interessiert sein, es auszuprobieren!

Source: https://habr.com/ru/post/de424613/


All Articles