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

С тех пор прошло слишком много недель и всего (включая противостояние между Apache Foundation и Facebook из-за лицензирования React.js с открытым исходным кодом), но я хотел вернуться к конкретной теме PWA: Рендеринг на стороне сервера.

В предыдущем посте мы достигли хороших результатов в Lighthouse, включив автономный доступ и добавив несколько метатегов в наше примерное приложение React. Однако у нас все еще есть большая проблема: Нашему приложению React требуется 8450 мс для запуска. Что здесь происходит?

Если вы знакомы с одностраничными приложениями, вы знаете, что большинство из них загружается с помощью JavaScript. Это означает, что до тех пор, пока эти файлы JavaScript не будут загружены, проанализированы и выполнены, все, что вы сможете увидеть, - это исходное содержимое базового HTML-документа.

Щелкните приложение правой кнопкой мыши, выберите «Просмотр исходного кода страницы», и вы увидите фактические элементы, которые видны при загрузке файлов JS:

Итак, фактически у нас есть пустая страница. При медленном мобильном соединении ваши пользователи будут видеть эти элементы некоторое время (8450 мс, согласно Lighthouse); что отгонит их.

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

Теперь, когда все элементы в нашем приложении генерируются React, как мы можем показать этот контент до того, как будет загружен React?

Простым решением может быть добавление содержимого в наш элемент DIV с идентификатором «app». Давайте попробуем что-то вроде этого:

<div id="app">
    <h1>Loading</h1>

    We are loading your tacos
</div>

Посмотрим, помогло ли это:

Нет, Lighthouse умнее, чем мы думали. Он не считает наше маленькое сообщение о загрузке «значимым» содержанием. Что еще мы можем сделать?

Рендеринг на стороне сервера - наиболее распространенное решение этой проблемы. Он в основном полагается на внутренний сервер для предварительного расчета начального состояния и компонентов нашего приложения и возврата их в базовом HTML.

В нашем случае есть модуль react-dom / server для NodeJS, который делает то же самое, что React делает в браузере, но на бэкэнде: создает компоненты и возвращает проанализированный HTML в ответе на основной документ.

Однако у нас есть огромная проблема. Мы все бессерверны. У нас нет серверной стороны для рендеринга на стороне сервера. У нас нет вариантов?

Плагины Webpack

Webpack обрабатывает наш исходный код и уже создает наш index.html. Мы можем подключиться к Webpack с помощью плагинов. Мы можем использовать эти плагины для предварительной обработки нашего приложения React в процессе сборки Webpack и создания index.html (который в нашем примере создается HtmlWebpackPlugin), который будет иметь те же элементы, что и response-dom / server, которые будут возвращаться при обычном рендеринге на стороне сервера.

На высоком уровне мы будем выполнять наше приложение React в виртуальной модели DOM во время выполнения Webpacks, что сгенерирует элементы, которые будут отображаться в нашем приложении, и загрузит исходные данные. Затем мы вставим этот проанализированный HTML в наш статический index.html.

Помимо Webpack, мы воспользуемся преимуществами двух инструментов, чтобы получить этот результат: Express узла и JSDOM.

Даже когда эта конфигурация ориентирована на Webpack, вы можете использовать другие инструменты сборки, такие как Gulp или Grunt, если вы следуете тем же концепциям, представленным здесь.

Во-первых, убедитесь, что вы установили Express и JSDOM в проект как зависимости разработчика. Затем создайте следующий файл в src / webpack / plugin (расположение не имеет значения, в данном случае просто для того, чтобы конфигурация работала из коробки):

Этот плагин выполняет следующие задачи:

  • Мы создаем сервер Express, который будет указывать на нашу папку dist. JSDOM будет использовать этот сервер для разрешения наших статических ресурсов, в частности встроенных ресурсов JavaScript. Мы также добавляем папку json, поскольку в нее мы получаем данные из (data.json). В реальном приложении вы можете не включать это в сервер Express и указывать data.json в строке 48 на реальную конечную точку REST.
  • В строке 33 мы подключаемся к процессу сборки Webpack, чтобы получить HTML, полученный из HtmlWebpackPlugin.
  • В строке 40 мы создаем виртуальную модель DOM с помощью JSDOM и указываем ее на наш сервер Express. JSDOM проанализирует содержимое index.html, созданное HtmlWebpackPlugin, и разрешит все ссылки JS и CSS с сервера Express.
  • В строке 50 мы добавляем слушателя, который будет выполняться после завершения загрузки виртуальной DOM. Оттуда мы можем манипулировать им как обычным DOM.
  • В строке 53 мы делаем запрос к data.json, который является данными, которые загружаются на страницу, и вставляем их в нашу виртуальную DOM в строке 62. Это гарантирует, что приложение имеет доступ к начальной загрузке данных. без дополнительного HTTP-запроса к переменной __PRELOADED_STATE__.
  • В строках 65 и 66 мы берем обработанный HTML-код (в котором уже есть инициализированное приложение React) и возвращаем его в процесс сборки Webpack. Это будет в index.html (этот шаг также позволит Webpack правильно сгенерировать HTML с помощью сервера Dev).

