Из комментариев к статье стало понятно, что очень многие люди склоняются в сторону экосистемы Create React App (он же React Scripts). Это вполне разумно, т.к. это самый популярный и простой в использовании продукт (благодаря отсутствию конфигурации и поддержке ведущих людей React-сообщества), в котором, к тому же, есть почти все необходимое — сборка, режим разработки, тесты, статистика покрытия. Не хватает только серверного рендеринга.


В качестве одного из способов в официальной документации предлагается либо вбивать начальные данные в шаблон либо воспользоваться статическими слепками. Первый подход не позволит поисковикам нормально индексировать статичный HTML, а второй — не поддерживает проброс никаких начальных данных, кроме HTML (фраза из документации: this doesn't pass down any state except what's contained in the markup). Поэтому если используется Redux, то придется для рендеринга использовать что-то другое.


Я адаптировал пример из статьи для использования с Create React App, теперь он называется Create React Server и умеет запускать серверный рендеринг командой:


create-react-server --createRoutes src/routes.js --createStore src/store.js

При таком запуске никакой особой конфигурации не требуется, все делается через параметры командной строки. Если нужно — можно подсунуть так же свои шаблоны и обработчики.


Небольшое лирическое отступление. Как говорят авторы React Router — их сайты индексируются Гуглом без проблем и без всякого серверного рендеринга. Может это и так. Но одной из главных проблем является не только Гугл, но и быстрая доставка контента юзеру, и это может даже поважнее индексации, которую можно обмануть.


Установка


Для начала установим требующийся для этого примера пакеты:


npm install create-react-server --save-dev

Добавим файл .babelrc или секцию babel в файл package.json


{
    "presets": [
      "react-app"
    ]
}

Пресет babel-preset-react-app ставится вместе с react-scripts, но для серверного рендеринга нам надо явно на него сослаться.


Страница (т.е. конечная точка React Router)


Как и прежде, суть серверного рендеринга довольно проста: на сервере нам нужно определить на основе правил роутера, какой компонент будет показан на странице, выяснить, какие данные ему нужны для работы, запросить эти данные, отрендерить HTML, и выслать этот HTML вместе с данными на клиент.


Сервер берет конечный компонент, вызывает у него getInitialProps, внутри которого можно сделать диспатч экшнов Redux'а и вернуть начальный набор props (на случай, если Redux не используется). Метод вызывается как на клиенте, так и на сервере, что позволяет сильно упростить начальную загрузку данных.


// src/Page.js

import React, {Component} from "react";
import {connect} from "react-redux";
import {withWrapper} from "create-react-server/wrapper";
import {withRouter} from "react-router";

export class App extends Component {

    static async getInitialProps({location, query, params, store}) {
        await store.dispatch(barAction());
        return {custom: 'custom'}; // это станет начальным набором props при рендеринге
    };

    render() {
        const {foo, bar, custom, initialError} = this.props;
        if (initialError) return (<pre>Ошибка в функции getInitialProps: {initialError.stack}</pre>);
        return (
            <div>Foo {foo}, Bar {bar}, Custom {custom}</div>
        );
    }

}

// подключаемся к Redux Provider как обычно
App = connect(state => ({foo: state.foo, bar: state.bar})(App);

// подключаемся к WrapperProvider, который тянет initialProps с сервера
App = withWrapper(App);

// до кучи подключаемся к React Router
App = withRouter(App);

export default App;

Переменная initialError будет иметь значение, если в функции getInitialProps возникла ошибка, причем не важно где — на клиенте или на сервере, поведение одинаково.


Страница, которая будет использоваться как заглушка для 404 ошибок должна иметь статическое свойство notFound:


// src/NotFound.js

import React, {Component} from "react";
import {withWrapper} from "create-react-server/wrapper";

class NotFound extends Component {
    static notFound = true;
    render() {
        return (
            <div>404 Not Found</div>
        );
    }

}

export default withWrapper(NotFound);

Router


Функция createRoutes должна возвращать правила роутера, асинхронные роуты тоже поддерживаются, но для простоты это пока опустим:


// src/routes.js

import React from "react";
import {IndexRoute, Route} from "react-router";
import NotFound from './NotFound';
import App from './Page';

export default function(history) {
    return <Route path="/">
        <IndexRoute component={App}/>
        <Route path='*' component={NotFound}/>
    </Router>;
}

Redux


Функция createStore должна принимать начальное состояние в качестве параметра и возвращать новый Store:


// src/store.js

import {createStore} from "redux";

function reducer(state, action) { return state; }

export default function (initialState, {req, res}) {
    if (req) initialState = {foo: req.url};
    return createStore(
        reducer,
        initialState
    );
}

Когда функция вызывается на сервере, второй параметр будет иметь объекты Request и Response из NodeJS, можно вытащить некую информацию и вложить ее в начальное состояние.


Главная входная точка


Соберем все воедино, а также добавим специальную обертку для получения initialProps с сервера:


// src/index.js

import React from "react";
import {render} from "react-dom";
import {Provider} from "react-redux";
import {browserHistory, match, Router} from "react-router";
import {WrapperProvider} from "react-router-redux-middleware/wrapper";

import createRoutes from "./routes";
import createStore from "./store";

const Root = () => (
    <Provider store={createStore(window.__INITIAL_STATE__)}>
        <WrapperProvider initialProps={window.__INITIAL__PROPS__}>
            <Router history={browserHistory}>{createRoutes()}</Router>
        </WrapperProvider>
    </Provider>
);

render((<Root/>), document.getElementById('root'));

Запуск простого сервера через консольную утилиту


Добавим скрипты в секцию scripts файла package.json:


{
    "build": "react-scripts build",
    "server": "create-react-server --createRoutes src/routes.js --createStore src/store.js
}

И запустим


npm run build
npm run server

Теперь если мы откроем http://localhost:3000 в браузере — мы увидим страницу, подготовленную на сервере.


В этом режиме результат сборки сервера нигде не хранится и каждый раз вычисляется на лету.


Запуск сервера через API и сохранение результатов сборки


Если возможностей командной строки стало мало, или требуется хранить результаты сборки сервера, то всегда можно создать сервер не через CLI, а через API.


Установим в дополнение к предыдущим пакетам babel-cli, он понадобится для сборки сервера:


npm install babel-cli --save-dev

Добавим скрипты в секцию scripts файла package.json:


{
    "build": "react-scripts build && npm run build-server",
    "build-server": "NODE_ENV=production babel --source-maps --out-dir build-lib src",
    "server": "node ./build-lib/server.js"
}

Таким образом клиентская часть будет по-прежнему собираться Create React App (React Scripts), а серверная — с помощью Babel, который заберет все и src и положит в build-lib.


// src/server.js

import path from "path";
import express from "express";
import {createExpressServer} from "create-react-server";
import createRoutes from "./createRoutes";
import createStore from "./createStore";

createExpressServer({
    createRoutes: () => (createRoutes()),
    createStore: ({req, res}) => (createStore({})),
    outputPath: path.join(process.cwd(), 'build'),
    port: process.env.PORT || 3000
}));

