Auf der Suche nach einer Silberkugel: Schauspieler + FRP in React

    Nur wenige Leute schreiben bereits auf Perl, aber die berühmte Maxime von Larry Walla "Behalte einfache Dinge und harte Dinge möglich" ist zur gängigen Formel für effektive Technologie geworden. Es kann nicht nur hinsichtlich der Komplexität der Aufgaben, sondern auch hinsichtlich des Ansatzes interpretiert werden: Eine ideale Technologie sollte einerseits die schnelle Entwicklung von mittleren und kleinen Anwendungen (einschließlich "Nur-Schreiben") ermöglichen, andererseits Werkzeuge für eine durchdachte Entwicklung bieten komplexe Anwendungen, bei denen Zuverlässigkeit, Wartbarkeit und Strukturiertheit an erster Stelle stehen. Oder sogar in die menschliche Ebene übersetzen: für den Juni zugänglich sein und gleichzeitig die Wünsche des Signor erfüllen.


    Jetzt beliebt Redaks können auf beiden Seiten kritisiert werden - nehmen Sie zumindest die Tatsache in Kauf, dass das Schreiben selbst einer elementaren Funktionalität zu vielen Zeilen führen kann, die durch mehrere Dateien getrennt sind - aber wir werden nicht tiefer gehen, da dies bereits viel gesagt wurde.


    "Es ist, als ob Sie alle Tische in einem Raum und die Stühle im anderen Raum aufbewahren"
    - Juha Paananen, der Erfinder der Bacon.js-Bibliothek, über Redax

    Die Technologie, über die heute diskutiert wird, ist keine Silberkugel, sondern behauptet, die angegebenen Kriterien zu erfüllen.


    Mrr ist eine funktional reaktive Bibliothek, die das Prinzip "alles ist ein Fluss" bekennt. Die wichtigsten Vorteile des funktional-reaktiven Ansatzes für mrr: Prägnanz, Ausdrucksstärke des Codes sowie ein einheitlicher Ansatz für synchrone und asynchrone Datenumwandlungen.


    Auf den ersten Blick klingt dies nicht nach einer Technologie, die für Anfänger leicht zugänglich ist: Das Konzept des Flows ist schwer verständlich, im Frontend ist es nicht so üblich, da es sich hauptsächlich um solche obszönen Bibliotheken wie Rx handelt. Und vor allem ist es nicht ganz klar, wie die Flüsse basierend auf dem grundlegenden Action-Response-Update-DOM-Schema zu erklären sind. Aber ... wir werden nicht abstrakt über Ströme sprechen! Sprechen wir über verständlichere Dinge: Ereignisse, Zustand.


    Nach Rezept kochen


    Ohne in den Dschungel von FRP einzusteigen, folgen wir einem einfachen Formalisierungsmuster des Themenbereichs:


    • Erstellen Sie eine Liste mit Daten, die den Status der Seite beschreiben und in der Benutzeroberfläche verwendet werden, sowie deren Typ.
    • Erstellen Sie eine Liste mit Ereignissen, die vom Benutzer auf der Seite auftreten oder von diesem generiert werden, und den Datentypen, die mit ihnen übertragen werden
    • Erstellen Sie eine Liste der Prozesse, die auf der Seite ausgeführt werden
    • Zusammenhänge erkennen.
    • beschreiben Sie Abhängigkeiten mit geeigneten Operatoren.

    In diesem Fall benötigen wir das Wissen der Bibliothek nur im letzten Stadium.


    Nehmen wir ein vereinfachtes Beispiel für einen Webshop, der eine Liste von Produkten mit Seitenumbruch und Filterung nach Kategorien sowie einen Einkaufswagen enthält.


    1. Die Daten, auf deren Basis die Schnittstelle aufgebaut wird:


      • Produktliste (Array)
      • ausgewählte Kategorie (String)
      • Anzahl Seiten mit Waren (Anzahl)
      • Liste der im Warenkorb befindlichen Waren (Array)
      • aktuelle Seite (Nummer)
      • Anzahl der Artikel im Warenkorb (Anzahl)

    2. Ereignisse (mit "Ereignisse" sind nur Sofortereignisse gemeint. Aktionen, die im Laufe der Zeit stattfinden - Prozesse - müssen in separate Ereignisse zerlegt werden):


      • Startseite (nichtig)
      • Kategorieauswahl (String)
      • Produkt in den Warenkorb legen (Artikelobjekt)
      • Entnahme von Waren aus dem Warenkorb (Produkt-ID, die entfernt wird)
      • Zur nächsten Seite der Produktliste gehen (Nummer - Seitennummer)

    3. Prozesse: Dies sind Aktionen, die auf einmal oder nach einiger Zeit mit verschiedenen Ereignissen beginnen und dann enden können. In unserem Fall ist dies der Download von Produktdaten vom Server, was zwei Ereignisse nach sich ziehen kann: Erfolgreicher Abschluss und Abschluss mit Fehler.


    4. Abhängigkeiten zwischen Ereignissen und Daten. Beispielsweise hängt die Liste der Produkte vom Ereignis ab: "erfolgreiches Laden der Produktliste". "Laden der Produktliste starten" - von "Seite öffnen", "Aktuelle Seite auswählen", "Kategorie auswählen". Machen Sie eine Liste der Form [Element]: [... Abhängigkeiten]:


      {
          requestGoods: ['page', 'category', 'pageLoaded'],
          goods: ['requestGoods.success'],
          page: ['goToPage', 'totalPages'],
          totalPages: ['requestGoods.success'],
          cart: ['addToCart', 'removeFromCart'],
          goodsInCart: ['cart'],
          category: ['selectCategory']
      }


    Oh ... aber das ist fast der Code für mrr!



    Es müssen nur noch Funktionen hinzugefügt werden, die die Beziehung beschreiben. Sie haben vielleicht erwartet, dass Ereignisse, Daten und Prozesse in mrr unterschiedliche Entitäten sind - aber nein, das sind alles Streams! Unsere Aufgabe besteht nur darin, sie richtig zu verbinden.


    Wie Sie sehen, gibt es zwei Arten von Abhängigkeiten: "Daten" von "Ereignis" (z. B. Seite von goToPage) und "Daten" von "Daten" (goodsInCart aus Warenkorb). Für jeden von ihnen gibt es geeignete Ansätze.


    Am einfachsten geht es mit den "Daten aus den Daten": Hier fügen wir einfach eine reine Funktion "Formel" hinzu:


    goodsInCart: [arr => arr.length, 'cart'],

    Bei jeder Änderung des Cart-Arrays wird der Wert von goodsInCart neu berechnet.


    Wenn unsere Daten von einem Ereignis abhängen, ist auch alles ganz einfach:


    
    category: 'selectCategory',
    /*
    то же саме что 
    category: [a => a, 'selectCategory'],
    */
    goods: [resp => resp.data, 'requestGoods.success'],
    totalPages: [resp => resp.totalPages, 'requestGoods.success'],

    Die Konstruktion der Form [Funktion, ... Threads-Argumente] ist die Basis von mrr. Um ein intuitives Verständnis zu erhalten, eine Analogie mit Excel zu zeichnen, werden die Flüsse in mrr auch Zellen genannt, und die Funktionen, mit denen sie berechnet werden, sind Formeln.


    Wenn wir Daten haben, die von mehreren Ereignissen abhängig sind, müssen wir ihre Werte einzeln transformieren und dann mit dem Merge-Operator in einem Stream zusammenführen:


    /*
    да, оператор merge - просто строка, это нормально
    */
        page: ['merge', 
            [a => a, 'goToPage'], 
            [(a, prev) => a < prev ? a : prev, 'totalPages', '-page']
        ],
        cart: ['merge', 
            [(item, arr) => [...arr, item], 'addToCart', '-cart'],
            [(id, arr) => arr.filter(item => item.id !== id), 'removeFromCart', '-cart'],
        ],

    In beiden Fällen beziehen wir uns auf den vorherigen Wert der Zelle. Um eine Endlosschleife zu vermeiden, beziehen wir uns passiv auf die Zellen des Warenkorbs und der Seite (das Minuszeichen vor dem Zellennamen): Ihre Werte werden in die Formel eingesetzt, aber wenn sie sich ändern, wird die Neuberechnung nicht gestartet.


    Alle Streams werden entweder auf Basis anderer Streams erstellt oder vom DOM ausgegeben. Aber wie sieht es mit dem Fluss "Startseite" aus? Glücklicherweise ist es nicht nötig, componentDidMount zu verwenden: In mrr gibt es einen speziellen $ start-Thread, der signalisiert, dass die Komponente erstellt und eingehängt wurde.


    "Prozesse" werden asynchron berechnet, während bestimmte Ereignisse von ihnen ausgegeben werden. Der Operator "geschachtelt" hilft uns dann:


    requestGoods: ['nested', (cb, page, category) => {
        fetch("...")
        .then(res => cb('success', res))
        .catch(e => cb('error', e));
    }, 'page', 'category', '$start'],

    Bei Verwendung des verschachtelten Operators ist das erste Argument eine Rückruffunktion zum Ausgeben bestimmter Ereignisse. In diesem Fall sind sie von außen über den Namensraum der Stammzelle erreichbar, z.


    cb('success', res)

    In der Formel "requestGoods" wird die Zelle "requestGoods.success" aktualisiert.


    Um die Seite korrekt zu rendern, bevor unsere Daten berechnet werden, können Sie deren Anfangswerte angeben:


    {
        goods: [],
        page: 1,
        cart: [],
    },

    Markup hinzufügen Wir erstellen eine React-Komponente mit der withMrr-Funktion, die ein reaktives Verknüpfungsschema und eine Renderfunktion akzeptiert. Um einen Wert in einen Stream "einzufügen", verwenden wir die $ -Funktion, die Event-Handler erstellt (und im Cache speichert). Nun sieht unsere vollständig funktionierende Anwendung so aus:


    import { withMrr } from'mrr';
    const App = withMrr({
        $init: {
            goods: [],
            cart: [],
            page: 1,
        },
        requestGoods: ['nested', (cb, page = 1, category = 'all') => {
            fetch('https://reqres.in/api/products?page=', page, category).then(r => r.json())
            .then(res => cb('success', res))
            .catch(e => cb('error', e))
        }, 'page', 'selectCategory', '$start'],
        goods: [res => res.data, 'requestGoods.success'],
        page: ['merge', 'goToPage', [(a, prev) => a < prev ? a : prev, 'totalPages', '-page']],
        totalPages: [res => res.total_pages, 'requestGoods.success'],
        category: 'selectCategory',
        cart: ['merge', 
            [(item, arr) => [...arr, item], 'addToCart', '-cart'],
            [(id, arr) => arr.filter(item => item.id !== id), 'removeFromCart', '-cart'],
        ],
    }, (state, props, $) => {
        return (<section><h2>Shop</h2><div>
                Category: <selectonChange={$('selectCategory')}><option>All</option><option>Electronics</option><option>Photo</option><option>Cars</option></select></div><ulclassName="goods">
                { state.goods.map((item, i) => { 
                    const cartI = state.cart.findIndex(a => a.id === item.id);
                    return (<likey={i}>
                        { item.name }
                        <div>
                            { cartI === -1 && <buttononClick={$("addToCart", item)}>Add to cart</button> }
                            { cartI !== -1 && <buttononClick={$("removeFromCart", item.id)}>Remove from cart</button> }
                        </div></li>);
                }) }
            </ul><ulclassName="pages">
                { new Array(state.totalPages).fill(true).map((_, p) => {
                    const page = Number(p) + 1;
                    return (
                        <liclassName="page"onClick={$('goToPage', page)} key={p}>
                            { page }
                        </li>
                    );
                }) }
            </ul></section>
        <section>
            <h2>Cart</h2>
            <ul>
                { state.cart.map((item, i) => { 
                    return (<likey={i}>
                        { item.name }
                        <div><buttononClick={$("removeFromCart", item.id)}>Remove from cart</button></div></li>);
                }) }    
            </ul>
        </section>);
    });
    exportdefault App;
    

    Aufbau


    <select onChange={$('selectCategory')}>

    bedeutet, dass bei einer Feldänderung der Wert in den selectCategory-Stream "durchgeschoben" wird. Aber welchen Wert? Der Standard ist event.target.value, aber wenn wir etwas anderes pushen müssen, spezifizieren wir es mit dem zweiten Argument wie folgt:


    <button onClick={$("addToCart", item)}>

    Alles hier - und Ereignisse, Daten und Prozesse - sind Streams. Das Auslösen eines Ereignisses bewirkt eine Neuberechnung von Daten oder Ereignissen in Abhängigkeit davon usw. entlang der Kette. Der Wert des abhängigen Streams wird mithilfe der Formel berechnet, die den Wert oder das Versprechen zurückgeben kann (dann wartet mrr auf seine Auflösung).


    Die mrr-API ist sehr knapp und kurz - in den meisten Fällen benötigen wir nur 3-4 grundlegende Operatoren, und viele Dinge können ohne sie erledigt werden. Fügen Sie eine Fehlermeldung hinzu, wenn Sie eine Liste von Produkten, die eine Sekunde lang angezeigt werden, nicht erfolgreich laden konnten:


    hideErrorMessage: [() =>newPromise(res => setTimeout(res, 1000)), 'requestGoods.error'],
    errorMessageShown: [
        'merge',
        [() =>true, 'requestGoods.error'],
        [() =>false, 'hideErrorMessage'],
    ],

    Salz, PfefferZucker - nach Geschmack


    Es gibt mrr und syntaktischen Zucker, der für die Entwicklung optional ist, ihn aber beschleunigen kann. Zum Beispiel der Toggle-Operator:


    errorMessageShown: ['toggle', 'requestGoods.error', [() =>newPromise(res => setTimeout(res, 1000)), 'showErrorMessage']],

    Eine Änderung im ersten Argument setzt den Zellenwert auf true und den zweiten auf false.
    Der Ansatz, die Ergebnisse einer asynchronen Aufgabe über die aufeinanderfolgenden und aufeinanderfolgenden Subzellenzellen zu zerlegen, ist ebenfalls so üblich, dass hierfür ein spezieller Versprechungsoperator verwendet werden kann (automatische Beseitigung der Race-Bedingung):


        requestGoods: [
            'promise', 
            (page = 1, category = 'all') => fetch('https://reqres.in/api/products?page=', page, category).then(r => r.json()), 
            'page', 'selectCategory', '$start'
        ],

    Eine ziemlich große Funktion, nur ein paar Dutzend Zeilen. Unser bedingter Juni ist zufrieden - er hat einen Arbeitscode geschrieben, der sich als ziemlich kompakt erwies: Die gesamte Logik passt in eine Datei und auf einen Bildschirm. Aber der Signor blickt ungläubig hervor: Eka Nepal ... Sie können so etwas schreiben und es unter / recompose / etc verwenden.


    Ja, das kannst du! Natürlich ist es unwahrscheinlich, dass der Code noch kompakter und strukturierter ist, aber darum geht es nicht. Stellen wir uns vor, dass sich das Projekt entwickelt, und wir müssen die Funktionalität auf zwei separate Seiten aufteilen: eine Liste mit Waren und einen Warenkorb. Außerdem muss der Datenkorb natürlich für beide Seiten global gespeichert werden.


    Ein Ansatz, eine Schnittstelle


    Hier kommen wir zu einem anderen Problem der Reaktionsentwicklung: Die Existenz heterogener Ansätze für die Verwaltung des Zustands lokal (innerhalb der Komponente) und global auf der Ebene der gesamten Anwendung. Ich bin sicher, viele standen vor einem Dilemma: eine Logik lokal oder global zu implementieren? Oder eine andere Situation: Es stellte sich heraus, dass ein Teil der lokalen Daten global gespeichert werden muss, und Sie müssen einige Funktionen neu schreiben, z. B. von der Neuzusammenstellung bis zum Neuaufbau.


    Die Opposition ist natürlich künstlich und nicht in der Herrschaft: Sie ist gleich gut und die Hauptsache ist einheitlich! - sowohl für die lokale als auch für die globale Staatsverwaltung geeignet. Im Allgemeinen benötigen wir keinen globalen Status, wir haben einfach die Möglichkeit, Daten zwischen Komponenten auszutauschen. Daher ist der Status der Root-Komponente "global".


    Das Schema unserer Anwendung sieht nun folgendermaßen aus: Die Wurzelkomponente, die die Liste der Produkte im Korb enthält, und zwei verschachtelte Produkte und einen Korb, wobei die globale Komponente "Abhören" -Streams "in den Korb legen" und "aus dem Korb nehmen" aus den untergeordneten Komponenten.


    const App = withMrr({
        $init: {
            cart: [],
            currentPage: 'goods',
        },
        cart: ['merge', 
            [(item, arr) => [...arr, item], 'addToCart', '-cart'],
            [(id, arr) => arr.filter(item => item.id !== id), 'removeFromCart', '-cart'],
        ],
    }, (state, props, $, connectAs) => {
        return (
            <div>
                <menu>
                    <li onClick={$('currentPage', 'goods')}>Goods</li>
                    <li onClick={$('currentPage', 'cart')}>Cart{ state.cart && state.cart.length ? '(' + state.cart.length + ')' : '' }</li>
                </menu>
                <div>
                    { state.currentPage === 'goods' && <Goods {...connectAs('goods', ['addToCart', 'removeFromCart'], ['cart'])}/> }
                    { state.currentPage === 'cart' && <Cart {...connectAs('cart', { 'removeFromCart': 'remove' }, ['cart'])}/> }
                </div>
            </div>
        );
    })

    const Goods = withMrr({
        $init: {
            goods: [],
            page: 1,
        },
        goods: [res => res.data, 'requestGoods.success'],
        requestGoods: [
            'promise', 
            (page = 1, category = 'all') => fetch('https://reqres.in/api/products?page=', page, category).then(r => r.json()), 
            'page', 'selectCategory', '$start'
        ],
        page: ['merge', 'goToPage', [(a, prev) => a < prev ? a : prev, 'totalPages', '-page']],
        totalPages: [res => res.total, 'requestGoods.success'],
        category: 'selectCategory',
        errorShown: ['toggle', 'requestGoods.error', [cb =>newPromise(res => setTimeout(res, 1000)), 'requestGoods.error']],
    }, (state, props, $) => {
        return (<div>
            ...
        </div>);
    });
    

    const Cart = withMrr({}, (state, props, $) => {
        return (<div><h2>Cart</h2><ul>
                { state.cart.map((item, i) => { 
                    return (<div>
                        { item.name }
                        <div><buttononClick={$('remove', item.id)}>Remove from cart</button></div></div>);
                }) }    
            </ul></div>);
    });
    

    Es ist erstaunlich, wie wenig sich geändert hat! Wir verteilen die Ströme einfach in die entsprechenden Komponenten und legen "Brücken" zwischen ihnen! Durch das Verbinden von Komponenten mit der Funktion mrrConnect geben wir das Mapping für nachgelagerte und vorgelagerte Flüsse an:


    connectAs(
        'goods',
        /* вверх */
        ['addToCart', 'removeFromCart'], 
        /* вниз */
        ['cart']
    )

    Hier werden die Streams addToCart und removeFromCart aus der untergeordneten Komponente an die übergeordnete Komponente und der Cart-Fluss in entgegengesetzter Richtung weitergeleitet. Wir müssen nicht dieselben Streamnamen verwenden - wenn sie nicht übereinstimmen, verwenden wir Mapping:


    connectAs('cart', { 'removeFromCart': 'remove' })

    Der Remove-Stream aus der untergeordneten Komponente ist die Quelle für den removeFromCart-Stream im übergeordneten Element.


    Wie Sie sehen, ist das Problem der Wahl eines Speicherorts für Daten bei mrr vollständig beseitigt: Sie speichern Daten, wo sie logisch verursacht werden.


    Auch hier ist es nicht unmöglich, das Fehlen von Redax nicht zu bemerken: Darin sind Sie verpflichtet, alle Daten in einem zentralen Repository zu speichern. Sogar Daten, die nur von einer einzelnen Komponente oder deren Teilstruktur angefordert und verwendet werden können! Wenn wir im „Redox-Stil“ schreiben würden, würden wir auch das Laden und die Paginierung von Gütern auf globaler Ebene durchführen (aus Gründen der Gerechtigkeit - ein solcher Ansatz ist dank der Flexibilität von mrr auch möglich und hat das Recht auf Leben, den Quellcode ).


    Dies ist jedoch nicht notwendig. Geladene Waren werden nur in der Güterkomponente verwendet. Wenn wir sie also auf die globale Ebene bringen, werden wir nur den globalen Zustand verstopfen und anschwellen. Außerdem müssen wir veraltete Daten löschen (z. B. die Paginierungsseite), wenn der Benutzer erneut zur Produktseite zurückkehrt. Durch die Wahl der richtigen Speicherebene vermeiden wir solche Probleme automatisch.


    Ein weiterer Vorteil dieses Ansatzes besteht darin, dass die Anwendungslogik mit der Ansicht kombiniert wird. Dadurch können die einzelnen React-Komponenten als voll funktionsfähige Widgets und nicht als „dumme“ Vorlagen verwendet werden. Durch das Speichern von minimalen Informationen auf globaler Ebene (im Idealfall sind dies nur Sitzungsdaten) und der größte Teil der Logik in separate Komponenten der Seiten, wird die Codekonnektivität erheblich reduziert. Natürlich ist dieser Ansatz nicht überall anwendbar, aber es gibt eine Vielzahl von Aufgaben, bei denen der globale Zustand extrem klein ist und die einzelnen "Bildschirme" nahezu völlig unabhängig voneinander sind: beispielsweise verschiedene Verwaltungsbereiche usw. Im Gegensatz zu Redax, das uns dazu bringt, alles, was notwendig und nicht notwendig ist, auf die globale Ebene zu bringen, können Sie mit mrr Daten in separaten Teilbäumen speichern und so die Kapselung ermutigen und ermöglichen.


    Es lohnt sich zu reservieren: Der vorgeschlagene Ansatz hat natürlich nichts Revolutionäres! Komponentenunabhängige Widgets - waren einer der grundlegenden Ansätze, die seit Beginn der js-Frameworks verwendet wurden. Der wesentliche Unterschied besteht nur darin, dass mrr das Deklarationsprinzip befolgt: Komponenten können nur auf Flüsse anderer Komponenten hören, sie jedoch nicht beeinflussen (sowohl in der "Bottom-Up" - als auch in der "Top-Down-Richtung"), die sich vom Fluss unterscheidet -Ansatz). „Intelligente“ Komponenten, die nur Nachrichten mit nachgeschalteten und übergeordneten Komponenten austauschen können, entsprechen dem bekannten, aber in der Frontentwicklung wenig bekannten Modell von Akteuren (das Thema der Verwendung von Akteuren und Threads am Frontend ist in Einführung in die reaktive Programmierung gut gekaut ).
    Dies ist natürlich weit von der kanonischen Umsetzung der Akteure entfernt, aber das Wesentliche ist genau dies: Die Rolle der Akteure spielen Komponenten, die über Mrr-Streams Botschaften austauschen. Eine Komponente kann (deklarativ!) mithilfe von virtuellem DOM und React untergeordnete Komponentendarsteller erstellen und löschen: Die Renderfunktion definiert im Wesentlichen die Struktur der untergeordneten Akteure.


    Wenn wir einen bestimmten Rückruf über Requisiten von der übergeordneten Komponente an die untergeordnete Komponente „ablegen“, sollten wir anstelle der Standardeinstellung für das React den Fluss der untergeordneten Komponente von der übergeordneten Komponente abhören. Gleiches gilt für die entgegengesetzte Richtung, von Elternteil zu Kind. Sie fragen sich zum Beispiel: Warum übertragen Sie die Daten des Warenkorbs als Stream in die Komponente Cart, wenn wir sie ohne weiteres als Requisiten übergeben können? Was ist der unterschied Tatsächlich kann dieser Ansatz auch verwendet werden, jedoch nur, wenn auf den Wechsel der Requisiten reagiert werden muss. Wenn Sie jemals die Methode componentWillReceiveProps verwendet haben, wissen Sie, worum es sich handelt. Dies ist eine Art "Reaktivität für die Armen": Sie hören absolut alle Änderungen an Requisiten, bestimmen, was sich geändert hat, und reagieren.


    In mrr „fließen“ die Streams nicht nur aufwärts, sondern auch in der Hierarchie der Komponenten, sodass die Komponenten unabhängig auf Zustandsänderungen reagieren können. Damit können Sie die gesamte Leistungsfähigkeit von MRR-Reaktivwerkzeugen nutzen.


    const Cart = withMrr({
        foo: [items => { 
        // что-нибудь делаем
        }, 'cart'],
    }, (state, props, $) => { ... })

    Fügen Sie etwas Bürokratie hinzu


    Das Projekt wächst, es wird schwierig, die Namen der Streams zu verfolgen, die - oh, Horror! - in Reihen gespeichert. Nun, wir können Konstanten sowohl für Stream-Namen als auch für MR-Operatoren verwenden. Brechen Sie nun die Anwendung, indem Sie einen kleinen Tippfehler machen, wird es schwieriger.


    import { withMrr } from'mrr';
    import { merge, toggle, promise } from'mrr/operators';
    import { cell, nested, $start$, passive } from'mrr/cell';
    const goods$ = cell('goods');
    const page$ = cell('page');
    const totalPages$ = cell('totalPages');
    const category$ = cell('category');
    const errorShown$ = cell('errorShown');
    const addToCart$ = cell('addToCart');
    const removeFromCart$ = cell('removeFromCart');
    const selectCategory$ = cell('selectCategory');
    const goToPage$ = cell('goToPage');
    const Goods = withMrr({
        $init: {
            [goods$]: [],
            [page$]: 1,
        },
        [goods$]: [res => res.data, requestGoods$.success],
        [requestGoods$]: promise((page, category) => fetch('https://reqres.in/api/products?page=', page).then(r => r.json()), page$, category$, $start$),
        [page$]: merge(goToPage$, [(a, prev) => a < prev ? a : prev, totalPages$, passive(page$)]),
        [totalPages$]: [res => res.total, requestGoods$.success],
        [category$]: selectCategory$,
        [errorShown$]: toggle(requestGoods$.error, [cb =>newPromise(res => setTimeout(res, 1000)), requestGoods$.error]),
    }, ...);
    

    Was ist in der Blackbox?


    Was ist mit dem Testen? Die in der mrr-Komponente beschriebene Logik kann leicht von der Vorlage getrennt und anschließend getestet werden.


    Lassen Sie uns die mrr-Struktur separat aus unserer Datei exportieren.


    const GoodsStruct = {
        $init: {
            [goods$]: [],
            [page$]: 1,
        },
        ...
    }
    const Goods = withMrr(GoodsStruct, (state, props, $) => { ... });
    export { GoodsStruct }
    

    und dann in unsere Tests importieren. Mit einem einfachen Wrapper können wir
    einen Wert in einen Stream einfügen (als ob er aus einem DOM erstellt würde) und dann die Werte anderer davon abhängiger Threads überprüfen.


    import { simpleWrapper} from'mrr';  
    import { GoodsStruct } from'../src/components/Goods';  
    describe('Testing Goods component', () => {
        it('should update page if it\'s out of limit ', () => {
            const a = simpleWrapper(GoodsStruct);
            a.set('page', 10);
            assert.equal(a.get('page'), 10);
            a.set('requestGoods.success', {data: [], total: 5});
            assert.equal(a.get('page'), 5);
            a.set('requestGoods.success', {data: [], total: 10});
            assert.equal(a.get('page'), 5);
        })
    })

    Glitzer- und Armutsreaktivität


    Es ist erwähnenswert, dass die Reaktivität eine Abstraktion einer höheren Stufe ist, verglichen mit der "manuellen" Zustandsbildung, die auf Ereignissen im Redax basiert. Die Erleichterung der Entwicklung schafft einerseits die Möglichkeit, sich in den Fuß zu schießen. Betrachten Sie dieses Szenario: Der Benutzer wechselt zu Seite 5 und wechselt dann den Filter "Kategorie". Wir müssen die Liste der Produkte der ausgewählten Kategorie auf Seite 5 laden. Es kann sich jedoch herausstellen, dass es nur drei Seiten mit Produkten dieser Kategorie gibt. Im Falle eines "dummen" Backends lautet der Algorithmus unserer Aktionen wie folgt:


    • Anfragedatenseite = 5 & category =% category%
    • Nimm aus der Antwort den Wert der Seitenzahl
    • Wenn null Einträge zurückgegeben werden, fordern Sie die größte verfügbare Seite an

    Wenn wir es auf Redax implementieren, müssten wir eine große asynchrone Aktion mit der beschriebenen Logik erstellen. Bei einer Reaktivität auf mrr ist es nicht erforderlich, dieses Szenario separat zu beschreiben. In diesen Zeilen ist bereits alles enthalten:


        [requestGoods$]: ['nested', (cb, page, category) => {
            fetch('https://reqres.in/api/products?page=', page).then(r => r.json())
            .then(res => cb('success', res))
            .catch(e => cb('error', e))
        }, page$, category$, $start$],
        [totalPages$]: [res => res.total, requestGoods$.success],
        [page$]: merge(goToPage$, [(a, prev) => a < prev ? a : prev, totalPages$, passive(page$)]),

    Wenn der neue Wert von totalPages unter der aktuellen Seite liegt, aktualisieren wir den Seitenwert und initiieren eine erneute Ausführung der Anforderung an den Server.
    Wenn unsere Funktion jedoch denselben Wert zurückgibt, wird dies immer noch als Änderung des Seitenflusses mit nachfolgender Reklamation aller abhängigen Datenflüsse wahrgenommen. Um dies zu vermeiden, hat mrr eine besondere Bedeutung - überspringen. Als Rückgabe signalisieren wir: Es haben sich keine Änderungen ergeben, es muss nichts aktualisiert werden.


    import { withMrr, skip } from'mrr';
        [requestGoods$]: nested((cb, page, category) => {
            fetch('https://reqres.in/api/products?page=', page).then(r => r.json())
            .then(res => cb('success', res))
            .catch(e => cb('error', e))
        }, page$, category$, $start$),
        [totalPages$]: [res => res.total, requestGoods$.success],
        [page$]: merge(goToPage$, [(a, prev) => a < prev ? a : skip, totalPages$, passive(page$)]),

    Daher kann ein kleiner Fehler zu einer Endlosschleife führen: Wenn wir nicht "überspringen", sondern "vor" zurückgeben, wird die Seitenzelle geändert und eine wiederholte Anforderung wird angezeigt, und so weiter, im Kreis. Die Möglichkeit einer solchen Situation ist natürlich kein „bösartiger Fehler“ von FRP oder mrr, da die Möglichkeit einer endlosen Rekursion oder eines Zyklus nicht auf die Bösartigkeit struktureller Programmierungsideen hinweist. Es sollte jedoch verstanden werden, dass mrr noch ein gewisses Verständnis des Reaktionsmechanismus benötigt. Um auf die berühmte Messermetapher zurückzukommen, ist mrr ein sehr scharfes Messer, das die Arbeitseffizienz verbessert, aber auch einen ungeeigneten Arbeiter verletzen kann.


    Das Debuggen von mrr ist übrigens sehr einfach, ohne dass Erweiterungen installiert werden müssen:


    const GoodsStruct = {
        $init: {
           ...
        },
        $log: true,
        ...
    }

    Fügen Sie einfach $ log: true zur mrr-Struktur hinzu, und alle Änderungen an den Zellen werden in der Konsole angezeigt, sodass Sie sehen können, was sich ändert.


    Konzepte wie passives Zuhören oder der Wert von überspringen sind keine spezifischen "Krücken": Sie erweitern die Möglichkeiten der Reaktivität, so dass mit ihr die gesamte Logik der Anwendung beschrieben werden kann, ohne auf zwingende Ansätze zurückzugreifen. Ähnliche Mechanismen gibt es beispielsweise in Rx.js, aber ihre Schnittstelle ist dort weniger geeignet. Weitere Details zum passiven Zuhören und verschiedene Arten von Aussagen werden in einem früheren Artikel beschrieben: Mrr: Total FRP for React


    Quellcode des fertigen Beispiels.


    Ergebnisse


    • Dank der Abstraktion von FRP, der Ausdruckskraft und der Prägnanz von mrr können Sie mit wenig Code schnell und einfach eine Menge Funktionalität schreiben, und es werden keine Nudeln
    • Es ist jedoch nicht notwendig, komplexe Konzepte oder Dutzende von Operatoren zu studieren: Ein grundlegendes Verständnis der Reaktivität reicht aus, und für Anfänger gibt es sogar einen allgemeinen Formalisierungsalgorithmus
    • Wenn Sie plötzlich feststellen, dass Ihr Projekt weiter verfeinert und weiterentwickelt wird, können Sie die Struktur leicht umgestalten und verbessern, ohne den Code neu schreiben zu müssen.
    • Dank der Anordnung der gesamten Zustandsverwaltungslogik an einem Ort, in der Regel zusammen mit der Präsentation, können Sie auch in den von jemandem geschriebenen Code (Tische und Stühle schließlich zusammen!)
    • Die Zustandsänderungslogik ist jedoch leicht von der Darstellung zu trennen und wird bequem getestet.
    • Der mrr-Slogan: "Bewahren Sie Ihre Daten dort auf, wo Sie sie brauchen!" Wenn Sie die richtige Speicherebene wählen, ersparen Sie sich viele Schwierigkeiten.
    • Das Aufteilen des Anwendungszustands in lose gekoppelte Teile verringert die Gesamtkonnektivität und -komplexität
    • Seien Sie jedoch vorsichtig, wenn diese Teile zu viel voneinander wissen, kann es sinnvoll sein, sie zu kombinieren (die imaginäre Trennung des Codes macht die Struktur nur komplizierter und verringert die Zuverlässigkeit). Zäunen Sie keine voneinander abhängigen mehrstufigen Designs ein!
    • Unter den Minussen kann man auch festhalten: die mangelnde Unterstützung für das Tippen im Moment sowie das Prinzip von TMTOWTDI: die Möglichkeit, die gleiche Funktionalität auf verschiedene Arten zu schreiben, weshalb die Entwicklung optimaler und einheitlicher Entwicklungsansätze den Entwicklern selbst auf die Schultern fällt.

    PS


    Das Release des React mit Hook-Unterstützung wurde kürzlich veröffentlicht. In mrr können Sie also auch mit ihnen arbeiten, es sieht noch eleganter aus als mit einem Klassen-Wrapper:


    import useMrr from'mrr/hooks';
    functionFoo(props){
        const [state, $, connectAs] = useMrr(props, {
            $init: {
                counter: 0,
            },
            counter: ['merge', 
                [a => a + 1, '-counter', 'incr'],
                [a => a - 1, '-counter', 'decr']
            ],
        });
        return ( <div>
            Counter: { state.counter }
                <buttononClick={ $('incr') }>increment</button><buttononClick={ $('decr') }>decrement</button><Bar {...connectAs('bar')} /></div> );
    }

    Jetzt auch beliebt: