strokoff

Готовим Vue SSR c 0 до production

Сегодняшнюю статью я хочу посвятить обзору одного подхода, что практикую при сборке проектов в так называемый enterprise — под этим я подразумеваю, что вы пишите серьезный проект для бизнеса, где важно соблюдать общепринятые практики, поддерживать качество кода, обеспечивать тестирование и т.п.. Сегодня, мы будем собирать Vue приложение с применением SSR технологии, но не просто, как во всех уроках на коленке и localhost:3000, а подготовим настоящее действительно работающее приложение с учетом множества тонкостей. Т.к. статья рассчитана на middle+ фронтов, мы опустим тонкие devops\sysops настройки и попробуем обойтись без таких крутых и мощных вещей как kubernets или docker, будем разворачиваться так скажем на голом сервере. Реализованную по итогу статьи архитектуру легко обернуть или перенести в свой кластер или контейнер. Если вы решитесь повторить описанное в статье (много листинга кода я выложу) полезно будет параллельно держать открытым официальное vue руководство по ssr.  Осторожно! Много кода и конфигов!

Структура проекта

Мы не будем пользоваться такими инструментами как vue-cli и вообще шаблонами, будем по хардкору все настраивать сами и постигать тонкости построения фронта самостоятельно.Настроить все самому руками — единственный верный путь, если вы делаете что-то серьезное и хотите уметь поддерживать и развивать ваш продукт в дальнейшем, то без понимания его постройки не обойтись. Давайте обозначим нашу структуру проекта:

Сборка проекта

Первое, с чего мы начнем, это со сбора нашего проекта, давайте посмотрим на конфигурацию вебпака

Клиентская часть

Серверная часть

В целом конфигурация вебпака достаточно типичная, разница для сервера и клиента будет в наличии плагина Server\Client Vue plugin, как и описано в официальном руководстве. Также обратим внимание на стили, мы используем sass+postcss с некоторыми фичами, такие как sass-resources-loader, который позволяет импортировать переменные\миксины и прочее во все файлы компонентов без дополнительных инклудов. Eще одна интересная фича в options sass loader’a, которая выглядит так — data: Object.keys(sassVars).map(key => ‘$’ + key + «: » + ‘»‘ + sassVars[key] + ‘»;’).join(» «) — что происходит в этой строке? Мы импортируем в SASS не только общий контекст из 1 файла _context.sass. но и добавляем возможность прокинуть переменные из JS на момент постройки проекта. Для работы SSR это понадобится, но в определенных кейсах весьма полезным оказывается такая возможность. Ну и в примере сразу подключен SVG sprite loader и написан маленький компонент иконок, который к тому-же еще и кешируется на cтороне сервера.
Пример реализации компонента иконок

Запуск команд в проекте

В фронтенд индустрии очень часто можно увидеть использование package.json для хранения исполняемых команд для сборки этого самого package, это верно только для библиотек, для проекта такой подход выглядит также, как копать чайной ложкой котлован. Из мощных инструментов для управления командами рекомендую Makefile\Taskfile и просто bash) в нашем варианте будем использовать Taskfile это мультиплатформенная мощная утилитка для описания команд. Посмотрим на пример taskfile для проекта


Теперь можно увидеть доступные команды для разработчика набрав в терминале task по умолчанию в терминал выведется список команд, для которых указан desc. Не будем подробно рассматривать все возможности таскфайла, оставноимся на одном некрасивом месте, поясню: export $(node -e «const ENV = require(‘./config/env.{{.ENV}}.js’); for(const key in ENV) console.log(key + ‘=’ + ENV[key])») && ./node_modules/.bin/webpack —progress —colors —config build/webpack.client.js — данная строка запускает сборку вебпака переворачивая переменные из env.js файла в обычные envoirment переменные в терминальной команде, вы можете легко переделать этот момент на .env или иной подход, но в этом проекте используется pm2 который описывает env переменные в ecosystem.js. Теперь нам доступны команды task up которая поднимет нам dev окружение в котором мы будем разрабатывать, также доступны команда билда task build-development\staging\production.

Настраиваем dev окружение

Теперь поговорим о dev окружении и вообще о понятии окружения проекта. Все привыкли к тому, что существует dev и prod сборка и от части это верно, но только для сборки кода, т.е. когда например в dev виде ваш код имеет console.logs в сборке включены source-maps, работает режим HMR и нет необходимости в минификации кода. В production режиме же нам не нужен HMR, мы минифицируем наш код, по возможности убираем из него все лишнее. Но существует еще понятие окружения, как места запуска проекта, на вашей локальной машине с вашим локальным nginx например или ваше приложение строится в динамическом окружении и такие параметры как HOST_NAME переопределяются в последний момент через envoirment переменные. В таком случае нам нужно разделить понятия envoirment кода и окружения. В нашем случае существует еще envoirment staging который по сути является production сборкой, но API хосты прописаны для dev версии бекенда (отдельная история). Ну и наконец production, где и код и окружение в полном понимании соответствуют названию окружения. В проекте и листингах кода можно увидеть подключение env.conf.js. Рассмотрим его листинг

Что делает этот файл? По сути ЯВНО в одном месте определяет зависимость app переменных, от env переменных, что делает связь приложения с envoirment полностью прозрачной, можно считать этот файл соглашением в проекте + комментарии в этом файле превратятся в красивую esdoc документацию и автоматически загрузятся на gitlab pages (или github, как настроите). Таким образом мы получаем еще постоянно актуальную документацию по важным моментам в коде, важные переменные это appConf.ENV — имя окружения где запускаемся и appConf.NODE_ENV — переменная которая говорит в каком виде собран код production или dev. Также в моем случае в разных окружениях могут быть разные тестовые аккаунты и счетчики со своими ID и т.п. все это выносится в env.conf.js

С понятиями окружения разобрались, рассмотрим лучше dev окружение, мне как фронту хотелось бы просто одной командой запустить проект, просто писать код и не переключаясь на браузер видеть результаты (так конечно же не бывает и даже HMR просит перезагрузки полной порой( но мы стремимся к идеалу же) для этого мы в dev режиме поднимем nodejs http сервер к которому в renderer подключим webpack hot middleware и webpack dev middleware, по сути мы делаем практически тоже самое, что и в официальном руководстве (напоминаю, каждый момент настройки SSR в этой статье был основан на официальной доке)

Листинг /build/dev-server.js

Пример основан на hackernews с поправками на то, что я делал на fastify а не express.

Код и устройство приложения

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

По сути мы просто обернули в класс MyApp связющие части нашего приложения, Store, Router, API (обсудим ниже) в один класс. И экспортируем функцию создания нашего приложения. Код очень близок к официальному руководству.

Теперь давайте посмотрим на client/index.js

В клиентской части мы импортируем собственно наше приложение, инициализируем его делаем доступ к APP глобальным в коде а также делаем глобальный доступ к клиентской реализации cookies, одна из тонкостей с которой вы столкнетесь при реализации SSR помимо необходимости иметь единый интерфейс для общения с api (это решает axios) нам нужен еще единый иниерфейс для работы с cookies и тут одной библиотекой не обойтись т.к. на клиенте и сервере разный подход к управлению cookies, рассмотрим работу с ними чуть позже, а пока просто инициализируем наш интерфейс для работы с cookies с env:»browser». После инициализации кук, смотрим есть ли кука с токеном у пользователя, и если такая есть, подставляем ее в заголовки по умолчанию для нашего единого API axios инстанса Доступ к нему APP.API, теперь при каждом запросе через апи интерфейс, будет автоматически подставляться authorization header. Реализация авторизации может выглядеть иначе, но я оставил это в коде т.к. часто об этом спрашивают.

После инициализации приложения, проверки токена, и подмены INITIAL_STATE для vuex, дожидаемся ready сотояния роутера и добавляем хуки которые будут вызывать asyncData при переходах между роутами приложения. Также в моем примере есть проверка на asyncFail это на тот случай, когда один из asyncData компонентов не ответил при рендере на сервере, а страничку отрендерить все-так хочется) в этом месте можно реализовать fallback логику подгрузки данных повторно, а потом уже кидать пользователя на страницу с ошибкой, если повторно загрузить данные не удалось. Ну и после всех махинаций просто маунтим наше приложение в DOM — APP.Vue.$mount(‘#app’); :)

Теперь рассмотрим server/index.js

На серверной стороне инициализируем fastify сервер, создаем BundleRenderer для дев режима подключая заранее подготовленный build/dev-server.js также статику в dev режиме мы также будем отдавать через fastify, а в production режиме статичные файлы у нас будет отдавать nginx, для nodejs останется только рендер страничек.

Взаимодействие с API

Взаимодействие с API в целом отдельная история для отдельной статьи, если в кратце, то я создаю ApiClass в котором инициализирую axios instance, рассмотрим пример листинга


В начале мы импортируем сам инстанc axios

А также импортируем SwaggerApi класс от которого наследуемся, в моем случае я дополнительно генерирую методы апи на основе сваггер документации, этот ход позволяет фронту держать в коде всегда актуальный наброр бекенд методов с их параметрами. Ну и в дальнейшем управление обращениями к API у нас происходит центролизованно и мы можем навешивать свою логику и интерцепторы на запросы.

Работа с cookies

Для работы с cookies, нам необходимо реализовать единый интерфейс работы с куки, на клиенте куки ставятся в document.cookies, а на сервере куки ставятся в request.cookies. В моем случае реализация достаточно простая, мы просто в зависимости от env переменной, инициализируем либо cookies клиентскую либу, либо работаем через fastify-cookies


Конфигурируем приложение

Основная часть приложения уже позади) нам осталось сконфигурировать недостающие части прежде чем мы сможем перейти к выкладке. Давайте теперь добавим конфигов для pm2 и nginx, начнем с dev версии pm2, в отличии от production режима у нас нету необходимости делать целый кластер из нод и будет достаточно одного инстанса ноды для dev рарзработки, рассмотрим config/pm2.config.js


