Готовый к продакшену Vue SSR: 5 простых шагов
Я работаю в компании Namecheap на позиции Senior Software Engineer. В нашей компании мы используем Vue.js с серверным рендерингом для некоторых наших страниц. Настроить SSR может быть не так легко, поэтому я попытался описать этот процесс простыми шагами. Также, читая официальную документацию, можно подумать, что было бы полезно увидеть как приложение должно выглядеть в итоге. Поэтому я создал репозиторий с примером.
В этой статье мы рассмотрим, как настроить готовый к продакшену SSR для Vue-приложения, используя:
- Webpack 4;
- Babel 7;
- Node.js Express сервер;
- webpack-dev-middleware и webpack-hot-middleware для удобной разработки;
- Vuex для управления состоянием приложения;
- плагин vue-meta для управления метаданными.
Отмечу, что мы не будем останавливаться на деталях использования этих технологий, а сосредоточимся только на SSR. Надеюсь, это будет полезно. Поехали!
Шаг 1. Настройка webpack
Сейчас у вас, вероятно, уже есть какое-то Vue-приложение, а если нет, то можете использовать мой репозиторий в качестве отправной точки. Для начала взглянем на структуру наших папок и файлов:
Как видите, структура довольно распространенная, за исключением пары вещей, которые могут бросаться в глаза:
- есть два отдельных webpack-конфига для клиентского и серверного билдов:
webpack.client.config.js
иwebpack.server.config.js
; - есть две соответствующие точки входа:
client-entry.js
иserver-entry.js
.
Это на самом деле ключевой момент конфигурации нашего приложения. Вот отличная схема из официальной документации с обзором архитектуры, которую мы реализуем:
С клиентским конфигом, вероятно, вы уже имели дело. Он предназначен для сборки нашего приложения в простые JS- и CSS-файлы.
Серверный конфиг более интересен. С его помощью мы создадим специальный json-файл, который будет использоваться на стороне сервера для рендеринга простого HTML-кода нашего Vue-приложения. С этой целью мы используем vue-server-renderer/server-plugin
.
Еще одно отличие от клиентского конфига заключается в том, что здесь не нужно обрабатывать CSS-файлы, поэтому в нем нет соответствующих лоадеров и плагинов.
Как вы могли догадаться, все общие настройки клиентского и серверного конфигов мы вынесли в базовый конфиг.
Шаг 2. Создание точек входа
Перед тем как приступить к созданию клиентской и серверной точек входа в приложение, предлагаю взглянуть на файл app.js
:
Обратите внимание: вместо создания экземпляра приложения мы экспортируем фабричную функцию createApp()
. Если бы приложение работало только в браузере, то нам не пришлось бы беспокоиться о том, чтобы пользователи получали новый экземпляр Vue для каждого запроса. Но поскольку мы создаем приложение в Node.js процессе, наш код будет инициализирован один раз и останется в памяти того же контекста. Поэтому если мы будем использовать один экземпляр Vue для нескольких запросов, это может привести к ситуации, когда один пользователь получит состояние приложения другого. Чтобы избежать этого, мы должны создавать новый экземпляр приложения для каждого запроса. По этой же причине не рекомендуется использовать синглтоны с состоянием во Vue-приложении.
Каждое приложение, скорее всего, будет иметь какие-то метаданные, например title или description, которые должны отличаться на разных страницах. Вы можете реализовать это с помощью плагина vue-meta. Чтобы узнать, почему мы используем параметр ssrAppId
, перейдите по этой ссылке.
В клиентской точке входа мы вызываем createApp()
, передавая начальное состояние, установленное сервером, и монтируем приложение в DOM, после того как роутер завершил начальную навигацию. Также в этом файле вы можете импортировать глобальные стили и инициализировать директивы или плагины, которые работают с DOM.
Серверная точка входа в значительной степени описана в комментариях. Единственное, что я хотел бы добавить в отношении коллбэка router.onReady()
: если мы используем хук serverPrefetch
для предварительного получения данных в каких-то компонентах, он ждет, пока не зарезолвится промис, возвращаемый из хука. Мы увидим пример его использования чуть позже.
Хорошо, теперь мы можем добавить в package.json
скрипты для сборки нашего приложения:
Шаг 3. Запуск Express-сервера с Bundle Renderer
Чтобы преобразовать наше приложение в простой HTML на стороне сервера, мы будем использовать модуль vue-server-renderer
и файл ./dist/vue-ssr-server-bundle.json
, который мы сгенерировали, запустив скрипт build:server
. Давайте пока не будем думать о режиме разработки, обсудим это на следующем шаге.
Сначала нам нужно создать рендерер, вызвав метод createBundleRenderer()
и передав два аргумента: бандл, сгенерированный нами ранее, и следующие параметры:
runInNewContext
Помните проблему с общим состоянием между несколькими запросами, которую мы обсуждали на предыдущем шаге? Эта опция решает проблему, но создание нового контекста V8 и повторное построение бандла для каждого запроса является дорогостоящей операцией, поэтому рекомендуется установить этот флаг в значение false из-за возможных проблем с производительностью и остерегаться использования в приложении синглтонов с состоянием.
template
Специальный комментарий <!--vue-ssr-outlet-->
будет заменен на HTML, сгенерированным рендерером. И кстати, используя опцию template, рендерер автоматически добавит скрипт с объявлением глобальной переменной __INITIAL_STATE__
, которую мы используем в client-entry.js
при создании своего приложения.
Теперь, когда у нас есть экземпляр рендерера, мы можем сгенерировать HTML, вызвав метод renderToString()
и передав начальное состояние и текущий URL для роутера.
Шаг 4. Настройка dev-окружения
Что нам нужно для комфортной разработки Vue-приложения с SSR? Я бы сказал, следующее:
- запускать только один Node.js сервер без использования дополнительного
webpack-dev-server
; - регенерировать
vue-ssr-server-bundle.json
файл при каждом изменении исходного кода; - hot reloading.
Чтобы реализовать все эти вещи, можно воспользоваться функцией setupDevServer()
в server.js
файле (см. предыдущий шаг).
Эта функция принимает два аргумента:
app
— наше Express-приложение;onServerBundleReady()
— callback, который вызывается каждый раз при изменении исходного кода и создании новогоvue-ssr-server-bundle.json
. Он принимает бандл в качестве аргумента.
В файле server.js
мы передаем callback onServerBundleReady()
в виде стрелочной функции, которая принимает новый бандл и заново создает рендерер.
Обратите внимание: мы рекваерим все зависимости внутри функции setupDevServer()
, нам не нужно, чтобы они занимали память процесса в production-моде.
Теперь давайте добавим npm-скрипт для запуска сервера в дев-моде с использованием nodemon
:
"dev": "cross-env NODE_ENV=development nodemon ./server.js",
Шаг 5. Использование serverPrefetch()
Скорее всего, вам потребуется получать какие-то данные с сервера во время инициализации приложения. Вы можете сделать это, просто вызвав API-эндпойнт после маунта рутового компонента, но в этом случае ваш пользователь должен будет наблюдать спиннер — не самый лучший UX. Вместо этого мы можем получить данные во время SSR, используя хук компонента serverPrefetch()
, который был добавлен в версии 2.6.0 во Vue. Давайте добавим тестовый эндпойнт в наш сервер.
Мы вызовем этот эндпойнт в экшене getUsers
. Теперь давайте рассмотрим пример использования хука serverPrefetch()
в компоненте:
Как видите, мы используем serverPrefetch()
вместе с хуком mounted()
. Нам это нужно в тех случаях, когда пользователь переходит на эту страницу с другого роута на стороне клиента, поэтому массив users
будет пуст и мы вызываем API.
Также обратите внимание, как определяются title- и description-метаданные для конкретной страницы в свойстве metaInfo, предоставляемом плагином vue-meta.
Ну вот и все. Я думаю, что основные моменты настройки SSR для Vue.js рассмотрены, и надеюсь, что эти шаги помогли вам лучше понять весь процесс.