Pull to refresh

Конфигурация приложений на Angular. Лучшие практики

Reading time6 min
Views17K

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


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

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


{
   "architect":{
      "build":{
         "configurations":{
            "production":{
               "fileReplacements":[
                  {
                     "replace":"<APP_FOLDER>/src/environments/environment.ts",
                     "with":"<APP_FOLDER>/src/environments/environment.production.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 файлов


// environment-interface.ts
export interface EnvironmentInterface {
  production: boolean;
  apiUrl: string;
}

// environment.ts
export const environment: EnvironmentInterface = {
  production: false,
  apiUrl: 'https://api.url',
};

Не используйте 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 переменные во время деплоя или в рантайме


Многие команды нарушают правило “Build once, deploy many” делая сборку приложения для каждого окружения вместо того чтобы просто подменять конфигурацию в уже собранном билде.


Не создавайте отдельные сборки с разными конфигурациями, вместо этого используйте только одну продакшн сборку и подменяйте значения во время деплоя или во время исполнения кода. Есть несколько вариантов как сделать это:


Заменить значения плэйсхолдерами в 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.

Tags:
Hubs:
+12
Comments6

Articles