Eine Silberkugel finden: Schauspieler + FRP in Reaktion

Heutzutage schreiben nur wenige Leute in Perl, aber Larry Walls berühmte Maxime "Einfache und schwierige Dinge möglich halten" ist zur allgemein anerkannten Formel für effektive Technologie geworden. Es kann nicht nur im Hinblick auf die Komplexität der Aufgaben, sondern auch auf den Ansatz interpretiert werden: Eine ideale Technologie sollte einerseits die schnelle Entwicklung mittlerer und kleiner Anwendungen (einschließlich "Nur-Schreiben") ermöglichen und andererseits Werkzeuge für eine durchdachte Entwicklung bereitstellen komplexe Anwendungen, bei denen Zuverlässigkeit, Wartbarkeit und Strukturiertheit von größter Bedeutung sind. Oder sogar in die menschliche Ebene übersetzen: für den Jones zugänglich sein und gleichzeitig die Anforderungen der Signyors erfüllen.


Die mittlerweile beliebten Editoren können von beiden Seiten kritisiert werden - zumindest die Tatsache, dass das Schreiben selbst elementarer Funktionen zu vielen Zeilen führen kann, die über mehrere Dateien verteilt sind -, aber wir werden nicht tief gehen, da bereits viel darüber gesagt wurde.


"Du sollst alle Tische in einem Raum und die Stühle in einem anderen aufbewahren."
- Juha Paananen, Schöpfer der Bacon.js-Bibliothek, über den Herausgeber

Die Technologie, die heute diskutiert wird, ist keine Wunderwaffe, sondern behauptet, mit diesen Kriterien konsistenter zu sein.


Mrr ist eine funktionale reaktive Bibliothek, die sich zum Prinzip "Alles ist Fluss" bekennt. Die Hauptvorteile des funktional-reaktiven Ansatzes in mrr sind die Prägnanz, die Ausdruckskraft des Codes sowie ein einheitlicher Ansatz für synchrone und asynchrone Datentransformationen.


Auf den ersten Blick klingt dies nicht nach einer Technologie, die für Anfänger leicht zugänglich ist: Das Konzept eines Streams kann schwer zu verstehen sein, es ist im Front-End nicht so weit verbreitet, hauptsächlich in Verbindung mit dummen Bibliotheken wie Rx. Und vor allem ist nicht ganz klar, wie die Abläufe auf der Grundlage des grundlegenden DOM-Schemas „Action-Reaction-Update DOM“ zu erklären sind. Aber ... wir werden nicht abstrakt über Flüsse sprechen! Lassen Sie uns über verständlichere Dinge sprechen: Ereignisse, Zustand.


Kochen nach einem Rezept


Ohne in die Wildnis von FRP einzutauchen, folgen wir einem einfachen Schema zur Formalisierung des Themenbereichs:


  • Erstellen Sie eine Liste mit Daten, die den Status der Seite beschreiben und in der Benutzeroberfläche verwendet werden, sowie deren Typen.
  • Erstellen Sie eine Liste der Ereignisse, die vom Benutzer auf der Seite auftreten oder generiert werden, und der Datentypen, die mit ihnen übertragen werden
  • Erstellen Sie eine Liste der Prozesse, die auf der Seite ausgeführt werden
  • Bestimmen Sie die Abhängigkeiten zwischen ihnen.
  • Beschreiben Sie die Abhängigkeiten mithilfe geeigneter Operatoren.

Gleichzeitig benötigen wir erst in der letzten Phase Kenntnisse der Bibliothek.


