LoginSignup
23
18

More than 5 years have passed since last update.

【angular.io】Dependency Injection DI in Action まとめ

Posted at

Dependency Injectionまとめ

この章では、DIについてお話しします。
live example / download exampleで詳細を確認できます。

Application-wide dependencies

ルートアプリケーションコンポーネントであるAppComponentでアプリケーション全体で使用されている依存関係のプロバイダを登録します。

次の例は、@Componentメタデータプロバイダ配列にLoggerService、UserContext、およびUserServiceをインポートおよび登録する方法を示しています。

src/app/app.component.ts
import { UserContextService } from './user-context.service';
import { UserService }        from './user.service';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  providers: [ LoggerService, UserContextService, UserService ]
})
export class AppComponent {
/* . . . */
}

これらのサービスはすべてクラスとして実装されています。 サービスクラスは独自のプロバイダとして機能することができます。そのため、プロバイダ配列にリストするのは必要なすべての登録です。

プロバイダとは、サービスを作成または提供できるものです。 Angularは、newを使用してクラスプロバイダからサービスインスタンスを作成します。 Dependency Injectionのプロバイダに関する詳細を読んでください。

これらのサービスを登録したので、Angularはコンポーネントやサービスのコンストラクタ、アプリケーションのどこにでも挿入できます。

src/app/hero-bios.component.ts
constructor(logger: LoggerService) {
  logger.logInfo('Creating HeroBiosComponent');
}
src/app/user-context.service.ts
constructor(private userService: UserService, private loggerService: LoggerService) {
}

External module configuration

一般に、ルートアプリケーションコンポーネントではなく、NgModuleにプロバイダを登録します。

これは、サービスがどこにでも注入可能であることを期待するとき、またはアプリケーションが起動する前に別のアプリケーションのグローバルサービスを設定しているときに行います。

ここでは、コンポーネントルータの構成に、AppModuleのプロバイダリストにそのプロバイダをリストすることによって、デフォルト以外のlocation strategyが含まれている2番目のケースの例を示します。

src/app/app.module.ts
providers: [
  { provide: LocationStrategy, useClass: HashLocationStrategy }
]

@Injectable() and nested service dependencies

インジェクションされたサービスのコンシューマーは、そのサービスの作成方法を知らない。 それは気にしないでください。 そのサービスを作成してキャッシュするのは、依存関係注入の仕事です。

時々、サービスは他のサービスに依存しますが、それはまだ他のサービスに依存することがあります。 これらのネストされた依存関係を正しい順序で解決することもフレームワークの仕事です。 各ステップで、依存関係の消費者はコンストラクタで必要なものを宣言し、フレームワークが引き継ぎます。

次の例は、LoggerServiceUserContextの両方をAppComponentに注入する方法を示しています。

src/app/app.component.ts
constructor(logger: LoggerService, public userContext: UserContextService) {
  userContext.loadUser(this.userId);
  logger.logInfo('AppComponent initialized');
}

UserContextは、特定のユーザーに関する情報を収集するLoggerServiceUserServiceの両方に依存します。

user-context.service.ts
@Injectable()
export class UserContextService {
  constructor(private userService: UserService, private loggerService: LoggerService) {
  }
}

AngularがAppComponentを作成すると、依存性注入フレームワークはLoggerServiceのインスタンスを作成し、UserContextServiceの作成を開始します。 UserContextServiceには、フレームワークが既に持っているLoggerServiceと、まだ作成していないUserServiceが必要です。 UserServiceには依存関係がないため、依存関係注入フレームワークでnewを使用してインスタンス化できます。

依存性注入の素晴らしさは、AppComponentはこれについて何も気にしないということです。 コンストラクタ(LoggerServiceとUserContextService)で必要なものを宣言するだけで、フレームワークは残りの処理を行います。

すべての依存関係が設定されると、AppComponentはユーザー情報を表示します。

image.png

@Injectable()

UserContextServiceクラスの@Injectable()デコレータに注目してください。

user-context.service.ts
@Injectable()
export class UserContextService {
}

そのデコレータは、AngularがLoggerServiceUserServiceの2つの依存関係の型を識別できるようにします。

技術的には、@Injectable()デコレータは、独自の依存関係を持つサービスクラスに対してのみ必要です。 LoggerServiceは何にも依存しません。 @Ijectable()を省略すると、ロガーが動作し、生成されるコードはわずかに小さくなります。

しかし、サービスはあなたが依存関係を与えた瞬間を壊し、戻って@Injectable()を追加して修正する必要があります。 一貫性と将来の痛みを避けるために、@Injectable()を先頭から追加してください。

このサイトでは@Injectable()をすべてのサービスクラスに適用することを推奨していますが、バインドされているとは思わないでください。 一部の開発者は、必要な場所にだけ追加することを好みます。これは合理的なポリシーです。

AppComponentクラスには2つの依存関係もありますが、@ Injectable()はありません。 そのコンポーネントクラスは@Componentデコレータを持っているので、@ Injectable()は必要ありませんでした。 TypeScriptのAngularでは、依存関係の型を識別するために、デコレータが1つだけあれば十分です。

Limit service scope to a component subtree

注入されたすべてのサービスの依存関係はシングルトンであり、特定の依存インジェクタの場合、サービスのインスタンスは1つだけです。

しかし、Angularアプリケーションには、コンポーネントツリーと並行なツリー階層に配置された複数の依存インジェクタがあります。そのため、複数のコンポーネントで提供されている場合、特定のサービスを任意のコンポーネントレベルで複数回提供して作成できます。

デフォルトでは、1つのコンポーネントで提供されるサービス依存関係はすべての子コンポーネントで表示され、Angularは同じサービスインスタンスをそのサービスを要求するすべての子コンポーネントに挿入します。

したがって、ルートAppComponentで提供される依存関係は、アプリケーション内の任意のコンポーネントに注入できます。

ただ、それは常に望ましいとは限りません。場合によっては、サービスの可用性をアプリケーションの特定の領域に制限したいことがあります。

そのサービスをそのブランチのサブルートコンポーネントに提供することによって、注入されたサービスの範囲をアプリケーション階層のブランチに限定することができます。この例では、サブルートコンポーネントにサービスを提供することは、ルートAppComponentにサービスを提供することと似ています。構文は同じです。ここで、HeroServiceはHeroesBaseComponentで利用可能です.HeroSerBaseはプロバイダ配列内にあるためです。

src/app/sorted-heroes.component.ts
@Component({
  selector: 'unsorted-heroes',
  template: `<div *ngFor="let hero of heroes">{{hero.name}}</div>`,
  providers: [HeroService]
})
export class HeroesBaseComponent implements OnInit {
  constructor(private heroService: HeroService) { }
}

AngularがHeroesBaseComponentを作成すると、HeroServiceの新しいインスタンスも作成されます。このインスタンスは、コンポーネントとその子(存在する場合)にのみ表示されます。

また、HeroServiceをアプリケーション内の別のコンポーネントに提供することもできます。 それは、異なるインジェクタに住んでいるサービスの別のインスタンスになります。

HeroBiosComponentHeroOfTheMonthComponent、およびHeroesBaseComponentを含むサンプルコード全体に、このようなスコープ付きHeroServiceシングルトンの例が表示されます。 これらの各コンポーネントには、独立したヒーローのコレクションを管理する独自のHeroServiceインスタンスがあります。

Multiple service instances (sandboxing)

場合によっては、サービスの複数のインスタンスをコンポーネント階層の同じレベルに配置することが必要な場合があります。

良い例は、コンパニオンコンポーネントインスタンスの状態を保持するサービスです。 各コンポーネントごとに個別のサービスインスタンスが必要です。 各サービスは、それぞれ異なるコンポーネントのサービスと状態から隔離された独自の作業状態を持ちます。 これは、各サービスとコンポーネントのインスタンスには独自のサンドボックスがあるため、サンドボックスと呼ばれます。

HeroBioComponentの3つのインスタンスを表すHeroBiosComponentを想像してみてください。

app/hero-bios.component.ts
@Component({
  selector: 'hero-bios',
  template: `
    <hero-bio [heroId]="1"></hero-bio>
    <hero-bio [heroId]="2"></hero-bio>
    <hero-bio [heroId]="3"></hero-bio>`,
  providers: [HeroService]
})
export class HeroBiosComponent {
}

HeroBioComponentは、ヒーローの伝記を編集することができます。 HeroBioComponentHeroCacheServiceを使用して、そのヒーローに他の永続操作をフェッチ、キャッシュ、実行します。

src/app/hero-cache.service.ts
@Injectable()
export class HeroCacheService {
  hero: Hero;
  constructor(private heroService: HeroService) {}

  fetchCachedHero(id: number) {
    if (!this.hero) {
      this.hero = this.heroService.getHeroById(id);
    }
    return this.hero;
  }
}

明らかに、HeroBioComponentの3つのインスタンスは同じHeroCacheServiceを共有できません。 彼らは、どの主人公をキャッシュするかを決定するために互いに競争するでしょう。

HeroBioComponentは、メタデータプロバイダ配列にHeroCacheServiceをリストすることによって、独自のHeroCacheServiceインスタンスを取得します。

src/app/hero-bio.component.ts
@Component({
  selector: 'hero-bio',
  template: `
    <h4>{{hero.name}}</h4>
    <ng-content></ng-content>
    <textarea cols="25" [(ngModel)]="hero.description"></textarea>`,
  providers: [HeroCacheService]
})

export class HeroBioComponent implements OnInit  {
  @Input() heroId: number;

  constructor(private heroCache: HeroCacheService) { }

  ngOnInit() { this.heroCache.fetchCachedHero(this.heroId); }

  get hero() { return this.heroCache.hero; }
}

親のHeroBiosComponentは、heroIdに値をバインドします。 ngOnInitは、そのidをサービスに渡します。このサービスは、ヒーローを取り出してキャッシュします。 主人公のためのゲッターは、キャッシングされたヒーローをサービスから引き出します。 テンプレートは、このデータバインドされたプロパティを表示します。

live exmaple / downloadのサンプルでこの例を見つけ、3つのHeroBioComponentインスタンスが独自のキャッシュヒーローデータを持っていることを確認します。

image.png

Qualify dependency lookup with @Optional() and @Host()

今まで話したように、Angularはコンポーネント階層のどのレベルにでも依存関係を登録することができます。

コンポーネントが依存関係を要求すると、Angularはそのコンポーネントのインジェクタから開始し、最初の適切なプロバイダを見つけるまでインジェクタツリーを検索します。 Angularは、その途中に依存関係が見つからない場合、エラーをスローします。

ほとんどの場合、この動作が必要です。 しかし、検索を制限する必要がある場合や、見つからない依存関係に対応する必要がある場合があります。 Angularの検索動作は、@Hostおよび@Optionalデコレータを使用して個別にまたは一緒に使用できます。

@Optionalデコレータは、依存関係が見つからない場合、Angularが続行するよう指示します。 Angularは、代わりに注入パラメータをnullに設定します。

@Hostデコレータは、ホストコンポーネントの検索をstopさせます。

ホストコンポーネントは通常、依存関係を要求するコンポーネントです。 しかし、このコンポーネントを親コンポーネントに投影すると、その親コンポーネントがホストになります。 次の例では、この2番目のケースについて説明します。

Demonstration

HeroBiosAndContactsComponentは、上記HeroBiosComponentのリビジョンです。

src/app/hero-bios.component.ts
@Component({
  selector: 'hero-bios-and-contacts',
  template: `
    <hero-bio [heroId]="1"> <hero-contact></hero-contact> </hero-bio>
    <hero-bio [heroId]="2"> <hero-contact></hero-contact> </hero-bio>
    <hero-bio [heroId]="3"> <hero-contact></hero-contact> </hero-bio>`,
  providers: [HeroService]
})
export class HeroBiosAndContactsComponent {
  constructor(logger: LoggerService) {
    logger.logInfo('Creating HeroBiosAndContactsComponent');
  }
}

テンプレート

dependency-injection-in-action/src/app/hero-bios.component.ts
template: `
  <hero-bio [heroId]="1"> <hero-contact></hero-contact> </hero-bio>
  <hero-bio [heroId]="2"> <hero-contact></hero-contact> </hero-bio>
  <hero-bio [heroId]="3"> <hero-contact></hero-contact> </hero-bio>`,

<hero-bio>タグの間に新しい要素が追加されました。 対応するHeroContactComponentHeroBioComponentビューに投影し、HeroBioComponentテンプレートの<ng-content>スロットに配置します。

src/app/hero-bio.component.ts
template: `
  <h4>{{hero.name}}</h4>
  <ng-content></ng-content>
  <textarea cols="25" [(ngModel)]="hero.description"></textarea>`,

HeroContactComponentのヒーローの電話番号がヒーローの説明の上に投影されているように見えます。

image.png

修飾修飾子を示すHeroContactComponentは次のとおりです。

src/app/hero-contact.component.ts
@Component({
  selector: 'hero-contact',
  template: `
  <div>Phone #: {{phoneNumber}}
  <span *ngIf="hasLogger">!!!</span></div>`
})
export class HeroContactComponent {

  hasLogger = false;

  constructor(
      @Host() // limit to the host component's instance of the HeroCacheService
      private heroCache: HeroCacheService,

      @Host()     // limit search for logger; hides the application-wide logger
      @Optional() // ok if the logger doesn't exist
      private loggerService: LoggerService
  ) {
    if (loggerService) {
      this.hasLogger = true;
      loggerService.logInfo('HeroContactComponent can log!');
    }
  }

  get phoneNumber() { return this.heroCache.hero.phone; }

}

constructor parameters

src/app/hero-contact.component.ts
@Host() // limit to the host component's instance of the HeroCacheService
private heroCache: HeroCacheService,

@Host()     // limit search for logger; hides the application-wide logger
@Optional() // ok if the logger doesn't exist
private loggerService: LoggerService

heroCacheプロパティを装飾する@Host()関数は、親HeroBioComponentからのキャッシュサービスへの参照を確実に取得します。 Angularは、たとえコンポーネントツリーの上位のコンポーネントがそれを持っていても、親がそのサービスを欠いているとエラーをスローします。

2番目の@Host()関数は、loggerServiceプロパティを装飾します。 App内の唯一のLoggerServiceインスタンスは、AppComponentレベルで提供されています。 ホストHeroBioComponentには独自のLoggerServiceプロバイダがありません。

@Optional()関数でプロパティを修飾していない場合、Angularはエラーをスローします。 @Optional()のおかげで、AngularはloggerServiceをnullに設定し、残りのコンポーネントは適応します。

HeroBiosAndContactsComponentの実際の動作は次のとおりです。

image.png

@Host()デコレータをコメントアウトすると、AngularはAppComponentレベルでロガーを見つけるまで、インジェクタの祖先ツリーを歩きます。 ロガーロジックが起動し、ヒーローディスプレイに無償の「!!!」と表示され、ロガーが見つかったことを示します。

image.png

一方、@Host()デコレータを復元して@Optionalをコメントアウトすると、ホストコンポーネントレベルで必要なロガーが不足しているため、アプリケーションが失敗します。
例外: LoggerServiceのプロバイダはありません! (HeroContactComponent - > LoggerService)

Inject the component's DOM element

場合によっては、コンポーネントの対応するDOM要素にアクセスする必要があります。 開発者はそれを避けるために努力していますが、多くの視覚効果やjQueryなどのサードパーティツールではDOMアクセスが必要です。

ここでは、Attribute DirectivesページのHighlightDirectiveを簡略化して示します。

src/app/highlight.directive.ts
import { Directive, ElementRef, HostListener, Input } from '@angular/core';

@Directive({
  selector: '[myHighlight]'
})
export class HighlightDirective {

  @Input('myHighlight') highlightColor: string;

  private el: HTMLElement;

  constructor(el: ElementRef) {
    this.el = el.nativeElement;
  }

  @HostListener('mouseenter') onMouseEnter() {
    this.highlight(this.highlightColor || 'cyan');
  }

  @HostListener('mouseleave') onMouseLeave() {
    this.highlight(null);
  }

  private highlight(color: string) {
    this.el.style.backgroundColor = color;
  }
}

このディレクティブは、ユーザーが適用されているDOM要素の上にマウスを置くと、背景をハイライト色に設定します。

Angularはコンストラクタのelパラメータを注入されたElementRefに設定します。これはDOM要素のラッパーです。 そのnativeElementプロパティは、ディレクティブが操作するDOM要素を公開します。

サンプルコードでは、ディレクティブのmyHighlight属性を2つの

タグに適用しています。最初に値を指定せずに(既定の色を生成する)、割り当てられた色値を使用します。
src/app/app.component.html
<div id="highlight"  class="di-component"  myHighlight>
  <h3>Hero Bios and Contacts</h3>
  <div myHighlight="yellow">
    <hero-bios-and-contacts></hero-bios-and-contacts>
  </div>
</div>

次の図は、タグにマウスを移動した場合の効果を示しています。

image.png

Define dependencies with providers

