$ express $ npm install react flummox isomorphic-fetch todomvc-app-css react-router --save $ npm install babel webpack babel-core babel-loader brfs transform-loader --save-dev
flummox
is that isomorphic Flux;react-router
- the router of the client part of our application;isomorphic-fetch
is the polyfill for the new fetch method, which replaces XMLHttpRequest.todomvc-app-css
- package with standard styles for TODOMVC applications;babel
, babel-core
, babel-loader
, brfs
and transform-loader
- ES6 / ES7 to ES5 translator and other auxiliary packages required when building a client application;webpack
- utility to build the client side.babel-node
, since It allows you to broadcast ES6 / ES7 code to ES5 on the fly. Therefore, we add a launch command to package.json
: "scripts": { "start": "babel-node --stage 0 ./bin/www" }
. βββ bin βββ client βββ public β βββ js βββ server β βββ storages βββ shared β βββ actions β βββ components β βββ handlers β βββ stores βββ utils
TodoList
, TodoInput
and TodoItem
- a list, a new task entry field and a separate list item (separate task), respectively. Components will be located in the shared/components
folder, stores (stores) in the shared/stores
folder, and actions (actions) in the shared/actions
folder.server
, client
and shared
folders, respectively. The shared
folder just contains all the isomorphic components that will be used on the client and the server. import React from 'react'; import TodoItem from './TodoItem'; class TodoList extends React.Component { onToggleStatus(id, completed) { this.props.onToggleStatus(id, completed); } onDeleteTask(id) { this.props.onDeleteTask(id); } render() { return ( <ul className="todo-list"> {this.props.tasks.map(task => <TodoItem key={task.id} task={task} onToggleStatus={this.onToggleStatus.bind(this, task.id)} onDeleteTask={this.onDeleteTask.bind(this, task.id)} /> )} </ul> ); } } export default TodoList;
import React from 'react'; class TodoItem extends React.Component { constructor(props) { super(props); this.state = props.task; } handleToggleStatus() { let completed = this.refs.completed.getDOMNode().checked; this.props.onToggleStatus(completed); this.setState({completed}); } handleDeleteTask() { this.props.onDeleteTask(); } render() { return ( <li className={this.state.completed ? 'completed' : ''}> <div className="view"> <input className="toggle" type="checkbox" defaultChecked={this.state.completed} onChange={this.handleToggleStatus.bind(this)} ref="completed" /> <label>{this.state.text}</label> <button className="destroy" onClick={this.handleDeleteTask.bind(this)} /> </div> <input className="edit" defaultValue={this.state.text} /> </li> ); } } export default TodoItem;
app.use(async function (req, res, next) { let flux = new Flux(); // , let router = Router.create({ routes: routes, location: req.url }); let {Handler, state} = await new Promise((resolve, reject) => { router.run((Handler, state) => resolve({Handler, state}) ); }); // , . β4 await performRouteHandlerStaticMethod(state.routes, 'routerWillRun', {state, flux}); // let html = React.renderToString( <FluxComponent flux={flux}> <Handler {...state} /> </FluxComponent> ); // , .. res.send(` <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>HabraIsoTODO</title> <link rel="stylesheet" href="/css/index.css"> </head> <body> <div id="app"> ${html} </div> </body> </html>` ); });
GET /api/tasks # POST /api/tasks # PUT /api/tasks # GET /api/tasks/active # GET /api/tasks/completed # DELETE /api/tasks/completed # PUT /api/tasks/:id # DELETE /api/tasks/:id #
import {Router} from 'express'; import MemoryStorage from './storages/MemoryStorage'; import http from 'http'; let router = new Router(); let storage = new MemoryStorage(); router.get('/tasks', async (req, res) => { res.json(await storage.list()); }); router.post('/tasks', async (req, res, next) => { if (!req.body.text || !req.body.text.length) { let err = new Error(http.STATUS_CODES[400]); err.status = 400; return next(err); } let task = await storage.save({ text: req.body.text.substr(0, 256), completed: false }); res.status(201).send(task); }); router.put('/tasks', async (req, res) => { let completed = req.body.completed; let tasks = (await storage.list()).map(task => { return storage.update(task.id, { text: task.text, completed: Boolean(completed) }); }); res.status(201).json(await Promise.all(tasks)); }); router.get('/tasks/active', async (req, res) => { res.json(await storage.list((task) => !task.completed)); }); router.get('/tasks/completed', async (req, res) => { res.json(await storage.list((task) => task.completed)); }); router.delete('/tasks/completed', async (req, res, next) => { let deleted = []; try { let items = await storage.list((task) => task.completed); items.forEach(async (item) => { deleted.push(item.id); await storage.remove(item.id); }); res.status(200).json({deleted}); } catch (err) { next(err); } }); router.get('/tasks/:id', async (req, res, next) => { let id = req.params.id; try { var item = await storage.fetch(id); res.status(200).send(item); } catch (err) { return next(err); } }); router.put('/tasks/:id', async (req, res, next) => { let id = req.params.id; try { var item = await storage.fetch(id); } catch (err) { return next(err); } let updated = item; Object.keys(req.body).forEach((key) => { updated[key] = req.body[key]; }); let task = await storage.update(id, updated); res.status(200).json(task); }); router.delete('/tasks/:id', async (req, res, next) => { let id = req.params.id; try { let removed = await storage.remove(id); res.status(200).send({id, removed}); } catch (err) { return next(err); } }); export default router;
import api from './server/routes'; // ... app.use('/api', api);
import http from 'http'; function clone(obj) { return JSON.parse(JSON.stringify(obj)); } export default class MemoryStorage { constructor() { this._items = { 1: { id: 1, text: 'Rule the World', completed: false }, 2: { id: 2, text: 'Be an Awesome', completed: true } }; } count() { return new Promise((resolve) => { resolve(Object.keys(this._items).length); }); } save(item) { return new Promise((resolve) => { let obj = clone(item); obj.id = Math.round(Math.random() * 10000000).toString(36); this._items[obj.id] = obj; resolve(obj); }); } fetch(id) { return new Promise((resolve, reject) => { if (!this._items[id]) { let err = new Error(http.STATUS_CODES[404]); err.status = 404; return reject(err); } resolve(this._items[id]); }); } update(id, item) { return new Promise((resolve, reject) => { let obj = clone(item); let existed = this._items[id]; if (!existed) { let err = new Error(http.STATUS_CODES[404]); err.status = 404; return reject(err); } obj.id = existed.id; this._items[obj.id] = obj; resolve(obj); }); } remove(id) { return new Promise((resolve, reject) => { if (!this._items[id]) { let err = new Error(http.STATUS_CODES[404]); err.status = 404; return reject(err); } delete this._items[id]; resolve(true); }); } list(check) { return new Promise((resolve) => { let items = Object.keys(this._items).map((key) => this._items[key]).reduce((memo, item) => { if (check && check(item)) { memo.push(item); } else if (!check) { memo.push(item); } return memo; }, []); resolve(items); }); } }
import {Flux} from 'flummox'; import TodoListAction from './actions/TodoActions'; import TodoListStore from './stores/TodoStore'; export default class extends Flux { constructor() { super(); this.createActions('todo', TodoListAction); this.createStore('todo', TodoListStore, this); } }
todo
. By this name we can get them anywhere in the application. import {Actions} from 'flummox'; import fetch from 'isomorphic-fetch'; // , .. location.host const API_HOST = 'http://localhost:3000'; class TodoListActions extends Actions { async getTasks() { return (await fetch(`${API_HOST}/api/tasks`, { headers: { 'Accept': 'application/json' } })).json(); } async getActiveTasks() { return (await fetch(`${API_HOST}/api/tasks/active`, { headers: { 'Accept': 'application/json' } })).json(); } async getCompletedTasks() { return (await fetch(`${API_HOST}/api/tasks/completed`, { headers: { 'Accept': 'application/json' } })).json(); } async deleteCompletedTasks() { return (await fetch(`${API_HOST}/api/tasks/completed`, { method: 'DELETE', headers: { 'Accept': 'application/json' } })).json(); } async createTask(task) { return (await fetch(`${API_HOST}/api/tasks`, { method: 'POST', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, body: JSON.stringify(task) })).json(); } async deleteTask(id) { return (await fetch(`${API_HOST}/api/tasks/${id}`, { method: 'DELETE', headers: { 'Accept': 'application/json' } })).json(); } async toggleTask(id, completed) { return (await fetch(`${API_HOST}/api/tasks/${id}`, { method: 'PUT', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, body: JSON.stringify({completed}) })).json(); } async toggleAll(completed) { return (await fetch(`${API_HOST}/api/tasks`, { method: 'PUT', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, body: JSON.stringify({completed}) })).json(); } } export default TodoListActions;
import {Store} from 'flummox'; class TodoListStore extends Store { constructor(flux) { super(); let actions = flux.getActionIds('todo'); // this.register(actions.getTasks, this.handleNewTasks); this.register(actions.getActiveTasks, this.handleNewTasks); this.register(actions.getCompletedTasks, this.handleNewTasks); this.register(actions.createTask, this.handleNewTask); this.register(actions.toggleTask, this.handleUpdateTask); this.register(actions.toggleAll, this.handleNewTasks); this.register(actions.deleteTask, this.handleDeleteTask); this.register(actions.deleteCompletedTasks, this.handleDeleteTasks); } handleNewTask(task) { if (task && task.id) { this.setState({ tasks: this.state.tasks.concat([task]) }); } } handleNewTasks(tasks) { this.setState({ tasks: tasks ? tasks : [] }); } handleUpdateTask(task) { let id = task.id; this.setState({ tasks: this.state.tasks.map(t => { return (t.id == id) ? task : t; }) }); } handleDeleteTask(task) { let id = task.id; this.setState({ tasks: this.state.tasks.map(t => { if (t.id != id) { return t; } }).filter(Boolean) }); } handleDeleteTasks({deleted}) { this.setState({ tasks: this.state.tasks.filter(task => deleted.indexOf(task.id) < 0 ) }); } } export default TodoListStore;
TodoStore
), we register handlers that will be automatically called when data is received from the server.react-router
allows you to set paths in a declarative style, which is exactly in the spirit of React. Let's declare the paths we need: import React from 'react'; import {Route, DefaultRoute, NotFoundRoute} from 'react-router'; import AppHandler from '../shared/handlers/AppHandler'; import TodoHandler from '../shared/handlers/TodoHandler'; export default ( <Route handler={AppHandler}> <DefaultRoute handler={TodoHandler} /> <Route name="all" path="/" handler={TodoHandler} action="all" /> <Route name="active" path="/active" handler={TodoHandler} action="active" /> <Route name="completed" path="/completed" handler={TodoHandler} action="completed" /> </Route> );
import React from 'react'; import {RouteHandler} from 'react-router'; class AppHandler extends React.Component { render() { return ( <div> <section className="todoapp"> <RouteHandler {...this.props} key={this.props.pathname} /> </section> </div> ); } } export default AppHandler;
import React from 'react'; import Flux from 'flummox/component'; import TodoList from '../components/TodoList'; import TodoInput from '../components/TodoInput'; import ItemsCounter from '../components/ItemsCounter'; import ToggleAll from '../components/ToggleAll'; class TodoHandler extends React.Component { static async routerWillRun({flux, state}) { let action = state.routes[state.routes.length - 1].name; let todoActions = flux.getActions('todo'); switch (action) { case 'active': await todoActions.getActiveTasks(); break; case 'completed': await todoActions.getCompletedTasks(); break; case 'all': default: await todoActions.getTasks(); break; } } async handleNewTask(text) { let actions = this.props.flux.getActions('todo'); await actions.createTask({text}); } async handleToggleStatus(id, status) { let actions = this.props.flux.getActions('todo'); await actions.toggleTask(id, status); } async handleToggleAll(status) { let actions = this.props.flux.getActions('todo'); await actions.toggleAll(status); } async handleDeleteTask(id) { let actions = this.props.flux.getActions('todo'); await actions.deleteTask(id); } async handleDeleteCompletedTasks(id) { let actions = this.props.flux.getActions('todo'); await actions.deleteCompletedTasks(); } render() { return ( <div> <header className="header"> <h1>todos</h1> <TodoInput handleNewTask={this.handleNewTask.bind(this)} /> </header> <section className="main"> <Flux connectToStores={['todo']}> <ToggleAll onToggleStatus={this.handleToggleAll.bind(this)} /> </Flux> <Flux connectToStores={['todo']}> <TodoList onToggleStatus={this.handleToggleStatus.bind(this)} onDeleteTask={this.handleDeleteTask.bind(this)} /> </Flux> </section> <footer className="footer"> <Flux connectToStores={['todo']}> <ItemsCounter count={0} /> </Flux> <ul className="filters"> <li> <a href="/">All</a> </li> <li> <a href="/active">Active</a> </li> <li> <a href="/completed">Completed</a> </li> </ul> <button className="clear-completed" onClick={this.handleDeleteCompletedTasks.bind(this)}> Clear completed </button> </footer> </div> ); } } export default TodoHandler;
TodoHandler
component is smart and you may have noticed the routerWillRun
static method, this is where the initial data loading takes place in the Store. How to call this method will be shown below. The remaining components, respectively, will be "stupid" and only react in a certain way to changes in the surrounding world and events from the user.<Flux />
component. Its connectToStores
property establishes a connection between the repository and the child component. Everything that is in the state
repository becomes available in the props
child component.TodoAction
for this, which describes the getTasks
method. In the Flummox example, a method is described with the terribly long name performRouteHandlerStaticMethod
, which should trigger the loading of data for the storage using the routerWillRun
method described above. export default async function performRouteHandlerStaticMethod(routes, methodName, ...args) { return Promise.all(routes .map(route => route.handler[methodName]) .filter(method => typeof method === 'function') .map(method => method(...args)) ); }
import performRouteHandlerStaticMethod from '../utils/performRouteHandlerStaticMethod'; await performRouteHandlerStaticMethod(state.routes, 'routerWillRun', {state, flux});
routerWillRun
, the routerWillRun
method will be called, which will load the necessary data into the Store and it will be displayed in the component.webpack
. He will help us build our application to work on the client. To do this, let's configure it. var path = require('path'); var webpack = require('webpack'); var DEBUG = process.env.NODE_ENV !== 'production'; var plugins = [ new webpack.optimize.OccurenceOrderPlugin() ]; if (!DEBUG) { plugins.push( new webpack.optimize.UglifyJsPlugin() ); } module.exports = { cache: DEBUG, debug: DEBUG, target: 'web', devtool: DEBUG ? '#inline-source-map' : false, entry: { client: ['./client/app.js'] }, output: { path: path.resolve('public/js'), publicPath: '/', filename: 'bundle.js', pathinfo: false }, module: { loaders: [ { test: /\.js/, loaders: ['transform?brfs', 'babel-loader?stage=0'] }, { test: /\.json$/, loaders: ['json-loader'] } ] }, plugins: plugins, resolve: { extensions: ['', '.js', '.json', '.jsx'] } };
bundle.js
in the bundle.js
file, so it must be connected on the client: <script type="text/javascript" src="/js/bundle.js"></script>
package.json
: "scripts": { "build": "webpack" }
$ npm run build
/public/js/bundle.js
will appear, which is the client version of our application.npm start
and see what happened.Source: https://habr.com/ru/post/256979/
All Articles