Angular 2と依存関係の注入

アンギュラーの2番目のバージョンはリリースに近づき、ますます多くの人々がそれに興味を持ち始めています。 これまでのところ、特にロシア語ではフレームワークに関する情報があまりなく、一部のトピックでますます多くの疑問が生じています。







多くの質問を提起する1つのトピックは、依存性注入です。 一部の人々は同様の技術に出くわしていません。 他の人は、他のフレームワークに存在する他の実装に使用されるため、Angular 2のフレームワーク内でどのように機能するかを完全に理解していません。







そして、手掛かりは、2番目の角度のDIが他のものとは実際には多少異なるという事実にあります。これは主に、2番目のバージョンの一般的なアプローチと哲学によるものです。 これは、アプリケーション全体が構築されるエンティティがコンポーネントであるという事実に基づいています 。 サービス層、ルーター、依存性注入システムは二次的であり、コンポーネント内でのみ意味があります。 これは、新しいフレームワークのアーキテクチャの理解の根底にある非常に重要なポイントです。







はじめに



これは、から2ページの改定です。 Angular 2での依存性注入に関するドキュメント: thisおよびthis







なぜTypescriptか

記事では、Typescriptを使用します。 なんで?

フレームワーク自体はTypescriptで記述されており、Angular2 + Typescriptバンドルに関する情報が最も多くなっています。

構文の観点から見たTypescriptコードは、ES標準の新しい実装、追加の型付け、およびいくつかのヘルパートリックです。 ただし、アプリケーションはJavascriptとDartの両方で作成できます。 JSバージョンでは、ES6 +構文を使用できませんが、コードの簡潔さと明瞭さが失われます。 また、新しい機能をサポートするようにBabelを構成する場合、構文的には、クラス、注釈/デコレーターなど、すべてがTSコードに非常に似ています。 まあ、型なしでのみなので、依存性注入は少し違って見えます。







依存関係の問題



抽象アプリケーションを作成し、コードを小さな論理的な部分に分割していることを想像してください(したがって、角度の用語と混同しないように、「コンポーネント」と呼びません 。ビジネスロジックを含む単なるサービスクラスにしましょう)。







export class Engine { public cylinders = 4; // default } export class Tires { public make = 'Flintstone'; public model = 'Square'; } export class Car { public engine: Engine; public tires: Tires; constructor() { this.engine = new Engine(); this.tires = new Tires(); } drive() {} }
      
      





もちろん、ここにはまったくロジックはありませんが、説明に非常に適しています。







それで問題は何ですか? 現時点では、 Car



、コンストラクターで手動で作成された2つのサービスに大きく依存しています。 Car



サービスの消費者の観点から見ると、これは良いことです。なぜなら、 Car



アディクションはそのアディクション自体を世話したからです。 ただし、たとえば、必要なパラメーターがEngineコンストラクターに渡されるようにする場合は、 Car



自体のコードを変更する必要があります。







 export class Engine2 { constructor(public cylinders: number) { } } export class Car { public engine: Engine; public tires: Tires; constructor() { this.engine = new Engine2(8); this.tires = new Tires(); } }
      
      





TSのコンストラクター
 //  ,          //       : export class Engine2 { public cylinders constructor(cylinders: number) { this.cylinders = cylinders } }
      
      





したがって、コンシューマーで依存関係のインスタンスを作成することはあまりよくありません。







Car



依存関係インスタンスが外部から渡されるようにコードを書き直します。







 export class Car { constructor(public engine: Engine, public tires: Tires) { } }
      
      





すでに良い。 サービス自体のコードが削減され、サービス自体がより柔軟になりました。 テストと構成が簡単です:







 class MockEngine extends Engine { cylinders = 8; } class MockTires extends Tires { make = "YokoGoodStone"; } let car = new Car(new Engine(), new Tires()); let supercar = new Car(new Engine2(12), new Tires()); var mockCar = new Car(new MockEngine(), new MockTires());
      
      





ただし、問題はCar



サービスのコンシューマーから始まります。サービス自体だけでなく、すべての依存関係を作成してから、作成したサービス依存関係のインスタンスをCar



コンストラクターに転送する必要があります。







そして、それぞれの新しいコンポーネントとそれぞれの新しい依存関係により、サービスインスタンスを作成することがますます困難になっています。 もちろん、 Car



サービスを作成するためのすべてのロジックを取り出すファクトリーを作成できます。







 export class CarFactory { createCar() { let car = new Car(this.createEngine(), this.createTires()); car.description = 'Factory'; return car; } createEngine() { return new Engine(); } createTires() { return new Tires(); } }
      
      





しかし、問題は特に少なくなることはありませんCar



依存関係を変更するときは、工場を手動で最新に保つ必要があります。







実装への道



どうすればコードを改善できますか? すべての消費者は、必要な依存関係サービスを知っています。 しかし、システムの接続性を減らすために、消費者はそれらを自分で作成するべきではありません。 すべてのサービスのインスタンスが作成および保存されるシングルトンクラスを作成できます。 このクラスでは、必要なサービスを作成する方法を決定し、たとえば特定のキーでそれらを取得できます。 次に、サービスでは、何らかの方法でそのようなシングルトンのインスタンスを取得するだけで十分であり、すでにそこから依存関係の既製のインスタンスを取得します。 このパターンは、ServiceLocatorと呼ばれます。 これは、制御の反転の一種です。 次に、コードは次のようになります。







 import {ServiceLocator} from 'service-locator.ts'; // ... let computer = ServiceLocator.instance.getService(Car) //     
      
      





一方では、消費者の緊密な接続とその依存関係を取り除きました。すべてのサービスは消費者の外部で作成されます。 しかし、他方では、消費者はサービスロケーターと密接に接続されています。各消費者は、サービスロケーターインスタンスがどこにあるかを知る必要があります。 さて、サービスの作成はまだ手動です。







何らかの方法で、その依存関係と依存関係インスタンスが配置される変数をコンシューマーで単純に示し、サービス自体の作成と実装を自動化できるようにしたいと考えています。







これは、DIフレームワークが行うことです。 挿入された依存関係のライフサイクルを管理し、これらの依存関係が必要な場所を追跡して実装します。 コンシューマーが要求した依存関係の作成されたインスタンスをコンシューマーに転送します。 サービスロケーターへの強い依存は消費者から消えます。DIフレームワークはロケーターと連携するようになりました。







作品の本質は次のとおりです。









また、DIフレームワークに応じて、これらの項目はコード内で異なって見えます。







角度1



このフレームワークの2番目のバージョンのデバイス、特にDIをよりよく理解するために、最初の部分の配置方法について少し説明したいと思います。







アプリケーションのライフサイクルは、いくつかの段階で構成されています。 2つの段階を強調したいと思います。









最上位にはモジュールがあります。 モジュールは、実際には、アプリケーション、サービス、コントローラー、ディレクティブ、フィルターなどのさまざまな部分を登録および保存できる単なるオブジェクトです。 また、モジュールには、アプリケーションの対応する段階で起動される設定呼び出しと実行呼び出しを含めることができます。







そのため、最初のバージョンでは依存性注入は次のようになります。







コード
  // - function factory() { var privateField = 2; return { publicField: 'public', publicMethod: function (arg) { return arg * privateField; } }; } var module = angular.module('foo', []); //   //     'MyService'    //   ,  ,   - (2- )      module.factory('MyService', factory); //     'MyController' //            module.controller('MyController', function (MyService) { console.log(MyService.publicMethod(21)); //    })
      
      





はい、たくさんのニュアンスがあります。 たとえば、最初の格納庫にはすでに5種類のサービスがあります 。そのため、さまざまな方法でサービスを登録できます。 そして、コードを縮小するとき、関数の引数は変更される可能性があるため、依存関係を宣言するために別の構文を使用することをお勧めします...







しかし、私は最初の角度のジャングルを掘り下げたくないので、主なポイントのみを書きます。









Angular 2:新しい方法



角度の2番目のバージョンは、最初から記述された新しいフレームワークとして発表され、最初の部分のすべてのエラーを考慮しました。 バージョン2を使用したので、まさにその印象がありました。 不要なエンティティと概念はなくなりました。 残されたものはより良く、より便利になっただけで、革新はうまく適合し、論理的に見えます。







実際、最初の角度は、DIを使用して接着された有用なトリック、テクニック、およびパターンのセットでした。 しかし、その一部の一部はそれ自体で何らかの形であり、わずかにばらばらでした。 単一のコンセプトはありませんでした。









その結果、アプリケーションの構造が完全に異なる可能性があります。 しかし、自由の代わりに、これは通常、混合概念を意味しました。







コンポーネントアプローチ



Angular 2のコンポーネントとは何ですか? これは、特定のメタデータと関連するプレゼンテーションレイヤー(テンプレート)を持つクラスです。 クラスからコンポーネントを作成するには、これらの非常に具体的なメタデータを追加する必要があります。 最も簡単な方法は、ビューをViewModel(つまりクラス自体)にバインドする@Component



デコレータでラップすることです。 また、タイプ階層の観点から見ると、コンポーネントはディレクティブの特殊なケース( @Directive



デコレーターを使用して決定されます)であり、テンプレートがあります。







 @Component({ selector: 'app', template: `<h1>Hello, {{ greetings }}</h1>` }) export class AppComponent { greetings: string = 'World'; }
      
      





オブジェクトをデコレータに渡す必要があります。デコレータには、 selector



template



少なくとも2つの必須フィールドが含まれている必要があります。







selector



フィールドには、DOM内のコンポーネントを検索するためのCSSセレクタとして使用される文字列が含まれています。 任意の有効なセレクターを渡すことができますが、ほとんどの場合、HTMLタグの標準セットの一部ではないセレクタータグを使用します。 このようにして、カスタムタグが作成されます。







template



フィールドにはtemplate



ストリングが含まれており、セレクターで検出されたDOM要素のコンテンツを置き換えます。 テンプレートを含む行の代わりに、テンプレートファイルへのパスを含む行を渡すことができます(フィールドのみtemplateUrl



と呼ばれます)。 テンプレートの構文の詳細については、ドックのページまたはロシア語の翻訳を 参照してください







コンポーネント階層



最初の格納庫で何が悪かったのですか? スコープの階層がありましたが、サービス層はすべてに共通でした。 サービスは、アプリケーションの起動前に一度だけセットアップされ、シングルトンでさえありました。







ルーターにはまだ問題がありました。 オリジナルはかなり貧弱で、通常の階層を作成できませんでした。 UIルーターは機能が豊富で、複数のビューを使用でき、状態の階層を構築できました。

しかし、両方のルーターの主な問題は、このパスの階層全体がスコープの階層にまったく接続されておらず、非常に柔軟であるということです。







2番目のバージョンは何をしましたか? 私が言ったように、2番目の角度の基本はコンポーネントです。 アプリケーション全体は、ツリーのような階層構造を形成するコンポーネントのみで構成されています。 ルートコンポーネントは、ブートストラップ機能を使用してHTMLページに読み込まれます(ブラウザがターゲットプラットフォームとして使用されている場合)。 他のすべてのコンポーネントはルート内に配置され、コンポーネントのツリーを形成します。







コードの重複を避けながら、各コンポーネントができるだけ独立し、再利用可能で、自給自足できるようにする方法は?

コンポーネントの独立性を確保するために、このコンポーネントが機能するために必要なすべてのものを完全に記述することができるメタデータがあります:ルーティングの設定、ディレクティブのリスト、使用されるパイプおよびサービス。 サービス層を介して接続されないように、各コンポーネントには独自のルーターと独自のインジェクターがあります。 そして、それらはまた、階層を形成し、これは常にコンポーネントの階層に関連付けられます。







これが、Angular2のDIを他のDIフレームワークと区別するものです。格納庫では、アプリケーションに1つのインジェクターがなく、各コンポーネントが独自のインジェクターを持つことができます







Angular2の依存性注入



依存性注入は、2番目の角度でどのように見えますか? 現在、サービスはタイプごとに実装されています。 通常、実装はコンシューマーコンストラクターで行われます。







サービス



Angular 2のサービスは単純なクラスです。