このセクションでは、依存サービスを提供するプロバイダを記述する方法を示します。

依存性インジェクタからトークンを与えてサービスを取得します。

通常、Angularはコンストラクタパラメータとその型を指定してこのトランザクションを処理します。 パラメータタイプはインジェクタルックアップトークンとして機能します。 Angularはこのトークンをインジェクタに渡し、その結果をパラメータに代入します。 典型的な例を次に示します。

src/app/hero-bios.component.ts
constructor(logger: LoggerService) {
  logger.logInfo('Creating HeroBiosComponent');
}

Angularは、インジェクタにLoggerServiceに関連付けられているサービスを要求し、返された値をロガーパラメータに割り当てます。

インジェクタはどこでその価値を得ましたか? 内部コンテナにすでにその値が設定されている可能性があります。 そうでなければ、プロバイダーの助けを借りてそれを作ることができるかもしれません。 プロバイダとは、トークンに関連付けられたサービスを提供するためのレシピです。

インジェクタに要求されたトークンのプロバイダがない場合、インジェクタがそれ以上なくなるまでプロセスが繰り返される親インジェクタに要求を委譲します。 検索が無駄である場合、インジェクタは、要求がoptionalでない限り、エラーをスローします。

新しいインジェクタにはプロバイダがありません。 Angularは、作成するインジェクタを気にするいくつかのプロバイダで初期化します。 独自のアプリケーションプロバイダを手動で登録する必要があります。通常は、コンポーネントまたはディレクティブメタデータのプロバイダ配列に登録する必要があります。

src/app/app.component.ts
providers: [ LoggerService, UserContextService, UserService ]

Defining providers

シンプルなクラスのプロバイダは、これまでに最も典型的なものです。 あなたは、プロバイダ配列のクラスについて言及しています。

src/app/hero-bios.component.ts
providers: [HeroService]

これは、最も一般的な注入サービスがクラスのインスタンスであるため、簡単です。 しかし、クラスの新しいインスタンスを作成することによって、すべての依存関係を満たすことはできません。 依存関係の値を提供するには他の方法が必要です。つまり、プロバイダを指定する他の方法が必要です。

HeroOfTheMonthComponentの例は、多くの代替案とその理由を示しています。 視覚的にはシンプルです。いくつかのプロパティとロガーによって生成されたログです。

image.png

次のコードを見てください。

hero-of-the-month.component.ts
import { Component, Inject } from '@angular/core';

import { DateLoggerService } from './date-logger.service';
import { Hero }              from './hero';
import { HeroService }       from './hero.service';
import { LoggerService }     from './logger.service';
import { MinimalLogger }     from './minimal-logger.service';
import { RUNNERS_UP,
         runnersUpFactory }  from './runners-up';

@Component({
  selector: 'hero-of-the-month',
  templateUrl: './hero-of-the-month.component.html',
  providers: [
    { provide: Hero,          useValue:    someHero },
    { provide: TITLE,         useValue:   'Hero of the Month' },
    { provide: HeroService,   useClass:    HeroService },
    { provide: LoggerService, useClass:    DateLoggerService },
    { provide: MinimalLogger, useExisting: LoggerService },
    { provide: RUNNERS_UP,    useFactory:  runnersUpFactory(2), deps: [Hero, HeroService] }
  ]
})
export class HeroOfTheMonthComponent {
  logs: string[] = [];

  constructor(
      logger: MinimalLogger,
      public heroOfTheMonth: Hero,
      @Inject(RUNNERS_UP) public runnersUp: string,
      @Inject(TITLE) public title: string)
  {
    this.logs = logger.logs;
    logger.logInfo('starting up');
  }
}

The provide object literal

提供オブジェクトリテラルは、トークンと定義オブジェクトを取ります。 トークンは通常はクラスですが、必ずしもそうである必要はありません。

定義オブジェクトには、サービスのシングルトンインスタンスの作成方法を指定する必須プロパティがあります。 この場合、プロパティ。

useValue - 値プロバイダ
useValueプロパティを、プロバイダがサービスインスタンス(またの名をdependency object)として返すことができる固定値に設定します。

この技術を使用して、Webサイトのベースアドレスや機能フラグなどの実行時設定定数を提供します。 単体テストでバリュープロバイダーを使用して、運用サービスを偽物やモックに置き換えることができます。

HeroOfTheMonthComponentの例には、2つの値プロバイダがあります。 最初はHeroクラスのインスタンスを提供します。 二番目はリテラル文字列リソースを指定します:

dependency-injection-in-action/src/app/hero-of-the-month.component.ts
{ provide: Hero,          useValue:    someHero },
{ provide: TITLE,         useValue:   'Hero of the Month' },

ヒーロープロバイダートークンは、値がヒーローであり、注入されたヒーローの消費者がタイプ情報を必要とするため意味をなさないクラスです。

TITLEプロバイダトークンはクラスではありません。 これは、InjectionTokenと呼ばれる特別な種類のプロバイダルックアップキーです。 どんな種類のプロバイダでもInjectionTokenを使うことができますが、依存関係が文字列、数値、関数などの単純な値である場合に特に役立ちます。

値プロバイダの値を今定義する必要があります。 後で値を作成することはできません。 明らかにタイトル文字列リテラルはすぐに利用可能です。 この例のsomeHero変数は、ファイルの前に設定されています。

dependency-injection-in-action/src/app/hero-of-the-month.component.ts
const someHero = new Hero(42, 'Magma', 'Had a great month!', '555-555-5555');

他のプロバイダは、注入に必要なときに遅延して値を作成します。

useClass - クラスプロバイダ
useClassプロバイダは、指定されたクラスの新しいインスタンスを作成して返します。

この手法を使用して、共通クラスまたはデフォルトクラスの代替実装を置き換えます。 代わりに、別の戦略を実装したり、デフォルトのクラスを拡張したり、テストケースで実際のクラスの動作を偽ったりすることができます。

HeroOfTheMonthComponentには次の2つの例があります:

dependency-injection-in-action/src/app/hero-of-the-month.component.ts
{ provide: HeroService,   useClass:    HeroService },
{ provide: LoggerService, useClass:    DateLoggerService },

最初のプロバイダは、作成されるクラス(HeroService)がプロバイダの依存関係注入トークンでもある最も典型的なケースのde-sugared、拡張形式です。 これは、この短い形式で、好ましいショートフォームを解明しています。

2番目のプロバイダは、LoggerServiceにDateLoggerServiceを代入します。 LoggerServiceは既にAppComponentレベルで登録されています。 このコンポーネントがLoggerServiceを要求すると、代わりにDateLoggerServiceを受信します。

DateLoggerServiceはLoggerServiceから継承します。 各メッセージに現在の日付/時刻を追加します。

src/app/date-logger.service.ts
@Injectable()
export class DateLoggerService extends LoggerService
{
  logInfo(msg: any)  { super.logInfo(stamp(msg)); }
  logDebug(msg: any) { super.logInfo(stamp(msg)); }
  logError(msg: any) { super.logError(stamp(msg)); }
}

function stamp(msg: any) { return msg + ' at ' + new Date(); }

useExisting—the alias provider

useExistingプロバイダーは、トークンを別のトークンにマッピングします。 実際には、最初のトークンは、2番目のトークンに関連付けられたサービスの別名で、同じサービスオブジェクトにアクセスする2つの方法を作成します。

dependency-injection-in-action/src/app/hero-of-the-month.component.ts
{ provide: MinimalLogger, useExisting: LoggerService },

エイリアスインターフェイスを介してAPIを絞り込むことは、この手法の重要な使用例の1つです。 次の例は、その目的のエイリアシングを示しています。

LoggerServiceに大きなAPIがあり、実際の3つのメソッドとプロパティよりもはるかに大きいとします。 そのAPIサーフェスを実際に必要なメンバーだけに縮小したい場合があります。 ここで、MinimalLoggerクラスインタフェースは、APIを2つのメンバに縮小します。

src/app/minimal-logger.service.ts
// Class used as a "narrowing" interface that exposes a minimal logger
// Other members of the actual implementation are invisible
export abstract class MinimalLogger {
  logs: string[];
  logInfo: (msg: string) => void;
}

今度は、HeroOfTheMonthComponentの簡略版で使用するようにしてください。

src/app/hero-of-the-month.component.ts
@Component({
  selector: 'hero-of-the-month',
  templateUrl: './hero-of-the-month.component.html',
  // Todo: move this aliasing, `useExisting` provider to the AppModule
  providers: [{ provide: MinimalLogger, useExisting: LoggerService }]
})
export class HeroOfTheMonthComponent {
  logs: string[] = [];
  constructor(logger: MinimalLogger) {
    logger.logInfo('starting up');
  }
}

HeroOfTheMonthComponentコンストラクタのloggerパラメータはMinimalLoggerとして型指定されているため、LogScriptメンバとLogInfoメンバのみがTypeScript対応エディタで表示されます。

image.png

背後では、実際にはAnggerはloggerパラメータを、LoggingServiceトークンの下に登録されているフルサービスに設定します。これは上記のDateLoggerServiceになります。

useFactory—the factory provider

useFactoryプロバイダは、この例のようにファクトリ関数を呼び出すことによって依存関係オブジェクトを作成します。

dependency-injection-in-action/src/app/hero-of-the-month.component.ts
{ provide: RUNNERS_UP,    useFactory:  runnersUpFactory(2), deps: [Hero, HeroService] }

この技術を使用して、入力が注入されたサービスとローカル状態の組み合わせであるファクトリ関数を持つ依存関係オブジェクトを作成します。

依存オブジェクトは、クラスインスタンスである必要はありません。 それは何でもかまいません。 この例では、依存オブジェクトは、「Hero of the Month」コンテストまでのランナー名の文字列です。

地方の州は番号2であり、このコンポーネントが示すべきランナーの数です。 runnersUpFactoryは直ちに2で実行されます。

runnersUpFactory自体はプロバイダファクトリ関数ではありません。 真のプロバイダファクトリ関数は、runnersUpFactoryが返す関数です。

runners-up.ts
export function runnersUpFactory(take: number) {
  return (winner: Hero, heroService: HeroService): string => {
    /* ... */
  };
};

返された関数は、勝利のヒーローとヒーローサービスを引数として取ります。

Angularは、deps配列内の2つのトークンによって識別される注入された値からのこれらの引数を提供します。 2つのdeps値は、インジェクタがこれらのファクトリ関数の依存関係を提供するために使用するトークンです。

未公開の作業の後、この関数は名前の文字列を返し、AngularはHeroOfTheMonthComponentのrunnersUpパラメータにそれを挿入します

この関数は、HeroServiceから候補ヒーローを取得し、その2つをランナーアップに使用し、それらの連結名を返します。 完全なソースコードの live example /downloadのサンプルを見てください。

Provider token alternatives: the class-interface and InjectionToken

Angular依存性注入は、プロバイダトークンが返される依存オブジェクトの型でもあるクラスである場合、または通常はサービスと呼ばれる場合に最も簡単です。

しかし、トークンはクラスである必要はなく、クラスであっても返されるオブジェクトと同じ型である必要はありません。 それは次のセクションの主題です。

class-interface

以前の月のヒーローの例では、LoggerServiceのプロバイダのトークンとしてMinimalLoggerクラスが使用されていました。

dependency-injection-in-action/src/app/hero-of-the-month.component.ts
{ provide: MinimalLogger, useExisting: LoggerService },

