Введение
Как известно, у Angular есть свои инструменты, позволяющие разрабатывать приложения для различных сред.
Это достигается путем создания и использования в сборке файлов environment.<env>.ts
для соответствующей среды. Они позволяют переключаться между настройками для:
- разработки (
environment.ts
);
- тестирования (
environment.test.ts
);
- производства (
environment.prod.ts
).
Основные задачи файлов environment.ts
- Настройка API. Каждый файл может содержать различные URL для серверов API в зависимости от среды.
- Оптимизация. В производственном файле отключаются отладочные функции и включается оптимизация для повышения производительности.
- Контроль над переменными среды. Удобное управление переменными среды выполнения, такими как ключи API и флаги для активации или деактивации функций.
И все вроде бы хорошо — для каждой среды свой файл.
Проблема
Представьте, что по мере роста числа сред необходимо:
- каждый раз создавать отдельный файл
environment.<env>.ts
;
- создать отдельную конфигурацию сборки и указать
fileReplacements
;
- добавлять эту конфигурацию сборки в serve-build;
- добавлять команду
"my-app.build.<env>": "ng build — configuration <env>”
вpackage.json
.
И так каждый раз. Я уже не говорю о сквозных тестах в среде перед запуском в производство или специализированных сборках для ветвей функциональности.
И для чего? Чтобы использовать шаблонную команду в CI.
- run: npm run my-app:build:${{ SOME_VAR.ENV }}
Условно конвейер такого приложения можно изобразить следующим образом:

Вы можете мне возразить: «Это же базовое масштабирование и независимость CI от среды. Чтобы использовать TEST1, надо просто передать ENV=TEST1».
Нет, это неправильно. Почему? Потому что приложение знает обо всех средах, в которых оно используется, и отслеживать каждую конфигурацию — проблематично.
- Нужно добавить 1 новый параметр в конфигурацию? Придется обновить каждый файл, плюс нужно знать, какое значение требуется для каждой среды.
- Хотите изменить параметр для приложения в какой-то среде? Нужно зайти в репозитории, обновить файл и произвести запуск.
Таких примеров очень много, и каждый раз возникают проблемы с масштабируемостью и поддержкой.
Недостатки использования файлов environments.ts
- Необходимость поддержки всех файлов
environments.<env>.ts
.
- Создание альтернативных сборок для каждой среды в
angular.json
.
- Создание дубликатов образов Docker с несколькими параметрами, что приводит к неэффективному использованию пространства реестра.
- Необходимость запуска полного конвейера для каждой среды (<ENV>).
- Тесты выполняются на сборке, которая не будет предоставлена пользователю.
- Отсутствие гибкости при изменении параметров для отдельной среды.
- Невозможность совместного использования приложения в качестве HeadLess-решения.
Формулировка задания
Основные пункты, которые необходимо достичь:
- Убедиться, что приложение и образ Docker собираются только один раз.
- Приложение должно быть способно конфигурироваться для разных сред и определять только интерфейс необходимых переменных.
- Сохранять исходные файлы
environment.ts
иenvironment.prod.ts
, чтобы разделять пространство разработки и сборки приложения.
В результате должна получиться следующая схема использования и развертывания приложения:

В этой схеме:
- Envless Build pipeline — конвейер, который запускается только один раз, создает производственную сборку приложения и сохраняет ее в виде образа Docker в реестре.
- <Env> Deploy pipeline — конвейер выпуска (релиза), который уже знает, в какой среде (ENV) должно быть развернуто приложение.
Конфигурация приложения откладывается до последнего момента, ближе к моменту выпуска.
Решение 1. Получение конфигурации с сервера
Для реализации этого решения необходимо иметь сервер конечной точки API, в рамках которого можно получить все необходимые конфигурации.
Схема работы