Nehmen wir also ein vereinfachtes Beispiel eines Webshops, in dem eine Liste von Produkten mit Paginierung und Filterung nach Kategorien sowie ein Warenkorb vorhanden sind.


  1. Daten, auf deren Grundlage die Schnittstelle aufgebaut wird:


    • Warenliste (Array)
    • ausgewählte Kategorie (Linie)
    • Anzahl der Seiten mit Waren (Anzahl)
    • Liste der Produkte, die sich im Warenkorb befinden (Array)
    • aktuelle Seite (Nummer)
    • die Anzahl der Produkte im Warenkorb (Anzahl)

  2. Ereignisse (mit "Ereignissen" sind nur momentane Ereignisse gemeint. Aktionen, die für eine Weile stattfinden - Prozesse - müssen in separate Ereignisse zerlegt werden):


    • Eröffnungsseite (nichtig)
    • Kategorieauswahl (Zeichenfolge)
    • Waren in den Warenkorb legen (Objekt "Waren")
    • Entnahme von Waren aus dem Warenkorb (ID der zu löschenden Waren)
    • Gehen Sie zur nächsten Seite der Produktliste (Nummer - Seitenzahl).

  3. Prozesse: Dies sind Aktionen, die mit verschiedenen Ereignissen gleichzeitig oder nach einiger Zeit beginnen und dann enden können. In unserem Fall ist dies das Laden von Produktdaten vom Server, was zwei Ereignisse zur Folge haben kann: erfolgreicher Abschluss und Abschluss mit einem Fehler.


  4. Abhängigkeiten zwischen Ereignissen und Daten. Beispielsweise hängt die Produktliste vom Ereignis ab: "Erfolgreiches Laden der Produktliste". Und "Starten Sie das Laden der Warenliste" - von "Öffnen der Seite", "Auswählen der aktuellen Seite", "Auswählen einer Kategorie". Erstellen 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. Möglicherweise haben Sie erwartet, dass Ereignisse, Daten und Prozesse in mrr unterschiedliche Entitäten sind - aber nein, all dies sind Threads! Unsere Aufgabe ist es, sie richtig zu verbinden.


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


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


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

Jedes Mal, wenn das Warenkorb-Array geändert wird, wird der Wert von GoodsInCart neu gezählt.


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'], 

Das Design der Form [Funktion, ... Thread-Argumente] ist die Basis von mrr. Zum intuitiven Verständnis einer Analogie mit Excel werden Flüsse in mrr auch als Zellen bezeichnet, und die Funktionen, mit denen sie berechnet werden, werden als Formeln bezeichnet.


Wenn unsere Daten von mehreren Ereignissen abhängen, müssen wir ihre Werte einzeln transformieren und sie dann mit dem Zusammenführungsoperator zu einem Stream kombinieren:


 /* ,  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 verhindern, verweisen wir passiv auf die Warenkorb- und Seitenzellen (Minuszeichen vor dem Zellennamen): Ihre Werte werden in die Formel eingesetzt, aber wenn sie sich ändern, wird die Neuberechnung nicht gestartet.


Alle Threads werden entweder auf der Basis anderer Threads erstellt oder vom DOM ausgegeben. Aber was ist mit dem Stream "Eröffnungsseite"? Glücklicherweise müssen Sie componentDidMount nicht verwenden: In mrr gibt es einen speziellen Stream $ start, der signalisiert, dass die Komponente erstellt und gemountet wurde.


"Prozesse" werden asynchron berechnet, während wir bestimmte Ereignisse von ihnen ausgeben. Der "verschachtelte" Operator hilft uns hier:


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

Bei Verwendung des verschachtelten Operators wird dem ersten Argument eine Rückruffunktion für die Emission bestimmter Ereignisse übergeben. In diesem Fall sind sie von außen über den Namespace der Stammzelle zugänglich, z.


 cb('success', res) 

Innerhalb der requestGoods-Formel wird die Aktualisierung der requestGoods.success-Zelle durchgeführt.


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


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

Markup hinzufügen. Wir erstellen eine React-Komponente mit der withMrr-Funktion, die ein reaktives Verknüpfungsdiagramm und eine Renderfunktion akzeptiert. Um einen Wert in einen Stream zu "setzen", verwenden wir die $ -Funktion, die Ereignishandler erstellt (und zwischenspeichert). Jetzt sieht unsere voll funktionsfähige Anwendung folgendermaßen 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: <select onChange={$('selectCategory')}> <option>All</option> <option>Electronics</option> <option>Photo</option> <option>Cars</option> </select> </div> <ul className="goods"> { state.goods.map((item, i) => { const cartI = state.cart.findIndex(a => a.id === item.id); return (<li key={i}> { item.name } <div> { cartI === -1 && <button onClick={$("addToCart", item)}>Add to cart</button> } { cartI !== -1 && <button onClick={$("removeFromCart", item.id)}>Remove from cart</button> } </div> </li>); }) } </ul> <ul className="pages"> { new Array(state.totalPages).fill(true).map((_, p) => { const page = Number(p) + 1; return ( <li className="page" onClick={$('goToPage', page)} key={p}> { page } </li> ); }) } </ul> </section> <section> <h2>Cart</h2> <ul> { state.cart.map((item, i) => { return (<li key={i}> { item.name } <div> <button onClick={$("removeFromCart", item.id)}>Remove from cart</button> </div> </li>); }) } </ul> </section>); }); export default App; 

Bau


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

bedeutet, dass beim Ändern des Felds der Wert in den selectCategory-Stream "verschoben" wird. Aber was ist die Bedeutung? Standardmäßig ist dies event.target.value, aber wenn wir etwas anderes pushen müssen, geben wir es mit dem zweiten Argument an, wie hier:


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

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


Die mrr-API ist sehr präzise und prägnant - in den meisten Fällen benötigen wir nur 3-4 grundlegende Operatoren, und viele Dinge können ohne sie ausgeführt werden. Fügen Sie eine Fehlermeldung hinzu, wenn die Liste der Produkte nicht erfolgreich geladen wurde. Diese wird eine Sekunde lang angezeigt:


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

Salz, Pfeffer Zucker nach Geschmack


Es gibt auch syntaktischen Zucker in mrr, der für die Entwicklung optional ist, ihn aber beschleunigen kann. Zum Beispiel der Umschaltoperator:


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

Eine Änderung im ersten Argument setzt die Zelle auf true und im zweiten auf false.
Der Ansatz, die Ergebnisse einer asynchronen Aufgabe in Erfolgs- und Fehlerunterzellen zu "zerlegen", ist ebenfalls so weit verbreitet, dass Sie den speziellen Versprechungsoperator verwenden können (der die Rennbedingung automatisch beseitigt):


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

Viele Funktionen passen in nur ein paar Dutzend Zeilen. Unser bedingter Juni ist zufrieden - er hat es geschafft, Arbeitscode zu schreiben, der sich als ziemlich kompakt herausstellte: Die gesamte Logik passte in eine Datei und auf einen Bildschirm. Aber der Unterzeichner blinzelt ungläubig: Eka ist unsichtbar ... Sie können dies auf Hooks schreiben / neu komponieren / etc.


Ja, tatsächlich ist es das! Es ist natürlich unwahrscheinlich, dass der Code noch kompakter und strukturierter ist, aber darum geht es nicht. Stellen wir uns vor, das Projekt entwickelt sich und wir müssen die Funktionalität in zwei separate Seiten aufteilen: eine Produktliste und einen Warenkorb. Darüber hinaus müssen die Warenkorbdaten natürlich für beide Seiten global gespeichert werden.


Ein Ansatz, eine Schnittstelle


Hier kommen wir zu einem weiteren Problem der Reaktionsentwicklung: der Existenz heterogener Ansätze zur Verwaltung des Zustands lokal (innerhalb einer 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 einige lokale Daten global gespeichert werden müssen und Sie einen Teil der Funktionalität neu schreiben müssen, z. B. von Neuzusammenstellungen bis zum Editor ...


Der Kontrast ist natürlich künstlich und in mrr nicht: es ist gleich gut und vor allem - einheitlich! - Geeignet für lokales und globales Staatsmanagement. Im Allgemeinen benötigen wir keinen globalen Status. Wir können lediglich Daten zwischen den Komponenten austauschen, sodass der Status der Stammkomponente "global" ist.


Das Schema unserer Anwendung lautet nun wie folgt: Die Stammkomponente, die die Liste der Waren im Warenkorb enthält, und zwei Unterkomponenten: die Waren und der Warenkorb, und die globale Komponente "lauscht" den Flüssen "In den Warenkorb hinzufügen" und "Aus dem Warenkorb entfernen" aus 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 => new Promise(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> <button onClick={$('remove', item.id)}>Remove from cart</button> </div> </div>); }) } </ul> </div>); }); 

Es ist erstaunlich, wie wenig sich geändert hat! Wir haben einfach die Ströme in die entsprechenden Komponenten ausgelegt und „Brücken“ zwischen ihnen gelegt! Durch Verbinden der Komponenten mit der Funktion mrrConnect legen wir die Zuordnung für Downstream- und Upstream-Flows fest:


 connectAs( 'goods', /*  */ ['addToCart', 'removeFromCart'], /*  */ ['cart'] ) 

