Přejít k navigační liště

Zdroják » JavaScript » Potřebujeme Flux?

Potřebujeme Flux?

Články JavaScript

Flux je návrhový vzor spojený hlavně s knihovnou React. Je vždy potřeba s Reactem tento vzor používat? Existují i jiné přístupy, která nám práci s Reactem usnadní? Co když používám Angular? Na tyto otázky bychom měli najít odpověď v tomto článku.

Ti, kdo byli na PragueJS o Reactu nebo viděli záznam mé přednášky, možná postřehli, že jsem říkal, že Flux není v podstatě potřeba, když zvolíte jiný přístup k programování UI. Pojďme si to tedy trošku rozvést.

Co je to Flux?

Někteří z vás možná o Fluxu nikdy neslyšeli. Jedná se o návrhový vzor (lidi rádi tvrdí, že architektonický, ale to jsou všechny), který se aplikuje při návrhu jednosměrného toku dat v UI (opak oblíbeného obousměrného). Vychází z předpokladu, že výsledné UI je reprezentovatelné pomocí dat, které do něj tečou z úložišť (Store). Interakce uživatelů jsou reprezentovány pomocí akcí. Akce zpracovává Dispatcher, který následně notifikuje o změnách jednotlivá úložiště. Jeden obrázek je lepší než tisíc slov:

Flux

Na první pohled to dává smysl. Když se ale podíváme na konkrétní implementace, narazíme na některé nehezké věci. Např. komponenty znají svá úložiště, komponenty mají přístup k Dispatcheru. Další věcí je, že data – stav aplikace – jsou distribuovaná po několika úložištích aDispatcher musí často koordinovat změny na několika místech. To je v praxi celkem nepříjemné.

Flux - Stores

Některé problémy se dají obejít. Například, můžeme znalost úložiště extrahovat do komponenty typu Container, která bude komponentu do ní vloženou krmit daty z úložiště pomocí props, a tak dosáhneme znovupoužitelnosti a odstínění od znalosti konkrétního úložiště. Jak komponenta se znalostí úložišť vypadá, můžete vidět třeba v oficiální ukázce. Nevím jak vám, ale mně se tohle nelíbí. Vůbec.

„A jak jinak bys to chtěl udělat?“

Problém první: distribuovaný mutabilní stav

V odkazované ukázce je komponenta, která má vnitřní závislost na úložištích ThreadStore a MessageStore. Porušuje hned několik SOLID principů. Dále má komponenta jen svůj vnitřní stav, který je aktualizován při změně v úložištích. A pak si vemte, že v běžné aplikaci máte úložišť víc než dvě. Věci se začnou komplikovat.

Jedním z možných řešení je zříci se mutací. Když nebudeme potřebovat provádět lokální změny v datech, nebudeme ani potřebovat jednotlivá úložiště, bude nám stačit jedno jediné! No jo, ale když nebudu moci nic měnit, k čemu mi taková aplikace bude?

Mutace jsou úkazem lokálního programování. Každá data mají své místo a když dojde k jejich změně, tak je na jejich místě nahradíme. Tím ztrácíme celkem důležitý rozměr, a tím je čas. Nikdy už nebudeme vědět, jaká data byla historicky. K čemu by nám ale taková informace byla? Už jste někdy implementovali undo v aplikaci s distribuovaným mutabilním stavem? A povedlo se vám to dovést do zdárného konce? Respekt!

Immutabilní data mají tu vlastnost, že každá změna vytvoří data nová, změněná. Jistě znáte hned několik příkladů. Například, když k jedničce přičteme dvojku, tak dostaneme trojku a jednička s dvojkou mají pořád svou hodnotu. Nebo když si vezmeme podřetězec, také to původní řetězec nijak neovlivní. Immutabilitu můžeme využít ale i u komplexních datových struktur jako jsou listy, sety, vektory a mapy. A můžeme s nimi zacházet jako s hodnotami! To má hned několik výhod.

Jistě vás napadne, že to musí být ale hodně drahá sranda, vytvářet neustále nová data při každé změně. Jasně paměti i disky jsou celkem levný, ale stejně, plýtvat by se nemělo. Moderní implementace immutabilních datových struktur využívají sdílení. Když provedu nad daty nějakou změnu, většinou nezmění úplně zgruntu všechno, ale jen nějakou malou část. Nová data mohou, protože nemůže dojít ke změně, sdílet většinu dat s předchozí verzí. Prakticky se uloží jen diff. Znáte git, ne? Tak takhle přesně to funguje. :)

Takže, když eliminujeme mutace, můžeme se zbavit většiny úložišť a vytvořit jen jednoho správce celkového neměnného stavu aplikace. Někdo by mohl namítnout, “fůj, globální stav!” Ano, globální stav je fuj. Vaše databáze je taky globální stav a tu taky používáte dnes a denně. Protože  databáze nám umožňují dělat atomické změny, případně nám poskytují transakční přístup. Změna immutabilních struktur je také atomická a když všechno bude v jednom úložišti, tak transakčnost nepotřebujeme. Úložiště pak může snadno spravovat i historii. A máte undo skoro zadarmo!

import {Stack} from 'immutable';

class Application {

    constructor(initialData) {
        this.state = initialData;
        this.history = new Stack();
        this.future = new Stack();
    }

    get canUndo() {
        return this.history.some(() => true);
    }
    
    get canRedo() {
        return this.future.some(() => true);
    }

    updateData(data) {
        this.future = new Stack();
        this.history = this.history.push(this.state);
        this.state = data;
    }

    undo() {
        if (!this.canUndo) {
            return;
        }
        this.future = this.future.push(this.state);
        this.state = this.history.peek();
        this.history = this.history.pop();
    }

    redo() {
        if (!this.canRedo) {
            return;
        }
        this.history = this.history.push(this.state);
        this.state = this.future.peek();
        this.future = this.future.pop();
    }
}

Benefity nižší paměťové náročnosti získáme pouze, pokud metoda updateData bude dostávat data, která byla vytvořena změnou aktuálního stavu. Jak na to si ukážeme dále.

Když přejdeme na immutabilní stav, krom snadné implementace undo dostaneme i další výhody. React komponenty se rázem stanou mapovací funkcí, kterou aplikujeme na stav a dostaneme aktuální zobrazení. Představte si, jak se ladí chyby na dálku.

ZeserializujeteApplication (včetně jednotlivých kroků historie) a celé to pošlete někomu, kdo se vyzná. On si přehraje váš stav k sobě a vidí nejen co vidíte vy, ale i jak jste se tam dostali!

f(data0) → view0
f(data1) → view1

diff(view0, view1) → DOM changes

Další obrovskou výhodu představuje poslední řádek předchozího výpisu. Když používáme React, tak on počítá změny, ke kterým došlo od předchozího stavu, a na základě nich provede jen nutné změny v DOMu prohlížeče. Díky tomu je hodně efektivní a pekelně rychlý. Teď, když ale máme immutabilní stav, je výpočet rozdílů v datech otázkou porovnání referencí, případně hash kódu struktur. Díky tomu můžete snížit dobu vykreslování na polovinu někdy i na třetinu!

Problém druhý: natvrdo zadrátované posílání zpráv

Když se podíváme do oficiální ukázky, jak jsou implementovaná jednotlivá uložiště, tak narazíme opět na problém přímé závislosti, tentokrát na Dispatcheru, a ušlé příležitosti na polymorfní zpracování zpráv.

Již mnoho let úspěšně aplikuji vzory reaktivního programování, kde to jen dává smysl, a UI je jedno z míst, kde je reaktivní přístup k nezaplacení. Pravdou je, že Dispatcher je celkem jednoduchou implementací posílače zpráv. Což má své výhody i mnoho nevýhod. Osobně preferuji práci s reaktivními proudy. Když jsem zvažoval, jestli použít původní RxJS nebo BaconJs, natrefil jsem na malou a rychlou knihovnu Kefir.

Kefir se inspiruje Rx i Baconem, ale snaží se držet praktičnosti místo akademické čistoty. Například nevrací Disposable objekt, pro ukončení odběru zpráv. To nám ale v našem případě nevadí, k odběru zpráv se budeme hlásit většinou pouze v jednom objektu s životností jako stránka, na které je. Pravděpodobnost, že nám začne utíkat paměť neznámo kam, je velmi malá.

Takže teď uděláme z pohledu Fluxu prasárničku a spojíme Store a Dispatcher do jednoho objektu. Já si myslím, že ve skutečnosti je prasárnička tyhle věci od sebe oddělovat, protože to porušuje Single Responsibility Principle, ale opačným směrem. Jeden koncept rozkládá na menší subkoncepty a dělá z nich jakoby samostatné koncepty, i když ty tvrdé závislosti přímo křičí “my jedno jsme!”

import Kefir from 'kefir';
import {List, Stack} from 'immutable';
import {comp, filter, map} from 'transducers-js';

function register(stream, messageType, handler) {
    let xform = comp(
        filter(x => x.first() === messageType),
        map(x => x.rest()));
    stream.transduce(xform).onValue(handler);
}

class Application {

    constructor(initialData) {
        // ...
        this.stream = Kefir.emitter();
        this.on('update', x => this.updateData(x.first()));
    }

    // ... props canUndo, canRedo
    
    on(eventName, handler) {
        register(this.stream, eventName, handler);
    }

    emit() {
        this.stream.emit(new List(arguments));
    }

    // ... updateData, undo, redo
}

Třídu Application jsme rozšířili o možnost přijímání a posílání zpráv pomocí Kefírového emitteru a vystavení metod on – pro registraci k odběru jednotlivých zpráv – a emit, která zprávy posílá jako immutable List. Rovněž jsme přidali prvního příjemce zpráv typuupdate. K čemu nám bude dobrá, si ukážeme o něco později.

Obrovským benefitem reaktivních streamů je, že s nimi lze zacházet jako s kolekcemi. Jednotlivé zprávy mohu filtrovat a mapovat, redukovat a vůbec používat spoustu užitečných operátorů, které nám dovolí brutálním způsobem snížit komplexitu práce s asynchronním přístupem. Viz funkce register, která vybere zprávy požadovaného typu, vezme zbytek parametrů a nad tím pověsí handler, který tyto parametry bude přebírat a zpracovávat. Ale zpátky k vyšším cílům. :)

Aktuálně toho naše aplikace moc nedělá. Sice umí spravovat historii a komunikovat s okolím, ale to je trochu málo.
Naše data potřebujeme nějak zobrazit uživateli. K tomu využijeme React.

class Application {

    constructor(initialData, view) {
        // ...
        this.view = view;
    }
    
    // ... props & methods
    
    render() {
        this.view.setProps({
            data: this.state,
            messages: {
                emit: this.emit.bind(this),
                on: this.on.bind(this)
            },
            history: {
                canUndo: this.canUndo,
                undo: () => this.undo(),
                canRedo: this.canRedo,
                redo: () => this.redo()
            }
        });
    }
}

Nainjektujeme si React kořenovou komponentu a když zavoláme metodu render, tak jí předáme props, které obsahují kompletní stav k vyrenderování, možnost posílání zpráv a práci s historií. Všimněte si, že komponenty nepotřebují znát žádná úložiště ani Dispatcher, všechno dostanou nainjektované a potřebují znát maximálně dvě jednoduchá rozhraní.

Application

Jednoduchá implementace vzoru Inversion of Control a ani nepotřebujeme žádný kontejner!

Třídu Application máme už pomalu hotovou. Ještě nám zbývá jedna věc. Kdykoliv dojde ke změně dat, potřebujeme přerenderovat našeview. Přenášet tuhle zodpovědnost na uživatele by bylo krajně nezodpovědné, takže  využijeme reaktivního streamu a o změně stavu hezky informujeme zprávou state-changed. A k ní se i rovnou přihlásíme a vyvoláme přerenderování.

class Application {

    constructor(initialData, view) {
        // ...
        this.on('state-changed', () => this.render());
    }
    
    // ... 

    updateData(data) {
        // ...
        this.emit('state-changed', 'updateData', this.state);
    }

    undo() {
        // ...
        this.emit('state-changed', 'undo', this.state);
    }

    redo() {
        // ...
        this.emit('state-changed', 'redo', this.state);
    }
    
    // ...
}

Proč si posíláme i data, co změnu stavu vyvolalo a jaký ten stav je, když to v handleru neřešíme? I když je to porušení 4 pravidel jednoduchého návrhu, tak přidáním kouzelného řádku do konstruktoru získáme silný debugovací nástroj:

this.stream.map(x => x.toJS()).log('stream');

Nový problém: potřeba lokálních změn

Vyřešili jsme dva problémy, ale vytvořili jsme tím problém zcela nový. Pokud potřebujeme v komponentách udělat sebemenší změnu stavu, musí komponenta znát jeho strukturu, aby mohla změnu provést. Což, jak jistě uznáte, není vůbec elegantní ani jednoduché. Najednou přenášíme komplexitu, kterou za nás spravovala jednotlivá úložiště, přímo do komponent, které mají sloužit pouze k zobrazovaní dat. Ha!

Naštěstí jde o problém, který jde vyřešit velice snadno pomocí kurzorů! Kurzor je v podstatě pohled na některou z větví nebo listů celkového stavu. Kurzor si pamatuje cestu, kde se ve stromu nachází, a tváří se, jako by šlo jen o data, která potřebujeme v konkrétní komponentě. Kurzor můžete implementovat třeba jako to dělá estejs, já pro svou jednoduchost použiju contrib implementaci z Immutable.js:

import Cursor from 'immutable/contrib/cursor';
import {Component} from 'react';

class ApplicationView extends Component {

    cursorFor(keyPath) {
        let update = x => this.props.messages.emit('update', x);
        return Cursor.from(this.props.data, keyPath, update);
    }

    render() {
        return (
            <SubComponent data={this.cursorFor(['path', 'to', 'nested', 'data'])} />
        );
    }
}

Co je důležité, že v případě contrib implementace se do update předává celý strom, který obsahuje lokálně provedené změny. Ten pošleme ve zprávě update zpátky do Application a tím se nám celé kolečko uzavře. Problém vyřešen.

Co když používám AngularJS?

“To je všechno hezký, co tady ukazuješ, ale co když používám Angular?”