Реализация
Объявим интерфейс конфигурации и токен для его предоставления в приложении. Кроме того, создадим значение по умолчанию для начального состояния.
export interface IAppConfig {
apiHost: string,
imageHost: string
titleApp: string
}
export const APP_CONFIG_DEFAULT:IAppConfig = {
apiHost: 'https://my-backend.com',
imageHost: 'https://image-service.com',
titleApp: 'Production Angular EnvLess App'
}
export const APP_CONFIG_TOKEN: InjectionToken<IAppConfig> = new InjectionToken<IAppConfig>(
'APP_CONFIG_TOKEN'
)
Создадим функции, которые будут запрашивать конфигурацию и предоставлять ее. В случае ошибки сервера, предоставим приложению настройки по умолчанию для корректной работы.
function loadConfig(): Promise<IAppConfig> {
// или относительный путь /app/config
return fetch('http://localhost:3000/app/config').then(
(res) => res.json(),
() => APP_CONFIG_DEFAULT
)
}
export async function bootstrapApplicationWithConfig(
rootComponent: Type<unknown>,
appConfig?: ApplicationConfig
): Promise<ApplicationRef> {
return bootstrapApplication(rootComponent, {
providers: [
...appConfig?.providers || [],
{
provide: APP_CONFIG_TOKEN,
useValue: await loadConfig()
},
]
})
}
Заменим нативную функцию bootstrapApplication
новой функцией bootstrapApplicationWithConfig
.
// bootstrapApplication(AppComponent, appConfig)
bootstrapApplicationWithConfig(AppComponent, appConfig)
.catch((err) =>
console.error(err)
);
Сервер конфигурации приложения будет представлять собой простую конфигурацию на express
.
app.get('/app/config', (req, res) => {
res.json({
apiHost: 'https://my-backend.<ENV>.com',
imageHost: 'https://image-service.<ENV>.com',
titleApp: '<Env> - Angular EnvLess App'
});
});
Проверка во время выполнения
Поскольку приложение конфигурируется во время выполнения, проверить его работу даже в локальной среде, зная нужный URL, не составит труда.
В момент запуска приложения получаем конфигурацию по запросу без каких-либо проблем. Можно применить эту конфигурацию для других сервисов приложения, которые используют другие URL API для запросов.

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

Преимущества и недостатки
Помимо выполнения условий задания, есть и другие преимущества использования этого подхода:
- приложение конфигурируется во время выполнения и не требует повторного развертывания;
- конфигурация загружается до запуска приложения, что позволяет использовать параллельные запросы через токены
APP_INITIALIZER
, не заботясь о порядке получения конфигурации;
- в сквозных тестах легко перехватить запрос и отдать файл конфигурации для тестирования.
Но есть и обратная сторона:
- требуется знать URL для получения конфигурации или иметь один и тот же хост для фронтенд- и бэкенд-приложения;
- ответ от сервера может быть длинным, что повлияет на ожидания пользователя;
- необходимо иметь значение FALLBACK на случай ошибки запроса;
- необходимо иметь выделенную базу данных с конфигурацией для каждой среды;
- необходимо иметь API для чтения и изменения конфигурации администратором с выделенными правами доступа;
- увеличивается вероятность ошибок из-за конфигурации во время выполнения, нужно ее проверить во фронтенд-приложении;
- поддержка конфигурации обеспечивается бэкенд-разработчиками и DevOps-специалистами.
А есть ли возможность сохранить схему с получением конфигурации, чтобы не трогать базовый конвейер сборки, но не зависеть больше от сервера, учитывая его недостатки? Да, можно сконфигурировать образ Docker.
Решение 2. Конфигурирование образа Docker
Суть этого решения проста: вместо запроса к удаленному серверу, для получения конфигурации будет выполняться запрос к каталогу файлов, в котором расположено фронтенд-приложение.
Конфигурационный файл будет создан на этапе получения исходного образа Docker, заменив стандартный config.json
. По умолчанию каталог, в котором фронтенд-приложение выполняет запросы, — assets
или public
.
Конфигурационный файл будет создан на основе переменных ENV, которые были указаны при запуске конвейера развертывания. Если переменная ENV не найдена, будет использовано значение по умолчанию.
Схема работы
Решение элегантное, но потребует навыков работы не только во фронтенде, но и в сфере DevOps и CI-скриптов.
Для «обновления» файла config.json
нужно реализовать следующую схему работы:

Развертывание конвейера

