«Удачи тем, кто пытается использовать серверные приложения с разделением кода».
- Райан Флоренс, соавтор React Router

Вызов принят.

Немного истории о серверном рендеринге на Airbnb

Исторически Airbnb был приложением на Rails. Несколько лет назад ситуация начала меняться, мы начали использовать Rails просто как уровень данных, и вся логика рендеринга начала мигрировать в JavaScript в форме React. Для поддержки серверного рендеринга мы создали и открыли исходный код Hypernova, JavaScript-рендеринга как услуги… сервиса.

Сделав шаг вперед, мы представили маршрутизацию на стороне клиента и разделение кода на основе маршрутов с помощью React Router v3 в рамках нашей модернизации архитектуры. Это то, что обеспечило плавные переходы между страницами и меньшую загрузку начальной страницы.

Введите React Router v4

Серверный рендеринг + разделение кода сводятся к одному требованию. Чтобы они работали вместе, вы должны иметь возможность сопоставить с вашим текущим маршрутом перед рендерингом.

(Остальная часть этого поста довольно насыщена кодом, если вы еще не знакомы с RRv4, найдите время, чтобы прочитать превосходную документацию по React Router. Кроме того, не помешало бы просмотреть документация по разделению кода webpack .

Проблема в том, что React Router v4 переключился с централизованной конфигурации маршрута (с функцией getComponent для асинхронной загрузки) на децентрализованную версию. Маршруты теперь определены встроенным образом следующим образом:

Такое определение маршрутов означает, что вы не будете знать, какие маршруты / компоненты необходимы для визуализации вашей страницы, пока вы на самом деле не произведете рендеринг. Чтобы продемонстрировать, почему это проблема, представьте, что мы асинхронно загружаем About компонент.

Когда мы выполняем рендеринг на сервере, мы хотим отобразить весь контент, поэтому HTML-код для компонента About будет сгенерирован и вставлен в дерево DOM. На стороне клиента, однако, мы не узнаем, соответствует ли маршрут /about, и не узнаем, что нужно загрузить компонент About, пока не войдем в цикл рендеринга. Это вызовет ошибку несовпадения клиент / сервер, поскольку без загрузки компонента About клиент не создаст тот же HTML-код, что и сервер. Это также, вероятно, означает мигание контента и потраченный впустую рендеринг, что ухудшает восприятие ваших пользователей.

Повторная централизация маршрутов

Чтобы решить уникальные проблемы с встраиванием маршрутов, они создали react-router-config. Это позволит вам продолжить определение ваших маршрутов в централизованном месте и сопоставить их, прежде чем запускать начальный рендеринг. Используя библиотеку, наше определение маршрутов может выглядеть примерно так:

Децентрализация наших рецентрализованных маршрутов

(Я знаю, о чем вы думаете ...)

Использование react-router-config - это здорово, но предстоит еще немного поработать. react-router-config, похоже, не поддерживает асинхронную загрузку компонентов, а дочерние маршруты должны быть слишком явными. Обратите внимание, что все значения пути определены как полные пути.

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

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

Это чрезвычайно мощный инструмент, особенно для больших кодовых баз. Маршруты можно повторно использовать в нескольких местах, а логика компонентов может быть независимой от маршрута, поэтому их повторное использование также увеличивается. Это то, что делает возможным наш текущий переход в большое одностраничное приложение (SPA), поскольку мы можем добавлять дополнительные маршруты, не увеличивая наш основной поток. Поскольку наш SPA со временем разрастается и включает в себя все больше страниц продуктов, мы можем быть уверены, что каждая отдельная страница содержит только то, что необходимо.

Определение асинхронного маршрута

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

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

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

Обеспечение готовности маршрутов

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

Собираем все вместе

Теперь, когда все готово, мы, наконец, готовы к рендерингу нашего приложения. Вот как все выглядит в связке (за вычетом определений вспомогательных функций ensureReady, generateAsyncComponent и convertCustomRouteConfig)

Демо!

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

Что дальше

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

  • Открытие вопросов / запросов на вытягивание в react-router-config в надежде упростить этот процесс для других.
  • Изучаем, как уменьшить размер гидратации по мере того, как все больше страниц втягивается в наш SPA.

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

** Примечание по импорту ()

import(), который вы видите в приведенном выше коде, является относительно новым синтаксисом динамического импорта (в настоящее время этап 3). Этот новый синтаксис предназначен для поддержки асинхронной загрузки модулей ES. Мы используем его для замены require.ensure вызовов webpack в нашем источнике.

Чтобы помочь в этом, мы выпустили несколько преобразований babel для веб-пакетов. Динамический-импорт-веб-пакет и динамический-импортный узел.

Они могут преобразовать ваши большие require.ensure вызовы из этого:

К этому: