Reactive Web Stack:3RES – React, Redux, RethinkDB, Express, Socket.io

Tento příspěvek napsal Scott Hasbrouck. Najdete ho na Twitteru nebo jeho webu.

Bylo úžasné vidět, jak JavaScript v posledních několika letech skutečně vzplanul ve webových technologiích a nakonec se stal nejpoužívanějším jazykem v roce 2016, podle údajů StackOverflow. Moje historie s JavaScriptem začala asi před 15 lety, ne tak dlouho poté, co byl poprvé vydán jako součást Netscape Navigator 2, v roce 1996. Mým nejpoužívanějším učebním zdrojem byl DynamicDrive a jejich výukové programy a úryvky kódu „Dynamic HTML“ nebo DHTML – termín vytvořený Internet Explorerem 4. DHTML byla ve skutečnosti sada funkcí prohlížeče implementovaných pomocí JavaScriptu, CSS a HTML, které vám mohly poskytnout šikovné prvky, jako jsou tlačítka pro převrácení a burzovní titulky.

Dnes žijeme ve světě, kde se JavaScript rozrostl a převzal webovou technologii. Nejen v prohlížeči, ale podle stejné zprávy StackOverflow je to nyní nejoblíbenější backendový jazyk! Přirozeně se vždy najdou tací, kterým se jazyk nelíbí, když uvádějí věci, jako je snadnost vytváření globální proměnné nebo null jako objekt a undefined jako svůj vlastní datový typ. Ale zjistil jsem, že každý jazyk, který si osvojím, má své zvláštnosti, kterým se lze snadno vyhnout, jakmile se ho naučíte správně používat. A my se přece chceme stát odborníky v našem řemesle a skutečně se naučit ovládat naše nástroje, ne?

Zde jsou hlavní faktory (dobré nebo ne), o kterých se domnívám, že jsou proč JavaScript tak rychle ovládl internet:

  1. JavaScript je jediný univerzální jazyk na straně klienta.
  2. JavaScript se dá relativně snadno naučit, zvláště když pochází z jakéhokoli jiného jazyka podobného C.
  3. S příchodem Node.js může nyní JavaScript běžet na serverech (a Node/V8 je při tom extrémně efektivní z hlediska zdrojů).
  4. ES6 přišel v pravý čas a „opravil“ spoustu problémů se syntaxí JavaScriptu a chybějícími funkcemi.
  5. Vyspělé rozhraní front-end. Přiznejme si to, vytvoření frontendové aplikace ve vanilkovém JavaScriptu vyžaduje hodně disciplíny, aby se nepřeměnila na špagetový kód. React/Redux/Reflux a Angular poskytují rámec, který jej udržuje organizovaný.
  6. Šířka a kvalita projektů s otevřeným zdrojovým kódem a snadnost instalace těchto modulů pomocí npm.

Zejména příchod Node.js vyhnal přijetí JavaScriptu k historickému maximu. Díky tomu se musíme naučit pouze jeden jazyk pro celý zásobník a jsme schopni s ním budovat věci, jako jsou pracovníci na pozadí a servery HTTP! Dokonce jsem nedávno dokončil svou první knihu o nabíjení kreditních karet pomocí proužků pomocí JavaScriptu a Node.js – něco, co jsem si nikdy nemyslel, že budu schopen, když jsem se před více než deseti lety poprvé naučil jazyk. Takže ať se vám to líbí nebo ne, tady žijeme v JavaScriptovém internetovém světě. Ale tady jste . Můj odhad je, že se vám to asi líbí. Což je skvělé, vítejte! Protože se s vámi nyní chci podělit o to, jak se mi podařilo využít tento nový rozsáhlý svět JavaScriptu k vytvoření skutečně reaktivní sady webových aplikací – vše v jednom jazyce odshora dolů.

Sada 3RES

Jo, taky nevím, jak to vyslovit... trojky? Tak určitě. Začněme nahoře s Reactem.

