Как работает серверный рендеринг приложения на React, React-Router и Redux
Черепанов Сергей
сооснователь Fullstack Development
sergey@fullstack-development.com
Изоморфный — имеющий похожую форму, но все-таки отдельный объект
import _ from 'underscore';
import _ from 'lodash';Универсальный — применимый во всех случаях
Server
Запрос пользователя за страницей
Сервер отвечает простой статикой index.html
Client
Браузер загружает index.html и bundle.js
Конфигурация SPA (отработка импортов, инициализация
роутов и т.д.)
Роутер определяет по URL-у страницу
SPA подгружает начальные данные от API
SPA начинает постройку DOM-а с на основе данных
Роутер определяет по URL-у страницу
SPA подгружает начальные данные по URL
SPA рендерит всю разметку в строку
Зачем?
С универсальностью
814ms
1147ms
1312ms
1147ms
1517ms
БЕЗ универсальности
1. Yandex и Google умеют анализировать SPA;
2. Другие поисковые системы?
3. Проблемы с ботами других систем, например, социальных сетей, которые анализируют страницу при sharing
React Redux Universal Hot Example
Клиентский браузер
Фронтенд Сервер
Бэкенд с 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
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 синтаксис? На сервере?
для компиляции es6 и JSX на лету
require('babel-register')(config);•
• webpack-isomorphic-tools
В конфигах описываем для Webpack-isomorphic-tools, какие файлы ассетов ждать, и как каждый вид обрабатывать
webpack-assets.jsonwebpack-assets.jsonЕсли не js, то ищем файл в
и подставляет его содержимое
Вешает свой хук на все вызовы require, который проверяет, что импортируемый файл - не js
{
"javascript": {
"main": "/assets/main-d8c29e9b2a4623f696e8.js"
},
"styles": {
"main": "/assets/main-d8c29e9b2a4623f696e8.css"
}
}Собирать server.js с помощью webpack:
resolve.modulesDirectoriesсобрать все URLs для .css и .js файлов, вставить в <head>
еще парочка неожиданных хаков, чтобы все заработало :) (все описано в доках)
По прилету первого запроса на сервер первым делом создается 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
});Экземпляры класса - обертки для библиотеки по отправке HTTP-запросов:
superagentДобавление хоста к абсолютным путям;
стоило бы просто вообще к перейти)
axiosПередача кук из запроса на сам сервер, чтобы сохранить сессии и т.п. вещи;
Приведение API к более удобному виду (но это из-за
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
});
});@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 полученные данные
// previous code
loadOnServer({
...renderProps,
store,
helpers: { client }
}).then(() => {
// store is populated with server response
}); Если frontend-сервер и API находятся на одной машине, то данные можно получать с localhost!
// 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}
/>
)
);// 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
ReactDOM.render(...)Собираем заново клиентский store из уже инициализированного state
window.onerror(...).render()Метод должен быть чистым
Сравниваем с тем значением, что получено на основе клиентского рендеринга
React подключает обработчики событий к уже существующему DOM
checksums<script src="...?callback=initMap"></script>window.initMap = function () {
console.log(window.google);
}Наша компонента для инициализации использует объект (например, автодополнение для адресов)
window.googleInvoked 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
}
}
}Черепанов Сергей
сооснователь Fullstack Development
sergey@fullstack-development.com