Create React App (aka React Scripts) и серверный рендеринг с Redux и Router

    Из комментариев к статье стало понятно, что очень многие люди склоняются в сторону экосистемы 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.

    Поделиться публикацией
    Реклама помогает поддерживать и развивать наши сервисы

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

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


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


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

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

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

      • 0

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

        • 0

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


          $ 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
          • 0

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

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

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

            • 0

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


              Я же был счастлив, когда сидел на CRA. До той поры, пока не прочитал "Что взять за основу React приложения". А дальше беспрерывная борьба со сложностью окружения. Начитался issues — голова пухнет.

              • 0

                Да, но у CRA куча жестких ограничений… Я сам тоже к нему склоняюсь, но тем не менее, надо это осознавать.

              • 0

                Ещё одна годная статья по теме Server Side Rendering with React and Redux.

                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.