Сам конфиг по сути ничего не содержит, он подключает уже конфигурацию окружения на основе ENV переменной, сделано это для того, чтобы при запуске команд pm2 вы могли указывать 1 ecosystem file. Рассмотрим config/pm2.development.js

Подробные инструкции по pm2 ecosystem смотрите в официальном руководстве. В нашем случае мы объявляем 1 приложение SSR в fork режиме. Ничего особенного, но давайте посмотрим на build/pm2.production.js

А вот в production режиме запустим уже 2 приложения, blue и green версию SSR. Это необходимо для того, чтобы реализовать blue\green deploy обеспечив при этом zero downtime приложения. На этом шаге важно понимать, что я описываю пример деплоя на физическом сервере где будут сразу работать blue и green приложения, но их можно разделить на blue\green контейнеры и делать переключение уже например в докере или кубере, мы же рассмотрим нативный вариант реализации с помощью pipeline и bash, иначе эта статья никогда не закончится :D . Разница между blue и green только в порте на котором они находятся, в момент деплоя в nginx будем менять цвета в конфиге и делать nginx reload (не путать с restart). Давайте рассмотрим теперь и config/nginx.conf


Я постарался максимально доходчиво расписать nginx конфиг, вы можете обратить внимание, что установлено 2 upstream и при деплое мы будем реплейсить переменную ACTIVE_SSR и APP_ROOT.

Настройка сервера

Перед тем, как мы наконец задеплоимся, я лишь поясню некоторые моменты которые вам необходимо предварительно самостоятельно настроить на сервере

  1. Реализовать доступ по SSH ключу
  2. Установить nginx и установить конфиг
  3. Создать предварительно пустые папки /app-GREEN и /app-BLUE (не обязательно, это может сделать и rsync)
  4. Добавить пользователю для деплоя возможность запускать reload nginx от sudo не вводя пароль.

Каждый из шагов очень простой и останавливаться на этом я не буду. В моем случае деплой будет идти на сервер с ubuntu 18

Deploy to production

Мы добрались до финальной части нашего проекта, а именно деплоя в прод, ниже будет приведен листинг bash скрипта он расположен в корне /deploy.sh , который пошагово выполнит деплой

Пошагово процесс выглядит так:

  1. Получили активный цвет
  2. Потушили инстанс неактивного цвета
  3. Загрузили новые файлы
  4. Подняли неактивный цвет
  5. Подменили цвет в nginx
  6. Сделали reload nginx

Профит!) точнее почти профит, на этом этапе у нас уже есть все конфиги, сборка приложения, возможности линта, генерации доки и т.п., осталось все автоматизировать и просто начать писать код и пушить его в реп) Собственно на уровне git я и предлагаю сделать автоматизацию, я буду рассматривать вариант с gitlab CI\CD.

Собираем все в 1 pipeline

Т.к. я буду использовать возможности gitlab, не вижу смысла описывать инструкции, как их использовать, давайте просто рассмотрим готовый .gitlab-ci.yml написанный под этот проект и проговорим, что в нем происходит.

Pipeline будет разделен на следующие стадии

  1. spawn — первым что мы сделаем, это установим зависимости которые понадобятся нам на других этапах нашего пайпа. Выгрузим установленные node_modules в артифакты.
  2. lint — вторым этапом запустим eslint и проверим код на наличие errors, если в коде есть ошибки, пайп будет прерван и постройки не будет.
  3. build — теперь выполним сборку фронта, котрую мы будем загружать в начале на staging, а потом на production
  4. test-build — этап тестирования кода и сборки (бандла и чанков вебпака)
  5. stage — деплоимся на stage с помощью нашего deploy.sh скрипта
  6. test-stage — запускаем e2e тесты, приемочные тесты, в этот момент у нас есть рабочий staging чтобы его протестировать.
  7. deploy — и наконец после всех сборок и тестов, мы выкатываемся в production.
  8. cleanup — чистим кеш и временные файлы, чтобы не засорять нашего ci runner’a

Тутже можно добавить генерацию документации и выкладку ее в gitlabpages, добавить свои стадии деплоя по вкусу.

 

Надеюсь статья окажется полезной, буду рад помочь пройти такой путь новичкам, готовый gitlab repo принципиально не выкладываю) если будет отклик, в следующих статьях я расскажу, как сделать тоже самое, но уже в docker, как добавить service worker, замутим PWA с AMP ;) расскажу как можно в процесс добавить nodejs gateway для более удобной и быстрой работой с бекендом. Критика и комментарии приветствуются)

 


Последняя редакция 20 апреля, 2019 в 10:04