Создание изоморфного приложения на React и Flummox

Сегодня я хочу показать, как используя React, Flummox и Express, создать полноценное изоморфное приложение.

Идея изоморфности давно витала в воздухе, но никто не знал этого слова, поэтому ничего особо не менялось, пока не пришел airbnb.

За полгода до этого появился React, позже появился Flux и множество его реализаций, одна не хуже другой. Но все эти реализации ориентировались только на работу на клиентской стороне, они работали через синглтоны и, зачастую, их нельзя было нормально использовать на сервере. Я успел попробовать несколько, но ни одна мне не понравилась. Пока два месяца назад я не наткнулся на Flummox. Как заявляет разработчик, Flummox создан как раз для изоморфных приложений, он не использует синглтоны, присущие другим реализациям, и максимально прост в использовании.

Ожидается, что вы имеете опыт работы с React и слышали про Flux. Итак, поехали…
Забрать готовый код можно тут.

Шаг 0: Определение идеи


Наша идея состоит в создании приложения для записи других идей. Это будет TODO-лист (как Todo MVC) с сохранением данных на сервере. Требования такие:
  • добавление задач;
  • пометка задачи как "выполнено";
  • удаление (выполненных) задач;

В качестве базы данных будет использоваться внутренняя память процесса, но будет эмулировать использование внешней БД (данные возвращаются через Promise).
Заодно мы узнаем как уже сейчас можно потрогать ES2015 и ES2016 (далее для краткости я буду называть их ES6/ES7) в своём приложении.

Шаг 1: Установка необходимых пакетов


Для сервера у нас будет использоваться Express чтобы не было головной боли с низкоуровненвыми компонентами, Flummox чтобы оперировать данными и React чтобы удобно работать с DOM-деревом, а чтобы это всё запустить нам нужен Babel.

На этом шаге мы проинициализируем наше Express приложение и установим базовые компоненты.

$ 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

Что же мы только что поставили, помимо React:
  • flummox — тот самый изоморфный Flux;
  • react-router — роутер клиентской части нашего приложения;
  • isomorphic-fetch — полифилл для нового метода fetch, который пришел на замену XMLHttpRequest.
  • todomvc-app-css — пакет со стандартными стилями для TODOMVC приложений;
  • babel, babel-core, babel-loader, brfs и transform-loader — транслятор ES6/ES7 в ES5 и прочие вспомогательные пакеты, необходимые при сборке клиентского приложения;
  • webpack — утилита для сборки клиентской части.

Для запуска мы будем использовать babel-node, т.к. он позволяет на лету транслировать ES6/ES7 код в ES5. Поэтому добавим команду запуска в package.json:

"scripts": {
  "start": "babel-node --stage 0 ./bin/www"
}

Шаг 2: Скелет приложения


Приступим к написанию кода нашего приложения. Для этого создадим структуру каталогов, в которых будут лежать наши файлы:

.
├── bin
├── client
├── public
│   └── js
├── server
│   └── storages
├── shared
│   ├── actions
│   ├── components
│   ├── handlers
│   └── stores
└── utils

Теперь нужно определить структуру приложения и создать базовые компоненты: TodoList, TodoInput и TodoItem — список, поле ввода новой задачи и отдельный элемент списка (отдельная задача), соответственно. Компоненты будут лежать в папке shared/components, хранилища (stores) в папке shared/stores, а действия (actions) в папке shared/actions.
Логика приложения разделена на серверную, клиентскую и общую, и находится в папках server, client и shared, соответственно. Папка shared как раз и содержит все изоморфные компоненты, которые будут использовать на клиенте и сервере.

Код основных компонентов, нужных для отображения:

shared/components/TodoList.js
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;


shared/components/TodoItem.js
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;


Добавим обработчик всех (кроме API) поступающих запросов, которые будут обрабатываться роутером реакта (см. ниже):