Запустим:


npm run build
npm run server

Теперь если мы снова откроем http://localhost:3000 в браузере — то мы опять увидим ту же страницу, подготовленную на сервере.


Полный код примера можно посмотреть тут: https://github.com/kirill-konshin/react-router-redux-middleware/tree/master/examples/create-react-app.

Поделиться с друзьями
-->

Комментарии (8)


  1. eshimischi
    16.03.2017 09:58

    Бойлерплейтов для создания проектов на React/Redux много, create-react-app действительно учитывает многое. Я бы еще предложил посмотреть на проект Ryan Collins, который помимо всего предлагает Apollo / GraphQL из коробки и typescript версия


    1. dfuse
      16.03.2017 10:18

      В данной статье речь не про бойлерплейт, потому что create-react-app создает совершенно минимальный каркас, который бойлерплейтом с натяжкой можно назвать, все пишется с нуля руками за 5 минут, а про работу одним скриптом почти без настроек, по аналогии с react-scripts.


      Я тут сравнивал в статье разные фреймворки, и на мой взгляд, чем меньше бойлерплейта — тем лучше. В случае с Next.js или Create React App сама экосистема вообще не заметна, ноль конфигурации (в Next.js можно ее вытащить, если сильно надо), для Electrode надо некоторое кол-во конфигов и бойлерплейта, и это уже вызывает вопросы, как это поддерживать. Но и гибкости в тех, где нет конфигов — поменьше. Но иногда и это на пользу идет, меньше ненужных телодвижений, когда все нельзя. Бойлерплейты все имеют неустранимый недостаток — после инициализации проект уходит своим путем, а бойлерплейт — своим. Нельзя просто взять и синхронизировать проект с последними наработками автора бойлерплейта. А CRA и Next с Electrode — можно, любые оптимизации, которые вносят авторы, будут Вам доступны.


      Apollo и GraphQL для потребления прекрасны, для написания своего сервера — тоже хороши, но есть нюансы, особенно если клиент не только вы сами, т.е. нужен еще и REST ;). Ссылку Вашу посмотрю, спасибо.


      1. eshimischi
        16.03.2017 10:25
        +1

        Да я согласен, что речь не про «бойлерплейты», просто привел пример других подходов к решению одних и тех же задач по сути. Кстати по поводу create-react-app есть форк человека, который добавил туда полезные фичи kitze, описание — Medium


        1. dfuse
          16.03.2017 10:33

          Выглядит многообещающе. Но тут есть страшный момент — когда у автора энтузиазм иссякнет не знает никто. Как часто будут апдейты и синкапы с апстримом? Через год можно оказаться без поддержки и с багами, которые никто не будет фиксить. Страшно как-то, это ведь не библиотека, а вся инфраструктура на форке.


  1. comerc
    16.03.2017 15:40

    Вот это уже интересно! Попробую воткнуть в свой новый проект.


  1. comerc
    17.03.2017 01:33

    А не запускается пример:


    $ git clone git@github.com:kirill-konshin/create-react-server.git
    $ cd ~/create-react-server/examples/create-react-app
    $ npm update
    $ npm install create-react-server --save-dev
    $ npm start

    Failed to compile.
    
    Error in ~/create-react-server/src/wrapper.js
    Module not found: 'react' in ~/create-react-server/src
    
     @ ~/create-react-server/src/wrapper.js 1:12-28


    1. dfuse
      17.03.2017 02:06

      Нужно в корне репозитория тоже сделать npm install, связано это с тем, что примеры используют код из /src, которому нужны peerDependencies главного пакета. Я напишу инструкцию в README или поменяю код примеров, чтоб легче было ставить. А вообще — с таким либо в issues на Github, либо в личку.


  1. comerc
    17.03.2017 08:29

    А вообще — с таким либо в issues на Github, либо в личку.

    Гы-гы. Для сравнения — perfect world.