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

?v=1

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

Когда вы создали 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.

© Habrahabr.ru