Реализация
В отличие от получения config.json с сервера, нужно, чтобы ключи в конфигурации соответствовали имени переменной ENV при обновлении конфигурации.
- Локальные
assets/config.json
:
{
"APP_ENV_API_HOST": "https://local.my-backend.com",
"APP_ENV_API_IMAGE_HOST": "https://local.image-service.com",
"APP_ENV_TITLE_APP": "Local - Angular EnvLess App"
}
- Функция запроса конфигурации с обновленным URL:
function loadConfig(): Promise<IAppConfig> {
// относительный хост
// путь public или assets
return fetch('/config.json').then(
(res) => res.json(),
)
}
Пример скрипта для создания нового config.json
и обновления образа Docker:
#!/bin/bash
set -x
set -e
# Пример - среды для конфигурации
APP_ENV_API_HOST="https://<ENV>.my-backend.com"
APP_ENV_API_IMAGE_HOST="https://<ENV>.image-service.com"
# Настройки
PORT=4110
NGINX_PORT=80
CONTAINER_NAME="angular-envless-container"
IMAGE="angular-envless"
NEW_IMAGE="patched-angular-envless"
CONFIG_NAME="config.json"
APP_PATH="/usr/share/nginx/html"
#Шаг 1
temp_container_run(){
docker run -it -d -p $PORT:$NGINX_PORT --name $CONTAINER_NAME $IMAGE
}
#Шаг 2
temp_container_get_config(){
docker cp $CONTAINER_NAME:$APP_PATH/$CONFIG_NAME ./$CONFIG_NAME
}
#Шаг 3
create_config_json(){
temp_container_get_config
if [[ ! -f "./$CONFIG_NAME" ]]; then
echo "Config file not found in the specified directory."
temp_container_stop
temp_container_rm
return 1
fi
# Извлечение ключей и значений из JSON
KEY_VALUES=$(awk -F '[:,]' '/:/{gsub(/"| /,""); print $1 "=\"" $2 "\""}' "./$CONFIG_NAME")
# Создание нового объекта JSON
PROD_CONFIG="{"
# Передача ключей и значений
for PAIR in $KEY_VALUES; do
# Separating key and value
KEY=$(echo $PAIR | cut -d '=' -f 1)
DEFAULT_VALUE=$(echo $PAIR | cut -d '=' -f 2 | sed 's/,$//')
# Проверьте, есть ли значение в переменных среды
VALUE=${!KEY}
# Если переменная среды отсутствует, используем значение из исходного файла
if [[ -z "$VALUE" ]]; then
VALUE=$DEFAULT_VALUE
else
VALUE="\"$VALUE\""
fi
# Добавьте ключ и значение в объект JSON
PROD_CONFIG+="\"$KEY\":$VALUE,"
done
# Удалите последнюю запятую и закройте объект JSON
PROD_CONFIG=${PROD_CONFIG%,}
PROD_CONFIG+="}"
# Сохранение результата в файл
echo "$PROD_CONFIG" > "./$CONFIG_NAME"
echo "Config updated successfully and saved to ./$CONFIG_NAME"
}
#Шаг 4
temp_container_upsert_config(){
docker cp ./$CONFIG_NAME $CONTAINER_NAME:$APP_PATH/$CONFIG_NAME
}
#Шаг 5
temp_container_commit(){
docker commit --pause $CONTAINER_NAME $NEW_IMAGE
}
#Шаг 6.1
temp_container_stop(){
docker stop $CONTAINER_NAME
}
#Шаг 6.2
temp_container_rm(){
docker rm $CONTAINER_NAME
}
main(){
temp_container_run
create_config_json
temp_container_upsert_config
temp_container_commit
temp_container_stop
temp_container_rm
}
main
Проверка во время выполнения
После выполнения этого скрипта останется только запустить образ Docker в контейнере и убедиться, что переменные среды применены к конфигурации.

Код работает нормально, учитывая, что переменная APP_ENV_TITLE_APP
не была передана при настройке образа Docker.
Преимущества и недостатки
Благодаря такому подходу, влияние сервера на конфигурацию фронтенд-приложения снижается. В дополнение к преимуществам конфигурирования через сервер, получаем следующие плюсы:
- конфигурация доступна немедленно, и задержки при ее получении минимальны;
- не требуется значение FALLBACK в случае ошибки запроса;
- не требуется API или внешнего администрирования конфигурации;
- сборка надежна настолько, что не может быть нарушена во время выполнения;
- нет необходимости создавать отдельную базу данных для каждой среды.
Но нельзя не заметить и недостатки:
- повышенная сложность инфраструктуры развертывания приложения;
- требуется отдельное развертывание конвейера;
- необходима поддержка и проверка DevOps-инженерами;
- необходимо знать нужные ENV-переменные и задавать заранее определенный список для конфигурации приложения в момент начала развертывания.
Заключение
Представленные решения подходят в случаях, когда требуется гибкость конфигурации и независимое управление фронтенд-приложением. Если ваше текущее приложение не требует гибкой настройки, DevOps-инженеры эффективно управляют памятью регистра Docker или вы не планируете создавать HeadLess-приложение, то можете использовать файлы environment.ts
, как и раньше.
В некоторых ситуациях, когда вы не знаете возможных вариантов реализации и использования приложения в начале разработки, такой подход может сэкономить много времени в будущем и дать полный контроль над управлением сборкой.
Читайте также:
- Angular: наведение мостов между HttpClient и Signals
- Новая эра Angular: беззоновое обнаружение изменений
- Повышение безопасности Angular-приложения путем интеграции OCR и биометрии
Читайте нас в Telegram, VK и Дзен
Перевод статьи Maksim Dolgikh: Creating Envless Angular-application