Suguru Inatomi
Posted on November 4, 2019
この記事では、Angular v9.0にて非推奨となる entryComponents
機能が、なぜ非推奨になるのかについてできるだけ簡単に解説します。
Angular - Deprecated APIs and Features
はじめに
解説を始める前に、重要な点をあらかじめ書き記しておきます。
- もしIvyをオプトアウトする場合は、
entryComponents
は引き続き必要です。決して削除しないでください。 - いままで
entryComponents
機能を使ったことがない方が新たになにか覚える必要はありません。興味がなければ過去のものとして無視してください。
entryComponents
とは何なのか
v9.0 で非推奨となる entryComponents
とは何だったのかということをまずは振り返りましょう。
entryComponents
は多くの場合、 動的なコンポーネント を実現するために利用されます。動的なコンポーネントとは、AngularのテンプレートHTML内に登場せず、コードの実行によって生成されるコンポーネントです。テンプレートHTMLを静的に検査しても宣言が見つからないことから 動的 と呼ばれます。
もっとも代表的なユースケースはダイアログやモーダルのようなケースです。コンポーネントクラスの処理が実行されることで動的にコンポーネントが表示されます。このようなコンポーネントはテンプレートHTML内に宣言されません。
たとえばAngular CDKのOverlay APIを使ってコンポーネントをオーバーレイ上に表示するには次のようなコードを書きます。
export class AppComponent {
constructor(private overlay: Overlay) {}
openModal() {
const overlayRef = overlay.create();
const modalPortal = new ComponentPortal(MyModalComponent);
overlayRef.attach(modalPortal);
}
}
このとき、動的に表示したい MyModalComponent
は、 それが宣言される NgModule
の entryComponents
配列に追加される必要があります。
@NgModule({
declarations: [AppComponent, MyModalComponent],
entryComponents: [MyModalComponent],
})
export class AppModule {}
なぜ entryComponents
が必要なのか
Angularに慣れている人にとっては、もはや当たり前のように「モーダルを実装するときは entryComponents
」というルーチンになってしまっているかもしれませんが、そもそもなぜこれが必要なのでしょうか。その理由は、Angular v8までのAoTテンプレートコンパイラと、そのテンプレートコンパイラが生成する実行コードに理由があります。
ここで以降の説明の簡単のため、v8以前のAoTコンパイラを ViewEngine (VE) コンパイラと呼びます。
コンポーネントの生成とComponentFactory
動的コンポーネントの生成には ComponentFactoryResolver
というAPIを使います。このAPIはコンポーネントクラスから、そのコンポーネントに対してAoTコンパイラが生成した ComponentFactory オブジェクトを返すものです。
export class AppComponent {
constructor(private cfr: ComponentFactoryResolver) {}
ngOnInit() {
const componentFactory = this.cfr.resolveComponentFactory(SomeComponent);
}
}
先ほど紹介したCDKのOverlayやPortalの機能も、この ComponentFactoryResolver
を利用しています。そして、 entryComponents
に追加されたコンポーネントだけがこの resolveComponentFactory
メソッドの引数に使えます。もし追加されていなければ次のようなエラーが表示されます。
つまり、 entryComponents
とは、「あるコンポーネントのComponentFactoryを解決可能にする」ための機能であると言えます。ではなぜ entryComponents
に追加されていないコンポーネントのComponentFactoryは解決できないのでしょうか。すべてのコンポーネントは等価ではないのでしょうか?
ViewEngineはTree-shakableなComponentFactoryを生成する
その答えは半分YESです。ViewEngineのAoTコンパイラは NgModule.declarations
配列に指定されたすべてのコンポーネントのComponentFactoryを生成しています。しかし、それが ComponentFactoryResolver
から解決可能になっていないのです。
この様子は実際にAoTコンパイルの結果を見るとはっきりとわかります。Angular CLIのプロジェクトであれば、 ngc -p ./tsconfig.app.json
とコマンドを実行すれば tsc-out
ディレクトリにAoTコンパイル結果が出力されます。その中には、すべてのコンポーネントに対して ./some.component.ngfactory.js
のような ComponentFactoryの生成コードを見ることができます。
これでViewEngineではどのコンポーネントにもComponentFactoryは存在していることがわかります。しかし、これらのComponentFactoryは どこからも参照されていません。つまりAngular CLI(の内部で使われているwebpack)のビルドでは、不要なコードとしてバンドルに含められないのです。これが、ComponentFactoryResolver
によってComponentFactoryを解決できないコンポーネントがある理由です。バンドルサイズ削減のために、不要なコードを含めない仕組みになっているのです。
ComponentとComponentFactoryの分断
しかしこれはおかしい話です。ソースコード中で SomeComponent
を参照しているのだからそのComponentFactoryは必要なコードとしてバンドルに含められるべきです。
ここがViewEngineの限界です。ViewEngineのAoTコンパイラはComponentFactoryの生成コードを元のコンポーネントクラスとは別のファイルに出力します。つまり、 some.component.ts
に対して some.component.js
と some.component.ngfactory.js
を出力します。したがって、アプリケーションで SomeComponent
への参照があったとしても、 SomeComponent
のComponentFactoryには一切参照が届かないのです。
entryComponents
は ComponentFactoryResolver
をセットアップする
ここでようやく entryComponents
の出番です。 NgModule
の entryComponents
に追加されたコンポーネントのComponentFactoryは、AoTコンパイラが特別に解釈して ComponentFactoryResolver
で解決できるように参照を作ります。その様子は AoTコンパイル後の app.module.ngfactory.js
で見ることができます。
AoTコンパイルの生成コードをはじめて見る方は驚くかもしれませんが、今回注目すべき点は2ヶ所。インポート文と ComponentFactoryResolver
のプロバイダ宣言です。見ての通り、 AppModuleNgFactory
から参照されているのは app.component.ngfactory
だけです。そして、 ComponentFactoryResolver
の近くにある配列には AppComponentNgFactory
だけがセットされています。
それでは、 SomeComponent
を entryComponents
配列に追加してもう一度AoTコンパイルしてみましょう。 app.module.ngfactory.js
に変化があるはずです。
新たに some.component.ngfactory
への参照が追加され、ComponentFactoryResolver
の近くにある配列に SomeComponentNgFactory
が追加されています。実はこの配列こそが ComponentFactoryResolver
が解決できるコンポーネントのリストです。
つまり、entryComponents
によって NgModule
のコンパイル結果に影響を与えることで、動的に利用したいコンポーネントのComponentFactoryがTree-shakingされないように、ComponentFactoryResolver
から解決可能な参照を保持することができるのです。
なぜ entryComponents
が非推奨になるのか
ViewEngineにおいて entryComponents
がなぜ必要だったかを簡単に説明しましたが、なぜv9からは非推奨となるのでしょうか。それはViewEngineに変わるAngularの新しい IvyコンパイラがViewEngineの抱える問題を根本から解決したからです。
Ivyは同一ファイルにコード生成する
IvyのAoTコンパイラは元のコンポーネントファイルと同じファイル、しかも同じクラスの静的フィールドとしてコード生成します。実際にAoTコンパイル結果を見てみましょう。v9では次のような生成コードになります。IvyではAoTコンパイルによって追加される独自のファイルは一切ありません。
some.component.js
は次のようになっています。3行目にあるのは元の SomeComponent
から @Component
デコレーターが除去されたクラスです。そしてデコレーターの中に定義されていたセレクターやテンプレートなどのメタデータが、 9行目以降のAoTコンパイラによる生成コードに変換されています。
ここで重要なことは、 SomeComponent
のAoTコンパイル後コードが、 SomeComponent
クラスと密に結合していることです。これにより、 SomeComponent
を参照すれば自動的に SomeComponent
のコンポーネント生成に必要なすべての情報を解決できます。
つまり、 app.module.js
で some.component.js
をインポートしているだけで、 SomeComponent
のComponentFactoryは解決可能になるのです。
これが、 Angular v9でIvyによって entryComponents
が非推奨になる理由です。 entryComponents
の代替となる新たな方法に変わるのではなく、そもそも根本的に動的コンポーネントと静的コンポーネントを区別する必要がなくなるのです。
Tree-shakingの問題は?
ここまで読んだ方はもしかすると entryComponents
がなくなることで、ViewEngineと比べてバンドルサイズが増えるのではないかと疑っているかもしれません。確かに、コンポーネントの生成コードだけを考えると、ViewEngineと比べてTree-shaking可能な領域は減っています。しかしIvyではその他のいくつもの改善によってトータルではほとんどのユースケースでバンドルサイズが削減されます。
もっとも大きな改善は、Angularのテンプレート機能がTree-shakableになることです。詳細は割愛しますが、 [prop]="someValue"
や (eventName)="onEvent($event)"
など、すべてのテンプレートの機能が個別にTree-shakingされます。アプリケーションで一度も使わなかったテンプレート機能はバンドルに含まれません。
また、コンポーネントと生成コードが同一ファイルになることでクラス定義やimport/exportのオーバーヘッドもなくなり、より少ないコードだけを生成すればよくなりました。また、ViewEngineではコンポーネントが子コンポーネントになる場合とホストコンポーネントになる場合で別の生成関数を定義していましたが、Ivyではひとつの生成関数に統合されるので、これによっても生成コードのサイズは減っています。
トレードオフはありつつも、Ivyでは差分コンパイルのスピード、バンドルサイズの削減、内部アーキテクチャの単純化などの複合的な視点で、Ivyのアーキテクチャを選択しています。
動的コンポーネントを超えた遅延コンポーネントへ
IvyのAoTコンパイラは同一クラスの静的フィールドにComponentFactoryを生成すると説明しました。この変更による恩恵は entryComponents
が不要になるだけではありません。ひとつのコンポーネントに関するコードが1ファイルに含まれることで、Dynamic Importによるコンポーネントの遅延読み込みも将来的には可能になります。
つまり、次のように動的な import()
文で取得した SomeComponent
クラスでも ComponentFactoryResolver
で解決できるということです。
export class AppComponent {
constructor(private cfr: ComponentFactoryResolver) {}
ngOnInit() {
import('./some/some.component')
.then(m => this.cfr.resolveComponentFactory(m.SomeComponent))
.then(someCompFactory => {
console.log(someCompFactory);
});
}
}
IvyではすべてのコンポーネントのComponentFactoryがバンドルに含められると説明しましたが、それはテンプレートHTMLやTypeScriptコードの中で 静的に 参照されている場合だけです。もし SomeComponent
がこの Dynamic Import以外でまったく参照されていなければ、 Angular CLIは SomeComponent
そのものを別バンドルに分離し、遅延読み込み可能にします。モーダル用のコンポーネントであれば、初期読込されるJavaScriptにはコンポーネントを含めず、モーダルを表示するイベントが発生したときに初めて遅延読み込みすればいいわけです。
このようにViewEngineからIvyにアーキテクチャ変更したことによって、いままでは覚えるしかなかった「Angularではできない」や「Angularではこのようにする」といった慣例的な制約がいくつも取り払われていきます。そして不要になった(陳腐化した)APIは非推奨となっていきます。
非推奨化は必ずしも代替APIへの置き換えを意味するわけではないということを覚えておきましょう。
まとめ
- v8までのViewEngineでは
entryComponents
が無ければComponentFactoryの解決ができなかった - Ivyではすべてのコンポーネントが常にComponentFactoryを保持しているため、いつでもどのコンポーネントも動的に利用できるようになる
-
entryComponents
の非推奨化は代替APIへの置き換えではなく、そもそも動的コンポーネントと静的コンポーネントの区別が不要になったということである
Ivyについての詳しい話は、 AngularConnect 2019での次のセッションをおすすめします。
Posted on November 4, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.