[Из песочницы] Конфигурация приложений на Angular. Лучшие практики
Как управлять файлами конфигурации среды и целями
Когда вы создали angular приложение с помощью Angular CLI или Nrwl Nx tools у вас всегда есть папка с фалами конфигурации окружения:
/src/environments/
└──environment.ts
└──environment.prod.ts
Можно переименовать environment.prod.ts в environment.production.ts например, также можно создавать дополнительные файлы конфигурации такие как environment.qa.ts или environment.staging.ts.
/src/environments/
└──environment.ts
└──environment.prod.ts
└──environment.qa.ts
└──environment.staging.ts
Файл environment.ts используется по умолчанию. Для использования остальных файлов необходимо открыть angular.json и настроить fileReplacements секцию в build конфигурации и добавить блоки в serve и е2е конфигурации.
{
"architect":{
"build":{
"configurations":{
"production":{
"fileReplacements":[
{
"replace":"/src/environments/environment.ts",
"with":"/src/environments/environment.production.ts"
}
]
},
"staging":{
"fileReplacements":[
{
"replace":"/src/environments/environment.ts",
"with":"/src/environments/environment.staging.ts"
}
]
}
}
},
"serve":{
"configurations":{
"production":{
"browserTarget":"app-name:build:production"
},
"staging":{
"browserTarget":"app-name:build:staging"
}
}
},
"e2e":{
"configurations":{
"production":{
"browserTarget":"app-name:serve:production"
},
"staging":{
"browserTarget":"app-name:serve:staging"
}
}
}
}
}
Для сборки или запуска приложения с конкретным окрудением используйте команды:
ng build --configuration=staging
ng start --configuration=staging
ng e2e --configuration=staging
Кстати
ng build --prod
всего лишь сокращенный вариант
ng build --configuration=production
Не используйте environment файлы напрямую, только через DI
Использование глобальных переменных и прямых импортов нарушает ООП подход и усложняет тестируемость ваших классов. Поэтому лучше создать сервис который можно инжектить в ваши компоненты и другие сервисы. Вот пример такого сервиса с возможностью указывать дефолтное значение.
export const ENVIRONMENT = new InjectionToken<{ [key: string]: any }>('environment');
@Injectable({
providedIn: 'root',
})
export class EnvironmentService {
private readonly environment: any;
// We need @Optional to be able start app without providing environment file
constructor(@Optional() @Inject(ENVIRONMENT) environment: any) {
this.environment = environment !== null ? environment : {};
}
getValue(key: string, defaultValue?: any): any {
return this.environment[key] || defaultValue;
}
}
@NgModule({
imports: [
BrowserModule,
HttpClientModule,
AppRoutingModule,
],
declarations: [
AppComponent,
],
// We declare environment as provider to be able to easy test our service
providers: [{ provide: ENVIRONMENT, useValue: environment }],
bootstrap: [AppComponent],
})
export class AppModule {
}
Отделяйте конфигурацию окружения и бизнес логики
Конфигурация окружения включает в себя только свойства которые относятся к окружению, например apiUrl. В идеале конфигурация окружения должна состоять из двух свойств:
export const environment = {
production: true,
apiUrl: 'https://api.url',
};
Также в этот конфиг можно добавить свойство для включения дебаг режима debugMode: true или можно добавить имя сервера на котором запущено приложение environmentName: «QA», но не забывайте что это очень плохая практика если ваш код знает что-либо о сервере на котором он запущен.
Никогда не храните какую-либо секретную информацию или пароли в конфигурации окружения.
Другие настройки конфигурации такие как maxItemsOnPage или galleryAnimationSpeed должны храниться в другом месте и желательно использоваться через configuration.service.ts который может получать настройки с какого то эндпоинта или просто загружая config.json из папки assets.
1. Асинхронный подход (используйте когда конфигурация может измениться в рантайме)
// assets/config.json
{
"galleryAnimationSpeed": 5000
}
// configuration.service.ts
// ------------------------------------------------------
@Injectable({
providedIn: 'root',
})
export class ConfigurationService {
private configurationSubject = new ReplaySubject(1);
constructor(private httpClient: HttpClient) {
this.load();
}
// method can be used to refresh configuration
load(): void {
this.httpClient.get('/assets/config.json')
.pipe(
catchError(() => of(null)),
filter(Boolean),
)
.subscribe((configuration: any) => this.configurationSubject.next(configuration));
}
getValue(key: string, defaultValue?: any): Observable {
return this.configurationSubject
.pipe(
map((configuration: any) => configuration[key] || defaultValue),
);
}
}
// app.component.ts
// ------------------------------------------------------
@Component({
selector: 'app-root',
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
galleryAnimationSpeed$: Observable;
constructor(private configurationService: ConfigurationService) {
this.galleryAnimationSpeed$ = this.configurationService.getValue('galleryAnimationSpeed', 3000);
interval(10000).subscribe(() => this.configurationService.load());
}
}
2. Синхронный подход (используйте когда конфигурация почти никогда не меняется)
// assets/config.json
{
"galleryAnimationSpeed": 5000
}
// configuration.service.ts
// ------------------------------------------------------
@Injectable({
providedIn: 'root',
})
export class ConfigurationService {
private configuration = {};
constructor(private httpClient: HttpClient) {
}
load(): Observable {
return this.httpClient.get('/assets/config.json')
.pipe(
tap((configuration: any) => this.configuration = configuration),
mapTo(undefined),
);
}
getValue(key: string, defaultValue?: any): any {
return this.configuration[key] || defaultValue;
}
}
// app.module.ts
// ------------------------------------------------------
export function initApp(configurationService: ConfigurationService) {
return () => configurationService.load().toPromise();
}
@NgModule({
imports: [
BrowserModule,
HttpClientModule,
AppRoutingModule,
],
declarations: [
AppComponent,
],
providers: [
{
provide: APP_INITIALIZER,
useFactory: initApp,
multi: true,
deps: [ConfigurationService]
}
],
bootstrap: [AppComponent],
})
export class AppModule {
}
// app.component.ts
// ------------------------------------------------------
@Component({
selector: 'app-root',
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
galleryAnimationSpeed: number;
constructor(private configurationService: ConfigurationService) {
this.galleryAnimationSpeed = this.configurationService.getValue('galleryAnimationSpeed', 3000);
}
}
Подменяйте environment переменные во время деплоя или в рантайме
Не создавайте отдельные сборки с разными конфигурациями, вместо этого используйте только одну продакшн сборку и подменяйте значения во время деплоя или во время исполнения кода. Есть несколько вариантов как сделать это:
Заменить значения плэйсхолдерами в environment файлах которые будут заменены в итоговой сборке во время деплоя
export const environment = {
production: true,
apiUrl: 'APPLICATION_API_URL',
};
Во время деплоя строка APPLICATION_API_URL должна быть заменена на реальный адрес апи сервера.
Использовать глобальные переменные и инжектить конфиг файлы с помощью docker volumes
export const environment = {
production: true,
apiUrl: window.APPLICATION_API_URL,
};
// in index.html before angular app bundles
Спасибо за внимание к статье, буду рад конструктивной критике и комментариям.
Также присоединяйтесь к нашему сообществу на Medium, Telegram или Twitter.