 interface User { username: string; email: string; } export class UserService { getCurrent(): User { return { username: 'Admin', email: 'admin@example.com' }; } }
      
      





サービス登録



サービスを実装するには、まず登録する必要があります。 インジェクターを手動で作成する必要はありません。 bootstrap



関数が呼び出されると、アングル自身がグローバルインジェクターを作成します。







 bootstrap(AppComponent);
      
      





2番目の引数には、プロバイダーを含む配列を渡すことができます。 したがって、サービスを使用可能にする1つの方法は、そのクラスをリストに追加することです。







 bootstrap(AppComponent, [UserService]);
      
      





このコードにより、アプリケーション全体でサービスを利用できるようになります。 ただし、これを行うことは常に良いとは限りません。 フレームワークの開発者は、システム全体で必要な場合にのみ、この場所にシステムプロバイダーのみを登録することをお勧めします。 たとえば、ルーター、フォーム、およびHttpサービスのプロバイダー。







サービスを登録する2番目の方法は、プロバイダーフィールドのコンポーネントメタデータに追加することです。







 import {Component} from 'angular2/core'; import {bootstrap} from 'angular2/platform/browser'; @Component({ selector: 'app', providers: [UserService], template: `<h1>App</h1>`, }) export class AppComponent { } bootstrap(AppComponent);
      
      





コンポーネントへのサービスの埋め込み



サービスを実装する最も簡単な方法は、コンストラクターを使用することです。 TypeScriptは型をサポートするため、次のように記述します。







 @Component({ selector: 'app', providers: [UserService], template: ` <h1>App</h1> Username: {{ user.username }} <br> Email: {{ user.email }} `, }) export class AppComponent { user: User; constructor(userService: UserService) { this.user = userService.getCurrent(); } } bootstrap(AppComponent);
      
      





それだけです! UserService



登録されUserService



いる場合、angularは必要なインスタンスをコンストラクター引数に挿入します。







サービスへのサービスの展開



サービスが依存関係自体を注入するには、 @Injectable



デコレータでサービスをラップする必要があります。 開発者は、すべてのサービスにこのデコレータを追加することをお勧めします。サービス内の依存関係が必要になるかどうかわからないためです。 だから彼らのアドバイスに従ってください。







 import {Injectable} from 'angular2/core'; @Injectable() //   export class Logger { logs: string[] = []; log(message: string) { this.logs.push(message); console.log(message); } } @Injectable() //   export class UserService { constructor(private _logger: Logger) {} //        getCurrent() { this._logger.log(' ...'); return { username: 'Admin', email: 'admin@example.com' }; } }
      
      





Logger



サービスを登録することを忘れないでください。そうしないと、角度がエラーをスローします。







 EXCEPTION: No provider for Logger! (AppComponent -> UserService -> Logger)
      
      





そのため、 Logger



をコンポーネントプロバイダーのリストに追加します。







 providers: [UserService, Logger],
      
      





オプションの依存関係



実装されたサービスがオプションの場合、 @Optional



アノテーションを追加する必要があります。







