Как настроить приложение 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. Пожалуйста, смотрите список всех статей этой серии ниже:

Примечание от JavaScript In Plain English:

Мы всегда заинтересованы в продвижении качественного контента. Если у вас есть статья, которую вы хотели бы отправить в JavaScript In Plain English, отправьте нам электронное письмо по адресу [email protected] с вашим именем пользователя Medium, и мы добавим вас в качестве автора.