ADR-UI-014. Рантайм-конфигурация
Фронт должен знать адрес API (и другие параметры среды) в момент старта. Параметры различаются
по средам (local/dev/stage/prod), но сборка одна. Фиксируем: параметры грузятся из config.json в
рантайме (не build-time environment.ts) через provideAppInitializer до bootstrap; apiBaseUrl берётся
только из RuntimeConfigService; деплой подменяет только config.json.
1. Контекст и постановка задачи
Приложению нужен адрес API (и потенциально другие параметры) на старте. Эти параметры различаются между средами (локальная разработка, dev-стенд, stage, прод), но артефакт сборки должен быть один — иначе выпуск в каждую среду требует отдельного билда, что усложняет релиз и повышает риск расхождений.
Стандартный шаблон Angular (environments/environment.ts) не подходит: он работает на этапе
сборки, а не старта. Нужен способ подменять конфиг у уже собранного приложения.
У нас фронт и API на стенде — на разных origin, поэтому apiBaseUrl — абсолютный, а бэк
держит CORS-allowlist по среде (см. связанный backend-ADR).
2. Драйверы решения
- Один билд на все среды — конфиг подменяется без пересборки.
- Конфиг гарантированно готов к первому запросу — никакой код не обратится к API раньше, чем конфиг загружен.
- Прозрачный доступ —
apiBaseUrlберётся инъекцией очевидного сервиса, без чтенияwindowи глобальных объектов. - Стандартный механизм Angular — без хаков в
index.html.
3. Рассмотренные варианты
- A.
config.json+provideAppInitializer+fetch— Angular ждёт резолва инициализатора до bootstrap; конфиг кладётся в сервис-сигнал. - B.
<script src="config.js">вindex.html— синхронно выставляет глобальный объект до bootstrap. Без асинхронного ожидания, но завязан наwindowи тег<script>. - C.
environment.ts— build-time; требует пересборки на каждую среду. Отклонено.
4. Решение
Выбран Вариант A: config.json грузится через provideAppInitializer и кладётся в
RuntimeConfigService.
4.1. Контракт config.json
Файл — в src-ui/public/config.json (Angular кладёт содержимое public/ в корень dist).
Это рантайм-артефакт: на каждой среде заменяется своей версией средствами деплоя.
{ "apiBaseUrl": "https://sok-api.dev.example" }
| Ключ | Тип | Назначение |
|---|---|---|
apiBaseUrl |
string | Origin API — без завершающего слэша и без /api. |
Расширение состава ключей — дополнением этого раздела, без нового ADR.
4.2. RuntimeConfigService
providedIn: 'root', хранит конфиг в signal;load()—fetch('config.json', { cache: 'no-store' }), вызывается изprovideAppInitializer;apiBaseUrl— геттер; при обращении до инициализации бросает явную ошибку (не «тихий» undefined).
provideAppInitializer(() => inject(RuntimeConfigService).load()),
4.3. Связка с генерированным API-клиентом
Базовый URL сгенерированного клиента (@api/icd) задаётся токеном BASE_PATH — через фабрику
из RuntimeConfigService (ADR-UI-012):
{ provide: BASE_PATH, useFactory: () => inject(RuntimeConfigService).apiBaseUrl },
Токен резолвится при первом создании api-сервиса (открытие страницы) — уже после инициализатора, конфиг гарантированно загружен.
4.4. Правила
apiBaseUrl— только изRuntimeConfigService. Никакихwindow.*, прямого чтенияconfig.json, импорта изenvironment.ts.config.jsonне кэшируется —no-storeвfetchи на отдаче с сервера; иначе смена конфига потребует жёсткого обновления у каждого пользователя.- В репозитории лежит
config.jsonс локальными значениями (http://localhost:5080); реальные значения сред — в артефактах деплоя (генерятсяdevops/configs-generator). - Секретов в
config.jsonнет — значения видны в network tab; только публичные параметры.
5. Положительные следствия
- Один билд → все среды; деплой меняет только
config.json. - К первому обращению к API конфиг уже загружен — нет гонок.
- Расширение конфига — поле в интерфейсе и в
config.json, без пересборки потребителей.
6. Отрицательные следствия и компромиссы
- Старт добавляет один HTTP-запрос (сотни байт). На медленной сети заметно; при необходимости —
переход на синхронный
<script>(вариант B). - Значения конфига видны в браузере — секреты туда класть нельзя.
7. Проверка
- Подмена
config.jsonв собранном dist меняет адрес API без пересборки. - Обращение к
apiBaseUrlдо инициализации даёт явную ошибку, неundefined. - В коде нет обращений к API мимо
RuntimeConfigService/ токенаBASE_PATH.
8. Открытые вопросы / отложено
- Доп. ключи (
appVersion, флаги фич) — по мере необходимости, дополнением этого ADR. - Отдача
config.jsonбез кэша на CDN/прокси — настройка инфраструктуры;no-storeобязателен.