 import {Optional, Injectable} from 'angular2/core'; @Injectable() //   export class UserService { constructor(@Optional() private _logger: Logger) {} //        getCurrent() { this._logger.log(' ...'); return { username: 'Admin', email: 'admin@example.com' }; } }
      
      





Logger



登録を忘れた場合でも、エラーは発生しません。







プロバイダー



プロバイダーは、実行時に実装されたサービスの特定のバージョンを提供します。 実際、サービス自体ではなく、プロバイダーを常に登録しています。 ほとんどの場合、それらは一致します。

フレームワークにはProvider



クラスがありProvider



。 インジェクターが依存関係をインスタンス化する方法を説明します。







プロバイダーのリスト(コンポーネントまたはブートストラップ関数)にサービスクラスを追加するとき、実際には、これは次のことを意味します。







 [Logger], //        [new Provider(Logger, {useClass: Logger})], //  ,   provide [provide(Logger, {useClass: Logger})],
      
      





Provider



クラスのコンストラクターとprovide



関数は、どちらも2つの引数を取ります。









実際、実装がタイプごとに行われると言ったとき、私は完全な真実を伝えませんでした。 実装を行うことができるトークンは、クラスだけでなく、後でそれ以上にすることができます。







代替サービスプロバイダー



Logger



クラスの代わりにBetterLogger



クラスのインスタンスをサービスとして使用するとします。 アプリケーション全体でBetterLogger



Logger



依存関係を検索および変更する必要はありませんuseClass



オプションを使用してLogger



プロバイダーを登録するだけです。







 [provide(Logger, {useClass: BetterLogger})]
      
      





代替クラスに、元のサービスにはないある種の依存関係がある場合でも:







 @Injectable() class EvenBetterLogger { logs:string[] = []; constructor(private _timeService: TimeService) { } log(message: string) { message = `${this._timeService.getTime()}: ${message}`; console.log(message); this.logs.push(message); } }
      
      





とにかく、簡単に使用できます。必要な依存関係を登録するだけです。







 [ TimeService, provide(Logger, {useClass: EvenBetterLogger}) ]
      
      





プロバイダーのエイリアス



古いOldLogger



ロガーOldLogger



依存する古いコンポーネントがあるとします。 このサービスには、新しいNewLogger



ロガーと同じインターフェースがあります。 しかし、何らかの理由で、その古いコンポーネントを変更することはできません。 したがって、古いロガーの代わりに新しいものが必要です。 これを行おうとすると:







 [ NewLogger, provide(OldLogger, {useClass: NewLogger}) ]
      
      





これは私たちが望んでいたものではありません。新しいロガーの2つのコピーが作成されます。 1つは古いものが実装されている場所で使用され、もう1つは新しいロガーが導入されている場所で使用されます。 どこでも使用される新しいロガーのインスタンスを1つだけ作成するには、 useExisting



オプションでプロバイダーを登録します。







 [ NewLogger, provide(OldLogger, {useExisting: NewLogger}) ]
      
      





バリュープロバイダー



サービスプロバイダーを置き換えるために別のクラスを作成するのではなく、既成の値を使用する方が簡単な場合があります。 例:







 //   ,     ,    Logger let silentLogger = { logs: ['Silent logger says "Shhhhh!". Provided via "useValue"'], log: () => {} }
      
      





既製のオブジェクトを使用するには、 useValue



オプションを使用してプロバイダーを登録します。







 [provide(Logger, {useValue: silentLogger})]
      
      





工場プロバイダー/工場プロバイダー



最初から入手できない情報を使用して、プロバイダーを動的に登録する必要がある場合があります。 たとえば、この情報はセッションから取得でき、時々異なる場合があります。 また、実装されたサービスはこの情報に独立してアクセスできないと想定しています。

そのような場合は、工場/工場プロバイダーを使用してください。







EvenBetterLogger



ように、別のサービスからの情報を必要とする特定のBookService



サービスをEvenBetterLogger



ましょう。 AuthService



データを使用してユーザーが承認されているかどうかを確認するとします。 しかし、 EvenBetterLogger



とは異なり、サービスを直接実装することはできません。 この場合、 BookService



AuthService



アクセスできません。 サービスは次のようになります。







 @Injectable() export class AuthService { isLoggedIn: boolean = false; } @Injectable() export class BookService { books: any[]; // ,   extraBooks: any[]; // ,     constructor(private _logger: Logger, private _isLoggedIn: boolean) {} getBooks() { if (this._isLoggedIn) { this._logger.log(' '); return [...this.books, ...this.extraBooks]; } this._logger.log(' '); return this.books; } }
      
      





Logger



, boolean-.

- BookService



, :







 let bookServiceFactory = (logger: Logger, authService: AuthService) => { return new BookService(logger, authService.isLoggedIn); }
      
      





, , useFactory



, deps



— :







 [provide(BookService, {useFactory: bookServiceFactory, deps: [Logger, AuthService]})
      
      







- , , . , . , :







 let logger: Logger = this._injector.get(Logger);
      
      





, - :







 constructor(private _logger: Logger) {}
      
      





.









? , , , ..







, -, . , , - :







 export interface Config { apiEndpoint: string, title: string } export const CONFIG: Config = { apiEndpoint: 'api.heroes.com', title: 'Dependency Injection' };
      
      





, . :







 // FAIL [provide(Config, {useValue: CONFIG})] // FAIL constructor(private _config: Config) {}
      
      





: .

, Java C# ( DI- ), . . , JavaScript. , interface



— TypeScript, . , .







問題解決



OpaqueToken



, - :







 import {OpaqueToken} from 'angular2/core'; export let APP_CONFIG = new OpaqueToken('app.config');
      
      





OpaqueToken



, .

:







 providers: [provide(APP_CONFIG, {useValue: CONFIG})]
      
      





, @Inject



:







 constructor(@Inject(APP_CONFIG) private _config: Config) {}
      
      





, , .







, :







 [provide('Congig', {useValue: CONFIG})] //... constructor(@Inject('Config') private _config: Config) {}
      
      







, Angular2- — . . .







? , -, , . , - , . , - . , providers



.







, . - - . , - . . , : , . .







, . . , , .







? providers



, . , bootstrap



.

providers



, Injector.resolveAndCreate([...])



, . parent



, . , . , .







, :







コード
 import {bootstrap} from 'angular2/platform/browser'; import {Injectable, Component} from 'angular2/core'; @Injectable() class LoggerA { logs: string[] = []; log(message: string) { this.logs.push(message); console.log('Logger a: ' + message); } } @Injectable() class LoggerB { logs: string[] = []; log(message: string) { this.logs.push(message); console.log('Logger b: ' + message); } } @Component({ selector: 'child', providers: [LoggerA], template: ` <div> <h4>Child</h4> <button (click)="update()">Update</button> <p>Logs:</p> <strong>LogA: <pre>{{ logA.logs | json }}</pre></strong> <strong>LogB: <pre>{{ logB.logs | json }}</pre></strong> </div>` }) export class ChildComponent { constructor(public logA: LoggerA, public logB: LoggerB) {} update() { this.logA.log('Child: A'); this.logB.log('Child: B'); } } @Component({ selector: 'app', providers: [LoggerA, LoggerB], directives: [ChildComponent], template: ` <div> <div style="display: inline-block; vertical-align: top;"> <h3>App</h3> <button (click)="update()">Update</button> <p>Logs:</p> <strong>LogA: <pre>{{ logA.logs | json }}</pre></strong> <strong>LogB: <pre>{{ logB.logs | json }}</pre></strong> </div> <div style="display: inline-block; vertical-align: top;"> <child></child> </div> </div>` }) export class AppComponent { constructor(public logA: LoggerA, public logB: LoggerB) {} update() { this.logA.log('App: A'); this.logB.log('App: B'); } } bootstrap(AppComponent);
      
      





http://plnkr.co/edit/nbpmh3wb5g34WetQ3AAE?p=preview







2 2 . 2 ( LoggerA



LoggerB



), — LoggerA



. Update



, LogB



, , LoggerB



, . LoggerA



. , — .







, Angular2 ? 1- . , .







結論





:










All Articles