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