Pouze frontendové knihovny

Reagovat

React je deklarativní způsob vytváření uživatelských rozhraní, který se silně opírá o rozšíření syntaxe podobné XML, nazvané JSX. Vaše aplikace se skládá z „komponent“ – z nichž každá zapouzdřuje malé, často opakovaně použitelné části vašeho uživatelského rozhraní. Každá z těchto komponent má svůj vlastní neměnný stav, který obsahuje informace o tom, jak by se komponenty měly vykreslit. Stav má čistě funkci setter (žádné vedlejší účinky) a neměl by být měněn přímo. Tento přehled navrhovaného zásobníku 3RES bude vyžadovat pouze základní znalosti React. Samozřejmě se chcete stát mistrem Reactu! Nezapomeňte se dozvědět více o Reactu na SurviveJS – jedné z nejlepších komplexních knih React s bezplatnou verzí.

Redux

Pokud React zapouzdří všechny vaše komponenty uživatelského rozhraní, Redux zapouzdří všechna vaše data reprezentovaná jako objekt JavaScriptu. Tento objekt stavu je neměnný a neměl by být upravován přímo, ale pouze odesláním akce. Tímto způsobem může kombinace React/Redux automaticky reagovat změny stavu a aktualizaci příslušných prvků DOM tak, aby odrážely nové hodnoty. Redux má úžasnou dokumentaci – pravděpodobně jednu z nejlepších pro jakoukoli open source knihovnu, kterou jsem použil. Aby toho nebylo málo, Redux má také 30 bezplatných videí na egghead.

Frontend a Backend knihovny

Socket.IO

Vaše dosavadní webové aplikace s největší pravděpodobností při komunikaci se serverem spoléhaly na AJAX – který je postaven na JavaScriptovém rozhraní API společnosti Microsoft s názvem XMLHttpRequest. Pro mnoho jednorázových akcí vyvolaných uživatelem, jako je přihlášení, má AJAX velký smysl. Je však extrémně nehospodárné se na něj spoléhat u dat, která se průběžně aktualizují, a pro více klientů. Jediným skutečným způsobem, jak to zvládnout, je pravidelné dotazování backendu v krátkých intervalech s žádostí o nová data. WebSockets jsou relativně novou technologií, která nebyla do roku 2011 ani standardizována. WebSocket otevírá nepřetržitě čekající TCP spojení a umožňuje rámce dat, která mají být odeslána buď serverem nebo klientem. Spouští se „handshake“ HTTP jako požadavek na upgrade. Nicméně, podobně jako často nepoužíváme vanilla XMLHttpRequest API (věřte mi, musel jsem to udělat, nechcete to implementovat sami a podporujete každý prohlížeč), také obvykle nepoužíváme JavaScript WebSocket API přímo. Socket.io je nejrozšířenější knihovna pro komunikaci WebSocket na straně klienta i serveru a také implementuje zálohu XMLHttpRequest/polling pro případ selhání WebSockets. Tuto knihovnu budeme používat ve spojení se změnami RethinkDB (popsanými níže) a Redux, abychom neustále aktualizovali stavy všech našich klientů pomocí naší databáze!

Backendové knihovny a technologie

RethinkDB

RethinkDB je open-source datové úložiště NoSQL, které ukládá dokumenty JSON. Často se srovnává s MongoDB, ale je mnohem lepší v mnoha klíčových ohledech, které jsou relevantní pro fungování našeho zásobníku 3RES. Primárně RethinkDB vychází z krabice s dotazem changefeeds – možnost připojit k dotazu posluchač událostí, který bude přijímat aktualizace v reálném čase, kdykoli bude dokument vybraný tímto dotazem přidán, aktualizován nebo odstraněn! Jak bylo uvedeno výše, budeme vydávat události Socket.io z našich změnových kanálů RethinkDB. Kromě toho lze RethinkDB úžasně snadno škálovat pomocí shardingu a implementuje redundanci s replikací. Má úžasný program pro vývojáře a křišťálově čistou dokumentaci a neustále se zlepšuje díky zpětné vazbě od inženýrů, jako jsme my.

Expresní

A konečně, naše aplikace bude stále muset přijímat požadavky HTTP jako trasy. Express je uznávaný minimalistický rámec Node.js pro vytváření tras HTTP. Použijeme to pro vše, co vyžaduje jednorázovou událost, která je mimo rozsah Socket.io:počáteční načtení stránky, přihlášení, registrace, odhlášení atd.

Sestavení kódu serveru

Naše vzorová aplikace bude jednoduchý kontrolní seznam úkolů bez ověřování. Jednou z mých běžných stížností je, že ukázková aplikace pro jednoduchý tutoriál má obrovskou kódovou základnu – jen je příliš časově náročné vybrat relevantní části aplikace. Takže tato ukázková aplikace bude velmi minimální, ale bude ukazovat přesně jeden příklad každého požadovaného kusu této sady pro reaktivitu end-to-end. Jediná složka je /public složku se všemi našimi zabudovanými JavaScripty. Jedním z důležitých bodů, který tato aplikace v tomto duchu vynechává, je ověřování a relace – kdokoli na internetu může číst a upravovat úkoly! Pokud máte zájem o přidání ověřování do této aplikace pomocí Socket.io i Express, na svém webu mám kompletní návod, jak to udělat!

Začněme backendem. Nejprve musíte získat kopii RethinkDB a poté ji spustit pomocí:

[Sidenote]

Čtení blogových příspěvků je dobré, ale sledování videokurzů je ještě lepší, protože jsou poutavější.

Mnoho vývojářů si stěžovalo, že na Node je nedostatek dostupného kvalitního videomateriálu. Sledování videí na YouTube je rušivé a platit 500 $ za videokurz Node je šílené!

Jděte se podívat na Node University, která má na Node ZDARMA videokurzy:node.university.

[Konec vedlejší poznámky]

$ rethinkdb

Jakmile spustíte RethinkDB, přejděte do super praktického webového rozhraní na adrese http://localhost:8080. Klikněte na záložku ‚Tabulky‘ v horní části, poté přidejte databázi s názvem ‚3RES_Todo‘, a jakmile je vytvořena, přidejte tabulku s názvem ‚Todo‘.

Kompletní kód pro tuto ukázku je na Github, takže si zde jen projdeme klíčové body, za předpokladu, že jste obeznámeni se základy Node.js. Repo obsahuje všechny požadované moduly v package.json , ale pokud chcete ručně nainstalovat moduly potřebné pro backendovou část aplikace, spusťte:

$ npm install --save rethinkdb express socket.io

Nyní, když máme požadované balíčky, nastavíme základní aplikaci uzlu, která slouží index.html .

// index.js

// Express
var express = require('express');
var app = express();
var server = require('http').Server(app);
var path = require('path');

// Socket.io
var io = require('socket.io')(server);

// Rethinkdb
var r = require('rethinkdb');

// Socket.io changefeed events
var changefeedSocketEvents = require('./socket-events.js');

app.use(express.static('public'));

app.get('*', function(req, res) {
  res.sendFile(path.join(__dirname + '/index.html'));
});

r.connect({ db: '3RES_Todo' })
.then(function(connection) {
    io.on('connection', function (socket) {

        // insert new todos
        socket.on('todo:client:insert', function(todo) {
            r.table('Todo').insert(todo).run(connection);
        });

        // update todo
        socket.on('todo:client:update', function(todo) {
            var id = todo.id;
            delete todo.id;
            r.table('Todo').get(id).update(todo).run(connection);
        });

        // delete todo
        socket.on('todo:client:delete', function(todo) {
            var id = todo.id;
            delete todo.id;
            r.table('Todo').get(id).delete().run(connection);
        });

        // emit events for changes to todos
        r.table('Todo').changes({ includeInitial: true, squash: true }).run(connection)
        .then(changefeedSocketEvents(socket, 'todo'));
    });
    server.listen(9000);
})
.error(function(error) {
    console.log('Error connecting to RethinkDB!');
    console.log(error);
});