Код
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>`
    );
});


Отлично, теперь у нас есть основной обработчик клиентских запросов.

Здесь мы используем новые возможности, которые станут доступны в ES7 — async/await. Они позволяют избавить код от callback-hell (который раньше приходилось решать с помощью замечательного модуля async или подобных).

Совет: оберните все операции внутри этого обработчика в try-catch блок для отлова ошибок. Т.к. если что-то сломается внутри, то без try-catch вы не увидите сообщения об ошибке.

Шаг 3: API


Добавим немного API, которое позволит взаимодействовать клиенту и серверу. Будем использовать REST подход, т.к. он идеально вписывается в данную задачу. Определим базовые пути:

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       # удалить определённую задачу

Затем запишем их в виде роутов:

server/routes.js
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);

Т.к. данные должны где-то храниться, то давайте создадим хранилище:

server/storages/MemoryStorage.js
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);
        });
    }
}


Этот компонент будет хранить наши задачи в памяти процесса. Внимательный читатель мог заметить, что мы возвращаем Promise из всех методов. Это как раз то место, где эмулируется работа с внешней БД.

Также видно, что этот компонент в некоторых методах бросает HTTP ошибки. В реальном мире так делать нельзя, потому что хранилище ничего не должно знать про HTTP.

Шаг 4: Компоненты и хранилище


API это, конечно хорошо, но нам нужно ещё всё связать с компонентами. Для этого создадим набор Action'ов и Store, которые и будут общаться с сервером, возвращая состояние для отрисовки в наши компоненты.

Сперва объявим наши Action и Store в главном классе Flux'а:

shared/Flux.js
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. По этому имени мы сможем получить их в любом месте приложения.

Теперь объявим сами действия и хранилище:

shared/actions/TodoActions.js
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;


shared/stores/TodoStore.js

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;


На самом деле, с сервером общается только Action, а Store лишь хранит данные и связывает их с компонентами.

В конструкторе хранилища (TodoStore) мы регистрируем обработчики, которые будут автоматически вызываться при получении данных от сервера.

Теперь после вызова метода из Action'а, он будет автоматически обновлять состояние Store, а тот, в свою очередь, состояние компонента.

Шаг 5: Роутинг


Одной из важнейших составляющих любого современного приложения является роутинг. Клиентский роутинг отдаётся в компонент реакта и уже он решает что показывать.
react-router позволяет задавать пути в декларативном стиле, что как раз в духе React'а. Давайте объявим нужные нам пути:

client/routes.js
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>
);


Как видно, для каждого маршрута есть собственный обработчик (handler). Обработчики в нашем приложении будут загружать данные и являться т.н. "умными" (см. прикреплённые ссылки) компонентами. Их будет два:

shared/handlers/AppHandler.js
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;


shared/handlers/TodoHandler.js
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 является "умным" и вы могли заметить статический метод routerWillRun, именно там происходит первоначальная загрузка данных в Store. Как вызывать этот метод будет показано ниже. Остальные компоненты, соответственно, будут "глупыми" и лишь реагировать определённым образом на изменение окружающего мира и события от пользователя.

Также видно, что некоторые компоненты оборачиваются в компонент <Flux />. Его свойство connectToStores устанавливает связь между хранилищем и дочерним компонентом. Всё, что находится в state хранилища становится доступно в props дочернего компонента.

Шаг 6: Рендеринг главной страницы


Настало время отрендерить наши компоненты. Но чтобы это сделать правильно, нам нужно предварительно загрузить все существующие задачи. Как вы помните, задачи загружаются через HTTP API. Но у нас для этого есть TodoAction, в котором описан метод getTasks. В примере к Flummox описан метод с жутко-длинным названием performRouteHandlerStaticMethod, который должен вызвать загрузку данных для хранилища с помощью описанного выше метода routerWillRun.

Добавим его себе.

utils/performRouteHandlerStaticMethod.js
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, который загрузит необходимые данные в Store и они отобразятся в компоненте.

Шаг 7: Сборка клиентской части


Мы не зря установили webpack. Он поможет нам собрать наше приложение для работы на клиенте. Для этого давайте его сконфигурируем.

webpack.config.js
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, поэтому его надо подключить на клиенте:

<script type="text/javascript" src="/js/bundle.js"></script>


Добавим команду для сборки в package.json:

"scripts": {
  "build": "webpack"
}

Теперь можно запускать сборку:

$ npm run build

Спустя некоторое время появится файл /public/js/bundle.js, который и является клиентской версией нашего приложения.

Шаг 8: Посмотрим что получилось


Мы только что создали изоморфное приложение. Теперь можем запустить его npm start и посмотреть что получилось.

Послесловие


Я старался донести свои мысли максимально понятно, поэтому некоторые вещи сильно упрощены и сделаны не совсем правильно. Также, чтобы не запутывать код, в некоторых местах нет проверок на ошибки.

Также хочется выразить благодарность Ивану Кречетову за вычитку черновика и рекомендации по улучшению качества кода.

Полезное чтение:



Happy coding!
Share post

Similar posts

Comments 25

    0
    А как понимать «изоморфное приложение»? Какой-то слишком запутанный термин.
      0
      Это такое гибридное (хотя это не совсем правильный термин) приложение, которое использует один и тот же код для рендеринга на клиенте (в данном случае в браузере) и на сервере. Это позволяет не дублировать логику и шаблоны, как в случае с обычными приложениями. Это можно проиллюстрировать такой картинкой:
      Картинка
      image
        0
        Если коротко, то Client JS + Server JS = Isomorphic JS
        • UFO just landed and posted this here
          0
          еще в копилку полезного чтения: www.smashingmagazine.com/2015/04/21/react-to-the-future-with-isomorphic-apps
            0
            Этот пример заработал, а в статье нет — ошибки какие-то. Интересно, зачем такие длинные названия аттрибутов «reactid» — изоморфные приложения должны беречь каждый передаваемый байт)
          • UFO just landed and posted this here
              0
              Спасибо за статью. Познавательно
                0
                Подскажите, можно ли как-то ускорить webpack в вашей демо? У меня на Atom машинке оно собирается больше трёх минут. Кеширование почему-то вообще не попадает. webpack --profile --json показывает гору «причин», но у меня опыта работы с NodeJS мало, а с webpack вообще нет.

                Мы с друзьями собираемся попробовать JS для нового проекта, сейчас мы живём на Python (в большей степени на Django замученной до Flask) и по большому счёту нам всё нравится кроме случаев, когда проект развивается в сторону клиентской части (JS).
                Отсюда есть желание найти «идеальный инструмент в мире JS» и оставить Python для background задач.

                На первый взгляд мне понравилось ваше Demo, но чтобы не было мучительно больно потом, я бы хотел задать несколько вопросов сейчас:
                1. Практичен ли описанный вами набор компонент для строительства большого проекта?
                2. Какие подводные камни вы знаете/подозреваете?
                3. Какие-нибудь комментарии относительно производительности babel, NodeJS и других причастных?
                4. Может вы встречали другие серебрянные пули? :) (Например, у меня вот просто несваримость с Vanilla JS и если бы не ES6/ES7, я бы не рискнул нырять в этот мир JS)
                  +1
                  По поводу webpack'а не подскажу. Решение специально пока что не искал. По поводу вопросов:

                  1. большой проект у меня пока что только пишется, но уже могу сказать, что некоторые части лучше выносить в отдельные модули или хотя бы папки (отделить, например блог, галлерею и т.п., от остального сайта).
                  2. из подводных камней, некоторые неожиданные проблемы с async/await, но о них я написал в статье (про заверните в try-catch иначе не увидите ошибок). Также недавно столкнулся с проблемой интернационализации приложения с помощью react-intl. Всё в целом хорошо, но хранение переводов — небольшая боль. Нет дефолтных переводов, надо везде таскать все переводы для текущего состояния иначе ловим исключения. А ещё нужно приучить себя всегда закрывать теги: <MyComponent /> — работает, <MyComponent></MyComponent> — тоже работает, <MyComponent> (не закрыл) — не скомпилится. И ещё одно, вы могли заметить, что при рендеринге на сервере делается запрос к API, и после рендеринга и запуска скриптов на клиенте делается снова такой же запрос. Я сейчас работаю над устранением этого досадного бага. По итогам намерен написать статью. Вы можете погуглить решения по словам rehydrate/dehydrate (так называлось это в fluxible) или же serialize/unserialize. Автор flummox уже вроде как запилил фикс, Но я ещё не смотрел.
                  3. babel просто транслирует ES6/ES7 → ES5, он не выполняет код сам. Node.JS шустрая, основной затык обычно в запросах к БД. А вот с React на сервере уже не всё так хорошо. Синтетические тесты (ab -c 5 -t 10 ...) TODO списка на моём i7 с 8GiB памяти показали всего лишь около 200RPS, что не очень плохо, если сравнивать с каким-нибудь PHP. Но довольно медленно, если сравнить с другими шаблонизаторами на Node.JS. И при увеличении количества компонентов, которые рендерятся для текущего состояния, производительность продолжает падать. Этого можно избежать в некоторых случаях, если использовать некоторые оптимизации.
                  4. серебрянной пули не существует :) Уже давно существуют всякие Meteor'ы, Derby.JS и т.д. Но это полноценные фреймворки и иногда с ними нужно бороться, чтобы сделать что-то нестандартное (сам не работал, но знающие люди так говорят).

                  Желаю удачи в вашем проекте :)
                    +1
                    И это снова я.

                    Знаете ли вы об Este.js? Он очень напоминает вашу солянку. А про Catberry? Он не использует React и Babel, но тем не менее интересен реализацией progressive rendering и изоморфен.

                    На счёт benchmark'ов: github.com/catberry/catberry/issues/168

                    Меня просто интересует ваше мнение, так как я всё никак не остановлюсь с выбором (просто куча других задач и этот JS у меня не в приоритете пока)
                      +1
                      Каким-то чудом Este.JS обошел меня стороной. Выглядит очень интересным, спасибо, обязательно утащу оттуда какие-нибудь решения. Насчёт него пока ничего не могу сказать, т.к. внутрь особо не смотрел. Но интересно, как они победили синглтоны и прочие грабли.
                      На Catberry смотрел несколько месяцев назад, но тогда мне не понравилось, что нужно компоненты держать в catberry_* каталогах, меня крайне раздражают вендор-префиксы. Ещё хотелось для шаблонизации использовать именно React, т.к. он чисто внешне близок к HTML — опять же, на мой вкус.
                      Ещё, я раньше не сталкивался с термином progressive rendering в данном контексте, можете немного пояснить его?

                      P.S. Думаю, по поводу Catberry pragmadash лучше может ответить.
                        0
                        С автором Catberry, pragmadash, я пообщался на github, куда я дал ссылку. Но очевидно, что любому автору «своё родное» ближе и понятнее, поэтому я и изучаю взгляды со стороны. :)

                        Пока что React меня привлекает больше по одной простой причине — за ним стоит Facebook и уже достаточно немалое сообщество, а я в JS мир серверной разработки только сейчас окунаюсь, да и клиентский JS у меня был достаточно костылеобразный (дикие смеси VanillaJS, jQuery и AngularJS...). Кроме того, вслед за ReactJS вполне логичное продолжение изучения идёт в React Native (вот не сложилось у меня с Java для нативной разработки под Android, да и яблочных девайсов у меня больше нет тоже).

                        Честно говоря, у меня уже мозг пухнет от всего этого разнообразия в JS… Сейчас пытаюсь понять как сдружить Este.JS с реальным миром, где нужна авторизация в API, ну и, собственно, API сервер не пойму на чём писать… С одной стороны раз уже за JS взялся, то логично на нём и API сервер делать, но уж больно сильный соблазн спокойно жить на Python и не мучать себя, хотя и на Python фреймворков для API сервера уже понаписали не один и не два, но всё же меньше, чем на JS :)

                        Если у вас появятся какие-то комментарии к моему потоку мыслей — я буду очень благодарен.
                          +1
                          API можно/нужно писать на том, что удобнее. Главное — HTTP интерфейс для клиента. У себя в pet-project я решил разделить renderer-app и API на кучку модных нынче микросервисов, которые общаются между собой и клиентом по HTTP.
                            0
                            Мои мысли относительно API на JS основаны на том, что поддерживать один язык для проекта проще, чем два (я не говорю о себе, ведь на Python у меня уже большой опыт, а JS мне придётся подтянуть при разработке клиентской части).

                            Какие языки/фреймворки/библиотеки вы используете для API сервера?
                              +1
                              Пока что только JS (Express, Restify, коллега настойчиво советует Loopback) и PHP (legacy код).
                                +1
                                Из того, что я нашёл, я пытался выбрать между Loopback и SailsJS, но поглядываю на Restify просто из-за того, что он очевидным образом самый гибкий (но в этом же и его слабость — слишком много нужно писать самому). Express для API мне кажется перебором, как Django для этих целей использовать — можно, но зачем?

                                Спасибо за ответ, буду ковыряться, я дотошный)))
                        • UFO just landed and posted this here
                            0
                            Вы спрашиваете что-то вроде: «а как мне выкинуть из Catberry основную часть и использовать вместо выкинутого куска React, но так чтобы вы все остальное работало также». Серьезно?
                            Если я правильно понял, то автор не имел ввиду выбрасывание чего-то из Catberry, а говорил, что ему понравился React, но так как в Catberry были шаблоны, то он ему не подошёл.
                              +1
                              Спасибо за ответ.

                              Вы спрашиваете что-то вроде: «а как мне выкинуть из Catberry основную часть и использовать вместо выкинутого куска React, но так чтобы вы все остальное работало также»

                              Нет же. Я просто сказал, что мне хочется именно реакт с его компонентами.

                              текущая архитектура и подход React никогда не позволят ему реализовать прогрессивный рендеринг

                              К сожалению приходится чем-то жертвовать.
                              • UFO just landed and posted this here
                      0
                      Спасибо!
                      А какие вещи, например, сделаны не совсем правильно?
                        0
                        MemoryStorage, например, не должен ничего знать о HTTP и, тем более, не должен бросать HTTP ошибку.
                        Ошибки в app.js лучше проверять одним try-catch блоком, чтобы не возникало повисших соединений.
                        Ещё в MemoryStorage мы явно возвращаем Promise. Это можно избежать, если функция явно объявлена как async.
                        Ну и надо писать больше тестов :)

                        Уверен, есть ещё недочёты, но пока не могу их вспомнить.
                        0
                        Есть вопрос.
                        Action — это же функция, возвращающая результат. Если я вызываю action из компонента и не пользуюсь результатом, а жду изменения из хранилища — это потому, что я такой ответственный? Если да, то, разве это не странный дизайн? Или я что- то упустил?
                          0
                          Да, с виду это немного странно. Но суть здесь в том, что на один Store может быть подписано несколько компонентов, и при его обновлении, обновятся все связанные компоненты.

                        Only users with full accounts can post comments. Log in, please.