Hier werden die Streams addToCart und removeFromCart aus der untergeordneten Komponente an die übergeordnete Komponente gesendet, und der Warenkorb-Stream wird zurückgegeben. Es ist nicht erforderlich, dieselben Stream-Namen zu verwenden. Wenn diese nicht übereinstimmen, verwenden wir die Zuordnung:


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

Der Stream zum Entfernen aus der untergeordneten Komponente ist die Quelle für den Stream "removeFromCart" im übergeordneten Stream.


Wie Sie sehen, ist das Problem der Auswahl eines Datenspeicherorts bei mrr vollständig beseitigt: Sie speichern Daten dort, wo sie logisch bestimmt sind.


Auch hier kann man den Nachteil des Editors nicht übersehen: darin müssen Sie alle Daten in einem einzigen zentralen Repository speichern. Sogar die Daten, die nur von einer separaten Komponente oder ihrem Teilbaum angefordert und verwendet werden dürfen! Wenn wir im "redaktionellen Stil" schreiben würden, würden wir auch das Laden und Paginieren von Waren auf die globale Ebene bringen (fairerweise ist dieser Ansatz dank der Flexibilität von mrr auch möglich und hat das Recht auf Leben, Quellcode ).


Dies ist jedoch nicht erforderlich. Die geladenen Waren werden nur in der Komponente der Waren verwendet. Wenn wir sie auf die globale Ebene bringen, verstopfen wir nur den globalen Zustand und blasen ihn auf. Außerdem müssen wir veraltete Daten (z. B. eine Paginierungsseite) löschen, wenn der Benutzer wieder zur Produktseite zurückkehrt. Durch die Auswahl der richtigen Datenspeicherebene vermeiden wir solche Probleme automatisch.


Ein weiterer Vorteil dieses Ansatzes besteht darin, dass die Anwendungslogik mit der Präsentation kombiniert wird, sodass wir einzelne React-Komponenten als voll funktionsfähige Widgets und nicht als „dumme“ Vorlagen wiederverwenden können. Indem wir ein Minimum an Informationen auf globaler Ebene halten (im Idealfall sind dies nur Sitzungsdaten) und den größten Teil der Logik in separate Seitenkomponenten zerlegen, verringern wir die Kohärenz des Codes erheblich. Natürlich ist dieser Ansatz nicht immer anwendbar, aber es gibt eine große Anzahl von Aufgaben, bei denen der globale Zustand extrem klein ist und die einzelnen „Bildschirme“ fast völlig unabhängig voneinander sind: zum Beispiel verschiedene Arten von Administratoren usw. Im Gegensatz zum Editor, der uns dazu bringt, alles, was benötigt und nicht benötigt wird, auf die globale Ebene zu bringen, können Sie mit mrr Daten in separaten Teilbäumen speichern, die Kapselung fördern und ermöglichen und so unsere Anwendung von einem monolithischen "Kuchen" in einen geschichteten "Kuchen" verwandeln.


Erwähnenswert ist natürlich, dass der vorgeschlagene Ansatz nichts Revolutionäres enthält! Komponenten, in sich geschlossene Widgets, sind seit dem Aufkommen von js Frameworks einer der grundlegenden Ansätze. Der einzige signifikante Unterschied besteht darin, dass mrr dem deklarativen Prinzip folgt: Komponenten können nur auf die Flüsse anderer Komponenten hören, diese aber nicht beeinflussen (was muss sie von unten nach oben oder von oben nach unten tun, was sich vom Fluss unterscheidet? Ansatz). Intelligente Komponenten, die nur Nachrichten mit den zugrunde liegenden und übergeordneten Komponenten austauschen können, entsprechen dem beliebten, aber wenig bekannten Akteurmodell in der Front-End-Entwicklung (das Thema der Verwendung von Akteuren und Threads im Front-End wird im Artikel Einführung in die reaktive Programmierung ausführlich behandelt ).
Dies ist natürlich weit entfernt von der kanonischen Implementierung von Akteuren, aber das Wesentliche ist genau das: Die Rolle von Akteuren wird von Komponenten gespielt, die Nachrichten über MPP-Streams austauschen. Eine Komponente kann (deklarativ!) dank des virtuellen DOM und React untergeordnete Komponentenakteure erstellen und löschen: Die Renderfunktion bestimmt im Wesentlichen die Struktur untergeordneter Akteure.


Anstelle der Standardsituation für die Reaktion sollten wir, wenn wir die übergeordnete Komponente über Requisiten in einen bestimmten Rückruf "fallen lassen", den Fluss der untergeordneten Komponente vom übergeordneten Element abhören. Das Gleiche ist in die entgegengesetzte Richtung, von Eltern zu Kind. Zum Beispiel könnten Sie fragen: Warum die Warenkorbdaten als Stream an die Warenkorbkomponente übertragen, wenn wir sie ohne weiteres einfach als Requisiten übergeben können? Was ist der Unterschied? In der Tat kann dieser Ansatz auch verwendet werden, jedoch nur, bis auf Änderungen an den Requisiten reagiert werden muss. Wenn Sie jemals die componentWillReceiveProps-Methode verwendet haben, wissen Sie, worum es geht. Dies ist eine Art "Reaktivität für die Armen": Sie hören absolut alle Änderungen der Requisiten, bestimmen, was sich geändert hat, und reagieren. Diese Methode wird jedoch bald aus React verschwinden, und es kann die Notwendigkeit einer Reaktion auf "Signale von oben" entstehen.


In mrr „fließt“ nicht nur die Komponentenhierarchie nach oben, sondern auch nach unten, sodass die Komponenten unabhängig auf Statusänderungen reagieren können. Auf diese Weise können Sie die volle Leistung von mrr jet tools nutzen.


 const Cart = withMrr({ foo: [items => { // -  }, 'cart'], }, (state, props, $) => { ... }) 

Fügen Sie ein wenig Bürokratie hinzu


Das Projekt wächst, es wird schwierig, die Namen der Flüsse im Auge zu behalten, die - oh, Horror! - werden in Zeilen gespeichert. Nun, wir können Konstanten sowohl für Stream-Namen als auch für mrr-Anweisungen verwenden. Jetzt wird es schwieriger, eine Anwendung durch einen kleinen Tippfehler zu brechen.


 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 => new Promise(res => setTimeout(res, 1000)), requestGoods$.error]), }, ...); 

Was ist in der Black Box?


Was ist mit Testen? Die in der mrr-Komponente beschriebene Logik lässt sich leicht von der Vorlage trennen und anschließend testen.


Lassen Sie uns die mrr-Struktur getrennt von unserer Datei erstellen.


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

und dann importieren wir es in unseren Tests. Mit einem einfachen Wrapper können wir
Fügen Sie den Wert in den Stream ein (als ob er aus dem DOM stammt) und überprüfen Sie dann die Werte anderer Threads, die davon abhängen.


 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); }) }) 

Glanz und Armut der Reaktivität


