Манифест 12-факторных приложений внес весомый вклад в процесс разработки и эксплуатации веб-приложений, но это по-большей части коснулось бекендов, и обошло стороной фронтенды. Большенство пунктов манифеста или не применимы к фронтендам, или выполняются сами собой, но с номером 3 конфигурация есть вопросики.
В оригинальном манифесте сказано: Сохраняйте конфигурацию в среде выполнения. На практике это означает, что конфигурацию нельзя хранить внутри исходного кода или финального артефакта. Ее нужно передавать в приложение по время запуска. У этого правила есть практические применения, вот парочка:
-
Приложение в разных окружениях должно обращаться к разным бекендам. На продакшене к продакшн API, на стейдженге к стейджинг API, а при запуске интеграционных тестов к специальному мок-серверу.
-
Для е2е тестов нужно снижать время ожидания реакции пользователя. Например, на если на сайте после 10 минут бездействия что-то происходит, то для тестового сценария можно уменьшить этот интервал до минуты.
SSR
Если фронтенд-приложение содержит в себе SSR, то задача
становится чуточку легче. Конфигурацию передают как переменные
окружения в приложение на сервере, при рендере она попадает в ответ
клиенту как глобальные переменные, объявленные в
<script>
в самом начале страницы. На клиенте же
достаточно эти переменные подхватывать из глобальной области
видимости и использовать.
Недавно мы в Авиасейлс делали приложение для серверного рендеринга кусочков сайта и столкнулись с этой задачей. Результат мой тиммейт заопенсорсил isomorphic-env-webpack-plugin.
Прекрасный Next.js умеет так из коробки, ничего специально делать не нужно.
CSR
Другое дело, если приложение набор статических файлов. Переменные окружения нельзя передать на клиент, ведь мы не контролируем сервер, а все файлы уже заранее сгенерированы и лежат на диске. В таком случае, на этот фактор часто забивают.
Два самых популярных способа хранить конфигурацию фронтенд приложения с клиентским рендерингом:
-
Положить к исходным текстам завести в коде файлик
config.js
, и сложить в него параметры. Это работает, но некоторые крайние случаи могут сильно испортить жизнь. Часто в таких файлах появляется проверка если текущий хост такой-то, то ходить на дев-бекенд, если хост другой ходить на прод-бекенд. Такой подход плохо дружит с переменным количеством окружений когда на каждый PR поднимается новый стенд. -
Передавать при сборке артефакта. Обычно это делают через
DefinePlugin
для Webpack. Этот подход лучше первого для каждого окружения можно собирать отдельный артефакт и добавлять туда специфичные параметры. Возникает проблема на продашкн поедет не тот же артефакт, что тестировался. Технически, нет гарантии, что в разных окружениях версия будет работать одинаково. Иногда это приводит к печальным последствиям.
Есть несколько способов получше.
Прокси-сервер
Подавляющее большенство фронтенд-приложений подчиняются двум критериям:
-
Файлы ресурсов раздаются через nginx, либо перед приложением стоит nginx как реверс-прокси. Тут nginx можно заменить на любой аналог.
-
Единственная конфигурация, необходимая приложению адреса разных API.
Для таких приложений проблему конфигурации можно решить так в
клиентском коде все запросы отправить на текущий домен, а на
стороне nginx роутить эти запросы в конкретные бекенды. Будем
отправлять запросы на /user-api/path
вместо
https://user.my-service.io/path
, на
/auth-api/path
вместо
https://auth.other-service.io/path
и так далее.
Дальше инструкция специфична для nginx в Docker-контейнере
Начиная с версии 1.19 официальный Docker-образ nginx умеет
использовать переменные окружения в конфигурационных файлах. Для
этого нужно создать файл конфигурации с суффиксом
.template
и поместить его в директорию
/etc/nginx/templates
. При старте сервер подхватит
переменные окружения, пройдёт по шаблонам и создаст финальные файлы
конфигурации.
Типичная конфигурация nginx для SPA будет выглядеть так:
server { listen 8080; root /srv/www; index index.html; server_name _; location /user-api { proxy_pass ${USER_API_URL}; } location /auth-api { proxy_pass ${AUTH_API_URL}; } location / { try_files $uri /index.html; }}
Dockerfile в этом случае будет примерно таким:
FROM node:14.15.0-alpine as buildWORKDIR /app# сборка фронтенд ассетов# ...FROM nginx:1.19-alpineCOPY ./default.conf.template /etc/nginx/templates/default.conf.templateCOPY --from=build /app/public /srv/wwwEXPOSE 8080
Теперь достаточно запустить контейнер и передать переменные окружения, на основе которых nginx создаст конфигурационные файлы.
Так, косвенным образом, фронтенд приложение получит параметры в момент запуска.
Живой пример можно посмотреть в этом проекте.
Такую схему можно реализовать и с другими серверами. Caddy поддерживает переменные окружения в конфигурационных файлах из коробки, а Traefik умеет в динамические конфигурации.
Если в приложении конфигурация не исчерпывается путями до API, вариант с прокси-сервером для хранения параметров не подходит.
Генерация файла с конфигурацией
Проделав этот путь можно пойти дальше и генерировать конфигурационные файлы не для прокси-сервера, а напрямую для фронтенда. Это позволит передавать любые параметры, не зависеть от способа раздачи статических файлов и места их хранения.
При старте приложения можно подхватывать переменные окружения и записывать их в JS-файл:
window.__ENV__ = { USER_API_URL: 'https://user.my-service.io/', AUTH_API_URL: 'https://auth.other-service.io/',};
А потом раздавать этот файл тем же способом, что и остальные
статические файлы. На клиенте забирать параметры из этой глобальной
переменной и использовать в приложении. Дополнительно нужно будет
добавить <script>
в HTML-страницу с
приложением.
Дальше инструкция специфична для nginx в Docker-контейнере
Важно отметить, что отправлять все переменные окружения на
клиент может быть опасно, часто в них хранится приватная информация
ключи доступа до API, пароли и токены. Поэтому, лучше явно
перечислить имена переменных, которые будут отправлены в браузер в
файле env.dict
:
BACK_URLGOOGLE_CLIENT_ID
Теперь простым Bash-скриптом generate_env.sh
будем
доставать значения из окружения и складывать в JS-файл:
#!/bin/bashfilename='/etc/nginx/env.dict'# Начало JS-файлаconfig_str="window._env_ = { "# Конкатенируем переменную в JS-файлwhile read line; dovariable_str="${line}: \"${!line}\""config_str="${config_str}${variable_str}, "done < $filename# Конец JS-файлаconfig_str="${config_str} };"# Сохраняем файл на дискecho "Creating config-file with content: \"${config_str}\""echo "${config_str}" >> /srv/www/config.env.js# Добавляем <script> в конец всех HTML-файловsed -i '/<\/body><\/html>/ i <script src="http://personeltest.ru/aways/habr.com/confit.env.js"></script>' *.html
Я не большой знаток Bash, вероятно получилось странно. Этот скрипт призван показать общую идею, а не использоваться в проекте напрямую.
Теперь при старте контейнера вместо запуска nginx нужно
выполнить этот скрипт, а потом уже запустить nginx. Заведём точку
входа cmd.sh
, которая сделает это:
#!/bin/bashbash /etc/nginx/generate_env.shnginx -g "daemon off;"
И теперь немного подправим Dockerfile:
FROM node:14.15.0-alpine as buildWORKDIR /app# сборка фронтенд ассетов# ...FROM nginx:1.19-alpine# В стандартной поставке Alpine нет Bash, установим егоRUN apk add bashCOPY ./default.conf /etc/nginx/conf.d/COPY --from=build /app/public /srv/wwwCOPY ./cmd.sh /etc/nginx/cmd.shCOPY ./generate_env.sh /etc/nginx/generate_env.shCOPY ./env.dict /etc/nginx/env.dict EXPOSE 8080CMD ["bash", "/etc/nginx/cmd.sh"]
После этих манипуляций можно передавать на фронтенд любые
параметры через переменные окружения нужно отметить переменную в
env.dict
и передать ее при запуске контейнера.
Живой пример можно посмотреть в этом проекте.
Если внимательно посмотреть на этот вариант, станет понятно, что он почти не отличается от варианта с SSR приведённого в начале статьи. Для удобства можно воспользоваться isomorphic-env-webpack-plugin, и дописать пару скриптов: генерации файла и вставки ссылки на него в HTML.
В этой схеме есть еще один небольшой эдж-кейс обычно в имена файлов с ресурсами добавляют хеш содержимого, чтобы в браузере без проблем кешировать все файлы навсегда по имени. В таком случае нужно немного усложнить скрипт генерации файла с переменными, хешировать содержимое и добавлять результат в имя файла.
Заключение
Правильная работа с параметрами фронтенд-приложения помогает создавать надежные и удобные в эксплуатации системы. Достаточно отделить конфигурацию от приложения и перенести ее в среду выполнения, чтобы радикально улучшить комфорт членов команды и снизить количество потенциальных ошибок.
А как вы передаёте конфиги в клиентские приложения?