AngularJS  používá obousměrný data-binding, který je o hodně komplexnější, ale benefity, které nám přináší immutabilní datové struktury, můžeme čerpat i v Angularu. Dirty checking je jedno z nejbolestivějších míst a immutabilní data nám ho pomůžou vyléčit stejně jako u Reactu. Zkuste knihovnu angular-immutable, hodně věcí vám usnadní a bude to znát i na výkonu. Co se týče posílání zpráv pomocí reaktivních streamů, můžete použít třeba Rx.angular.

Závěr

Asi jsem to explicitně nezmínil, ale všechnu tu zbytečnou komplexitu se nám podařilo vyhnat díky aplikaci několika málo návrhových principů a technik. SOLID a 4 pravidla jednoduchého návrhu už jsem zmiňoval. A ty techniky? Věřte nebo ne, jde o funkcionální programování bez akademických strašáků. Ruku v ruce s objekty. A ačkoliv se v článku nevyskytuje ani jeden test, veškerá funkcionalita v článku popsaná byla vytvořena pomocí TDD. Nevěříte?

test-results

Zdrojové kódy z ukázek jsem zkompletoval a najdete je v odkazovaném gistu.

Komentáře

Subscribe
Upozornit na
guest
40 Komentářů
Nejstarší
Nejnovější Most Voted
Inline Feedbacks
View all comments
steida

Vynikající článek Aleši, bravo. Jen pár poznámek.

Ad „Problém první: distribuovaný mutabilní stav“
Flux vznikl ještě před tím, než byli immutabilní struktury v JavaScriptu dostupné. Ano vím Mori, ale stejně. Protože jsem nové Este vyhodil nedávno, už jsem mohl využít immutable.js, a stav ze storů odstranit. Princip Fluxu ale není v ukládání stavu ve storech, to byla spíše znouzecnost. V Este jsem Flux upravil tak, aby zůstal zásadní princip, ale stav byl na jednom místě. A to bez použití vlastní implementace, čistě jen úpravou designu. Props se používají stále, ale jen pro potomky kompozitních component, aby bylo možné jednoduše využít PureRenderMixin. Samotné kompozitní komponenty čtou ale ze storů, ale tam nevidím rozdíl mezi předáním prop, a načtením pomocí ze storu pomocí get metody. Story jsou readonly, takže je nelze z komponent měnit. Výhodou je, že komponenta je v UI nezávislá, tedy že její rodiče neznají její props. Samozřejmě že to lze i jinak, ale i tak by každý měl Flux znát. Vědět, k čemu jsou akce a dispatcher, a proč jde o násobně čistší řešení, než Backbone nebo event observing. Pak se samozřejmě může posunout dále. Takže říct, nepotřebujeme Flux… no chápu, taky bych ten titulek takhle napsal :-), ale pravda je taková, že koncepty Fluxu: Akce, unidirectional data flow a story jako reduce, svět znát sakra potřebuje.

Ad „Problém druhý: natvrdo zadrátované posílání zpráv“
„polymorfní zpracování zpráv“, tim myslíš, že zprávy jsou instance? Tomu jsem se snažil vyhnout, dle mého se takové zprávy špatně auditují, mapují na event sourcing, a hlavně pokud uděláš instanci, někdo jiný ji může změnit, a jsme tam, kde jsme byli – muttable hell. Možné řešení by mohl být http://facebook.github.io/immutable-js/docs/#/Record, ale než si budu jist, je imho Este.js řešení, akce je akce (jak tautologické!) nejlepší.

Ad „uděláme z pohledu Fluxu prasárničku a spojíme Store a Dispatcher“
Tohle imho dělá i ještě nepublikovaný Relay, ale i tak je dobré znát Flux, jinak lidi nebudou chápat, proč je tak nevržen.

V novém Este jsem se snažil o maximální čistotu návrhu, aniž bych musel implementovat vlastní Flux, nebo použít cokoliv jiného než základní dispatcher. Jde především o patterny. I tak je aplikace mnohem čistější, než cokoliv co jsem kdy před Fluxem viděl, nepočítám-li ClojureScript, Elm, a podobné. A samozřejmě, každý může vzít Este.js, a upravit ho dále podle tvého článku, nebo místo dispatcheru použít csp-js, nebo si počkat na Relay.

Ad stores. Speciálně v Este nedrží stav, ale poskytují jen různé pohledy, filtery, reduce, sort atd. nad global app state, tohle je dobré mít v aplikaci dělené podle features, takže by si programátor stejně musel nějaké „story“ vytvořit.

Ad immutable.js contrib/cursor. Ten jsem zvažoval, ale nějak jsem nepochopil, v čem je lepší než mé řešení, které se bez nej obejde. Navíc contrib/cursor má dost issues. Přehlédl jsem něco?

