оригинал:Demystifying server-side rendering in React
автор:Alex Moldovan
Демистификация рендеринга React Server
Давайте подробнее рассмотрим функцию, позволяющую создавать универсальные приложения с помощью React — React Server-Side Rendering.
Рендеринг на стороне сервера (далее SSR) — это процесс рендеринга веб-сайта, созданного фронтенд-фреймворком, в виде шаблона бэкэнд-рендеринга.
Приложения, которые могут отображаться как на сервере, так и на клиенте, называются универсальными приложениями.
Почему ССР
Чтобы понять, зачем нам SSR, нам сначала нужно понять эволюцию веб-приложений за последние 10 лет.
Это тесно связано с появлением одностраничных приложений (далее именуемых SPA). По сравнению с традиционными приложениями SSR, SPA имеет большие преимущества с точки зрения скорости и взаимодействия с пользователем.
Но есть проблема. Первоначальный запрос SPA на стороне сервера обычно возвращает HTML-файл без структуры DOM, содержащий только набор ссылок CSS и JS. Затем приложению необходимо дополнительно получить некоторые данные для отображения соответствующих HTML-тегов.
Это означает, что пользователю придется дольше ждать начального рендеринга. Это также означает, что поисковые роботы могут проанализировать вашу страницу как пустую.
Поэтому решение этой проблемы таково: сначала визуализируйте приложение на сервере (рендеринг выше сгиба), а затем используйте SPA на клиенте.
SSR + SPA = Universal App
Вы найдете термин «изоморфное приложение» в других статьях, это то же самое, что и универсальное приложение.
Теперь пользователям не нужно ждать, пока загрузится ваш JS, и они могут получить полностью отрендеренный HTML, как только первоначальный запрос вернет ответ.
Представьте, какой прирост скорости это может дать пользователям, работающим в медленных сетях 3G. Вместо того, чтобы ждать 20 секунд, пока сайт загрузится, вы можете получить контент на экране почти мгновенно.
Все запросы к вашему серверу теперь будут возвращать полностью отрендеренный HTML. Отличные новости для вашего отдела SEO! Поисковый робот проиндексирует все, что вы отображаете на своем сервере, точно так же, как это делается с другими статическими сайтами в Интернете.
Напомним, что SSR имеет следующие два преимущества:
- Более быстрое время рендеринга выше сгиба
- Полные индексируемые HTML-страницы (хорошо для SEO)
Понимание SSR шаг за шагом
Давайте рассмотрим пошаговый итеративный подход к созданию полного экземпляра SSR. Мы начинаем с API-интерфейсов React, связанных с рендерингом на стороне сервера, а затем постепенно добавляем контент.
Вы можете понять каждый шаг сборки, следуя этому репозиторию и просматривая определенные там теги.
базовые настройки
Во-первых, чтобы использовать SSR, нам нужен сервер. Мы будем использовать простой экспресс-сервис для рендеринга нашего приложения React.
server.js:
import express from "express";
import path from "path";
import React from "react";
import { renderToString } from "react-dom/server";
import Layout from "./components/Layout";
const app = express();
app.use( express.static( path.resolve( __dirname, "../dist" ) ) );
app.get( "/*", ( req, res ) => {
const jsx = ( <Layout /> );
const reactDom = renderToString( jsx );
res.writeHead( 200, { "Content-Type": "text/html" } );
res.end( htmlTemplate( reactDom ) );
} );
app.listen( 2048 );
function htmlTemplate( reactDom ) {
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>React SSR</title>
</head>
<body>
<div id="app">${ reactDom }</div>
<script src="./app.bundle.js"></script>
</body>
</html>
`;
}
В строке 10 мы указываем папку, в которой Express должен обслуживать статические файлы.
Мы создали маршрут для обработки всех нестатических запросов. Этот маршрут возвращает обработанную строку HTML.
Обратите внимание, что мы используем один и тот же плагин Babel как для клиентского, так и для серверного кода, поэтому модули JSX и ES6 можно найти вserver.js
работать в.
Соответствующая функция рендеринга на клиентеReactDOM.hydrate
. Эта функция получит приложение React, которое было обработано сервером, и прикрепит обработчики событий.
Чтобы увидеть полный пример, проверьте репоbasic
ярлык.
Отлично! Вы только что создали свое первое серверное приложение React!
React Router
Мы должны честно сказать, что это приложение пока не имеет большого функционала. Итак, давайте добавим еще несколько маршрутов и подумаем, как мы должны обрабатывать эту часть на стороне сервера.
/components/layout. это:
import { Link, Switch, Route } from "react-router-dom";
import Home from "./Home";
import About from "./About";
import Contact from "./Contact";
export default class Layout extends React.Component {
/* ... */
render() {
return (
<div>
<h1>{ this.state.title }</h1>
<div>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
<Link to="/contact">Contact</Link>
</div>
<Switch>
<Route path="/" exact component={ Home } />
<Route path="/about" exact component={ About } />
<Route path="/contact" exact component={ Contact } />
</Switch>
</div>
);
}
}
Теперь компонент Layout будет отображать несколько маршрутов на клиенте.
Нам нужно смоделировать маршрутизацию на сервере. Ниже вы можете увидеть изменения, которые должны быть сделаны.
сервер.js:
/* ... */
import { StaticRouter } from "react-router-dom";
/* ... */
app.get( "/*", ( req, res ) => {
const context = { };
const jsx = (
<StaticRouter context={ context } location={ req.url }>
<Layout />
</StaticRouter>
);
const reactDom = renderToString( jsx );
res.writeHead( 200, { "Content-Type": "text/html" } );
res.end( htmlTemplate( reactDom ) );
} );
/* ... */
На стороне сервера нам нужно передать наше приложение React на аутсорсинг.StaticRouter
, и датьStaticRouter
поставкаlocation
.
Примечание:context
Используется для отслеживания потенциальных действий перенаправления при рендеринге React DOM. Это должно быть обработано ответом 3XX от сервера.
доступны в том же репозиторииrouter
вкладку, чтобы увидеть полный пример маршрутизации.
Redux
Теперь, когда у нас есть функционал маршрутизации, давайте интегрируем Redux.
В простых сценариях мы управляем состоянием на стороне клиента через Redux. Но что, если нам нужно отображать части DOM на основе состояния? В это время необходимо инициализировать Redux на стороне сервера.
Если ваше приложение находится на сервереdispatch actions
, затем необходимо зафиксировать состояние и отправить его по сети клиенту вместе с результатом HTML. На стороне клиента мы загружаем это начальное состояние в Redux.
Сначала давайте посмотрим на серверный код:
/* ... */
import { Provider as ReduxProvider } from "react-redux";
/* ... */
app.get( "/*", ( req, res ) => {
const context = { };
const store = createStore( );
store.dispatch( initializeSession( ) );
const jsx = (
<ReduxProvider store={ store }>
<StaticRouter context={ context } location={ req.url }>
<Layout />
</StaticRouter>
</ReduxProvider>
);
const reactDom = renderToString( jsx );
const reduxState = store.getState( );
res.writeHead( 200, { "Content-Type": "text/html" } );
res.end( htmlTemplate( reactDom, reduxState ) );
} );
app.listen( 2048 );
function htmlTemplate( reactDom, reduxState ) {
return `
/* ... */
<div id="app">${ reactDom }</div>
<script>
window.REDUX_DATA = ${ JSON.stringify( reduxState ) }
</script>
<script src="./app.bundle.js"></script>
/* ... */
`;
}
Выглядит некрасиво, но нам нужно отправить клиенту полное состояние в формате JSON вместе с нашим HTML.
Тогда давайте посмотрим на клиента:
app.js
import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter as Router } from "react-router-dom";
import { Provider as ReduxProvider } from "react-redux";
import Layout from "./components/Layout";
import createStore from "./store";
const store = createStore( window.REDUX_DATA );
const jsx = (
<ReduxProvider store={ store }>
<Router>
<Layout />
</Router>
</ReduxProvider>
);
const app = document.getElementById( "app" );
ReactDOM.hydrate( jsx, app );
Обратите внимание, что мы вызываем дваждыcreateStore
, сначала на сервере, потом на клиенте. Однако на стороне клиента мы используем любое состояние, сохраненное на сервере, для инициализации состояния на клиенте. Этот процесс аналогичен гидратации РОВ.
доступны в том же репозиторииredux
tab, чтобы увидеть полный пример на Redux.
Fetch Data
Последняя сложная часть головоломки — загрузка данных. Предположим, у нас есть API, который предоставляет данные JSON.
В нашем репозитории я получаю все события сезона F1 2018 года из общедоступного API. Предположим, мы хотим отобразить все время на домашней странице.
Мы можем вызывать наш API из клиента после того, как приложение React смонтировано и все отрендерено. Но это создает плохой пользовательский опыт и может потребовать показа загрузчика или счетчика, прежде чем пользователь увидит соответствующий контент.
В нашем приложении SSR Redux сначала сохраняет данные на сервере, а затем отправляет данные клиенту. Мы можем воспользоваться этим.
Что, если бы мы сделали вызов API на сервере, сохранили результат в Redux, а затем отрендерили его клиенту, используя полный HTML с соответствующими данными?
Но как узнать, какой странице соответствует вызов API?
Во-первых, нам нужен другой способ объявления маршрутов. Давайте создадим файл конфигурации маршрутизации.
export default [
{
path: "/",
component: Home,
exact: true,
},
{
path: "/about",
component: About,
exact: true,
},
{
path: "/contact",
component: Contact,
exact: true,
},
{
path: "/secret",
component: Secret,
exact: true,
},
];
Затем мы статически объявляем требования к данным для каждого компонента:
/* ... */
import { fetchData } from "../store";
class Home extends React.Component {
/* ... */
render( ) {
const { circuits } = this.props;
return (
/* ... */
);
}
}
Home.serverFetch = fetchData; // static declaration of data requirements
/* ... */
пожалуйста, помните,serverFetch
Может быть назван свободно.
Уведомление,fetchData
— это действие преобразователя Redux, которое при отправке возвращает обещание.
На стороне сервера мы можем использоватьreact-router
Функция--matchPath
.
/* ... */
import { StaticRouter, matchPath } from "react-router-dom";
import routes from "./routes";
/* ... */
app.get( "/*", ( req, res ) => {
/* ... */
const dataRequirements =
routes
.filter( route => matchPath( req.url, route ) ) // filter matching paths
.map( route => route.component ) // map to components
.filter( comp => comp.serverFetch ) // check if components have data requirement
.map( comp => store.dispatch( comp.serverFetch( ) ) ); // dispatch data requirement
Promise.all( dataRequirements ).then( ( ) => {
const jsx = (
<ReduxProvider store={ store }>
<StaticRouter context={ context } location={ req.url }>
<Layout />
</StaticRouter>
</ReduxProvider>
);
const reactDom = renderToString( jsx );
const reduxState = store.getState( );
res.writeHead( 200, { "Content-Type": "text/html" } );
res.end( htmlTemplate( reactDom, reduxState ) );
} );
} );
/* ... */
Таким образом, мы получаем список компонентов, которые будут монтироваться только тогда, когда React начнет отображать строки по текущему URL-адресу.
Мы собираем требования к данным и ждем, пока все вызовы API вернут данные. Наконец, мы переходим к рендерингу на стороне сервера, где данные уже доступны в Redux.
доступны в том же репозиторииfetch-data
вкладку, чтобы увидеть полный пример сбора данных.
Как вы могли заметить, это приводит к снижению производительности, поскольку мы откладываем рендеринг до тех пор, пока данные не будут получены.
Вот тут-то и возникает компромисс, и вам нужно попытаться выяснить, какие звонки важны, а какие нет. Например, в приложении для электронной коммерции важно получить список продуктов, в то время как цены на продукты и фильтры на боковой панели могут загружаться лениво.
Helmet
Давайте посмотрим на SEO как на одно из преимуществ SSR. При использовании React вы можете захотеть<head>
Установите различные заголовки, метатеги, ключевые слова и т. д. в теге.
Помните, что обычно<head>
Теги не являются частью приложения React.
в этих обстоятельствахreact-helmetдает хорошее решение. И у него хорошая поддержка SSR.
import React from "react";
import Helmet from "react-helmet";
const Contact = () => (
<div>
<h2>This is the contact page</h2>
<Helmet>
<title>Contact Page</title>
<meta name="description" content="This is a proof of concept for React SSR" />
</Helmet>
</div>
);
export default Contact;
Вы просто добавляете свойhead
данные. Это позволяет изменять значения вне смонтированного приложения React на стороне клиента.
Теперь мы добавляем поддержку SSR:
/* ... */
import Helmet from "react-helmet";
/* ... */
app.get( "/*", ( req, res ) => {
/* ... */
const jsx = (
<ReduxProvider store={ store }>
<StaticRouter context={ context } location={ req.url }>
<Layout />
</StaticRouter>
</ReduxProvider>
);
const reactDom = renderToString( jsx );
const reduxState = store.getState( );
const helmetData = Helmet.renderStatic( );
res.writeHead( 200, { "Content-Type": "text/html" } );
res.end( htmlTemplate( reactDom, reduxState, helmetData ) );
} );
} );
app.listen( 2048 );
function htmlTemplate( reactDom, reduxState, helmetData ) {
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
${ helmetData.title.toString( ) }
${ helmetData.meta.toString( ) }
<title>React SSR</title>
</head>
/* ... */
`;
}
Теперь у нас есть полнофункциональный пример React SSR.
Мы начали с рендеринга простой строки HTML с помощью Express и постепенно добавили маршрутизацию, управление состоянием и выборку данных. Наконец, наши программные изменения выходят за рамки приложения React (обработкаhead
Этикетка)
См. https://github.com/alexnm/react-ssr для полного примера.
резюме
Как видите, SSR тоже не является большой проблемой. Но это может усложниться. Будет легче понять, если вы будете строить свои требования шаг за шагом.
Стоит ли применять SSR к вашему приложению? Как всегда, все зависит от ситуации. Это обязательно, если ваш сайт предназначен для тысяч пользователей. Если вы создаете приложение, подобное инструменту/панели мониторинга, оно вам, вероятно, не понадобится.
Конечно, эффективное использование универсальных приложений может действительно улучшить интерфейсное сообщество.
У вас есть аналогичный подход к SSR? Или вы думаете, что я что-то пропустил в этом посте? пожалуйста вTwitterнапишите мне.
Если вы нашли эту статью полезной, пожалуйста, помогите мне поделиться ею в сообществе.