Po několika řádcích Express/Node.js, které jste pravděpodobně viděli stokrát, první nové věci, které si všimnete, je připojení k RethinkDB. connect() metoda určuje databázi „3RES_Todo“, kterou jsme nastavili dříve. Jakmile je navázáno připojení, nasloucháme připojení Socket.io od klientů a poté řekneme Express, aby poslouchal jakýkoli port, který chceme. Událost připojení zase poskytuje soket, ze kterého vysíláme události.

Nyní, když máme připojení RethinkDB a Socket ke klientovi, nastavíme dotaz changefeed v tabulce RethinkDB ‚Todo‘! changes() metoda přijímá objektový literál vlastností, který použijeme dvě:includeInitial Vlastnost říká RethinkDB, aby jako první událost poslala celou tabulku, a pak naslouchá změnám. squash vlastnost zajistí, že současné změny budou zkombinovány do jediné události v případě, že dva uživatelé změní úkol ve stejném okamžiku.
Poslouchání událostí Socket.io před zahájením změny kanálu RehtinkDB nám umožňuje upravit dotaz uživatelem . Například v reálné aplikaci budete pravděpodobně chtít vysílat úkoly pro tuto konkrétní uživatelskou relaci, takže byste do svého dotazu RethinkDB přidali userId. Jak již bylo zmíněno dříve, pokud byste chtěli nějaký návod, jak používat relace se Socket.io, mám na svém blogu kompletní zápis.

Dále zaregistrujeme tři posluchače událostí soketu pro události vyvolané klientem:insert, update a delete. Tyto události zase vytvářejí nezbytné dotazy RethinkDB.

Nakonec uvidíte, že changefeed vyvolá funkci, kterou importujeme. Tato funkce přijímá dva argumenty:odkaz na soket a řetězec toho, co chceme nazývat tyto jednotlivé řádky v našich soketech (v tomto případě ‚todo‘). Zde je funkce handleru changefeed, která vysílá události Socket.io:

// socket-events.js

module.exports = function(socket, entityName) {
    return function(rows) {
        rows.each(function(err, row) {
            if (err) { return console.log(err); }
            else if (row.new_val && !row.old_val) {
                socket.emit(entityName + ":insert", row.new_val);
            }
            else if (row.new_val && row.old_val) {
                socket.emit(entityName + ":update", row.new_val);
            }
            else if (row.old_val && !row.new_val) {
                socket.emit(entityName + ":delete", { id: row.old_val.id });
            }
        });
    };
};

Jak můžete vidět, předávání socket odkaz a entityName , vrátí funkci, která přijímá kurzor řádků z RethinkDB. Všechny kurzory RethinkDB mají each() metodu, kterou lze použít k procházení kurzoru řádek po řádku. To nám umožňuje analyzovat new_val a old_val každého řádku a pak pomocí nějaké jednoduché logiky určíme, zda je každá změna insert , update nebo delete událost. Tyto typy událostí jsou poté připojeny k entityName string, pro vytváření událostí, které se mapují na objekty samotné entity, jako například:

'todo:new' => { name: "Make Bed", completed: false, id: ''48fcfafa-a2fa-454c-9ab4-8a5540d07ee0'' }

'todo:update' => { name: "Make Bed", completed: true, id: ''48fcfafa-a2fa-454c-9ab4-8a5540d07ee0'' }

'todo:delete' => { id: ''48fcfafa-a2fa-454c-9ab4-8a5540d07ee0'' }

Nakonec, abychom to vyzkoušeli, udělejme soubor index.html s jednoduchým JavaScriptem schopným naslouchat těmto událostem:

<html>
    <head>
        <script src="/socket.io/socket.io.js"></script>
        <script>
            var socket = io.connect('/');
            socket.on('todo:insert', function (data) {
                console.log("NEW");
                console.log(data);
            });
            socket.on('todo:update', function (data) {
                console.log("UPDATE");
                console.log(data);
            });
            socket.on('todo:delete', function (data) {
                console.log("DELETE");
                console.log(data);
            });
        </script>
    </head>
    <body>Checkout the Console!</body>
<html>

Pojďme to roztočit! Přejděte do svého terminálu (za předpokladu, že na jiné kartě stále běží RethinkDB) a spusťte:

$ node index.js

Otevřete v Chrome dvě karty:http://localhost:9000 a http://localhost:8080. Na kartě s naší jednoduchou aplikací uzlu otevřete konzoli JavaScriptu a všimnete si, že tam nic není – protože jsme ještě nepřidali žádné úkoly! Nyní otevřete konzolu RethinkDB na kartě portu 8080 v prohlížeči Chrome, přejděte na kartu Průzkumník dat a spusťte tento dotaz:

r.db("3RES_Todo").table("Todo").insert({ name: "Make coffee", completed: false })

Nyní se vraťte na svou druhou kartu Chrome pomocí aplikace Node. Viola! Je tu úkol, který jsme právě přidali do databáze, jasně identifikovaný jako nový záznam. Nyní zkuste úkol aktualizovat pomocí id, které RethinkDB přiřadila vašemu úkolu:

r.db("3RES_Todo").table("Todo").get("YOUR_TODO_ID").update({ completed: true })

Událost změny byla znovu rozpoznána jako aktualizace a nový objekt úkolu byl odeslán našemu klientovi. Nakonec smažeme úkol:

r.db("3RES_Todo").table("Todo").get("YOUR_TODO_ID").delete()

Náš handler changefeed to rozpoznal jako událost delete a vrátil objekt pouze s id (abychom jej mohli odstranit z pole úkolů v našem stavu Redux!).

Tím je dokončeno vše potřebné na backendu, abychom mohli v reálném čase přenášet úkoly a změny do našeho frontendu. Přejděme ke kódu React/Redux a k tomu, jak integrovat tyto události soketu s dispečery Redux.

Základní aplikace React Todo

Začněme tím, že si nastavíme požadavky na frontend a sbalíme je s WebPack. Nejprve nainstalujte požadované moduly (pokud jste stáhli repo a spustili npm install nemusíte to dělat):

$ npm install --save react react-dom material-ui react-tap-event-plugin redux react-redux
$ npm install --save-dev webpack babel-loader babel-core babel-preset-es2015 babel-preset-react babel-plugin-transform-class-properties

Nyní pojďme nastavit Webpack, naše webpack.config.js by měl také obsahovat babel a babel transform-class-properties plugin:

var path = require('path');
var webpack = require('webpack');

module.exports = {
    entry: './components/index.jsx',
    output: { path: __dirname + '/public', filename: 'bundle.js' },
    module: {
        loaders: [{
            test: /.jsx?$/,
            loader: 'babel-loader',
            exclude: /node_modules/,
            query: {
                presets: ['es2015', 'react'],
                plugins: ['transform-class-properties']
            }
        }]
    }
}

Všichni jsme připraveni začít budovat frontendovou aplikaci React/Redux! Pokud potřebujete oprášit React a/nebo Redux, pomohou vám zdroje zmíněné v úvodu. Pojďme odstranit kód, který jsme měli v index.html, abychom předvedli, jak Socket.IO funguje, přidejte několik písem, vložte id na prázdný div, ke kterému můžeme připojit aplikaci React, a importujte balíček webpack:

<html>
    <head>
        <link href='https://fonts.googleapis.com/css?family=Roboto:400,300,500' rel='stylesheet' type='text/css'>
        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css">
        <script src="/socket.io/socket.io.js"></script>
    </head>
    <body style="margin: 0px;">
        <div id="main"></div>
        <script src="bundle.js"></script>
    </body>
<html>

Vložme celé naše vykreslování React a některá další nastavení do components/index.js :

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import store from '../stores/todos.js';

import App from './app.jsx';

// Setup our socket events to dispatch
import TodoSocketListeners from '../socket-listeners/todos.js';
TodoSocketListeners(store);

// Needed for Material-UI
import injectTapEventPlugin from 'react-tap-event-plugin';
injectTapEventPlugin();

// Render our react app!
ReactDOM.render(<Provider store={store} ><App /></Provider>, document.getElementById('main'));

Všimněte si, že musíme importovat nepříjemný posluchač události klepnutí pro Material-UI (vypadá to, že pracují na odstranění tohoto požadavku). Po importu kořenové komponenty aplikace importujeme posluchač událostí soketu, který odesílá akce Redux, v /socket-listeners/todos.js :

// socket-listeners/todos.js
import io from 'socket.io-client';
const socket = io.connect('/');

export default function(store) {
    socket.on('todo:insert', (todo) => {
        store.dispatch({
            type: 'todo:insert',
            todo: todo
        });
    });

    socket.on('todo:update', function (todo) {
        store.dispatch({
            type: 'todo:update',
            todo: todo
        });
    });

    socket.on('todo:delete', function (todo) {
        store.dispatch({
            type: 'todo:delete',
            todo: todo
        });
    });
}

Tato funkce je poměrně přímočará. Vše, co děláme, je naslouchání soketovým událostem vysílaným z backendu socket-events.js . Poté odešlete vložené, aktualizované nebo odstraněné úkoly, které jsou zase spuštěny změnami RethinkDB. To spojuje všechna kouzla RehtinkDB/Socket!

A nyní vytvoříme komponenty React, které tvoří aplikaci. Jak je importováno v components/index.jsx , uděláme components/app.jsx :

import React from 'react';
import AppBar from 'material-ui/lib/app-bar';
import TodoList from './todoList.jsx';
import AddTodo from './addTodo.jsx';

import { connect } from 'react-redux';

class Main extends React.Component {
    render() {
        return (<div>
            <AppBar title="3RES Todo" iconClassNameRight="muidocs-icon-navigation-expand-more" />
            <TodoList todos={this.props.todos} />
            <AddTodo />
        </div>);
    }
}

function mapStateToProps(todos) {
    return { todos };
}

export default connect(mapStateToProps)(Main);

To vše je standardní React a React-Redux. Importujeme connect z react-redux a namapujte stav na rekvizity pro komponentu TodoList, což je components/todoList.jsx :

import React from 'react';
import Table from 'material-ui/lib/table/table';
import TableBody from 'material-ui/lib/table/table-body';
import Todo from './todo.jsx';

export default class TodoList extends React.Component {
    render() {
        return (<Table>
            <TableBody>
                {this.props.todos.map(todo => <Todo key={todo.id} todo={todo} /> )}
            </TableBody>
        </Table>);
    }
}

Seznam úkolů se skládá z tabulky Material-UI a my jednoduše mapujeme úkoly z rekvizit na jednotlivou komponentu Todo:

import React from 'react';
import TableRow from 'material-ui/lib/table/table-row';
import TableRowColumn from 'material-ui/lib/table/table-row-column';
import Checkbox from 'material-ui/lib/checkbox';
import IconButton from 'material-ui/lib/icon-button';

// Import socket and connect
import io from 'socket.io-client';
const socket = io.connect('/');

export default class Todo extends React.Component {
    handleCheck(todo) {
        socket.emit('todo:client:update', {
            completed: !todo.completed,
            id: todo.id
        });
    };

    handleDelete(todo) {
        socket.emit('todo:client:delete', todo);
    };