Ad kefir, jak řeší unidirectional flow, tedy že můžeš prstem (dobře, pomocí console.log) ukázat na místo app, skrze které tečou všechny události? Skládáním streamů? Hnaním všechno přes jeden stream? Jinak?
A ještě jedna otázka, Sebastian naznačuje, že Rx model jde proti Om modelu. Mrkni na diskuzi. Koukal jsem na odkaz, ale příklad se zná nekompletní. Nechtěl bys předělat Este TodoMVC tak, aby šlo porovnat s Kefírem?

A poslední otázka, přemýšlel jsi nad csp-js s transducers versus Rx ala Kefír? Mne se zdá csp-js ještě o trochu lepší, než Rx, ale tady se přiznám nemám silný názor.

Ještě toho mám v hlavě více, ale to si nechám na jindy. Ještě jednou díky za skvělý článek.

steida

Díky za vysvětlení polymorfního zpracování zpráv. Některé Flux implementace to tak dělají, ale já chtěl zachovat čistý Flux. Samozřejmě, šlo by to implementovat i s eventy nebo pattern matchingem, ale ani switch není nijak zlý, pokud se porovnávají pouze konstanty.

CSP má blocking zpracování? Zde na přednášce to tak nevypadá, transducery jsou tam demonstrovány také. Nicméně ano, máš pravdu, bez transpileru by to v současných prohlížečích nešlo. Avšak babeljs a csp-js jedou. Souhlasím, kefir je použitelný teď, kdežto csp-js s ES7 ještě potřebuje usadit.

Ad struktura aplikací, souhlasím a od Davida jsem to taky někde slyšel. Škoda, že nemám času si hrát s ClojureScriptem a jeho ekosystémem více.

Ad Cursor, díky :-)

AltmanO

CSP vyžaduje, aby zapisovaná data do kanálu někdo z druhé strany četl, pokud je nemá kdo číst, tak je proces blokovaný, dokud k přečtení nedojde.

Neni uplně pravda, CSP má asynchronní verze operací put a take. Kde se nemusí používat buffer a operace vložení neni blokující (nemusí být ani v go blocku). Když se použijí transducery, musí se použít bufferSize == 1 a tohle neblokující chování funguje pořád stejně.

příklad:

var myChan = csp.chan(1, transducers.map((n) => n*2));

csp.putAsync(myChan, 1);
csp.putAsync(myChan, 2);
csp.putAsync(myChan, 3);
csp.putAsync(myChan, 4);
csp.putAsync(myChan, 5);

csp.go(function* () {
    yield csp.timeout(10000);
    while(true) {
        console.log(yield myChan);
    }
});

Osobně myslím že CSP je flexibilnější než reaktivní streamy a dá se pohodlně použít na větší množství problémů.

AltmanO

Ten text z dokumentace hned potom pokračuje:

We can put and take values asynchronously from a channel:

var ch = csp.chan();

csp.takeAsync(ch, function(value) { return console.log("Got ", value); });

// After the put, the pending take will happen
csp.putAsync(ch, 42);
//=> "Got 42"

kdyby to opravdu nešlo a bylo blokující tak by ten příklad co jsem postnul o komentář výš nefungoval.
Pořád si myslím že reaktivní streamy oproti CSP nemají co nabídnout.

steida

Blbě jsem se vyjádřil, samozřejmě jsem myslel napsat vlastní implementaci TodoMVC tak, aby byla funkčně paritní s Este TodoMVC.

AltmanO

Pěkně napsané :)

Přijde mi že jak článek tak i Este se ve velké míře inspirovali ClojureScriptem (immutable hodnoty) a zejména Om knihovnou (globální stav, ref-cursory). V ClojureScriptu je pořád ještě spoustu myšlenek, který ve světě JS nejsou, je dobře že se dobrý nápady se šíří mezi jazykama.

Tobiáš Potoček

Na přednášce jsem byl a už tehdy mě napadlo pár připomínek, a po přečtení článku jich mám ještě více.

Dispatcher musí často koordinovat změny na několika místech

Jak toto může nastat? Nemá Dispatcher fungovat tak, že přijme akci a rozešle ji na všechny Story?A Story si pak samy rozhodnou, jestli akci zpracují či ne. Aspoň tak to Facebook nadefinoval, když uvedl Flux. O jakou koordinaci se jedná?

React komponenty se rázem stanou mapovací funkcí, kterou aplikujeme na stav a dostaneme aktuální zobrazení.

Jaktože toto neplatí už v „klasickém“ Fluxu? Veškerý stav je uložený ve Storech a je zpřístupněn přes read-only rozhraní. Obsah Storů přesně definuje stav uživatelského rozhraní. Už samotné slovo „uniderctional“ jednoznačně implikuje souvislost s funkcemi. A s tím souvisí toto:

Problém první: distribuovaný mutabilní stav

Dále má komponenta jen svůj vnitřní stav, který je aktualizován při změně v úložištích. A pak si vemte, že v běžné aplikaci máte úložišť víc než dvě. Věci se začnou komplikovat.

