postimage

APP_INITIALIZER to wbudowany w Angulara InjectionToken. Pod InjectionToken można zarejestrować wartość, funkcję albo serwis. Token ten można wstrzyknąć do komponentu lub serwisu. Przykład zdefiniowania MY_TOKEN:

export const MY_TOKEN = new InjectionToken<string>('MY_TOKEN');

Zarejestrowanie wartości Hello pod MY_TOKEN:

@NgModule({
// (...)
providers: [{
  provide: MY_TOKEN,
  useValue: 'Hello',
}]
// (...)
})
export class AppModule { }

Wstrzyknięcie wartości MY_TOKEN do serwisu oraz wyświetlenie w konsoli przeglądarki Hello:

@Injectable()
export class MyService {

  constructor(@Inject(MY_TOKEN) public value: string) {
    console.log(value);
  }

}

Natomiast, dzięki tokenowi APP_INITIALIZER, możliwe jest wykonanie funkcji, lub zestawu funkcji, które zostaną wykonane przed uruchomieniem aplikacji (bootstraping).

Przykład

Prosty przykład wywołania dwóch funkcji przed startem aplikacji:

export function appInit1() {
  return () => console.log('Hello from appInit1!');
}

export function appInit2() {
  return () => console.log('Hello from appInit2!');
}

Parametr multi pozwala na rejestrację dwóch lub więcej funkcji pod APP_INITIALIZER:

@NgModule({
// (...)
providers: [{
  provide: APP_INITIALIZER,
  useFactory: appInit1,
  multi: true
},
{
  provide: APP_INITIALIZER,
  useFactory: appInit2,
  multi: true
}],
// (...)
})
export class AppModule { }

W konsoli przeglądarki pojawią się poniższe komunikaty:

Hello from appInit1!
Hello from appInit2!

Przykład z Promise

Do APP_INITIALIZER można także przekazać funkcję, która zwróci Promise! Angular poczeka, aż wszystkie zwrócone Promise’y zostaną rozwiązane (resolved).

export function appInit() {
  return () => new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('Hello from appInit');
      resolve();
    }, 2000);
  })
}

@NgModule({
// (...)
providers: [{
  provide: APP_INITIALIZER,
  useFactory: appInit,
  multi: true
}],
// (...)
})
export class AppModule { }

W rezultacie, po 2 sekundach od wywołania funkcji appInit, w konsoli zostanie wyświetlona wiadomość: Hello from appInit.

Zaawansowany przykład

Do funkcji uruchamianej przed bootstrapem aplikacji możliwe jest wstrzyknięcie serwisu. W poniższym przykładzie aplikacja frontendowa ściąga konfigurację wymaganą do poprawnego działania. Na backendzie wystawiony jest plik conf.json, serwowany przez http-server. Interface Configuration jest modelem danych z pliku conf.json, zawiera tylko pole name. Serwis AppInitService wywołuje żądanie typu GET na /api/conf.json.

export interface Configuration {
  name;
}

@Injectable()
export class AppInitService {

  constructor(private httpClient: HttpClient) {
  }

  init(): Promise<Configuration> {
    return this.httpClient.get<Configuration>('api/conf.json')
           .toPromise();
  }
}
export function appInit(appInitService: AppInitService) {
  return () => appInitService.init().then(configuration => console.log(configuration));
}

@NgModule({
// (...)
providers: [
  AppInitService,
  {
    provide: APP_INITIALIZER,
    useFactory: appInit,
    deps: [AppInitService],
    multi: true
  }]
// (...)
})
export class AppModule { }

Powyższy kod wyświetli w konsoli konfigurację z pliku conf.json.

{name: "Test App name"}

Implementacja w Angularze

Przyjrzyjmy się teraz, w jaki sposób APP_INITIALIZER został zaimplementowany w samym Angularze. W pliku application_init.ts znajduje się definicja InjectionToken.

export const APP_INITIALIZER = new InjectionToken<Array<() => void>>('Application Initializer');

Początek bootstrapowania aplikacji w Angular wygląda następująco:

platformRef#bootstrapModuleFactory()
return _callAndReportToErrorHandler(exceptionHandler, ngZone !, () => {
       const initStatus: ApplicationInitStatus = moduleRef.injector.get(ApplicationInitStatus);
       initStatus.runInitializers(); // (1)
       return initStatus.donePromise.then(() => {
         this._moduleDoBootstrap(moduleRef); // (2)
         return moduleRef;
       });

W punkcie (1) w serwisie ApplicationInitStatus wywołana jest funkcja runInitializers. Po zakończeniu ApplicationInitStatus, Angular przeprowadza bootstrap komponentu.

ApplicationInitStatus#runInitializers()
  runInitializers() {
    // (...)
    const asyncInitPromises: Promise<any>[] = [];

    if (this.appInits) {
      for (let i = 0; i < this.appInits.length; i++) {
        const initResult = this.appInits[i]();
        if (isPromise(initResult)) {
          asyncInitPromises.push(initResult);
        }
      }
    }

    Promise.all(asyncInitPromises).then(() => { complete(); }).catch(e => { this.reject(e); });

    // (...)
  }

Metoda runInitializers sprawdza wywołania, które zwróciły Promise i czeka, aż wszystkie funkcje zostaną zakończone (resolve).

Zastosowania

Do czego można zastosować APP_INITIALIZER?

  • obsługa powiadomień push z serwera (comety),
  • pobranie konfiguracji np. CSRF token,
  • monitorowanie aktywności użytkownika,
  • keep alive,
  • keycloak - polecam świetny wpis Michała Hoji na ten temat źródło.

Nawet, jeżeli w swojej aplikacji nie używamy APP_INITIALIZER, sam Angular wykorzystuje go do poprawnego działania. Przykłady użycia w Angularze: