Angular w praktyce

Jak działa APP_INITIALIZER? Czyli co musisz wiedzieć o dynamicznej konfiguracji?

Rozwijając aplikację przeznaczoną do uruchamiania na wielu środowiskach musisz zadecydować jak dostarczysz konfiguracje odpowiednich zmiennych w zależności od tych środowisk
APP_INITIALIZER

Rozwijając aplikację przeznaczoną do uruchamiania na wielu środowiskach musisz zadecydować jak dostarczysz konfiguracje odpowiednich zmiennych w zależności od tych środowisk

TLDR;

  • environment.ts to nie jest najlepszy sposób dostarczenia konfiguracji środowiskowej
  • Nie wprowadzaj konfiguracji środowiskowej w czasie budowania aplikacji
  • Injection Token – APP_INITIALIZER służy do wygodnego i efektywnego dostarczania konfiguracji z zewnątrz
  • Przygotowałem dla Ciebie minimalny przykład rozwiązania problemu poprzez zastosowanie APP_INITIALIZER. Znajdziesz go na StackBlitz

PIERWSZE PODEJŚCIE

Z najprostszym przykładem zmiennej środowiskowej mamy do czynienia przy konfiguracji API. W zależności od środowiska będziesz używał innego adresowania URL.

  • dev: https://yourdomain.dev.org/api/resources
  • test: https://yourdomain.test.org/api/resources
  • prod: https://yourdomain.prod.org/api/resources

W wielu projektach Angularowych, które widziałem, rozwiązywano ten problem poprzez użycie analogicznych plików dostarczonych przez framework Angular: environment.dev.ts, environment.test.ts oraz environment.prod.ts

Pliki te dla naszego przykładu wyglądałyby następująco

// environment.dev.ts
export const environment = {
  production: true,
  apiUrl: "https://yourdomain.dev.org/api/resources"
};

// environment.test.ts
export const environment = {
  production: true,
  apiUrl: "https://yourdomain.test.org/api/resources"
};

// environment.prod.ts
export const environment = {
  production: true,
  apiUrl: "https://yourdomain.prod.org/api/resources"
};


Nie można powiedzieć, że to podejście jest bardzo złe lub niespełniające postawionych założeń. Natomiast spójrz co się dzieje jeśli wybierzesz ten sposób na trzymanie swoich zmiennych i chcesz wykonać pełny cykl dostarczenia aplikacji na produkcje.

  1. Jeśli wszystko działa dobrze lokalnie to aplikacja jest budowana na środowisko development z użyciem pliku environment.dev.ts
  2. Jeśli wszystko działa dobrze na środowisku development, to promujemy ją na środowisko test. Aby aplikacja działała na środowisku test, musi zostać ponownie zbudowana przy użyciu tym razem pliku environment.test.ts
  3. Jeśli aplikacja przeszła testy na środowisku test i jest gotowa trafić na produkcję to podejmujemy analogiczne kroki. Musimy zatem wybudować aplikację jeszcze raz z użyciem environment.prod.ts

Zauważ proszę jeden fakt. Aplikacja zanim trafi spod ręki programisty na środowisko produkcyjne musi zostać wybudowana trzy razy (!!!).

Pierwsze wnioski jakie się nasuwają to:

  • Jeśli chcesz dostarczyć małą zmianę na produkcje to musisz kilkukrotnie powtórzyć ten sam proces budowania przez co czas dostarczenia zmiany wydłuży Ci się znacząco
  • Nigdy nie będziesz miał pewności, że przetestowany artefakt ze środowiska dev czy test, poprawnie zbuduje się na środowisku prod
  • Nigdy także nie będziesz miał pewności, że poprawnie zbudowany artefakt dla środowiska prod będzie co do bajtu zgadzał się z tym co zostało wcześniej wybudowane i przetestowane na dev i test.

Na podstawie tych wniosków i własnych doświadczeń mogę śmiało Ci powiedzieć, że pliki typu environment.ts to nie jest dobre miejsce na trzymanie większości typów konfiguracji. Dokładniej mówiąc:

Nie powinno dostarczać się niektórych typów konfiguracyjnych w momencie budowania artefaktu.

Jeśli zatem nie zdecydujesz się na umieszczenie swojej konfiguracji w plikach environment.ts, to co możesz zrobić?

PODEJŚCIE Z UŻYCIEM APP_INITIALIZER

Z wielką pomocą przychodzi Injection Token APP_INITIALIZER dzięki któremu możesz dostarczyć do swojej aplikacji dodatkowe funkcje inicjalizujące. Ta funkcjonalność framework’u Angular zapewnia, że dostarczone funkcje wykonają się przy uruchamianiu aplikacji, kiedy jest jeszcze ona inicjalizowana. Funkcje te mogą zwracać obiekt Promise i dopóki się on nie wykona, nasza aplikacja nie zostanie zainicjalizowana.

Czy to dobre miejsce i sposób na przekazanie konfiguracji? Oczywiście! Nawet w oficjalnej dokumentacji framework’u Angular możesz przeczytać:

You can, for example, create a factory function that loads language data or an external configuration, and provide that function to the APP_INITIALIZER token. That way, the function is executed during the application bootstrap process, and the needed data is available on startup.


DYNAMICZNA KONFIGURACJA W PRAKTYCE

Po pierwsze będziesz potrzebować pliku w którym umieścisz szczegóły konfiguracji i nie będzie ten plik częścią budowanego artefaktu w momencie wykonania ng build. Ten plik konfiguracyjny jest pobierany na żądanie w momencie uruchomienia aplikacji.

// ./src/assets/config/configuration.json
{
  "apiUrl": "https://yourdomain.dev.org/api/resources"
}

dla przyjętego prostego modelu konfiguracji

export type Configuration = {
  apiUrl: string;
}

Będziesz także potrzebować service, który będzie miał odpowiedzialność pobrania konfiguracji i udostępniania jej w czasie działania aplikacji.

 public loadConfiguration() {
    return this._http
      .get("src/assets/config/configuration.json")
      .toPromise()
      .then((configuration: Configuration) => {
        this._configuration = configuration;
        return configuration;
      })
      .catch((error: any) => {
        console.error(error);
      });
  }
}

Ostatnim krokiem jest użycie powyższej metody za pomocą tokenu APP_INITIALIZER. Używa się go w sekcji providers dla określonego NgModule.

Warto rozważyć stworzenie osobnego modułu z przeznaczoną odpowiedzialnością pobierania konfiguracji zamiast dodawania poniższego kodu do głównego app.module.ts

providers: [
    {
      provide: APP_INITIALIZER,
      useFactory: loadConfiguration,
      deps: [ConfigurationLoader],
      multi: true
    }
  ]

Funkcja inicjalizująca loadConfiguration została dostarczona za pomocą parametru useFactory. Sama funkcja wygląda następująco

export function loadConfiguration(configService: ConfigurationLoader) {
  return () => configService.loadConfiguration();
}

Wykonując powyższe kroki zapewniasz swojej aplikacji następujące zachowanie

  • w momencie startu aplikacji metodę loadConfiguration z instancji ConfigurationLoader zostaje uruchomiona i powoduje to rozpoczęcie pobierania konfiguracji z lokalizacji /assets/config/configuration.json
  • aplikacja czeka do momentu załadowania konfiguracji z /assets/config/configuration.json
  • Aplikacja zostaje zainicjalizowana, a konfiguracja dostępna jest poprzez metodę getConfiguration instancji ConfigurationLoader

Cała magia polega na tym, że umieszczona konfiguracja w katalogu assets/config nie jest brana pod uwagę w momencie budowania. Zawartość katalogu assets możesz modyfikować w dowolnym momencie bez przebudowywania aplikacji. Zatem schemat dostarczenia aplikacji na produkcję wygląda teraz następująco

  1. Jeśli wszystko działa dobrze lokalnie to aplikacja budowana jest na środowisku development i plik /assets/config/configuration.json jest dostarczany z zewnątrz
  2. Jeśli wszystko działa dobrze na środowisku development, to aplikacja promowana jest na środowisko test. W celu działania aplikacji na środowisku test bierzemy artefakt już wcześniej wybudowany i podmieniamy plik /assets/config/configuration.json, który tym razem zawiera konfigurację specyficzną dla test
  3. Jeśli aplikacja przeszła testy na środowisku test i jest gotowa trafić na produkcję to ponownie bierzemy ten sam wybudowany artefakt, przepychamy go na środowisko prod i podmieniamy analogiczny plik konfiguracyjny /assets/config/configuration.json dla środowiska prod.

Wow! Stosując APP_INITIALIZER i dynamiczną podmianę konfiguracji właśnie ograniczyłeś proces budowania aplikacji w całym cyklu dostarczenia tylko do jednego razu! Zapewniłeś także, że Twój wybudowany artefakt na początkowym środowisku będzie zgadzał się co do pojedynczego bajtu z tym co trafi na środowisko produkcyjne – końcowe.

Jeśli chcesz zobaczyć jak działa to rozwiązanie to przygotowałem dla Ciebie live przykład na StackBlitz

Podsumowanie

  • environment.ts to nie jest najlepszy sposób dostarczenia konfiguracji środowiskowej
  • nie wprowadzaj konfiguracji środowiskowej w czasie budowania aplikacji
  • Injection Token – APP_INITIALIZER służy do wygodnego i efektywnego dostarczania konfiguracji z zewnątrz
  • Przygotowałem dla Ciebie minimalny przykład rozwiązania problemu poprzez zastosowanie APP_INITIALIZER. Znajdziesz go na StackBlitz



Jeśli artykuł był dla Ciebie wartościowy lub masz jakieś sugestie to podziel się nimi ze mną pisząc na adres marcin@angularwpraktyce.pl

Chętnie usłyszę także Twoje propozycje na kolejne artykuły w których postaram się wyjaśnić nurtujące Cię zagadnienia.

Oczywiście jeśli nie chcesz żeby Cię nie ominęły inne artykuły i niespodzianki, które dla Ciebie przygotujemy zapisz się na nasz newsletter.

Podziel się artykułem

Share on facebook
Share on twitter
Share on linkedin
Marcin Milewicz

Programista z pasji od prawie półtorej dekady, profesjonalnie przeszło 8 lat. Lead UI Developer. Po godzinach Toastmaster, a także miłośnik podróży, gór oraz psychologii. Silnie nastawiony na innowacje technologiczne i rozwój osobisty.

Marcin Milewicz

Lead Software Developer