55. Конфигурация окружений
58. Конфигурация окружений 🔧
Заголовок раздела «58. Конфигурация окружений 🔧»Привет! Яша здесь. Управление конфигурацией — это то, что отличает хорошее приложение от production-ready. Сегодня разберём всё: от environment.ts до секретов и feature flags 🔐
Стандартный паттерн environment.ts
Заголовок раздела «Стандартный паттерн environment.ts»Angular CLI создаёт два файла по умолчанию:
// src/environments/environment.ts — для разработкиexport const environment = { production: false, apiUrl: 'http://localhost:3000/api', wsUrl: 'ws://localhost:3000', analyticsKey: '', featureFlags: { newDashboard: true, betaFeature: false, }, // Никогда не храните секреты здесь! Это попадёт в бандл // apiSecret: '...' ← НЕВЕРНО!};// src/environments/environment.prod.ts — для продакшенаexport const environment = { production: true, apiUrl: 'https://api.example.com', wsUrl: 'wss://api.example.com', analyticsKey: 'GA-XXXXX', featureFlags: { newDashboard: false, betaFeature: false, },};// angular.json — file replacements при сборке{ "configurations": { "production": { "fileReplacements": [ { "replace": "src/environments/environment.ts", "with": "src/environments/environment.prod.ts" } ] }, "staging": { "fileReplacements": [ { "replace": "src/environments/environment.ts", "with": "src/environments/environment.staging.ts" } ] } }}ng build --configuration=stagingng serve --configuration=stagingТипизация environment (правильный подход)
Заголовок раздела «Типизация environment (правильный подход)»export interface Environment { production: boolean; apiUrl: string; wsUrl: string; analyticsKey: string; featureFlags: FeatureFlags; logging: LoggingConfig;}
export interface FeatureFlags { newDashboard: boolean; betaCheckout: boolean; aiAssistant: boolean; maintenanceMode: boolean;}
export interface LoggingConfig { level: 'debug' | 'info' | 'warn' | 'error'; remote: boolean;}
// environment.ts — реализует интерфейсimport { Environment } from './environment.model';
export const environment: Environment = { production: false, apiUrl: 'http://localhost:3000', wsUrl: 'ws://localhost:3000', analyticsKey: '', featureFlags: { newDashboard: true, betaCheckout: true, aiAssistant: true, maintenanceMode: false, }, logging: { level: 'debug', remote: false, },};APP_INITIALIZER: runtime конфигурация
Заголовок раздела «APP_INITIALIZER: runtime конфигурация»Проблема environment.ts — значения вшиты в бандл на этапе сборки. Для runtime конфигурации (разные значения для одного бандла в разных средах) используем APP_INITIALIZER:
import { Injectable } from '@angular/core';import { HttpClient } from '@angular/common/http';
export interface AppConfig { apiUrl: string; wsUrl: string; featureFlags: Record<string, boolean>; version: string;}
@Injectable({ providedIn: 'root' })export class ConfigService { private config: AppConfig | null = null;
constructor(private http: HttpClient) {}
// Вызывается ПЕРЕД запуском приложения load(): Promise<void> { return this.http .get<AppConfig>('/assets/config.json') .toPromise() .then(config => { this.config = config!; console.log('Конфигурация загружена:', config?.version); }) .catch(err => { console.error('Ошибка загрузки конфигурации:', err); // Используем fallback конфигурацию this.config = this.getDefaultConfig(); }); }
get<K extends keyof AppConfig>(key: K): AppConfig[K] { if (!this.config) { throw new Error('ConfigService: конфигурация не загружена. Проверь APP_INITIALIZER'); } return this.config[key]; }
isFeatureEnabled(flag: string): boolean { return this.config?.featureFlags[flag] ?? false; }
private getDefaultConfig(): AppConfig { return { apiUrl: '/api', wsUrl: '/ws', featureFlags: {}, version: 'unknown', }; }}// app.module.ts — регистрация APP_INITIALIZERimport { APP_INITIALIZER, NgModule } from '@angular/core';import { ConfigService } from './config.service';
export function initializeApp(configService: ConfigService) { // Возвращаем функцию, возвращающую Promise return () => configService.load();}
@NgModule({ providers: [ { provide: APP_INITIALIZER, useFactory: initializeApp, deps: [ConfigService], multi: true, // multi: true — можно иметь несколько инициализаторов! } ]})export class AppModule {}// assets/config.json — загружается в runtime{ "apiUrl": "https://api.production.example.com", "wsUrl": "wss://api.production.example.com", "version": "2.1.0", "featureFlags": { "newDashboard": true, "betaCheckout": false, "aiAssistant": false, "maintenanceMode": false }}Docker: инжекция переменных окружения
Заголовок раздела «Docker: инжекция переменных окружения»FROM node:20-alpine as buildWORKDIR /appCOPY package*.json ./RUN npm ciCOPY . .RUN ng build --configuration=production
FROM nginx:alpineCOPY --from=build /app/dist/my-app /usr/share/nginx/htmlCOPY nginx.conf /etc/nginx/nginx.confCOPY docker-entrypoint.sh /docker-entrypoint.shRUN chmod +x /docker-entrypoint.shENTRYPOINT ["/docker-entrypoint.sh"]# docker-entrypoint.sh — генерирует config.json из ENV переменных#!/bin/shcat > /usr/share/nginx/html/assets/config.json << EOF{ "apiUrl": "${API_URL:-/api}", "wsUrl": "${WS_URL:-/ws}", "version": "${APP_VERSION:-unknown}", "featureFlags": { "newDashboard": ${FEATURE_NEW_DASHBOARD:-false}, "aiAssistant": ${FEATURE_AI_ASSISTANT:-false} }}EOFexec nginx -g "daemon off;"services: frontend: image: my-angular-app:latest environment: - API_URL=https://api.example.com - WS_URL=wss://api.example.com - APP_VERSION=2.1.0 - FEATURE_NEW_DASHBOARD=true - FEATURE_AI_ASSISTANT=falseFeature Flags: система управления
Заголовок раздела «Feature Flags: система управления»import { Injectable, inject } from '@angular/core';import { ConfigService } from './config.service';import { BehaviorSubject, Observable } from 'rxjs';import { map } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })export class FeatureFlagService { private config = inject(ConfigService); private overrides = new BehaviorSubject<Record<string, boolean>>({});
isEnabled(flag: string): boolean { const override = this.overrides.value[flag]; if (override !== undefined) return override; return this.config.isFeatureEnabled(flag); }
isEnabled$(flag: string): Observable<boolean> { return this.overrides.pipe( map(overrides => { if (overrides[flag] !== undefined) return overrides[flag]; return this.config.isFeatureEnabled(flag); }) ); }
// Для A/B тестирования и дебага override(flag: string, value: boolean): void { this.overrides.next({ ...this.overrides.value, [flag]: value }); console.log(\`Feature flag "\${flag}" overridden to \${value}\`); }
resetOverrides(): void { this.overrides.next({}); }}
// feature-flag.directive.ts — структурная директива@Directive({ selector: '[appFeatureFlag]', standalone: true,})export class FeatureFlagDirective implements OnInit { @Input('appFeatureFlag') flag!: string;
constructor( private vcr: ViewContainerRef, private template: TemplateRef<unknown>, private featureFlags: FeatureFlagService, ) {}
ngOnInit(): void { if (this.featureFlags.isEnabled(this.flag)) { this.vcr.createEmbeddedView(this.template); } }}
// Использование// <div *appFeatureFlag="'newDashboard'">Новый дашборд!</div>Injection Token для конфигурации
Заголовок раздела «Injection Token для конфигурации»// app.config.ts — типизированный токенimport { InjectionToken } from '@angular/core';
export interface ApiConfig { baseUrl: string; timeout: number; retries: number;}
export const API_CONFIG = new InjectionToken<ApiConfig>('API_CONFIG');
// Провайдинг@NgModule({ providers: [ { provide: API_CONFIG, useFactory: (config: ConfigService) => ({ baseUrl: config.get('apiUrl'), timeout: 30000, retries: 3, }), deps: [ConfigService], } ]})
// Использование@Injectable({ providedIn: 'root' })export class UserService { constructor(@Inject(API_CONFIG) private apiConfig: ApiConfig) { console.log('API baseUrl:', this.apiConfig.baseUrl); }}Секреты и безопасность
Заголовок раздела «Секреты и безопасность»// ❌ НЕВЕРНО — секреты в environment.ts (попадут в бандл!)export const environment = { jwtSecret: 'my-super-secret-key', // видно в браузере! databasePassword: 'password123', // видно в браузере! stripeSecretKey: 'sk_live_...', // видно в браузере!};
// ✅ ВЕРНО — секреты только на бэкенде// Фронтенд знает только публичные ключиexport const environment = { stripePublishableKey: 'pk_live_...', // публичный — можно googleMapsApiKey: 'AIza...', // публичный — можно};
// ✅ Публичные ключи в runtime config (не в бандле)// config.json:// { "stripePublishableKey": "pk_live_..." }Playground 🎮
Заголовок раздела «Playground 🎮»Интерактивная система feature flags: