Универсальный JS
Как работает серверный рендеринг приложения на React, React-Router и Redux
Черепанов Сергей
сооснователь Fullstack Development
sergey@fullstack-development.com
Терминология №1
Изоморфный — имеющий похожую форму, но все-таки отдельный объект
import _ from 'underscore';
import _ from 'lodash';
Терминология №2
Универсальный — применимый во всех случаях

Принципиальная схема
Server
Запрос пользователя за страницей
Сервер отвечает простой статикой index.html
Client
Браузер загружает index.html и bundle.js
Конфигурация SPA (отработка импортов, инициализация
роутов и т.д.)
Роутер определяет по URL-у страницу
SPA подгружает начальные данные от API
SPA начинает постройку DOM-а с на основе данных
Роутер определяет по URL-у страницу
SPA подгружает начальные данные по URL
SPA рендерит всю разметку в строку
Скорость
SEO
Общая кодовая база
Зачем?
Скорость

С универсальностью


814ms
1147ms
1312ms
Скорость

1147ms

1517ms
БЕЗ универсальности
SEO
1. Yandex и Google умеют анализировать SPA;
2. Другие поисковые системы?
3. Проблемы с ботами других систем, например, социальных сетей, которые анализируют страницу при sharing
erikras/react-redux-universal-hot-example
React Redux Universal Hot Example
- React
- React Router
- Babel
- Webpack
- Redux
- ESLint
- Express (for frontend server)
- Webpack-isomorphic-tools
Как это работает у нас
Классическое SPA + дополнительный frontend server
Клиентский браузер
Фронтенд Сервер
Бэкенд с API
Как это работает у нас
Подробная схема
Server
User
Client
SPA начинает постройку DOM-а с самого нуля
handles
server.js
request
incoming
(create store)
initialization
Redux
match the
React-Router
current page
Initializing requests
to our API
React start building
DOM
request
index.html with content
user gets dynamic
bundle script with
React and Redux start with
already initialized state
Classic SPA begins
client.js entry point
Начнем по порядку, с server.js
Frontend Server
Simple Express.JS server with one main middleware:
import React from 'react';
const app = new Express();
app.use((req, res) => {
// пример
res.send(ReactDOM.renderToString(
<App store={store} />
));
});
ES6 и JSX синтаксис? На сервере?
JSX и импорты .css в node.js?
для компиляции es6 и JSX на лету
require('babel-register')(config);
Вариант №1
•
• webpack-isomorphic-tools
Webpack-isomorphic-tools
-
В конфигах описываем для Webpack-isomorphic-tools, какие файлы ассетов ждать, и как каждый вид обрабатывать
webpack-assets.json
webpack-assets.json
-
Если не js, то ищем файл в
и подставляет его содержимое
- Во время сборки Webpack-isomorphic-tools кастомным плагином создает
-
Вешает свой хук на все вызовы require, который проверяет, что импортируемый файл - не js
Webpack-assets.json
{
"javascript": {
"main": "/assets/main-d8c29e9b2a4623f696e8.js"
},
"styles": {
"main": "/assets/main-d8c29e9b2a4623f696e8.css"
}
}
JSX и импорты .css в node.js?
Собирать server.js с помощью webpack:
Вариант №2
- 2 конфига (для сервера и для клиента) и 2 webpack-сервера с вотчерами
- поддержку source map сделать руками (пока)
resolve.modulesDirectories
-
собрать все URLs для .css и .js файлов, вставить в <head>
- заменить style-loader на (fake-style-loader или css-loader/locals)
-
еще парочка неожиданных хаков, чтобы все заработало :) (все описано в доках)
- дублирование всех ассетов для обеих сборок? (вопрос еще поднят в universal-webpack)
Инициализация Redux
По прилету первого запроса на сервер первым делом создается store
app.use((req, res) => {
const client = new ApiClient(req); // Next Slide
const memoryHistory = createHistory(req.originalUrl);
const store = createStore(memoryHistory, client);
// … more code here
});
ApiClient
Экземпляры класса - обертки для библиотеки по отправке HTTP-запросов:
superagent
-
Добавление хоста к абсолютным путям;
стоило бы просто вообще к перейти)
axios
-
Передача кук из запроса на сам сервер, чтобы сохранить сессии и т.п. вещи;
-
Приведение API к более удобному виду (но это из-за
React-Router match the current page
app.use((req, res) => {
// previous code
match(
{
store,
routes: getRoutes(store),
location: req.originalUrl
},
(error, redirectLocation, renderProps) => {
if (redirectLocation) {
res.redirect(redirectLocation.pathname);
} else if (error) {
res.status(500);
}
// … more code here
});
});
Initializing requests to our API
Redux Async Connect
@asyncConnect({
lunch: (params, helpers) => Promise.resolve(
{id: 1, name: 'Borsch'}
)
})
class App extends React.Component {
render() {
const lunch = this.props.lunch
return (<div>{lunch.name}</div>);
}
}
-
Задерживает рендеринг страницы до тех пор, пока не выполнены асинхронные задачи;
-
Позволяет сохранить в state полученные данные
Redux Async Connect
// previous code
loadOnServer({
...renderProps,
store,
helpers: { client }
}).then(() => {
// store is populated with server response
});
-
Если frontend-сервер и API находятся на одной машине, то данные можно получать с localhost!
React рендерит HTML в строку
// previous code
const component = (
<Provider store={store} key="provider">
<ReduxAsyncConnect {...renderProps} />
</Provider>
);
res.send(
'<!doctype html>\n' +
ReactDOM.renderToString(
<Html
assets={webpackIsomorphicTools.assets()}
component={component}
store={store}
/>
)
);
index.html
// Html.js
class Html extends Component {
static propTypes = {
assets: PropTypes.object,
component: PropTypes.node,
store: PropTypes.object,
};
render() {
const content = component ? ReactDOM.renderToString(component) : '';
return (
<html lang="en-us">
<head>
// meta tags, links to css etc
</head>
<body>
<div id="content" dangerouslySetInnerHTML={{ __html: content }} />
<script
dangerouslySetInnerHTML={{
__html: `window.__data=${serialize(store.getState())};` }}
charSet="UTF-8"
/>
<script src={assets.javascript.main} {...mainScriptAttrs} charSet="UTF-8"/>
</body>
</html>
);
}
}
1
2
3
4
client.js - входная точка на клиенте
ReactDOM.render(...)
-
Собираем заново клиентский store из уже инициализированного state
- Подключение React-Router
- _
window.onerror(...)
Как React подключается к уже отданной верстке
.render()
-
Метод должен быть чистым
-
Сравниваем с тем значением, что получено на основе клиентского рендеринга
-
React подключает обработчики событий к уже существующему DOM
checksums
Old-school библиотеки
<script src="...?callback=initMap"></script>
window.initMap = function () {
console.log(window.google);
}
Наша компонента для инициализации использует объект (например, автодополнение для адресов)
window.google

Client-side detection
Invoked once, only on the client (not on the server), immediately after the initial rendering occurs
class SomeEl extends Component {
componentDidMount() {
this.setState({inBrowser: true});
}
render() {
if (this.state.inBrowser) {
// render with window.google
}
}
}
Область применения
- Большой тяжелый проект с кучей запросов при старте некоторых страниц (например, лента какого-нибудь сложного агрегатора);
- Большие требования к скорости (начальной загрузке страницы);
- Большие требования к полной SEO-совместимости
Спасибо
Черепанов Сергей
сооснователь Fullstack Development
sergey@fullstack-development.com
Sergey
By julya_key09
Sergey
- 460