Добавьте следующее в webpack.config.js, после HtmlWebpackPlugin:

new ReactServerHTMLPlugin({}),

Если вы соберете проект и проверите index.html внутри папки dist, вы заметите, что наше приложение теперь находится там:

<html lang="en"><head>
        <meta charset="utf-8">
        <meta http-equiv="Content-type" content="text/html; charset=utf-8">
        <title>Taco Galery</title>
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <link rel="manifest" href="/manifest.json">
        <meta name="theme-color" content="#999999">
    <link rel="shortcut icon" href="favicon.ico"><script>window.__PRELOADED_STATE__ = "[\r\n{\r\n\t\"id\":1,\r\n\t\"name\":\"Al pastor\",\r\n\t\"price\":\"2.45\",\r\n\t\"description\": \"Mexico style pork tacos\"\r\n},\r\n{\r\n\t\"id\":2,\r\n\t\"name\":\"Beef fajita\",\r\n\t\"price\":\"3.45\",\r\n\t\"description\": \"Beef fajita tacos, with onion and cilantro\"\r\n},\r\n{\r\n\t\"id\":3,\r\n\t\"name\":\"Fish tacos\",\r\n\t\"price\":\"3.45\",\r\n\t\"description\": \"Fresh fish with onions, letuce and carrots\"\r\n}\r\n]"</script></head>
    <body>
        <div id="app"><section data-reactroot="" class="container"><h1>Today tacos</h1><div><div><h2>Al pastor</h2><span><!-- react-text: 7 -->Price: $<!-- /react-text --><!-- react-text: 8 -->2.45<!-- /react-text --></span><p>Mexico style pork tacos</p></div><div><h2>Beef fajita</h2><span><!-- react-text: 13 -->Price: $<!-- /react-text --><!-- react-text: 14 -->3.45<!-- /react-text --></span><p>Beef fajita tacos, with onion and cilantro</p></div><div><h2>Fish tacos</h2><span><!-- react-text: 19 -->Price: $<!-- /react-text --><!-- react-text: 20 -->3.45<!-- /react-text --></span><p>Fresh fish with onions, letuce and carrots</p></div></div></section></div>
        <link rel="stylesheet" type="text/css" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
    <script type="text/javascript" src="vendor.bundle.js"></script><script type="text/javascript" src="main.js"></script>
</body></html>

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

class Main extends React.Component {
   constructor(props) {
      super(props);
      this.state = {
         tacos:[]
      };
   }
...

К этому:

class Main extends React.Component {
   constructor(props) {
      super(props);
      var data = JSON.parse(window.__PRELOADED_STATE__) ;
      this.state = data || {
         tacos:[]
      };
   }

Таким образом, если __PRELOADED_STATE__ существует, мы будем использовать его значение в качестве исходных данных для нашего приложения. В противном случае он начнется с пустого JSON.

Загрузите приложение и запустите Lighthouse:

Наша начальная нагрузка увеличилась с 8450 мс до 2340 мс. Теперь Маяк стал счастливее!

У этого подхода есть большой недостаток. Он предполагает, что содержимое data.json будет таким же, когда страница загружается для пользователя, чем когда вы создаете свой проект. В большинстве случаев это предположение будет ложным.

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

Отсутствие сервера за нашим пользовательским интерфейсом делает создание PWA более сложным процессом. Мы не можем полагаться на обычные методы рендеринга на стороне сервера, но мы можем запускать веб-серверы и виртуальные модели DOM для предварительной обработки нашего приложения и создания файлов HTML, которые будут содержать все элементы полностью загруженного SPA.