とんかつ時々あんどーなつ

〜たとえ低空でも飛行していられるように〜

Angular アプリケーション初期化時に DI が必要な処理をする方法

はじめに

Angular には injection context と呼ばれる DI が可能なコンテキストが限られている。 アプリケーション実装中に DI するタイミングとしてよく目にするのは @Component@Injectable のついた class の初期化時ではないだろうか?

あまり意識することはないかもしれないが injection context でない main.ts などでは DI できない。

しかし、場合によってはアプリケーション初期化時に DI をしたいときもある。今回はその方法を紹介する。

アプリケーション初期化時に DI する方法

準備

まずプロバイダーとして登録するための class を定義する。今回は以下のような class を用意した。

@Injectable({ providedIn: 'root' })
export class Tracer {
  #isInitialized = false;

  init(): void {
    if (this.#isInitialized) {
      throw new Error('Tracer is already initialized');
    }

    // アプリケーション初期化時に必要なことを実行

    this.#isInitialized = true;
  }

  doSomething(): void {
    if (!this.#isInitialized) {
      throw new Error('Tracer is not initialized.');
    }

    // 何かの処理を実行
    console.log('Running something in Tracer...');
  }
}

init() をアプリケーション初期化時に実行し、doSomething()init() が呼ばれていないとエラーになる。 アプリケーション初期化時に実行したいので main.ts で DI したいところだが、前述したようにに main.ts は injection context ではないため実行時に NG0203 が発生する。

そのため別の方法を検討する必要がある。

bootstrapApplication のタイミングで呼び出せるようにする

Angular にはアプリケーション初期化処理のための DI トークンが存在する。それが APP_INITIALIZER である。 APP_INITIALIZER を使うと任意の関数をアプリケーション起動時に DI し、初期化中にその関数を実行することができる。

プロバイダーとして登録するには bootstrapApplication() の第 2 引数の ApplicationConfig に追加する。 以下がその記述例になる。

export const appConfig: ApplicationConfig = {
  providers: [
    ...
    {
      provide: APP_INITIALIZER,
      useValue: () => inject(Tracer).init(),
      multi: true,
    },
  ],
};

これでアプリケーションの初期化時に処理を挟むことができるようになる。 APP_INITIALIZER は初期化処理用の特別なトークンで、このトークンに対して複数のプロバイダーを登録する可能性が考えられるため multi: true を使うことがお作法になっている。

また、これを簡略化した API も存在する。それが provideAppInitializer() である。

angular/packages/core/src/application/application_init.ts at 20.0.4 · angular/angular · GitHub

provideAppInitializer() を使うと以下のように書くことができる。

export const appConfig: ApplicationConfig = {
  providers: [
    ...
    provideAppInitializer(() => inject(Tracer).init()),
  ],
};

元々は APP_INITIALIZERシンタックスシュガーとして登場したが v19 で APP_INITIALIZER が非推奨になったため、現在は provideAppInitializer() を使う方が推奨されている。

初期化の詳細を隠すために provideXxx() 関数を用意してもよい。

// tracer.ts

export function provideTracer(): EnvironmentProviders {
  return provideAppInitializer(() => inject(Tracer).init());
}

// main.ts
export const appConfig: ApplicationConfig = {
  providers: [
    ...
    provideTracer(),
  ],
};

これでアプリケーション初期化時に任意の処理を挟むことができる。

おわりに

アプリケーション初期化時に必要な処理はそこまで多くない。基本的に AppComponent でやればいいことは AppComponent でやるほうがよい。

一方で、外部サービスを用いたエラートラッキングの準備などはできるだけ早い段階でおこないたい。 その場合のひとつの方法として provideAppInitializer() を覚えておくとよさそうである。