Демистификация рендеринга на стороне сервера в React

demistifikacziya renderinga na storone servera v react

Давайте подробнее рассмотрим функцию, позволяющую строить универсальный программы с Реагировать.

Визуализация на стороне сервера – SSR с этого момента – это способность a интерфейсный фреймворк для воспроизведения разметки во время работы на a серверная система.

Вызываются программы, имеющие возможность рендеринга как на сервере, так и на клиенте универсальные программы.

Зачем беспокоиться?

Чтобы понять, зачем нужен РСБ, нам нужно понять эволюцию веб-приложений за последние 10 лет.

Это тесно связано с ростом Одностраничная программаСПА с этого момента. SPA предлагают большие преимущества в скорости и UX по сравнению с традиционными серверными программами.

Но здесь есть загвоздка. Начальный запрос сервера обычно возвращает an пуст HTML файл с кучей ссылок на CSS и JavaScript (JS). Затем необходимо получить внешние файлы, чтобы отобразить соответствующую разметку.

Это означает, что пользователю придется ждать подольше начальный рендер. Это также означает, что сканеры могут считать вашу страницу пустой.

Итак, идея состоит в том, чтобы сначала отобразить ваше приложение на сервере, а затем использовать возможности SPA на клиенте.

SSR + SPA = универсальное приложение*

* Вы найдете термин изоморфное приложение в некоторых статьях — то же самое.

Теперь пользователю не нужно ждать, пока ваш JS загрузится, и получает a полностью предоставлены HTML как только исходный запрос возвращает ответ.

Представьте огромные улучшения для пользователей, которые используют навигацию в медленных сетях 3G. Вместо того чтобы ждать более 20 секунд для загрузки веб-сайта, вы почти мгновенно получаете содержимое на их экране.

1*v0wyppYBPaeqoeKRplinvQ

И теперь все запросы, поступающие к вашему серверу, возвращают полностью воспроизведенный HTML. Отличные новости для вашего отдела SEO!

Теперь веб-сканеры будут видеть ваш сайт так же, как и любой другой статический сайт в Интернете индекс все содержимое, которое вы воспроизводите на сервере.

Итак, подытоживая, два основных преимущества, которые мы получаем от SSR:

  • Быстрое время воспроизведения начальной страницы
  • Полностью индексированные HTML-страницы

Понимание РСБ – шаг за шагом

Давайте используем итеративный подход, чтобы создать наш полный пример SSR. Мы начинаем с React API для серверного рендеринга, и на каждом шагу будем что-то добавлять в смесь.

Вы можете следить за этим хранилищем и метками, определенными там для каждого шага.

Основная настройка

В первую очередь. Для использования SSR нам нужен сервер! Мы будем использовать простой Экспресс приложение, которое воспроизведет наше приложение React.

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="
        </body>
        </html>
    `;
}

Нам нужно сказать Express обслуживать наши статические файлы из папки вывода – строка 10.

Мы создаем маршрут, обрабатывающий все нестатические входящие запросы. Этот маршрут ответит воспроизведенным HTML.

Мы используем renderToString — строки 13–14 — чтобы превратить наш начальный JSX в a string который мы вставляем в шаблон HTML.

Обратите внимание, что мы используем те же плагины Babel для кода клиента и для кода сервера. Поэтому JSX и Модули ES работа внутри server.js.

Соответствующий метод на клиенте сейчас есть ReactDOM.hydrate . Эта функция будет использовать серверное приложение React и присоединять обработчики событий.

import ReactDOM from "react-dom";
import Layout from "./components/Layout";

const app = document.getElementById( "app" );
ReactDOM.hydrate( <Layout />, app );

Чтобы просмотреть полный пример, просмотрите basic тег в хранилище.

Это! Вы только что создали свой первый предоставлен сервером Приложение React!

Маршрутизатор React

Мы должны быть честными здесь, приложение не делает многое. Итак, давайте добавим несколько маршрутов и посмотрим, как мы обрабатываем серверную часть.

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>
        );
    }
}

The Layout компонент теперь отображает несколько маршрутов клиента.

Нам нужно имитировать настройки маршрутизатора на сервере. Ниже можно увидеть основные изменения, которые нужно сделать.

/* ... */
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 компонент и обеспечить location.

Как замечание, context используется для отслеживания потенциальных перенаправлений при визуализации React DOM. Это необходимо решить с помощью ответа 3XX от сервера.

Полный пример можно увидеть на router тег в том же хранилище.

Redux

Теперь, когда у нас есть маршрутизации, давайте интегрируем Redux.

В простом сценарии нам необходим Redux для управления состоянием на клиенте. Но что если нам нужно отобразить части DOM на основе этого состояния? Имеет смысл инициализировать Redux на сервере.

Если ваше приложение есть диспетчеризация действия на серверэто нужно восторг состояние и отправьте его по проводу вместе с 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="
        
        /* ... */
    `;
}

