React-redux-universal-hot-example — это горячий шаблон для обеспечения универсального рендеринга. Он объединяет webpack-isomorphic-tools для поддержки рендеринга React Redux как на стороне клиента, так и на стороне сервера.

Как гласит слоган «Все лучшие современные практики в одном примере», озаглавленный этим шаблоном, я погружаюсь в эту структуру, чтобы найти в ней хитрости. Далее следуют особенности и вопросы, которые я поднял.

Как добиться рендеринга на стороне сервера (SSR)?

В традиционном программировании на React клиент получает только html-файл без элементов пользовательского интерфейса и серию файлов Javascript. После этого клиент начинает отображать элементы пользовательского интерфейса. Однако это может привести к задержке рендеринга, которая иногда недопустима. Кроме того, клиент ничего не отображает, если Javascript браузера отключен.

Возьмем, к примеру, React-boilerplate, он отправляет html-файл напрямую, пока клиент получает веб-страницу:

app.get('*', (req, res) => res.sendFile(path.resolve(outputPath, 'index.html')));

Чтобы добиться рендеринга на стороне сервера, мы можем применить ReactDOM.renderToString для рендеринга первого html-файла с начальным набором элементов пользовательского интерфейса:

res.send('<!doctype html>\n' +
  ReactDOM.renderToString(<Html 
    assets= {webpackIsomorphicTools.assets()} 
    component={component} 
    store={store}/>));

В котором Html — это компонент React, определенный как:

В этом случае сервер пытается отображать компонент Html каждый раз, когда клиент получает страницу, и возвращает весь HTML-файл, отображаемый в пользовательском интерфейсе, клиенту. После этого клиент начнет рендерить свою страницу и заменит страницу сервера.

Как предварительно загрузить данные перед рендерингом на стороне сервера?

Поскольку SSR рендерит только первый снимок и сразу же отправляет html клиенту, это означает, что некоторые асинхронные действия не могут быть выполнены, и клиент получит компоненты, которые не полностью инициализированы. Чтобы отложить рендеринг компонента, в примере применяется Redux-async-connect, чтобы гарантировать загрузку данных перед первым рендерингом.

@asyncConnect([{
  promise: ({store: {dispatch, getState}}) => {
    const promises = [];
  if (!isInfoLoaded(getState())) {
    promises.push(dispatch(loadInfo()));
  }
  if (!isAuthLoaded(getState())) {
    promises.push(dispatch(loadAuth()));
  }
  return Promise.all(promises);
  }
}])

Как сделать состояния магазина согласованными?

Для состояний в хранилище Redux пример обеспечивает согласованность между сервером и клиентом, помещая всю карту состояний в window.__data в Html.js:

<body>
  <div id="content" dangerouslySetInnerHTML={{__html: content}}/>
  <script dangerouslySetInnerHTML={{__html:  `window.__data=${serialize(store.getState())};`}} charSet="UTF-8"/> 
  <script src={assets.javascript.main} charSet="UTF-8"/>
</body>

И клиент инициализирует свое хранилище, извлекая window.__data в client.js:

const store = createStore(_browserHistory, client, window.__data);

И этот трюк поддерживает согласованность состояний между сервером и клиентом.

Как гарантировать согласованность визуализированного результата?

В этом примере client.js проверяет контрольную сумму отрендеренного html, чтобы гарантировать, что первый результат, отрендеренный клиентом, равен результату от сервера:

if (!dest || !dest.firstChild || !dest.firstChild.attributes || !dest.firstChild.attributes['data-react-checksum']) {
    
  console.error('Server-side React render was discarded. Make sure that your initial render does not contain any client-side code.');
}

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

Однако некоторые сторонние компоненты React не могут отображаться в SSR, например, Carousel и Gridfy. Поскольку эти компоненты используют окно для измерения макета, который недоступен на сервере. Мы должны избегать использования этих компонентов, пока они не будут поддерживаться в универсальном рендеринге.

Что делает webpack-isomorphic-tools?

Инструмент isomorphic является ядром этого примера, он исправляет require() для ресурсов на сервере Node с плагином webpack, и это делает возможным SSR. Плагин генерирует webpack-assets.json и сопоставляет активы с загрузчиком webpack:

{
  ...
  assets:
  {
     "./assets/images/husky.jpg":"/assets/esfs0fa6a254a6ebf2ad.jpg",
     "./src/components/Header/Header.scss": {
       "headerItems": "headerItems___2uGFX",
       "headerNavBar": "headerNavBar___8vLWj",
       "_style": "..."
    }
  }
}

Этот подход делает исходные коды отдельными модулями, поскольку стили имеют суффикс хеша, поэтому разные модули не будут использовать стили с одинаковыми именами. Однако это затрудняет импорт стилей из node_modules.

Поддержка динамического заголовка

Поскольку SSR динамически отображает весь HTML-код, мы можем воспользоваться этим, чтобы вставить динамический заголовок на страницу. Helmet предоставляет решение для достижения этой цели в Html.js:

const head = Helmet.rewind();
return (
  <html lang="en-us">
    <head>
      {head.base.toComponent()}
      {head.title.toComponent()}
      {head.meta.toComponent()}
      {head.link.toComponent()}
      {head.script.toComponent()}
    ...
    </head>
    ...
  </html>

и поместите компонент Helmet в App.js (или другой компонент):

<div>
  <Helmet {...config.app.head}/>
  {this.props.children}
</div>

API-сервер

React-redux-universal-hot-example поддерживает набор API на порту 3030, который демонстрирует, как отправлять асинхронные действия Redux. Я пропускаю трассировку этой части, так как эта реализация не масштабируется для производства.

Ссылка

React-redux-universal-hot-example: https://github.com/erikras/react-redux-universal-hot-example

webpack-isomorphic-tools: https://github.com/halt-hammerzeit/webpack-isomorphic-tools