В новом году начнем общение с вами с затравочной статьи о серверном рендеринге (server-side rendering). В случае вашей заинтересованности возможна более свежая публикация о Nuxt.js и дальнейшая издательская работа в этом направлении
С появлением современных фреймворков и библиотек для JavaScript, которые предназначены, прежде всего, для создания интерактивных веб-страниц и одностраничных приложений, сильно изменился весь процесс показа страниц пользователю.
До пришествия приложений, полностью генерируемых на JS в браузере, HTML-разметка выдавалась клиенту в ответ на HTTP-вызов. Это могло происходить путем возврата статического HTML-файла с контентом, либо путем обработки отклика при помощи какого-либо серверного языка (PHP, Python или Java), причем, более динамическим образом.
Подобное решение позволяет создавать отзывчивые сайты, работающие гораздо быстрее стандартных сайтов, действующих по модели запрос-отклик, поскольку при работе устраняется время, проводимое запросом в пути.
Типичный отклик, отправляемый сервером на запрос к сайту, написанному на React, будет выглядеть примерно так:
<!DOCTYPE html><html lang="en"> <head> <meta charset="utf-8"> <link rel="shortcut icon" href="http://personeltest.ru/aways/habr.com/favicon.ico"> <title>React App</title> </head> <body> <div id="root"></div> <script src="http://personeltest.ru/aways/habr.com/app.js"></script> </body></html>
Выбрав этот отклик, наш браузер также выберет пакет
app.js
, содержащий наше приложение, и через одну-две
секунды полностью отобразит страницу.В этот момент уже можно воспользоваться встроенным в браузер инспектором HTML, чтобы просмотреть весь отображенный HTML. Однако, заглянув в исходный код, мы не увидим ничего кроме HTML, приведенного выше.
Почему это проблема?
Хотя такое поведение и не доставит проблем большинству наших пользователей либо при разработке приложения, оно может стать нежелательным, если:
- Конечный пользователь работает с медленным интернет-соединением, что особенно актуально для мобильных устройств,
- Конечный пользователь работает с маломощным устройством, например, со смартфоном старого поколения
Если с демографической точки зрения ваша целевая аудитория относится к одной из этих групп, то работать с сайтом будет неудобно в частности, пользователям придется довольно долго ждать, уставившись в надпись Loading (или того хуже в пустой экран).
Ладно, но в демографическом отношении моя целевая аудитория точно не относится ни к одной из этих групп, так стоит ли мне волноваться?
Есть еще две вещи, которые следует учитывать при работе с приложением, рендеринг в котором выполняется на стороне клиента: поисковики и присутствие в социальных сетях.
Сегодня из всех поисковиков лишь Google обладает некоторыми возможностями отобразить и сайт и учесть его JS прежде, чем отобразить страницу. Кроме того, хотя Google и сможет отобразить индексную страницу вашего сайта, известно, что могут возникать проблемы с навигацией по сайтам, на которых реализован роутер.
Это означает, что вашему сайту будет очень непросто забраться в топ выдачи любого поисковика кроме Google.
Та же проблема просматривается и в социальных сетях, например, в Facebook если ссылкой на ваш сайт поделятся, то ни его название, ни картинка-превью не отобразятся как следует.
Как решить эту проблему
Есть несколько способов ее решения.
A Попробуйте оставить все ключевые страницы вашего сайта статическими
Когда создается сайт-платформа, куда пользователю придется входить под своим логином, а без входа в систему контент посетителю не предоставляется, можно попробовать оставить статическими (написанными на HTML) общедоступные страницы вашего сайта, в частности, индекс, о нас, контакты и не использовать JS при их отображении.
Поскольку ваш контент ограничен требованиями по входу в систему, он не будет индексироваться поисковиками, и им нельзя будет поделиться в социальных сетях.
B Генерируйте части вашего приложения в виде HTML-страниц в процессе сборки
В проект можно добавить такие библиотеки как react-snapshot; они используются для генерации HTML-копий страниц вашего приложения и для сохранения их в специально предназначенном каталоге. Затем этот каталог развертывается наряду с пакетом JS. Таким образом, HTML будет подаваться с сервера вместе с откликом, и ваш сайт увидят в том числе те пользователи, у которых отключен JavaScript, а также заметят поисковики и т.д.
Как правило, сконфигурировать react-snapshot не составляет труда: достаточно добавить библиотеку в ваш проект и изменить сборочный скрипт следующим образом:
"build": "webpack && react-snapshot --build-dir
static"
Недостаток такого решения заключается в следующем: весь контент, который мы хотим сгенерировать, должен быть доступен во время сборки мы не можем обратиться к каким-либо API, чтобы получить его, мы также не можем заранее сгенерировать контент, зависящий от данных, предоставляемых пользователем (например, от URL).
C Создать на JS приложение, использующее серверный рендеринг
Один из важнейших выигрышных моментов у современного поколения приложений на JS заключается в том, что их можно запускать как на клиенте (в браузере), так и на сервере. Так удается генерировать HTML для страниц, являющихся более динамичными, таких, чей контент в период сборки еще не известен. Подобные приложения часто называют изоморфными или универсальными.
Два наиболее популярных решения, обеспечивающих серверный рендеринг для React:
- next.js github.com/zeit/next.js
- Gatsby github.com/gatsbyjs/gatsby
Создайте собственную реализацию SSR
Важно: если вы собираетесь попробовать самостоятельно создать собственную реализацию SSR для приложений на React, то должны будете обеспечить работу node-бэкенда для вашего сервера. Вы не сможете развернуть это решение на статическом хосте, как в случае со страницами github.
Первым делом нам понадобится создать приложение, точно как в случае с любым другим приложением React.
Давайте создадим входную точку:
// index.jsimport React from 'react';import { render } from 'react-dom';import App from './App.js';render(<App />, document.getElementById('root'));
И компонент-приложение (App):
// App.jsimport React from 'react';const App = () => { return ( <div> Welcome to SSR powered React application! </div> );}
А также оболочку, чтобы загрузить наше приложение:
// index.html<!doctype html><html> <head> <meta charset="utf-8" /> </head> <body> <div id="root"></div> <script src="http://personeltest.ru/aways/habr.com/bundle.js"></script> </body></html>
Как видите, приложение получилось довольно простым. В рамках этой статьи мы не будем пошагово разбирать все шаги, необходимые для генерации правильной сборки webpack+babel.
Если запустить приложение в его текущем состоянии, то на экране появится сообщение-приветствие. Просмотрев исходный код, вы увидите содержимое файла
index.html
, но приветственного
сообщения там не будет. Для решения этой проблемы добавим серверный
рендеринг. Для начала добавим 3 пакета:yarn add express pug babel-node --save-dev
Express это мощный веб-сервер для node, pug движок-шаблонизатор, который можно использовать с express, а babel-node это обертка для node, обеспечивает транспиляцию на лету.
Сначала скопируем наш файл
index.html
и сохраним его
как index.pug
:// index.pug
<!doctype html>
<html>
<head>
<meta charset=utf-8 />
</head>
<body>
<div id=root>!{app}</div>
<script src=bundle.js></script>
</body>
</html>
Как видите, файл практически не изменился, не считая того, что теперь в HTML вставлено
!{app}
. Это переменная
pug
, которая впоследствии будет заменена реальным
HTML.Создадим наш сервер:
// server.jsimport React from 'react';import { renderToString } from 'react-dom/server';import express from 'express';import path from 'path';import App from './src/App';const app = express();app.set('view engine', 'pug');app.use('/', express.static(path.join(__dirname, 'dist')));app.get('*', (req, res) => { const html = renderToString( <App /> ); res.render(path.join(__dirname, 'src/index.pug'), { app: html });});app.listen(3000, () => console.log('listening on port 3000'));
Разберем этот файл по порядку.
import { renderToString } from 'react-dom/server';
Библиотека react-dom содержит отдельный именованный экспорт
renderToString
, работающий подобно известному нам
render
, но отображает не DOM, а HTML в виде
строки.
const app = express();app.set('view engine', 'pug');app.use('/', express.static(path.join(__dirname, 'dist')));
Мы создаем новый инстанс сервера express и сообщаем ему, что собираемся использовать движок-шаблонизатор
pug
. В
данном случае мы могли бы обойтись обычным считыванием файла и
выполнением операции поиск с заменой, но такой подход в самом деле
неэффективен и может спровоцировать проблемы из-за множественного
доступа к файловой системе, либо проблемы с кэшированием.В последней строке мы приказываем express искать файл в каталоге
dist
, и, если запрос (напр. /bundle.js
)
совпадает с файлом, присутствующем в этом каталоге, то выдать его в
ответ.
app.get('*', (req, res) => {});
Теперь мы приказываем express добавить обработчик на каждый несовпавший URL в том числе, на наш несуществующий файл
index.html
(как вы помните, мы переименовали его в
index.pug
, и его нет в каталоге
dist
).
const html = renderToString( <App />);
При помощи
renderToString
мы отображаем наше
приложение. Код выглядит точно как у входной точки, но такое
совпадение не является обязательным.
res.render(path.join(__dirname, 'src/index.pug'), { app: html});
Теперь, когда у нас есть отображенный HTML, мы приказываем express отобразить в ответ файл
index.pug
и заменить
переменную app
тем HTML, что мы получили.app.listen(3000, () => console.log('listening on port
3000'));
Наконец, мы обеспечиваем запуск сервера и настраиваем его так, чтобы он слушал порт 3000.
Теперь нам осталось всего лишь добавить нужный скрипт в
package.json
:
"scripts": { "server": "babel-node server.js"}
Теперь, вызвав
yarn run server
, мы должны получить
подтверждение, что сервер действительно работает. Переходим в
браузере по адресу localhost:3000, где мы,
опять же, должны увидеть наше приложение. Если мы просмотрим
исходный код на данном этапе, то увидим:
<!doctype html><html> <head> <meta charset="utf-8" /> </head> <body> <div id="root">div data-reactroot="">Welcome to SSR powered React application!</div></div> <script src="bundle.js"></script> </body></html>
Если все выглядит именно так, это означает, что серверный рендеринг работает как положено, и можно приступать к расширению вашего приложения!
Зачем же нам по-прежнему нужен bundle.js?
В случае такого крайне простого приложения, которое рассмотрено здесь, включать bundle.js не обязательно без этого файла наше приложение все равно останется работоспособным. Но в случае с реальным приложением включить этот файл все-таки потребуется.
Это позволит браузерам, умеющим обрабатывать JavaScript, взять работу на себя и далее взаимодействовать с вашей страницей уже на стороне клиента, а тем, что не умеют разбирать JS перейти на страницу с нужным HTML, который возвратил сервер.
О чем необходимо помнить
Притом, что серверный рендеринг выглядит достаточно незамысловато, при разработке приложений нужно обращать внимание на некоторые темы, на первый взгляд не вполне очевидные:
- Любое состояние, сгенерированное на стороне сервера, не будет
передаваться в состояние клиентского приложения. Это означает, что,
если ваша серверная часть выберет некоторые данные и использует их
для отображения HTML, то эти данные не попадут в
this.state
, которое увидит браузер componentDidMount
не вызывается на сервере это означает, что не будут вызываться никакие операции по выборке данных, которые вы привыкли там размещать. В принципе, это хорошо, поскольку вы должны предоставлять нужные вам данные в виде пропсов. Помните, что отображение нужно отложить (вызвавres.render
) до тех пор, пока данные не будут выбраны. Из-за этого посетители могут заметить некоторые задержки в работе сайта- если вы собираетесь использовать роутер react (напр. @reach/router или react-router) то должны убедиться, что приложению передается правильный URL, когда оно отображается на сервере. Обязательно почитайте об этом в документации!