Это выглядит некрасиво, но нам нужно отправить полное состояние JSON вместе с нашим HTML.

Затем смотрим на клиента:

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 дважды сначала на сервере, затем на клиенте. Однако на клиенте мы инициализируем состояние любым состоянием, сохраненным на сервере. Этот процесс подобен гидратации DOM.

Полный пример можно увидеть на redux тег в том же хранилище.

Получить данные

Последней частью головоломки является загрузка данных. Здесь дело становится немного сложнее. Скажем, у нас есть API, обслуживающий данные JSON.

В нашей кодовой базе я получаю все события сезона Формулы-1 2018 из публичного API. Скажем, мы хотим отразить все события на Домой страницы.

Мы можем вызвать наш API только с клиента после того, как приложение React будет смонтировано и все отрендерировано. Но это плохо повлияет на UX, потенциально показывая прямольник или загрузчик до того, как пользователь увидит соответствующее содержимое.

У нас уже есть Redux как способ хранения данных на сервере и отправка их клиенту.

Что если мы сделаем наши вызовы API на сервере, сохраним результаты в Redux, а затем воспроизведем полный HTML с соответствующими данными для клиента?

Но как мы можем знать, какие звонки нужно совершить?

Во-первых, нам нужен другой способ объявления маршрутов. Поэтому мы переходим к так называемому конфигурационному файлу маршрутов.

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 thunk, которое возвращает Promise во время отправки.

На сервере мы можем использовать специальную функцию от react-routerзвонил matchRoute.

/* ... */
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 тег в том же хранилище.

Вы наверняка заметили, что это приводит к снижению производительности, поскольку мы задерживаем визуализацию, пока не будут получены данные.

Здесь вы начинаете сравнивать показатели и делаете все возможное, чтобы понять, какие звонки важны, а какие нет. К примеру, получение продуктов для программы электронной коммерции может быть решающим, но цены и фильтры боковой панели могут загружаться отложено.

Шлем

В качестве бонуса, давайте рассмотрим SEO. При работе с React можно установить другие значения в своем <head> тег. Например, вы можете захотеть seт title, мета теги, ключслова и т.д.

Имейте в виду, что <head> обычно не является частью программы React!

react-helmet поможет вам в этом сценарии. И у него большая поддержка РСБ.

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 в контексте Экспресс Приложение Мы постепенно добавили маршрутизацию, управление состоянием и получение данных. Наконец, мы обработали изменения вне программы React.

Окончательная кодовая база включена master в том же хранилище, которое упоминалось ранее.

Вывод

Как вы видели, SSR не является большой проблемой, но она может быть сложной. И это гораздо легче понять, если вы строите свои потребности шаг за шагом.

Следует ли добавлять SSR в вашу программу? Как обычно, все зависит. Это обязательно, если ваш веб-сайт общедоступный и доступный сотням тысяч пользователей. Но если вы создаете приложение, похожее на инструмент или информационную панель, это может быть не стоит усилий.

Однако использование возможностей универсального приложения является шагом вперед для интерфейсного сообщества.

Используете ли вы подобный подход для SSR? Или вы думаете, что я что-нибудь пропустил? Напишите мне сообщение ниже или в Twitter.

Если вы нашли эту статью полезной, помогите мне поделиться ею с сообществом!

Добавить комментарий

Ваш адрес email не будет опубликован.