Angular は「大規模なフロントエンド プロジェクト向け」に設計されたフロントエンド フレームワークとして、実際に参考にして学ぶ価値のある設計が数多くあります。このシリーズは、主にこれらの設計と機能の実装原理を学習するために使用されます。この記事では、Angular の最大の機能である依存関係注入に焦点を当て、Angular におけるマルチレベルの依存関係注入の設計について紹介します。 【おすすめ関連チュートリアル:「Angularチュートリアル」】
前回の記事では、AngularにおけるInjectot
インジェクター、 Provider
プロバイダー、インジェクター機構について紹介しました。では、Angular アプリケーションでは、コンポーネントとモジュールはどのように依存関係を共有するのでしょうか? 同じサービスを複数回インスタンス化することはできるのでしょうか?
コンポーネントとモジュールの依存関係注入プロセスは、Angular のマルチレベルの依存関係注入設計から切り離すことができません。見てみましょう。
前述したように、Angular のインジェクターは継承可能で階層的です。
Angular には、2 つのインジェクター階層があります。
ModuleInjector
モジュール インジェクター: @NgModule()
または@Injectable()
アノテーションを使用して、この階層で ModuleInjector を構成します。 ModuleInjector
ElementInjector
インジェクター:すべての DOM 要素にモジュールを暗黙的に作成します。 インジェクターと要素インジェクターは両方ともツリー構造です。しかし、それらの階層はまったく同じではありません。
モジュールインジェクタの階層構造は、アプリケーション内のモジュール設計に関係するだけでなく、プラットフォームモジュール(PlatformModule)インジェクタとアプリケーションモジュール(AppModule)インジェクタの階層構造も持っています。
Angular の用語では、プラットフォームは Angular アプリケーションが実行されるコンテキストです。 Angular アプリケーションの最も一般的なプラットフォームは Web ブラウザですが、モバイル デバイスのオペレーティング システムや Web サーバーである場合もあります。
Angular アプリケーションが開始されると、プラットフォーム層が作成されます。
プラットフォームは、Web ページ上の Angular のエントリ ポイントであり、ページ上で実行される各 Angular アプリケーションは 1 つだけあり、すべての共通サービスはAngular
主にモジュール インスタンスの作成や破棄などの機能が含まれます:
@Injectable() エクスポートクラス PlatformRef { // インジェクターをプラットフォーム インジェクター コンストラクターとして渡します (private _injector: Injector) {} // オフライン コンパイル用に指定されたプラットフォームの @NgModule のインスタンスを作成します bootstrapModuleFactory<M>(moduleFactory: NgModuleFactory<M>, options?: BootstrapOptions): Promise<NgModuleRef<M>> {} // 指定されたランタイム コンパイラを使用して、指定されたプラットフォームの @NgModule のインスタンスを作成します bootstrapModule<M>( moduleType: タイプ<M>、 コンパイラーオプション: (コンパイラーオプション&ブートストラップオプション)| Array<CompilerOptions&BootstrapOptions> = []): Promise<NgModuleRef<M>> {} // プラットフォーム破棄時に呼び出されるリスナーを登録 onDestroy(callback: () => void): void {} // プラットフォーム インジェクターを取得します // プラットフォーム インジェクターは、ページ上のすべての Angular アプリケーションの親インジェクターであり、シングルトン プロバイダーを提供します get injector(): Injector {} // 現在の Angular プラットフォームとページ上のすべての Angular アプリケーションを破棄します。これには、プラットフォームに登録されているすべてのモジュールとリスナーの破棄も含まれます destroy() {}実際
、
プラットフォームが ( bootstrapModuleFactory
メソッドで) 起動すると、 ngZoneInjector
がngZone.run
に作成され、インスタンス化されたすべてのサービスが Angular ゾーンに作成され、 ApplicationRef
(ページ上で実行されている Angular アプリケーション) が角度ゾーン 外側に作成されます。
ブラウザで起動すると、ブラウザ プラットフォームが作成されます。export
const platformBrowser: (extraProviders?: StaticProvider[]) => PlatformRef = createPlatformFactory(platformCore, 'ブラウザ', INTERNAL_BROWSER_PLATFORM_PROVIDERS); // このうち、platformCore プラットフォームは他のプラットフォームに含める必要があります。 import const platformCore = createPlatformFactory(null, 'core', _CORE_PLATFORM_PROVIDERS);
プラットフォーム ファクトリ (上記のcreatePlatformFactory
など) を使用してプラットフォームを作成する場合、ページのプラットフォーム暗黙的に初期化されます:
export function createPlatformFactory( parentPlatformFactory: ((extraProviders?: StaticProvider[]) => PlatformRef)|null、名前: string、 プロバイダー: StaticProvider[] = []): (extraProviders?: StaticProvider[]) => PlatformRef { const desc = `プラットフォーム: ${name}`; const marker = new InjectionToken(desc); // DI トークンを返す (extraProviders: StaticProvider[] = []) => { let platform = getPlatform(); // プラットフォームが作成されている場合、処理は実行されません if (!platform || platform.injector.get(ALLOW_MULTIPLE_PLATFORMS, false)) { if (parentPlatformFactory) { // 親プラットフォームがある場合は、親プラットフォームを直接使用し、対応するプロバイダーを更新しますparentPlatformFactory( Provides.concat(extraProviders).concat({provide: マーカー、useValue: true})); } それ以外 { const injectedProviders: StaticProvider[] = Provides.concat(extraProviders).concat({provide: マーカー、useValue: true}, { 提供: INJECTOR_SCOPE、 useValue: 'プラットフォーム' }); // 親プラットフォームがない場合は、新しいインジェクターを作成し、プラットフォームを作成します createPlatform(Injector.create({providers: injectedProviders, name: desc})); } } 戻りassertPlatform(マーカー); };上記のプロセスを通じて
、
Angular アプリケーションがプラットフォームを作成するときに、プラットフォームのモジュール インジェクターであるModuleInjector
が作成されることがわかります。前のセクションのInjector
定義から、 NullInjector
がすべてのインジェクターの最上位であることもわかります
。 静的 NULL: インジェクター = new NullInjector(); したがって
、プラットフォーム モジュール インジェクターの上に、 NullInjector()
があります。プラットフォーム モジュール インジェクターの下には、アプリケーション モジュール インジェクターもあります。
各アプリケーションには少なくとも 1 つの Angular モジュールがあります。ルート モジュールは、このアプリケーションの起動に使用されるモジュールです:
@NgModule({ プロバイダー: APPLICATION_MODULE_PROVIDERS }) エクスポート クラス ApplicationModule { // ApplicationRef は、コンポーネント コンストラクター(appRef: ApplicationRef) を提供するためにブートストラップを必要とします {} AppModule
AppModule
アプリケーション モジュールはBrowserModule
によって再エクスポートされ、CLI のnew
コマンドを使用して新しいアプリケーションを作成すると、ルートAppModule
に自動的に組み込まれます。アプリケーションのルート モジュールでは、プロバイダーは、ブートストラップのルート インジェクターを構成するために使用される組み込み DI トークンに関連付けられます。
Angular は、ルート モジュール インジェクターにComponentFactoryResolver
も追加します。このパーサーはファクトリのentryComponents
ファミリーを保存するため、コンポーネントを動的に作成します。
この時点で、モジュール インジェクターの階層関係を簡単に整理できます。
モジュール インジェクター ツリーの最上位は、ルートと呼ばれるアプリケーション ルート モジュール (AppModule) インジェクターです。
ルートの上には 2 つのインジェクターがあり、1 つはプラットフォーム モジュール (PlatformModule) インジェクターで、もう 1 つはNullInjector()
です。
したがって、モジュール インジェクターの階層は次のようになります。
実際のアプリケーションでは、次のようになります。
Angular DI には階層化されたインジェクション アーキテクチャがあり、下位レベルのインジェクターも独自のサービス インスタンスを作成できることを意味します。
前述したように、Angular にはモジュール インジェクターと要素インジェクターという 2 つのインジェクター階層があります。
Angular で遅延ロード モジュールが広く使用され始めたとき、依存関係注入システムにより遅延ロード モジュールのインスタンス化が 2 倍になるという問題が発生しました。
この修正では、新しい設計が導入されました。インジェクターは、要素用とモジュール用の 2 つの並列ツリーを使用します。
Angular は、他のすべてのコンポーネントのルート ビューであるすべてのentryComponents
のホスト ファクトリを作成します。
これは、動的な Angular コンポーネントを作成するたびに、ルート ビュー ( RootData
) がルート データ ( RootView
) とともに作成されることを意味します。
class ComponentFactory_ extends ComponentFactory<any>{ 作成する( injector: インジェクター、projectableNodes?: any[][]、rootSelectorOrNode?: string|any、 ngModule?: NgModuleRef<any>): ComponentRef<any> { if (!ngModule) { throw new Error('ngModule を提供する必要があります'); } const viewDef =solveDefinition(this.viewDefFactory); constComponentNodeIndex = viewDef.nodes[0].element!.componentProvider!.nodeIndex; //ルート データを使用してルート ビューを作成します const view = Services.createRootView( インジェクター、projectableNodes || []、rootSelectorOrNode、viewDef、ngModule、EMPTY_CONTEXT); // view.nodes のアクセサ const component = asProviderData(view,componentNodeIndex).instance; if (rootSelectorOrNode) { view.renderer.setAttribute(asElementData(view, 0).renderElement, 'ng-version', VERSION.full); } //コンポーネントを作成 return new ComponentRef_(view, new ViewRef_(view),component); }ルート データ ( RootData )
には
、
RootData
elInjector
ngModule
インジェクターへの参照が含まれています。
elInjector: インジェクター、ngModule: NgModuleRef<any>、rendererFactory: RendererFactory2、 projectableNodes: any[][]、rootSelectorOrNode: any): RootData { const サニタイザー = ngModule.injector.get(サニタイザー); const errorHandler = ngModule.injector.get(ErrorHandler); const renderer = rendererFactory.createRenderer(null, null); 戻る { ngモジュール、 インジェクター: elInjector、 投影可能なノード、 selectorOrNode: rootSelectorOrNode、 消毒剤、 レンダラーファクトリー、 レンダラー、 エラーハンドラ、 };この設計は比較的単純であるため
、
要素インジェクター ツリーを導入します。インジェクター階層を変更することで、モジュールとコンポーネントのインジェクターがインターリーブされ、遅延ロードされたモジュールが二重にインスタンス化されることを回避します。各インジェクターには親が 1 つだけあり、依存関係を取得するには各解決で正確に 1 つのインジェクターを見つける必要があるためです。
Angular では、ビューはテンプレートの表現であり、その中には要素ノードが含まれます
。 ... // このビューに表示される DI のパブリック プロバイダー publicProviders: {[tokenKey: string]: NodeDef}|null; //visiblePublicProviders と同じですが、この要素にあるプライベート プロバイダーも含まれます allProviders: {[tokenKey: string]: NodeDef}|null;
@Directive()
または@Component()
のproviders
属性で設定されていない限り、 ElementInjector
はデフォルトでは空です
。
Angular は、ネストされた HTML 要素の要素インジェクターを作成するとき、親要素インジェクターから要素インジェクターを継承するか、親要素インジェクターを子ノード定義に直接割り当てます。
子 HTML 要素の要素インジェクターにプロバイダーがある場合、それを継承する必要があります。それ以外の場合、子コンポーネント用に別のインジェクターを作成する必要はなく、必要に応じて親のインジェクターから依存関係を直接解決できます。
、要素インジェクターとモジュール インジェクターはどこから並列ツリーになるのでしょうか。
CLI のnew
コマンドを使用して新しいアプリケーションを作成するときに、アプリケーション ルート モジュール ( AppModule
) がルートAppModule
に自動的に含まれることはすでにわかっています。
アプリケーション ( ApplicationRef
) が起動 ( bootstrap
) すると、 entryComponent
が作成されます。
const compRef = componentFactory.create(Injector.NULL, [], selectorOrNode, ngModule);
このプロセスでは、ルート データ ( RootData
) を使用してルート ビュー ( RootView
) が作成されます。 ) とルート要素インジェクターが作成されます。ここで、 elInjector
はInjector.NULL
です。
ここで、Angular のインジェクター ツリーは、要素インジェクター ツリーとモジュール インジェクター ツリーに分かれており、これら 2 つの並列ツリーです。
Angular は、 @Component()
で指定されたproviders
コンポーネント インスタンスを作成するたびに、下位インジェクターを定期的に作成します。また、そのインスタンスの新しいサブインジェクターも作成します。同様に、新しいNgModule
が実行時にロードされると、Angular は独自のプロバイダーを使用してそのインジェクターを作成できます。
サブモジュール インジェクターとコンポーネント インジェクターは互いに独立しており、それぞれが提供されるサービス用に独自のインスタンスを作成します。 Angular がNgModule
またはコンポーネント インスタンスを破棄すると、これらのインジェクターとインジェクター内のサービス インスタンスも破棄されます。
上記では、Angular の 2 種類のインジェクター ツリー、モジュール インジェクター ツリーと要素インジェクター ツリーを紹介しました。では、依存関係を提供するときに、Angular はどのように問題を解決するのでしょうか?
Angular では、コンポーネント/命令の依存関係を取得するためにトークンを解決する際、ElementInjector
階層 (その親)ModuleInjector
階層 (その親) の2 段階で解決します
。プロセスは次のとおりです (「マルチレベル」を参照
)。インジェクター - 解決ルール):
コンポーネントが依存関係を宣言すると、Angular は独自のElementInjector
を使用してその依存関係を満たすことを試みます。
コンポーネントのインジェクターにプロバイダーがない場合、リクエストはその親コンポーネントのElementInjector
に渡されます。
これらのリクエストは、Angular がリクエストを処理できるインジェクターを見つけるか、祖先のElementInjector
がなくなるまで転送され続けます。
Angular がどのElementInjector
にもプロバイダーを見つけられない場合、リクエストが行われた要素に戻り、 ModuleInjector
階層を検索します。
それでも Angular がプロバイダーを見つけられない場合は、エラーがスローされます。
この目的のために、Angular では特別なマージ インジェクターが導入されています。
マージ インジェクター自体には値はなく、ビューと要素定義の単なる組み合わせです。
class Injector_ はインジェクターを実装します { コンストラクター(プライベート ビュー: ViewData、プライベート elDef: NodeDef|null) {} get(トークン: 任意、notFoundValue: 任意 = Injector.THROW_IF_NOT_FOUND): 任意の { const allowedPrivateServices = this.elDef ? (this.elDef.flags & NodeFlags.ComponentView) !== 0 : false; return Services.resolveDep( this.view、this.elDef、allowPrivateServices、 {フラグ: DepFlags.None、トークン、トークンキー: tokenKey(token)}、notFoundValue); }Angular が依存関係を解決する場合
、
マージ インジェクターは要素インジェクター ツリーとモジュール インジェクター ツリーの間のブリッジになります。 Angular がコンポーネントまたはディレクティブ内の特定の依存関係を解決しようとするとき、マージ インジェクターを使用して要素インジェクター ツリーを走査し、依存関係が見つからない場合はモジュール インジェクター ツリーに切り替えて依存関係を解決します。
class ViewContainerRef_ は ViewContainerData を実装します { ... // 親ビュー要素のクエリ インジェクター getparentInjector(): Injector { let view = this._view; let elDef = this._elDef.parent; while (!elDef && view) { elDef = viewParentEl(view); view = view.parent!; } ビューを返しますか? new Injector_(view, elDef) : new Injector_(this._view, null); }
インジェクターは継承可能です。つまり、指定されたインジェクターが依存関係を解決できない場合、親インジェクターに依存関係を解決するように要求します
。
特定の解析アルゴリズムは、 resolveDep()
メソッドに実装されています
。 view: ViewData、elDef: NodeDef、allowPrivateServices: ブール値、depDef: DepDef、 notFoundValue: any = Injector.THROW_IF_NOT_FOUND): any { // // mod1 // / // el1 mod2 // / //el2 // // el2.injector.get(token) をリクエストするときは、次の順序で最初に見つかった値を確認して返します。 // - el2.injector.get(トークン、デフォルト) // - el1.injector.get(token, NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR) -> モジュールをチェックしません // - mod2.injector.get(トークン、デフォルト) それ
が<child></child>
のようなテンプレートのルートAppComponent
コンポーネントである場合、Angular には 3 つのビューがあります:
<!-- HostView_AppComponent --> <マイアプリ></マイアプリ> <!-- View_AppComponent --> <子></子> <!-- View_ChildComponent --> 一部のコンテンツは
解析プロセスに依存しており、図に示すように、解析アルゴリズムはビュー階層に基づきます。
一部のトークンが子コンポーネントで解決される場合、Angular は次の処理を行います。
まず子要素インジェクターを調べ、 elRef.element.allProviders|publicProviders
チェックします。
次に、すべての親ビュー要素 (1) を反復処理し、要素インジェクター内のプロバイダーを確認します。
次の親ビュー要素がnull
(2) に等しい場合は、 startView
(3) に戻り、 startView.rootData.elnjector
(4) を確認します。
トークンが見つからない場合のみ、 startView.rootData module.injector
(5) を確認してください。
したがって、Angular は、コンポーネントを走査して特定の依存関係を解決するときに、特定の要素の親要素ではなく、特定のビューの親要素を検索します。ビューの親要素は次の方法で取得できます。
// コンポーネント ビューの場合、これはホスト要素です。 // 埋め込みビューの場合、これは含まれているビュー コンテナの親ノードのインデックスです。 エクスポート関数 viewParentEl(view: ViewData): NodeDef| null { constparentView = view.parent; if (parentView) { view.parentNodeDef を返します !.parent; } それ以外 { null を返します。 } 概要この
では主に、Angular のインジェクターの階層構造について説明します。Angular には、モジュール インジェクター ツリーと要素インジェクター ツリーという 2 つの並列インジェクター ツリーがあります。
要素インジェクター ツリーの導入は主に、依存関係注入の解析とモジュールの遅延読み込みによって引き起こされるモジュールの二重インスタンス化の問題を解決することです。要素インジェクター ツリーの導入後、Angular の依存関係解析プロセスも調整され、要素インジェクターや親ビュー要素インジェクターなどのインジェクターの依存関係の検索が優先されます。要素インジェクター、モジュール インジェクターでトークンが見つからない場合に限ります。で依存関係がクエリされます。