Jakým způsobem se věci začnou komplikovat? Změní se stav nějakého Storu, komponenta je na něj subscribnutá, tak se překreslí. Samozřejmě problém může být, když každý Store bude říkat něco jiného. Ale to je chyba konkrétní implementace, nikoliv architektury. A jak říká přímo Facebook: Single source of truth. Na to stačí dávat pozor.

Tj. problém s mutabilním distribuovaným stavem vyřešil už čistý Flux sám o sobě tím, že zavedl jednosměrný proud informací. Není mi jasné, jak použití immutable objektu na tom cokoliv mění.

No a pak je tu ten globální stav. Globální stav je fuj a vůbec nejde jen o mutace (i když ty jsou to nejhorší, samozřejmě). I když z globální stavu pouze čtu, tak to vytváří závislost (která nemusí být vždy hned patrná) a především se určitě se shodneme na tom, že v čím menším scopu/kontextu se programátor pohybuje, tím pro něj lépe.

To mě na JavaScriptových programech pořád překvapuje, jak běžně se používají globální instance objektů a služeb. Chápu, že prostředí okna prohlížeče k tomu tak trochu vybízí, nicméně i u PHP (kde rovněž pro každý request vzniká/zaniká celá samostatná instance prostředí) se už přišlo na to, že to není úplně dobrá praktika a běžně se používají DI kontejnery apod. U JavaScriptu a potažmo Reactu a Fluxu mě to překvapuje o to více, že díky node.js se to dá používat i na serveru, kde se nicméně kontext sdílí pro všechny příchozí requesty. Jak jsem se díky @steida poučil, lze používat singletonové Story i na serveru, ale osobně mi to pořád přijde více jako trik než ta správná cesta.

Ta databáze je pěkné přirovnání (rozvedu dále), ale ani s databází nepracuji jako s celkem. Databáze je rozdrobená na jednotlivé tabulky, přičemž nad každou skupinou tabulek jsou typicky 2-3 vrstvy abstrakce a v konečném důsledku s databází pracuji skrz nějaké čtivé unifikované rozhraní a měním data v omezeném rozsahu. Cursory toto suplují jen částečně. Čímž se dostávám k:

Takže teď uděláme z pohledu Fluxu prasárničku a spojíme Store a Dispatcher do jednoho objektu.

Důležitou součástí každého (klasického) Storu je i to veřejné API, skrz které čtou data komponenty, a pokud je to API navržené dobře, tak i ten kód komponenty je lépe čitelný (rozhodně lépe, než za přímého použití kurzorů). Z této perspektivy se mi líbí prezentované TodoMVC od @steida a jeho *state-less“ Story. Centrální immutable objekt stavu tady funguje jako databáze a jednotlivé Story jako repozitáře, přičemž skrz cesty v immutable objektu přistupujeme k jednotlivým „tabulkám“.

Nicméně se nemůžu zbavit pocitu, že z hlediska Fluxu se v konečném důsledku nezměnilo téměř vůbec nic. Vyměnili jsme úložiště, vyměnili jsme databázi, a to nám přineslo pár cool fičurek navíc:

  • Máme podporu pro historii (což je upřímně opravdu cool, ale v kolika projektech je to skutečně potřeba?)
  • Můžeme snadno serializovat/deserializovat stav celé aplikace (to šlo už předtím, jen to bylo o něco méně elegantní)
  • Je to rychlejší (nebyl React už sám o sobě hodně rychlý?)

Já tyto výhody samozřejmě nechci nějak zlehčovat, ale z hlediska Fluxu mi to přijde pouze jako incrementální zlepšení, tj. mnoho povyku pro nic.

Tobiáš Potoček

Díky za odpověď.

Ad Repozitáře: mně v zásadě jde jen o ten koncept „pojmenovávání dotazů“. Což může být klidně ta sada transducerů. Stejně tématicky blízké transducery budou v jednom modulu, odkud naimportované se budou používat podobně jak ten repozitář. A tomu modulu můžeme říkat („state-less“) Store, tj. je skoro zase jen slovíčkaření :)

K tomu výkonu Immutable objektu: Jak doopravydy je to to výkonné? Teď by se mi to třeba hodilo nasadit do jednoho projektu, jen je problém, že ze serveru proudí JSONy v řádu megabajtů. Můžu si dovolit něco takového verzovat? A vůbec, je možné immutable global store nasadit na něco jako je třeba Gmail, což je rozsáhlá aplikace a především má v prohlížeči uptime v řádech hodin i klidně desítek hodin. Během té doby bude ten immutable objekt bobtnat a bobtnat…

Tobiáš Potoček

No uznávám, že říkat „Store“ něčemu, co v sobě žádná data neobsahuje, je trochu na hlavu. :)

Každopádně díky za článek. Určitě uvítám další takové. Do budoucna bych bral nějaký příklad trochu větší aplikace napsané v Reactu/Fluxu, která bude komunikovat se serverem (což je stejně v praxi alfou i omegou každé takové aplikace). Ono v okamžiku, kdy člověk začne řešit přechody mezi URL, formuláře, stránkování atd, tak zjistí, že cest, jak to řešit, je mnoho, ale málokterá vede do cíle.

jiri.vrany

Pěkný příklad React/Flux s přechody mezi URL, taháním dat z API, stránkováním aj. udělal Geaeron – https://github.com/gaearon/flux-react-router-example

steida

Ad rychlejší: Tam jde spíše o to, že když to děláme „správně“, není třeba nic optimalizovat, vůbec. Prostě je to nejrychlejší jak to může být :-)

Ad singletonové: Singleton je instance, Este.js nepoužívá ve Fluxu třídy ani instance, jsou to jen funkce a funkce není singleton.

Ad jen čtu z globálního bez závislosti. Kdyby to bylo někde schované uvnitř kódu, pak ano. Pokud je závislost explicitně na začátku souboru vyjádřená pomocí import, je to ok.

Jinak s ostatními poznámkami souhlasím.

Tobiáš Potoček

Čistě formálně označení singleton v tomto případě není úplně ideální, nicméně ta myšlenka za tím souhlasí. Datově ten Store (ať už samostatně či součástí globálního immutable objektu state) existuje v celé aplikaci nejvýše jednou. Což na straně serveru, který má vyřizovat množství oddělených požadavků, je prostě trochu zvláštní přístup (byť samozřejmě řešení, jak se s tím vyrovnat, existuje a funguje).

S tím globálním stavem je to vtipné. Se slavně zavedl globální stav jakožto porušení starých pořádků, což kdyby byla skutečně pravda, tak by to byl průšvih, nicméně skutečnost je taková, že se pouze a jenom zavedlo oddělené úložiště pro Story na principu databáze, se kterým se dál pracuje (či mělo pracovat) jako s databází, tj. skrz další vrstvy abstrakce, které „globální stav“ zpět rozsekávají na drobnější oblasti, se kterými programátor pracuje.

Když se ale vrátím zpět k prvnímu odstavci, tak ve všech ostatních (webových) programech se běžně počítá s tím, že lze jednu databázi vyměnit za jinou či dokonce udržovat více paralelních spojení (tj. připojení k databázi není globální). Tady ale ten global state je skutečně jen jeden…

tacoberu

ad globální stav: Nebyl problém s globálním stavem jako zlem pouze v těch případech, kdy si funkce šahala na proměnné, které nedostala jako argument? To je určitě zlo, ale IMHO to není to, o čem autor hovořil.

tacoberu

K takovému pojetí globálního stavu bych se nevracel.

steida

lze jednu databázi vyměnit za jinou či dokonce udržovat více
paralelních spojení

Je jen jeden a tak to má být, protože máš v prohlížeči jen jednu instanci aplikace. Avšak můžeš ho kdykoliv celý změnit, jak ukazuje video v readme, můžeš ho ukládat a číst do localStorage, můžeš ze serveru dostat nový, to vše je už pouhý implementační detail.

Tobiáš Potoček

To určitě. Ale je tu paradoxní ten trend. V prostředí Apache/PHP se pro každý request také vytvoří jedna instance aplikace. Máme jeden globální prostor pevně svázaný s jedním requestem, tj. stejná situace. Ale stejné všechny moderní frameworky se odklánějí od statických tříd, singletonů atd. Celá aplikace funguje jako jeden instancovaný kontejner (tj. klidně v jednom requestu můžu pustit dvě instance Nette…).

Chtěl bych v Reactu/Fluxu vidět napsaný nějaký Drupal či WordPress. A ideálně s isomorfním frontendem. Všechno, co zatím vidím, jsou spíše jen technologická dema. Existuje už něco takového?

Tobiáš Potoček

Mno, není to tak dávno, co isomorfismus byl #1 buzzword :) Ale teď se to nějak ztrácí.

Každopádně tam strašně záleží, co za aplikace chceme psát. Kupř. když jsem poprvé viděl React, tak jsem si říkal, super, konečně se zbavíme všech šablonovacích jazyků a kódění v HTML. Místo toho bude mít skutečné komponenty, budeme rozhraní „stavět“ a ne splácávat z kousků HTML kódů. Krásně se to vykreslí na serveru a na klientu se bude pokračovat rychle JavaScriptově. Tj. nakombinují se výhody SPA a běžných webových aplikací. Napsat v něčem takovém WordPress se mi tehdy zdálo jako super nápad…

Jenda

Proč to borci v PHP dělají? To je jednoduché, za svoji kariéru jsem už několikrát migroval aplikaci z jednoho frameworku do druhého. To je věc, která se dělá dost často a může jít i o migraci z jedné verze frameworku na jinou. Třeba Zend 1 na Zend 2. No a každé statické volání, každý singleton a každá skrytá závislost na globálním prostoru tuto migraci ztěžuje. Naposledy (bavíme se o roku 2014) jsem pracoval na aplikaci s desítkami tisíc řádků zdrojového kódu a tam už taková migrace nezabere několik odpolední, tam to trvá týmu programátorů měsíce. A každý je rád za každou abstrakci, protože to takový proces velmi zjednoduší.

Jiří Knesl (autor)

Já bych řekl, že to dělají proto, že to dnes už není vůbec těžké (autowiring) a dává to při testování všemožné výhody. Dnes bych řekl, že není už potřeba nahrazovat pomocí DI úplně všechno, ale to většinou ani kontejner ani nijak nevynucuje.

Jiří Knesl

sorry za to (autor), zdroják si to nějak pamatuje z článku, co jsem kdysi napsal a dal pod něj komentář a teď mi to sype všude, snad to touhle změnou zase zapomene :)

Skalimil Vuk

Díky za skvělý článek,

na Kefir jsem narazil nedávno, streamy používám dlouho, eventy v pdosttaě také, ale takhle jednoduché propojení všeho do jednoho celku, to je vynikajicí myšlenka.

Jen mám dotaz (obecnější). ES6 lze nějak přímo spustit ? V čem? Podle tohoto https://kangax.github.io/compat-table/es6/ jestli tomu dobře rozumím, tak nejlepší je asi to prohnat to přes Traceur, nebo Babel (ten používá i estejs), ale to změní zdrojový kód a je to pak méně přehledné (jasně, nevýhoda starého ES5).

Takže do prohlížeče přes Babel?

Aha, v konzoli mohu použít babel-node ./App.js. Jsem ze staré školy a používám debugger, ačkoliv všichni kolem jen testují :-) Je možné debuggovat přímo ES6 ?

Skalimil Vuk

Dík.

Tu dědičnost z React.Component právě teď řeším. Starý způsob je předat cursorFor jako mixin přes React.createClass ?

A nový způsob tvorby komponent dle té ukázky by se použil jako vytváření dalších potomků z té třídy ApplicationView, kde by byla jen jiná metoda render?

class NejakaKomponenta extends ApplicationView

A když budu chtít použít nějaké cizí komponenty, tak do nich také budu muset nějak dostat podporu kurzoru, nebo ne?

Skalimil Vuk

Možná něco dělám blbě, ale snažím se ten příklad zprovoznit a zatímco Application.js je bez problémů, propojení s react ApplicationView mi činí potíže.

Došel jsem teda k tomu, že ES6 ApplicationVidew.js přeložím třeba takto:

browserify --debug --transform  [ reactify --es6 ]  ApplicationView.jsx > bundle.js

což ale samo o sobě nefunguje, protože ES6 syntakci import reactify neumí (a jsx také ne). Takže nejdříve potřeba změnit v ApplicationView.js:

// import Cursor from 'immutable/contrib/cursor';
var Cursor = require('immutable/contrib/cursor');

// import {Component} from 'react';
var Component = require('react').Component;
Skalimil Vuk

zdravím,

nechci obtěžovat, ale chtěl bych poprosit ještě o jednu radu.

Jak propojím Application a ApplicationView?

var Application = require("./Application");

var ApplicationView = require("./ApplicationView");

// aplikace vyžaduje dva parametry
// initialData - to je jasné
// view - to si myslím že je instance AppView, respektive React.Component

var appView = new ApplicationView();

var app = new Application(initialData, appView);

// a ted vyrenderovat, jenže jak?
// Normálně by bylo takto, ale tím vytvořím další instanci ApplicationView, která není propojená s instancí Application

React.render(
    <ApplicationView />,
    document.getElementById('container')
  );

// toto vrací chybu Invalid component element

 React.render(
       appView,
        document.getElementById('container')
      );

// a někde bych měl také volat, ne?
app.render()

Jak tedy vyrenderovat AppView?

díky moc

Kolemjdouci

Potřebujeme vůbec javascript?

petr

Ahoj,

chtel bych se zeptat, kolik streamu pouzivate v aplikaci. Jestli jeden jediny na uplne vse (vcetne zadosti o data ze serveru), nebo vice streamu podle ucelu.

Dik za odpoved.
P.

Petr

Ahoj,

chtel jsem se jeste zeptat, jak se ve FLUX resi, kdyz bych chtel v akci chce zavolat jinou akci?

Enum a statická analýza kódu

Mám jednu univerzální radu pro začínající programátorty. V učení sice neexistují rychlé zkratky, ovšem tuhle radu můžete snadno začít používat a zrychlit tak tempo učení. Tou tajemnou ingrediencí je statická analýza kódu. Ukážeme si to na příkladu enum.

Pocta C64

Za prvopočátek své programátorské kariéry vděčím počítači Commodore 64. Tehdy jsem genialitu návrhu nemohl docenit. Dnes dokážu lehce nahlédnout pod pokličku. Chtěl bych se o to s vámi podělit a vzdát mu hold.