MinimalLoggerは抽象クラスです。

dependency-injection-in-action/src/app/minimal-logger.service.ts
// Class used as a "narrowing" interface that exposes a minimal logger
// Other members of the actual implementation are invisible
export abstract class MinimalLogger {
  logs: string[];
  logInfo: (msg: string) => void;
}

通常、抽象クラスから継承します。 しかし、このアプリケーションのクラスはMinimalLoggerを継承しません。

LoggerServiceとDateLoggerServiceはMinimalLoggerから継承できます。 彼らは代わりにそれをインタフェースの方法で実装することができました。 しかし、彼らはどちらもしなかった。 MinimalLoggerは、依存性注入トークンとして排他的に使用されます。

このようにしてクラスを使用すると、それはクラスインタフェースと呼ばれます。 クラスインターフェイスの主な利点は、インターフェイスの厳密な型指定を取得できることと、通常のクラスと同じ方法でプロバイダトークンとして使用できることです。

クラスインタフェースは、コンシューマが呼び出すことができるメンバだけを定義する必要があります。 そのような狭いインターフェイスは、コンシューマクラスをコンシューマから切り離すのに役立ちます。

なぜMinimalLoggerはクラスであり、TypeScriptインターフェイスではないのですか?
インターフェイスはJavaScriptオブジェクトではないため、プロバイダトークンとしてインターフェイスを使用することはできません。 TypeScriptのデザインスペースにのみ存在します。 コードがJavaScriptに変換された後は消えます。

プロバイダトークンは、関数、オブジェクト、文字列、クラスなど、実際のJavaScriptオブジェクトでなければなりません。

クラスをインタフェースとして使用すると、実際のJavaScriptオブジェクト内のインタフェースの特性が得られます。

もちろん、実際のオブジェクトはメモリを占有します。 メモリコストを最小限に抑えるために、クラスには実装がありません。 MinimalLoggerは、この最適化されていない、事前に分解されたコンストラクタ関数用のJavaScriptに渡されます:

dependency-injection-in-action/src/app/minimal-logger.service.ts
var MinimalLogger = (function () {
  function MinimalLogger() {}
  return MinimalLogger;
}());
exports("MinimalLogger", MinimalLogger);

単一のメンバーがないことに注意してください。 これらのメンバーが型指定されているが実装されていない限り、クラスに追加するメンバーの数に関係なく、決して成長しません。 TypeScript MinimalLoggerクラスをもう一度見て、実装がないことを確認します。

InjectionToken

依存オブジェクトは、日付、数値、文字列などの単純な値、配列や関数などの形状のないオブジェクトです。

そのようなオブジェクトはアプリケーションインタフェースを持たないため、クラスによってうまく表現されません。 それらは、ユニークでシンボリックなトークンによってよりよく表現されます。これは、フレンドリな名前を持ちますが、同じ名前を持つ別のトークンと競合しないJavaScriptオブジェクトです。

InjectionTokenには、このような特徴があります。 Hero of the Monthの例、タイトル値プロバイダ、runnersUpファクトリプロバイダで2回発生しました。

dependency-injection-in-action/src/app/hero-of-the-month.component.ts
{ provide: TITLE,         useValue:   'Hero of the Month' },
{ provide: RUNNERS_UP,    useFactory:  runnersUpFactory(2), deps: [Hero, HeroService] }

次のようにTITLEトークンを作成しました:

dependency-injection-in-action/src/app/hero-of-the-month.component.ts
import { InjectionToken } from '@angular/core';

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

オプションの型パラメータは、依存型を開発者やツールに伝えます。 トークン記述は、別の開発援助です。

Inject into a derived class

別のコンポーネントから継承するコンポーネントを記述するときは注意してください。 基本コンポーネントに依存関係が注入されている場合は、それらを再提供して派生クラスに再注入し、コンストラクタを介して基本クラスに渡す必要があります。

このように考案された例では、SortedHeroesComponentはHeroesBaseComponentを継承し、ヒーローのソートされたリストを表示します。

image.png

HeroesBaseComponentはそれ自身で立つことができます。 英雄を得るためにHeroServiceの独自のインスタンスを要求し、データベースから到着した順に表示します。

src/app/sorted-heroes.component.ts
@Component({
  selector: 'unsorted-heroes',
  template: `<div *ngFor="let hero of heroes">{{hero.name}}</div>`,
  providers: [HeroService]
})
export class HeroesBaseComponent implements OnInit {
  constructor(private heroService: HeroService) { }

  heroes: Array<Hero>;

  ngOnInit() {
    this.heroes = this.heroService.getAllHeroes();
    this.afterGetHeroes();
  }

  // Post-process heroes in derived class override.
  protected afterGetHeroes() {}

}

コンストラクターをシンプルに保つ。 変数を初期化する以上のことはしません。 このルールは、コンポーネントがサーバと話すような劇的なことをする恐れなしに、コンポーネントをテスト下で構築することを安全にします。 そのため、コンストラクターではなくngOnInit内からHeroServiceを呼び出すのはこのためです。

ユーザーはヒーローをアルファベット順に見たいと思う。 元のコンポーネントを修正してサブクラス化し、ヒーローを提示する前にヒーローをソートするSortedHeroesComponentを作成します。 SortedHeroesComponentにより、基本クラスはヒーローをフェッチすることができます。

残念ながら、AngularはHeroServiceを直接ベースクラスに挿入することはできません。 このコンポーネントのHeroServiceを再度提供し、コンストラクタ内の基本クラスに渡す必要があります。

src/app/sorted-heroes.component.ts
@Component({
  selector: 'sorted-heroes',
  template: `<div *ngFor="let hero of heroes">{{hero.name}}</div>`,
  providers: [HeroService]
})
export class SortedHeroesComponent extends HeroesBaseComponent {
  constructor(heroService: HeroService) {
    super(heroService);
  }