    render() {
        return (<TableRow>
            <TableRowColumn>
                <Checkbox label={this.props.todo.name} checked={this.props.todo.completed} onCheck={this.handleCheck.bind(this, this.props.todo)} />
            </TableRowColumn>
            <TableRowColumn>
                <IconButton iconClassName="fa fa-trash" onFocus={this.handleDelete.bind(this, this.props.todo)} />
            </TableRowColumn>
        </TableRow>)
    }
}

Jednotlivé komponenty Todo připojují emitory pro události Socket.IO ke správným událostem uživatelského rozhraní pro zaškrtávací políčko a tlačítko delete. Toto odešle aktualizované nebo odstraněné úkoly do posluchačů událostí Socket na serveru.

Poslední komponentou React, kterou potřebujeme, je tlačítko pro přidání úkolů! Do pravého dolního rohu aplikace připojíme tlačítko pro přidání kurzoru:

import React from 'react';
import Popover from 'material-ui/lib/popover/popover';
import FloatingActionButton from 'material-ui/lib/floating-action-button';
import ContentAdd from 'material-ui/lib/svg-icons/content/add';
import RaisedButton from 'material-ui/lib/raised-button';
import TextField from 'material-ui/lib/text-field';

// Import socket and connect
import io from 'socket.io-client';
const socket = io.connect('/');

export default class AddTodo extends React.Component {
    constructor(props) {
        super(props);
        this.state = { open: false };
    };

    handlePopoverTap = (event) => {
        this.setState({
            open: true,
            anchor: event.currentTarget
        });
    };

    handlePopoverClose = () => {
        this.setState({ open: false });
    };

    handleNewTaskInput = (event) => {
        if (event.keyCode === 13) {
            if (event.target.value && event.target.value.length > 0) {

                // Emit socket event for new todo
                socket.emit('todo:client:insert', {
                    completed: false,
                    name: event.target.value
                });

                this.handlePopoverClose();
            }
            else {
                this.setState({ error: 'Tasks must have a name'});
            }
        }
    };

    render() {
        return (<div>
            <Popover
                open = { this.state.open }
                anchorEl = { this.state.anchor }
                anchorOrigin={{ horizontal: 'right', vertical: 'top' }}
                targetOrigin={{ horizontal: 'left', vertical: 'bottom' }}
                onRequestClose={this.handlePopoverClose}>
                <TextField
                    style={{ margin: 20 }}
                    hintText="new task"
                    errorText={ this.state.error }
                    onKeyDown={this.handleNewTaskInput} />
            </Popover>
            <FloatingActionButton onTouchTap={this.handlePopoverTap} style={{ position: 'fixed', bottom: 20, right: 20 }}>
                <ContentAdd />
            </FloatingActionButton>
        </div>)
    };
}

Metoda vykreslování této komponenty zahrnuje tlačítko Přidat, které pak zobrazí vyskakovací okno se vstupním polem. Vyskakovací okno je skryté a zobrazuje se na základě logické hodnoty state.open . S každým stisknutím klávesy vstupu vyvoláme handleNewTaskInput , který naslouchá na kód 13 (klávesa enter). Pokud je vstupní pole prázdné, zobrazí se chyba (poznámka pro vylepšení:bylo by dobré to ověřit na backendu). Pokud vstupní pole není prázdné, vygenerujeme nový úkol a zavřeme vyskakovací okno.

Teď jen potřebujeme trochu více standardní Redux, abychom to všechno spojili. Nejprve reduktor pro úkoly a zkombinujte je (plánujeme dopředu, až tuto aplikaci vybudujeme a budeme mít několik reduktorů):

// reducers/todos.js

// todos reducer
const todos = (state = [], action) => {
    // return index of action's todo within state
    const todoIndex = () => {
        return state.findIndex(thisTodo => {
            return thisTodo && thisTodo.id === action.todo.id;
        });
    };

    switch(action.type) {
        case 'todo:insert':
            // append todo at end if not already found in state
            return todoIndex() < 0 ? [...state, action.todo] : state;

        case 'todo:update':
            // Merge props to update todo if matching id
            var index = todoIndex();
            if (index > -1) {
                var updatedTodo = Object.assign({}, state[index], action.todo);
                return [...state.slice(0, index), updatedTodo, ...state.slice(index + 1)]
            }
            else {
                return state;
            }

        case 'todo:delete':
            // remove matching todo
            var index = todoIndex();
            if (index > -1) {
                return [...state.slice(0, index), ...state.slice(index + 1)];
            }
            else {
                return state;
            }

        default:
            return state;
    }
};

export default todos;

A kombinovat redukce:

// reducers/index.js

import { combineReducers } from 'redux';
import todos from './todos.js';

const todoApp = combineReducers({ todos });

export default todoApp;

Redukce mají obslužnou funkci pro kontrolu, zda todo již ve stavu existuje (všimnete si, že pokud necháte okno prohlížeče otevřené a restartujete server, socket.IO znovu vyšle všechny události klientovi). Aktualizace úkolu využívá Object.assign() vrátit nový objekt s aktualizovanými vlastnostmi úkolu. A konečně, delete využívá slice() – které na rozdíl od splice() vrací nové pole .

Akce pro tyto redukce:

// actions/index.js

// Socket triggered actions
// These map to socket-events.js on the server
export const newTodo = (todo) => {
    return {
        type: 'todo:new',
        todo: todo
    }
}

export const updateTodo = (todo) => {
    return {
        type: 'todo:update',
        todo: todo
    }
}

export const deleteTodo = (todo) => {
    return {
        type: 'todo:delete',
        todo: todo
    }
}

Pojďme to dát dohromady a vytvořit to pomocí webového balíčku!

$ webpack --progress --colors --watch

Náš konečný produkt je krásná a jednoduchá aplikace úkolů, která reaguje na všechny změny stavu pro všechny klienty. Otevřete dvě okna prohlížeče vedle sebe a zkuste přidávat, odškrtávat a mazat úkoly. Toto je velmi jednoduchý příklad toho, jak jsem spojil RethinkDB changefeeds, Socket.IO a stav Redux a ve skutečnosti jsem jen poškrábal povrch toho, co je možné. Autentizace a relace by z toho udělaly opravdu úžasnou webovou aplikaci. Dovedu si představit sdílený seznam úkolů pro skupiny uživatelů, jako jsou domácnosti, partneři atd., doplněný o zdroj událostí, který uvádí, kdo plní každý úkol, který se okamžitě aktualizuje všem uživatelům, kteří jsou přihlášeni k odběru každé konkrétní skupiny úkolů.

V budoucnu plánuji udělat více práce na nalezení obecnějšího způsobu, jak spojit dohromady jakékoli pole objektů ve stavu Redux, který vyžaduje méně standardních – způsob propojení stavové pole ke koncovému bodu Socket.IO podobné React-Redux connect() . Rád bych slyšel zpětnou vazbu od každého, kdo to udělal nebo plánuje implementovat tyto úžasné technologie společně v jednom balíčku!

Scott Hasbrouck

Bio:Scott je celoživotní softwarový inženýr, který rád sdílí své dovednosti s ostatními prostřednictvím psaní a mentorství. Jako sériový podnikatel je v současné době CTO společnosti ConvoyNow, jedné ze tří společností, které založil jako technický zakladatel a zavádí jeden až více než jeden milion uživatelů. Vždy hledá další dobrodružství prostřednictvím pěší turistiky na odlehlých místech, létání v malých letadlech a cestování.

Convoy je řešení technické podpory v domácnosti! Spojujeme zákazníky, kteří mají problémy s opravou nebo používáním svých zařízení, s přátelskými a znalými profesionály technické podpory.

Tento příspěvek napsal Scott Hasbrouck. Najdete ho na Twitteru nebo jeho webu.