Es ist erwähnenswert, dass Reaktivität eine Abstraktion auf einer höheren Ebene ist als die „manuelle“ Bildung eines Zustands basierend auf Ereignissen im Editor. Die Erleichterung der Entwicklung schafft einerseits die Möglichkeit, sich in den Fuß zu schießen. Stellen Sie sich dieses Szenario vor: Der Benutzer wechselt zu Seite 5 und wechselt dann den Filter "Kategorie". Wir müssen die Liste der Produkte der ausgewählten Kategorie auf der fünften Seite laden, aber es kann sich herausstellen, dass die Waren in dieser Kategorie nur drei Seiten haben. Im Fall des "dummen" Backends lautet der Algorithmus unserer Aktionen wie folgt:


  • Anforderungsdatenseite = 5 & Kategorie =% Kategorie%
  • Nehmen Sie aus der Antwort den Wert der Anzahl der Seiten
  • Wenn eine Anzahl von Datensätzen von Null zurückgegeben wird, fordern Sie die größte verfügbare Seite an

Wenn wir dies im Editor implementieren würden, müssten wir eine große asynchrone Aktion mit der beschriebenen Logik erstellen. Bei Reaktivität auf mrr muss dieses Szenario nicht separat beschrieben werden. Alles ist bereits in diesen Zeilen 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 totalPages-Wert kleiner als die aktuelle Seite ist, aktualisieren wir den Seitenwert und initiieren dadurch eine zweite Anforderung an den Server.
Wenn unsere Funktion jedoch denselben Wert zurückgibt, wird dies weiterhin als Änderung des Seitenstroms wahrgenommen, gefolgt von einer Neuberechnung aller abhängigen Streams. Um dies zu vermeiden, hat mrr eine besondere Bedeutung - überspringen. Wenn wir es zurückgeben, signalisieren wir: Es sind keine Änderungen aufgetreten, 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$)]), 

Ein kleiner Fehler kann uns also zu einer Endlosschleife führen: Wenn wir nicht "überspringen", sondern "prev" zurückgeben, ändert sich die Seitenzelle und es erfolgt eine zweite Anforderung usw. in einem Kreis. Die Möglichkeit einer solchen Situation ist natürlich kein „fehlerhafter Nachteil“ von FRP oder mrr, da die Möglichkeit einer unendlichen Rekursion oder einer Schleife nicht auf die fehlerhaften Ideen der strukturellen Programmierung hinweist. Es versteht sich jedoch, dass mrr noch ein gewisses Verständnis des Reaktivitätsmechanismus erfordert. Zurück zur bekannten Metapher der Messer: mrr ist ein sehr scharfes Messer, das die Arbeitseffizienz verbessert, aber auch einen unfähigen Arbeiter verletzen kann.


Übrigens ist die Abbuchung von mrr sehr einfach, ohne Erweiterungen zu installieren:


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

Fügen Sie einfach $ log: true zur mrr-Struktur hinzu, und alle Änderungen an den Zellen werden an die Konsole ausgegeben, sodass Sie sehen können, welche Änderungen und wie.


Konzepte wie passives Zuhören oder die Bedeutung von Überspringen sind keine spezifischen „Krücken“: Sie erweitern die Möglichkeiten der Reaktivität, sodass die gesamte Logik der Anwendung leicht beschrieben werden kann, ohne auf zwingende Ansätze zurückgreifen zu müssen. Ähnliche Mechanismen gibt es zum Beispiel in Rx.js, aber ihre Schnittstelle dort ist weniger bequem. : Mrr: FRP


.


Zusammenfassung


  • FRP, mrr ,
  • : ,
  • , ,
  • , , - ( - !)
  • mrr : " , !"
  • ,
  • , , ( ). !
  • : , , , TMTOWTDI: , - .

PS


. , mrr , , :


 import useMrr from 'mrr/hooks'; function Foo(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 } <button onClick={ $('incr') }>increment</button> <button onClick={ $('decr') }>decrement</button> <Bar {...connectAs('bar')} /> </div> ); } 

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


All Articles