  protected afterGetHeroes() {
    this.heroes = this.heroes.sort((h1, h2) => {
      return h1.name < h2.name ? -1 :
            (h1.name > h2.name ? 1 : 0);
    });
  }
}

ここで、afterGetHeroes()メソッドに注意してください。 あなたの最初の本能は、SortedHeroesComponentにngOnInitメソッドを作成し、ソートを行うことでした。 しかしAngularは、基底クラスのngOnInitを呼び出す前に、派生クラスのngOnInitを呼び出して、ヒーロー配列が到着する前にソートするようにします。 それは厄介なエラーを作り出します。

基本クラスのafterGetHeroes()メソッドをオーバーライドすると、問題が解決されます。

これらの複雑さは、コンポーネントの継承を避けることを主張します。

Find a parent component by injection

アプリケーションコンポーネントはしばしば情報を共有する必要があります。 データバインディングやサービス共有など、より疎結合された手法が望ましい しかし、あるコンポーネントが別のコンポーネントを直接参照して、そのコンポーネントの値にアクセスしたり、メソッドを呼び出すことがあるのは意味があります。

コンポーネントのリファレンスを取得するのは、Angularではややこしいことです。 Angularアプリケーションはコンポーネントのツリーですが、そのツリーを検査して走査するための公開APIはありません。

子参照を取得するためのAPIがあります。 APIリファレンスのQuery、QueryList、ViewChildren、およびContentChildrenを確認してください。

親参照を取得するための公開APIはありません。 しかし、すべてのコンポーネントインスタンスがインジェクタのコンテナに追加されるため、Angular dependency injectionを使用して親コンポーネントに到達することができます。

このセクションでは、そのためのテクニックについて説明します。

Find a parent component of known type

標準クラス注入を使用して、あなたが知っている型の親コンポーネントを取得します。

次の例では、親AlexComponentに、CathyComponentを含む複数の子があります。

parent-finder.component.ts
@Component({
  selector: 'alex',
  template: `
    <div class="a">
      <h3>{{name}}</h3>
      <cathy></cathy>
      <craig></craig>
      <carol></carol>
    </div>`,
})
export class AlexComponent extends Base
{
  name= 'Alex';
}

Cathyは、彼女のコンストラクタにAlexComponentを注入した後、Alexにアクセスできるかどうかを報告します。

parent-finder.component.ts
@Component({
  selector: 'cathy',
  template: `
  <div class="c">
    <h3>Cathy</h3>
    {{alex ? 'Found' : 'Did not find'}} Alex via the component class.<br>
  </div>`
})
export class CathyComponent {
  constructor( @Optional() public alex: AlexComponent ) { }
}

@Optional修飾子が安全のために存在していても、live example / downloadの例は、alexパラメータが設定されていることを確認します。

Cannot find a parent by its base class

具体的な親コンポーネントクラスがわからない場合はどうなりますか?

再利用可能なコンポーネントは、複数のコンポーネントの子である可能性があります。 金融商品に関する最新ニュースを表示するためのコンポーネントを想像してみてください。 ビジネス上の理由から、このニュースコンポーネントは、市場データストリームを変更することにより、親会社に直接頻繁に電話をかけます。

このアプリではおそらく12個以上の金融商品のコンポーネントが定義されています。 運が良ければ、それらはすべて、NewsComponentが理解できるAPIを持つ同じ基本クラスを実装します。

インターフェイスを実装するコンポーネントを探す方が良いでしょう。 TypeScriptインターフェイスはインターフェイスをサポートしていない透明化されたJavaScriptから消えるため、これは不可能です。 探す人工物はありません。

これは必ずしも良いデザインではありません。 この例では、コンポーネントが親の基本クラスを介してその親を注入できるかどうかを調べています。

サンプルのCraigComponentはこの質問を探求します。 振り返ると、AlexコンポーネントはBaseという名前のクラスから継承していることがわかります。

parent-finder.component.ts
export class AlexComponent extends Base

CraigComponentはalexコンストラクタパラメータにBaseを注入しようとし、成功した場合は報告します。

parent-finder.component.ts
@Component({
  selector: 'craig',
  template: `
  <div class="c">
    <h3>Craig</h3>
    {{alex ? 'Found' : 'Did not find'}} Alex via the base class.
  </div>`
})
export class CraigComponent {
  constructor( @Optional() public alex: Base ) { }
}

残念ながら、これは動作しません。 live example / download の例では、alexパラメータがnullであることが確認されています。 親をその基本クラスで注入することはできません。

Find a parent by its class-interface

親コンポーネントは、クラスインタフェースで見つけることができます。

親は、クラスインタフェーストークンの名前でエイリアスを提供することによって協力しなければならない。

Angularは常に独自のインジェクタにコンポーネントインスタンスを追加することを思い出してください。 それでアレックスをキャシーに早期に注射することができたのです。

別の方法で同じコンポーネントインスタンスを挿入し、そのプロバイダをAlexComponentの@Componentメタデータのproviders配列に追加する、別名をuseExisting定義付きのオブジェクトリテラルを提供します。

parent-finder.component.ts
providers: [{ provide: Parent, useExisting: forwardRef(() => AlexComponent) }],

Parentはプロバイダーのクラスインターフェイストークンです。 forwardRefは、作成した循環参照を、AlexComponentが自身を参照することによって破棄します。

アレックスの子コンポーネントの3分の1であるCarolは、以前と同じように親を親パラメータに挿入します。

parent-finder.component.ts
export class CarolComponent {
  name= 'Carol';
  constructor( @Optional() public parent: Parent ) { }
}

アレックスとその家族です:

image.png

Find the parent in a tree of parents with @SkipSelf()

コンポーネント階層の1つのブランチを想像してみましょう:Alice - > Barry - > Carol。 AliceとBarryの両方がParentクラスインタフェースを実装しています。

Barryは問題です。 彼は彼の親、Aliceに到達する必要があり、Carolの親でもあります。 つまり、Aliceを取得するためにParentクラスのインターフェイスを注入し、Carolを満たすためにParentを提供する必要があります。

ここはバリーです:

parent-finder.component.ts
const templateB = `
  <div class="b">
    <div>
      <h3>{{name}}</h3>
      <p>My parent is {{parent?.name}}</p>
    </div>
    <carol></carol>
    <chris></chris>
  </div>`;

@Component({
  selector:   'barry',
  template:   templateB,
  providers:  [{ provide: Parent, useExisting: forwardRef(() => BarryComponent) }]
})
export class BarryComponent implements Parent {
  name = 'Barry';
  constructor( @SkipSelf() @Optional() public parent: Parent ) { }
}

Barryのプロバイダの配列は、Alexのように見えます。 このような別名プロバイダを作成し続けるつもりなら、ヘルパ関数を作成する必要があります。

今のところ、Barryのコンストラクタを中心に:

Barry's constructor

constructor( @SkipSelf() @Optional() public parent: Parent ) { }

Carol's constructor

constructor( @Optional() public parent: Parent ) { }

これは、追加の@SkipSelfデコレータを除いて、Carolのコンストラクタと同じです。

  1. それはインジェクタに、親が何を意味するのか、それ自身の上にあるコンポーネントの親依存性の検索を開始するように指示します。

  2. @SkipSelfデコレータを省略すると、Angularは循環依存関係エラーをスローします。
    循環依存をインスタンス化できません! (BethComponent - > Parent - > BethComponent)

アリス、バリーとその家族です。

image.png

The Parent class-interface

クラスインタフェースは、基本クラスではなくインタフェースとして使用される抽象クラスであることを以前に学習しました。

この例では、Parentクラスインターフェイスを定義しています。

parent-finder.component.ts
export abstract class Parent { name: string; }

Parentクラスインタフェースは、型宣言を持つnameプロパティを定義しますが、実装はしません。 nameプロパティは、子コンポーネントが呼び出せる親コンポーネントの唯一のメンバーです。 そのような狭いインターフェースは、子コンポーネントクラスを親コンポーネントから切り離すのに役立ちます。

親として機能するコンポーネントは、AliceComponentのようにクラスインタフェースを実装する必要があります

parent-finder.component.ts
export class AliceComponent implements Parent

そうすることで、コードに明快さが加えられます。 しかし、それは技術的に必要ではありません。 AlexComponentには、Baseクラスで必要とされるnameプロパティがありますが、そのクラスシグネチャにはParent:

parent-finder.component.ts
export class AlexComponent extends Base

AlexComponentはParentを適切なスタイルの問題として実装する必要があります。 この例では、インターフェイスがなくてもコードがコンパイルされ実行されることを実証するだけではありません

A provideParent() helper function

同じ親エイリアスプロバイダのバリエーションを作成すると、すぐに古くなってしまいます。特に、forwardRefを使用すると、

dependency-injection-in-action/src/app/parent-finder.component.ts
providers: [{ provide: Parent, useExisting: forwardRef(() => AlexComponent) }],

そのロジックを以下のようなヘルパー関数に抽出することができます:

dependency-injection-in-action/src/app/parent-finder.component.ts

// Helper method to provide the current component instance in the name of a `parentType`.
const provideParent =
  (component: any) => {
    return { provide: Parent, useExisting: forwardRef(() => component) };
  };

これで、よりシンプルで有意義な親プロバイダをコンポーネントに追加できます。

dependency-injection-in-action/src/app/parent-finder.component.ts
providers:  [ provideParent(AliceComponent) ]

あなたはより良くすることができます。 ヘルパー関数の現在のバージョンでは、Parentクラスインタフェースのエイリアスのみが可能です。 アプリケーションには、さまざまな親タイプがあり、それぞれに独自のクラス・インターフェース・トークンがあります。

デフォルトは親になりますが、別の親クラスインタフェースにはオプションの第2パラメータも受け入れます。

dependency-injection-in-action/src/app/parent-finder.component.ts
// Helper method to provide the current component instance in the name of a `parentType`.
// The `parentType` defaults to `Parent` when omitting the second parameter.
const provideParent =
  (component: any, parentType?: any) => {
    return { provide: parentType || Parent, useExisting: forwardRef(() => component) };
  };

そして、それを別の親タイプで使う方法は次のとおりです:

dependency-injection-in-action/src/app/parent-finder.component.ts
providers:  [ provideParent(BethComponent, DifferentParent) ]

Break circularities with a forward class reference (forwardRef)

クラス宣言の順序はTypeScriptで重要です。 クラスが定義されるまで直接参照することはできません。

通常、これは問題ではありません。特に、ファイルごとに推奨される1つのクラスを遵守している場合は特に問題はありません。 しかし、循環参照は避けられないことがあります。 クラス 'A'がクラス 'B'を指し、 'B'が 'A'を指すときにバインドされています。 そのうちの1つを最初に定義する必要があります。

Angular forwardRef()関数は、Angularが後で解決できる間接参照を作成します。

Parent Finderサンプルは、中断することが不可能な循環クラス参照でいっぱいです。

このジレンマに直面するのは、クラスがプロバイダ配列内のAlexComponentと同じように自分自身を参照するときです。 providers配列は@Componentデコレータ関数のプロパティであり、クラス定義の上に表示する必要があります。

parent-finder.component.ts
providers: [{ provide: Parent, useExisting: forwardRef(() => AlexComponent) }],
23
18
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
18