Как настроить приложение React для рендеринга как на сервере, так и на клиенте, используя платформу ExpressJS.
В своей предыдущей статье о рендеринге на стороне сервера в React я кратко описал идею изоморфных и универсальных приложений. Я также показал преимущества и недостатки этого подхода. Сегодня я перейду к делу и покажу вам, как настроить универсальное приложение React с помощью фреймворка ExpressJS!
Сначала я опишу, как настроить приложение для клиента, а затем трансформирую его, чтобы оно также работало на стороне сервера. Это будет самый простой пример универсального приложения React. Позже, в следующих выпусках этой серии, я расширю свой код за счет использования Redux и response-router.
Отправная точка - пример компонента React
Если мы создаем универсальное приложение, нам приходится обрабатывать два случая: рендеринг на сервере и на клиенте. Когда веб-браузер запрашивает контент, наше приложение должно подготовить все необходимые данные, заполнить ими HTML-код и вернуть его браузеру. Затем все файлы JavaScript, содержащие клиентскую версию нашего приложения, будут загружены браузером. Благодаря этому станет возможным дальнейшее взаимодействие с нашим сайтом.
Для достижения вышеуказанных целей нам нужны две отправные точки в нашем приложении. Для сервера это будет файл server.js
, а для клиента client.js
это будет файл. Мы обсудим эти два файла позже, но сначала давайте создадим наш основной компонент React. Это может выглядеть так (App.js
):
Как видите, это простой компонент, который получает некоторый начальный текст из своего props
, присваивает его состоянию, а затем отображает его - и мы хотим, чтобы это выполнялось на сервере.
Кроме того, есть кнопка, которая изменяет значение свойства состояния text
. Это взаимодействие будет осуществляться на клиенте.
Клиентский рендеринг
Теперь, когда у нас есть компонент, мы можем его отрендерить. А пока сделаем это только для клиента. См. Ниже содержание файла client.js
:
Если вы какое-то время работали с React, вы должны быть знакомы с приведенным выше кодом. Благодаря методу render
объекта ReactDOM
мы можем вставить компонент App
в элемент HTML с идентификатором «app». Это происходит только после того, как наш файл JavaScript загружается в браузер (до этого отображается пустая страница). Обратите внимание, как исходный текст передается в компонент.
Конечно, если мы хотим, чтобы все это работало, мы должны добавить в наш проект несколько пакетов:
Обратите внимание, что я не использую здесь параметр --dev
. Это потому, что мне понадобятся эти пакеты не только в «связке» на стороне клиента, но и на сервере.
Клиентская подготовка
Следующим шагом является подготовка файла index.html
. К нему будет прикреплен наш клиентский скрипт (браузер загрузит его после загрузки индексного файла). У него также будет контейнер с идентификатором app, куда будет внедрено наше приложение. Для обработки всего этого будет использоваться webpack.
Начнем с установки всех необходимых пакетов:
Нам также понадобятся несколько пресетов Babel (предустановленные наборы плагинов Babel):
И последнее, но не менее важное: мы должны установить пакет полифиллов Babel (как для клиента, так и для сервера, поэтому параметр no--dev
):
Зачем нам это нужно? Что ж, Babel может переводить синтаксис JavaScript, но в последующих версиях ECMAScript вводятся различные собственные методы или глобальные объекты (например, Promise
). Благодаря babel-polyfill
мы можем эмулировать всю среду ES6 +.
Хорошо, теперь мы готовы добавить файл index.html
в наш проект:
Мы будем использовать этот файл в качестве отправной точки нашего приложения - по крайней мере, на данный момент - когда мы добавим серверную конфигурацию, этот файл больше не понадобится. Обратите внимание, контейнер «app» - здесь будет внедрено наше приложение React.
Вы также можете отметить, что клиентский скрипт не прикреплен - это потому, что он еще не готов. Он будет сгенерирован веб-пакетом, а затем введен в индексный файл с помощью подключаемого модуля веб-пакета под названием html-webpack-plugin
.
Конфигурация Webpack клиентской части
Теперь давайте настроим webpack для создания клиентского пакета. Для этого нам нужно добавить файл webpack.config.js
в наш проект. Пожалуйста, смотрите ниже его содержание:
Для тех, кто не знаком с настройкой webpack, позвольте мне вкратце объяснить все параметры, настроенные в приведенном выше примере. Сначала мы устанавливаем параметр mode
, который сообщает webpack использовать встроенные оптимизации.
В разделе entry
мы настраиваем точку входа пакета. Как видите, в настоящее время у нас есть только одна точка входа с именем client
, и она указывает на файл client.js
. Обратите внимание, что перед загрузкой этого файла мы также загружаем библиотеку babel-polyfill
.
Затем в разделе output
мы можем определить место размещения выходных файлов. Здесь мы устанавливаем для целевой папки значение build
, а имя файла - [name].js
, где [name]
- это заполнитель, который будет заменен именем точки входа (в нашем случае это просто client
).
Следующий раздел называется module
. Здесь мы можем настроить все загрузчики, которые будут использоваться для преобразования всех указанных типов файлов. В нашем случае мы используем babel-loader
для преобразования файлов *.js
и *.jsx
.
В конце концов настраиваем плагины и оптимизацию. Плагин HtmlWebpackPlugin
используется для вставки выходного связанного файла JavaScript в файл index.html
(мы обсуждали это ранее). В разделе optimization
мы говорим webpack поместить всех поставщиков (пакеты, импортированные из node_modules
) в отдельный vendor.js
файл.
Хорошо, теперь мы можем протестировать нашу конфигурацию. Мы можем сделать это, вызвав следующую команду (находящуюся в корневой папке проекта):
Это создаст каталог build
со всеми необходимыми файлами. Если вы хотите протестировать его, просто войдите в эту папку и запустите какой-нибудь локальный веб-сервер, например, на Mac вы можете запустить простой сервер Python:
Теперь откройте свой веб-браузер и откройте страницу http: // localhost: 8000.
Серверный рендеринг
В решении, которое я только что описал, мы просто вставляем сценарий, сгенерированный веб-пакетом, в файл index.html
. Когда веб-браузер отображает HTML-файл, внедренный сценарий загружается и затем немедленно вызывается. В результате дерево компонентов React переводится в элементы DOM и помещается в контейнер «приложение».
Теперь пора добавить в наше приложение рендеринг на стороне сервера. Наша цель - переместить последнюю часть (рендеринг и размещение в контейнере) на сервер. Благодаря этому браузер получит файл index.html
, уже выполненный должным образом, и пользователю не придется ждать загрузки скриптов.
Прежде чем мы начнем настраивать нашу конфигурацию, мы должны установить некоторые зависимости. Во-первых, «разработчик»:
Далее общий для обеих сред:
Позже я объясню, зачем они нам нужны.
Компонент Html React
В подходе только для клиента для подготовки файла index.html
мы использовали HtmlWebpackPlugin
. Поскольку теперь мы хотим отобразить все дерево компонентов React на сервере, мы должны использовать более сложный подход. Вот почему я представлю компонент Html.js
, который заменит файл index.html
. Пожалуйста, посмотрите ниже, как я это реализовал:
Вышеупомянутый компонент отображает более или менее ту же структуру HTML, что и index.html
. Он также получает два реквизита: children
и scripts
. Содержимое первого вводится в контейнер «приложение». Мы должны использовать атрибут dangerouslySetInnerHTML
, потому что children
содержит разметку HTML в виде строки, и мы не хотим, чтобы она экранировалась.
Опора script
- это массив URL-адресов. Мы сопоставляем их с серией script
элементов. Таким образом, мы прикрепим к нашему приложению все клиентские скрипты, сгенерированные webpack.
Если у нас есть компонент Html.js
, мы можем безопасно удалить файл index.html
.
Наконец: рендеринг на сервере
Теперь мы можем перейти к самой важной части этой статьи. Давайте создадим файл server.js
. Он будет точкой входа в приложение и будет вызываться на сервере на основе Node.js. Чтобы упростить работу с его сетевыми возможностями, мы будем использовать фреймворк ExpressJS (мы добавили его в наш проект несколько минут назад).
Хорошо, взгляните на файл server.js
:
Посмотрим, что у нас получилось ... Прежде всего, обратите внимание на импорт ReactDOMServer
из пакета react-dom/server
. Это позволяет нам отображать дерево компонентов React на сервере. Мы также импортируем несколько других пакетов, а также два уже созданных компонента (Html
и App
).
Следующее, что нужно сделать, это вызвать экспресс-метод и присвоить его результат константе app
. Таким образом мы инициализируем платформу ExpressJS. В следующей строке мы сообщаем express, где размещаются наши статические файлы (например, скрипты, сгенерированные webpack).
В следующей строке (вызов app.get
) мы начинаем наиболее захватывающую часть нашего примера, но мы обсудим ее чуть позже. Во-первых, давайте взглянем на последнюю строку - вызывая метод listen
объекта app
, мы запускаем все приложение - оно начинает прослушивать порт 3000.
Хорошо, теперь давайте вернемся к упомянутому выше вызову app.get
. Таким образом, мы можем обрабатывать запросы GET, отправленные веб-браузером. В качестве первого параметра вызова мы передаем адрес, который хотим обработать. В нашем случае это звездочка - это означает, что функция обратного вызова, переданная в качестве второго параметра вызова, будет вызываться для каждого запроса GET.
Функция обратного вызова принимает два параметра: req
(объект запроса) и res
(объект ответа). Мы воспользуемся вторым из них в конце метода, но сначала произойдет несколько вещей.
Прежде всего, мы определяем массив, содержащий пути к скриптам, сгенерированным webpack. Мы передадим его компоненту Html
- вы можете помнить, что там мы сопоставляем его с серией тегов script
.
Во-вторых, мы вызываем метод renderToString
объекта ReactDOMServer
. Мы передаем компонент App
как параметр этого метода. Обратите внимание, какой текст мы присваиваем его атрибуту initialText
. Таким образом, мы преобразуем все приложение React в строку и присваиваем ее константе appMarkup
.
В-третьих, мы вызываем другой метод объекта ReactDOMServer
- renderToStaticMarkup
. Он работает почти так же, как метод renderToString
. Единственное отличие состоит в том, что renderToStaticMarkup
опускает все атрибуты HTML, которые React добавляет в DOM во время рендеринга. Мы передаем компонент Html
в качестве параметра вызова. Он принимает массив children
и scripts
через свои атрибуты. Таким образом, мы оборачиваем дерево компонентов React, которое мы только что визуализировали, телом HTML и сохраняем его как строку в константе html
.
Наконец, мы просто вызываем метод send
объекта res
. Таким образом мы отправляем полностью отрисованное приложение в браузер.
Изменения в конфигурации webpack
Файл server.js
импортирует компоненты React и использует синтаксис ES6 +. Вот почему нам тоже нужно запускать его через webpack. См. Ниже измененный файл webpack.config.js
:
Короче говоря, давайте сосредоточимся здесь только на самом важном.
В приведенном выше примере у нас есть две конфигурации: clientConfig
для клиента и serverConfig
для сервера. Более того, у нас есть объект common
, который содержит общую часть конфигурации. Обратите внимание, как мы экспортируем нашу конфигурацию в конец файла - используя массив, мы можем передать более одной настройки в webpack.
Что касается клиентской части конфигурации, то почти ничего не изменилось - единственное, что использовать HtmlWebpackPlugin
больше нет необходимости. Кроме того, было добавлено свойство node
- с помощью этой опции мы можем настроить, следует ли использовать полифил или имитировать определенные глобальные объекты и модули Node.js. Также обратите внимание, как свойства name
и target
определяют цель вывода.
Теперь давайте посмотрим на серверную часть конфигурации. Как видите, мы разделяем конфигурацию загрузчиков с конфигурацией клиента (...common
). На этот раз свойство target
указывает на «узел». Мы также добавили параметр externals
, определяющий все зависимости, которые необходимы во время сборки, но не нужны в выходном файле (благодаря библиотеке webpack-node-externals
нам не нужно делать это самостоятельно). Конечно, самыми важными являются два свойства: entry
и output
. Помимо использования полифиллов babel, мы установили точку входа в файл server.js
, который мы только что определили несколько минут назад. Пакет вывода будет помещен в каталог build
.
Совместное использование исходного состояния
Хорошо, теперь, когда у нас есть приложение, настроенное как для сервера, так и для клиента, мы можем проверить, как оно работает. Давайте удалим папку bulild
(просто чтобы убедиться, что мы начали с чистой сборки) и вызовем следующую команду:
Приведенная выше команда соберет и серверный, и клиентский код, поместит его в папку build
и запустит приложение. Теперь откройте адрес http: // localhost: 3000 в своем браузере и посмотрите результат.
Это не совсем то, чего мы хотели достичь, верно? Когда браузер загружает наше приложение, он сначала отображает версию на стороне сервера (вы можете видеть текст «отображается на сервере» в течение миллисекунд), а затем запускает код на стороне клиента, который заменяет исходный текст на «отображаемый на клиенте». ”
Чтобы исправить это, мы должны поделиться исходным состоянием нашего приложения между сервером и клиентом. Для этого изменим код компонента Html.js
, как показано ниже:
Здесь были добавлены две вещи: во-первых, мы передали через props дополнительное свойство - initialState
; во-вторых, мы добавили скрипт, который конвертирует его в формат JSON и присваивает его свойству window.APP_STATE
.
Конечно, только что добавленный скрипт будет вызываться после отображения страницы в браузере, поэтому нам не нужно бояться, что объект window
не определен.
Пришло время также изменить файл server.js
:
Давайте взглянем на то, что здесь изменилось… Во-первых, был представлен объект initialState
. Во-вторых, мы передали его в компоненты App.js
и Html.js
.
Вы можете задаться вопросом, что означает следующее утверждение:
Это просто использование оператора распространения для передачи всех свойств объекта initialState
в качестве атрибутов компонента App.js
. Таким образом, приведенное выше утверждение аналогично приведенному ниже:
Последнее, что мы сделали, - это передали весь объект initialState
компоненту Html.js
. Таким образом, он будет добавлен к свойству window.APP_STATE
, как описано ранее.
В нашем проекте осталось сделать еще одно. Нам нужно получить значение начального состояния в клиентском коде. Мы можем сделать это, изменив файл client.js
:
Как мы уже знаем, этот код будет вызван в браузере, поэтому мы можем быть уверены, что объект window
доступен. Мы ожидаем, что здесь определено свойство APP_STATE
, и с помощью оператора распространения мы передаем все его свойства как атрибуты компонента App.js
.
Обратите внимание на второе различие, которое мы здесь ввели. Мы заменили метод render
объекта ReactDOM
вызовом метода hydrate
. Если мы предварительно визуализируем код React на сервере и хотим связать его с кодом React на стороне клиента, мы должны использовать метод hydrate
- иначе мы получим ошибку в консоли.
Хорошо, все готово! Вы можете проверить это еще раз, позвонив webpack && node ./build/server.js
и открыв в браузере http: // localhost: 3000. На этот раз вы увидите текст отображается на сервере, как и ожидалось.
Резюме
Надеюсь, что, несмотря на объем этого текста, я все разумно объяснил. Я думаю, что это довольно просто, и я надеюсь, что у вас не возникнет проблем с его использованием в ваших целях.
Пример, который мы сегодня обсуждали, доступен в моем репозитории GitHub. Я рекомендую вам клонировать его и поиграть с ним самостоятельно.
В следующем выпуске серии «Рендеринг на стороне сервера в React» я планирую объяснить, как обрабатывать Redux в нашем примере SSR.
P.S. Этот пост является частью серии статей о рендеринге на стороне сервера с использованием React. Пожалуйста, смотрите список всех статей этой серии ниже:
- Серверный рендеринг в React - введение
- Серверный рендеринг в React - Express.js
- Серверный рендеринг в React - Redux
- Серверный рендеринг в React - response-router
- Рендеринг на стороне сервера в React - работа с реальными данными
Примечание от JavaScript In Plain English:
Мы всегда заинтересованы в продвижении качественного контента. Если у вас есть статья, которую вы хотели бы отправить в JavaScript In Plain English, отправьте нам электронное письмо по адресу [email protected] с вашим именем пользователя Medium, и мы добавим вас в качестве автора.