paint-brush
Работа с переменными среды в выпусках фронтенд-приложенийк@egorgrushin
Новая история

Работа с переменными среды в выпусках фронтенд-приложений

к Egor Grushin11m2024/10/30
Read on Terminal Reader

Слишком долго; Читать

Приложения frontend традиционно требуют отдельных сборок для каждой среды (разработка, подготовка, производство), поскольку переменные среды внедряются во время сборки, что приводит к увеличению времени выпуска. Я предлагаю решение для единовременной сборки приложения frontend и внедрения переменных, специфичных для среды, во время развертывания с использованием заполнителей и скрипта для замены их фактическими значениями во время развертывания. Этот метод оптимизирует процесс выпуска, сокращает время сборки, обеспечивает согласованность между средами и минимизирует риск ошибок, связанных со сборкой. Кроме того, управляя хешированием имен файлов после внедрения переменных, этот подход поддерживает эффективное кэширование браузера и общую производительность приложения.
featured image - Работа с переменными среды в выпусках фронтенд-приложений
Egor Grushin HackerNoon profile picture
0-item


Миллионы приложений front-end управляют сборками, зависящими от среды. Для каждой среды — будь то разработка, подготовка или производство — необходимо создать отдельную сборку приложения front-end и настроить правильные переменные среды. Количество сборок увеличивается, если задействовано несколько приложений, что еще больше раздражает. Это было распространенной проблемой в течение долгого времени, но есть лучший способ обработки переменных среды. Я нашел способ оптимизировать этот процесс, и в этой статье я пошагово расскажу вам, как создать эффективный процесс, который сократит время сборки и поможет вам обеспечить согласованность между средами в ваших проектах.


Понимание переменных среды

Прежде чем начать, я думаю, нам следует сделать резюме. Веб-приложения почти всегда полагаются на переменные, известные как «переменные среды », которые часто включают внутренние системные конечные точки, системы интеграции, ключи платежных систем, номера релизов и т. д. Естественно, значения этих переменных различаются в зависимости от среды, в которой развернуто приложение.


Например, представьте себе приложение, которое взаимодействует с платежным шлюзом. В среде разработки URL платежного шлюза может указывать на песочницу для тестирования (https://sandbox.paymentgateway.com), тогда как в производственной среде он указывает на работающий сервис (https://live.paymentgateway.com). Аналогично, для каждой среды используются разные ключи API или любые другие настройки, специфичные для среды, чтобы обеспечить безопасность данных и избежать смешивания сред.


Проблемы разработки интерфейса

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


Однако с frontend-приложениями все становится несколько сложнее. Поскольку они запускаются в браузере пользователя, у них нет доступа к определенным значениям переменных среды. Чтобы решить эту проблему, значения этих переменных обычно «встраиваются» в frontend-приложение во время сборки. Таким образом, когда приложение запускается в браузере пользователя, все необходимые значения уже встроены в frontend-приложение.


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


Например , предположим, что у нас есть три среды:

  • разработка для внутреннего тестирования;

  • этап интеграционного тестирования;

  • и производство для клиентов.


Чтобы отправить свою работу на тестирование, вы собираете приложение и развертываете его в среде разработки. После завершения внутреннего тестирования вам нужно снова собрать приложение, чтобы развернуть его на этапе, а затем собрать его еще раз для развертывания в производстве. Если проект содержит более одного front-end-приложения, количество таких сборок значительно увеличивается. Кроме того, между этими сборками кодовая база не меняется — вторая и третья сборки основаны на одном и том же исходном коде.


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


Пример: у вас есть два приложения со временем сборки X и Y секунд. Для этих трех сред оба приложения займут 3X + 3Y времени сборки. Однако если бы вы могли собрать каждое приложение только один раз и использовать эту же сборку во всех средах, общее время сократилось бы всего до X + Y секунд, сократив время сборки в три раза.

Это может иметь большое значение в frontend pipelines, где ресурсы ограничены, а время сборки может варьироваться от нескольких минут до более часа. Проблема присутствует почти в каждом frontend-приложении по всему миру, и часто нет способа ее решить. Тем не менее, это серьезная проблема, особенно с точки зрения бизнеса.

Разве не было бы здорово, если бы вместо создания трех отдельных сборок вы могли бы просто сделать одну и развернуть ее во всех средах? Что ж, я нашел способ сделать именно это.


Оптимизация развертывания frontend. Руководство

Настройка переменных среды


  1. Во-первых, вам нужно создать файл в репозитории вашего проекта frontend, в котором будут перечислены требуемые переменные среды. Они будут использоваться разработчиком локально. Обычно этот файл называется .env.local , и его могут читать большинство современных фреймворков frontend. Вот пример такого файла:


     CLIENT_ID='frontend-development' API_URL=/api/v1' PUBLIC_URL='/' COMMIT_SHA=''


    Примечание: разные фреймворки требуют разных соглашений об именовании для переменных окружения. Например, в React вам нужно добавить REACT_APP_ к именам переменных. Этот файл не обязательно должен включать переменные, которые напрямую влияют на приложение; он также может содержать полезную отладочную информацию. Я добавил переменную COMMIT_SHA , которую мы позже извлечем из задания сборки, чтобы отслеживать коммит, на котором была основана эта сборка.


  2. Далее создайте файл с именем environment.js , в котором вы можете определить, какие переменные окружения вам нужны. Фронтенд-фреймворк внедрит их для вас. Например, для React они хранятся в объекте process.env :


     const ORIGIN_ENVIRONMENTS = window.ORIGIN_ENVIRONMENTS = { CLIENT_ID: process.env.CLIENT_ID, API_URL: process.env.API_URL, PUBLIC_URL: process.env.PUBLIC_URL, COMMIT_SHA: process.env.COMMIT_SHA }; export const ENVIRONMENT = { clientId: ORIGIN_ENVIRONMENTS.CLIENT_ID, apiUrl: ORIGIN_ENVIRONMENTS.API_URL, publicUrl: ORIGIN_ENVIRONMENTS.PUBLIC_URL ?? "/", commitSha: ORIGIN_ENVIRONMENTS.COMMIT_SHA, };


  1. Здесь вы извлекаете все начальные значения для переменных в объекте window.ORIGIN_ENVIRONMENTS , что позволяет вам просматривать их в консоли браузера. Кроме того, вам нужно скопировать их в объект ENVIRONMENT , где вы также можете задать некоторые значения по умолчанию, например: мы предполагаем, что publicUrl по умолчанию равен /. Используйте объект ENVIRONMENT везде, где эти переменные необходимы в приложении.


    На этом этапе вы выполнили все потребности местного развития. Но цель — справиться с различными средами.


  2. Для этого создайте файл .env со следующим содержимым:

 CLIENT_ID='<client_id>' API_URL='<api_url>' PUBLIC_URL='<public_url>' COMMIT_SHA=$COMMIT_SHA

В этом файле вам нужно указать заполнители для переменных, которые зависят от среды. Они могут быть любыми, если они уникальны и не пересекаются с вашим исходным кодом. Для дополнительной уверенности вы можете даже использовать UUID-идентификаторы – Универсальные уникальные идентификаторы.


Для тех переменных, которые не изменяются в разных средах (например, хэш коммита), вы можете либо напрямую записать фактические значения, либо использовать значения, которые будут доступны во время задания сборки (например, $COMMIT_SHA ). Фронтенд-фреймворк заменит эти заполнители фактическими значениями во время процесса сборки:


Файл
Файл

  1. Теперь у вас есть возможность вставлять реальные значения вместо плейсхолдеров. Для этого создайте файл inject.py (я выбрал Python, но вы можете использовать любой инструмент для этой цели), который должен сначала содержать сопоставление плейсхолдеров с именами переменных:


 replacement_map = { "<client_id>": "CLIENT_ID", "<api_url>": "API_URL", "<public_url>": "PUBLIC_URL", "%3Cpublic_url%3E": "PUBLIC_URL" }

Обратите внимание, что public_url указан дважды, и во второй записи есть экранированные скобки. Это необходимо для всех переменных, которые используются в файлах CSS и HTML.


  1. Теперь давайте добавим список файлов, которые мы хотим изменить (это будет пример для Nginx):


 base_path = 'usr/share/nginx/html' target_files = [ f'{base_path}/static/js/main.*.js', f'{base_path}/static/js/chunk.*.js', f'{base_path}/static/css/main.*.css', f'{base_path}/static/css/chunk.*.css', f'{base_path}/index.html' ]


  1. Затем мы создаем файл injector.py , в который мы получим сопоставление и список файлов артефактов сборки (таких как файлы JS, HTML и CSS), и заменяем заполнители значениями переменных из нашей текущей среды:


 import os import glob def inject_envs(filename, replacement_map): with open(filename) as r: lines = r.read() for key, value in replacement_map.items(): lines = lines.replace(key, os.environ.get(value) or '') with open(filename, "w") as w: w.write(lines) def inject(target_files, replacement_map, base_path): for target_file in target_files: for filename in glob.glob(target_file.glob): inject_envs(filename, replacement_map)


А затем в файле inject.py добавьте эту строку (не забудьте импортировать injector.py ):

 injector.inject(target_files, replacement_map, base_path)


  1. Теперь нам нужно убедиться, что скрипт inject.py запускается только во время развертывания. Вы можете добавить его в Dockerfile в команде CMD после установки Python и копирования всех артефактов:
 RUN apk add python3 COPY nginx/default.conf /etc/nginx/conf.d/default.conf COPY --from=build /app/ci /ci COPY --from=build /app/build /usr/share/nginx/html CMD ["/bin/sh", "-c", "python3 ./ci/inject.py && nginx -g 'daemon off;'"]That's it! This way, during each deployment, the pre-built files will be used, with variables specific to the deployment environment injected into them.


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


Файл: Файл


Обработка хеширования имен файлов для правильного кэширования браузера

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


  1. Сгенерируйте новый хеш для обновленных файлов.
  2. Добавьте этот новый хеш к именам файлов, чтобы браузер воспринимал их как новые файлы.
  3. Обновите все ссылки на старые имена файлов в вашем коде (например, операторы импорта), чтобы они соответствовали новым именам файлов.


Чтобы реализовать это, добавьте импорт библиотеки хэшей ( import hashlib ) и следующие функции в файл inject.py .


 def sha256sum(filename): h = hashlib.sha256() b = bytearray(128 * 1024) mv = memoryview(b) with open(filename, 'rb', buffering=0) as f: while n := f.readinto(mv): h.update(mv[:n]) return h.hexdigest() def replace_filename_imports(filename, new_filename, base_path): allowed_extensions = ('.html', '.js', '.css') for path, dirc, files in os.walk(base_path): for name in files: current_filename = os.path.join(path, name) if current_filename.endswith(allowed_extensions): with open(current_filename) as f: s = f.read() s = s.replace(filename, new_filename) with open(current_filename, "w") as f: f.write(s) def rename_file(fullfilename): dirname = os.path.dirname(fullfilename) filename, ext = os.path.splitext(os.path.basename(fullfilename)) digest = sha256sum(fullfilename) new_filename = f'{filename}.{digest[:8]}' new_fullfilename = f'{dirname}/{new_filename}{ext}' os.rename(fullfilename, new_fullfilename) return filename, new_filename


Однако не все файлы нужно переименовывать. Например, имя файла index.html должно оставаться неизменным, и для этого создайте класс TargetFile , который будет хранить флаг, указывающий, необходимо ли переименование:


 class TargetFile: def __init__(self, glob, should_be_renamed = True): self.glob = glob self.should_be_renamed = should_be_renamed


Теперь вам просто нужно заменить массив путей к файлам в inject.py на массив объектов класса TargetFile :

 target_files = [ injector.TargetFile(f'{base_path}/static/js/main.*.js'), injector.TargetFile(f'{base_path}/static/js/chunk.*.js'), injector.TargetFile(f'{base_path}/static/css/main.*.css'), injector.TargetFile(f'{base_path}/static/css/chunk.*.css'), injector.TargetFile(f'{base_path}/index.html', False) ]


И обновите функцию inject в injector.py , включив переименование файла, если установлен флаг:


 def inject(target_files, replacement_map, base_path): for target_file in target_files: for filename in glob.glob(target_file.glob): inject_envs(filename, replacement_map) if target_file.should_be_renamed: filename, new_filename = rename_file(filename) replace_filename_imports(filename, new_filename, base_path)


В результате файлы артефактов будут иметь следующий формат именования: <origin-file-name> . <injection-hash> . <extension> .


Имя файла до инъекции:

Имя файла перед инъекцией


Имя файла после инъекции: Имя файла после инъекции


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


Решение для оптимизированного развертывания

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


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


Вместо того, чтобы нуждаться в N сборках, вам понадобится только одна. Для предстоящего релиза вы можете просто развернуть сборку, которая уже была протестирована, что также помогает устранить потенциальные проблемы с ошибками, поскольку одна и та же сборка будет использоваться во всех средах. Кроме того, скорость выполнения этого скрипта несравнимо выше, чем даже у самой оптимизированной сборки. Например, локальные бенчмарки на MacBook 14 PRO, M1, 32 ГБ следующие:


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


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