Хабрахабр

[Из песочницы] Конфигурация приложений на Angular. Лучшие практики

Как управлять файлами конфигурации среды и целями

Когда вы создали angular приложение с помощью Angular CLI или Nrwl Nx tools у вас всегда есть папка с фалами конфигурации окружения:

<APP_FOLDER>/src/environments/ └──environment.ts └──environment.prod.ts

Можно переименовать environment.prod.ts в environment.production.ts например, также можно создавать дополнительные файлы конфигурации такие как environment.qa.ts или environment.staging.ts.

<APP_FOLDER>/src/environments/ └──environment.ts └──environment.prod.ts └──environment.qa.ts └──environment.staging.ts

Для использования остальных файлов необходимо открыть angular.json и настроить fileReplacements секцию в build конфигурации и добавить блоки в serve и е2е конфигурации. Файл environment.ts используется по умолчанию.

] }, "staging":{ "fileReplacements":[ { "replace":"<APP_FOLDER>/src/environments/environment.ts", "with":"<APP_FOLDER>/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<any>(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<any> { 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<number>; 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<void> { 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
<script src="environment.js"></script>

Спасибо за внимание к статье, буду рад конструктивной критике и комментариям.

Также присоединяйтесь к нашему сообществу на Medium, Telegram или Twitter.

Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»