ngIvyのSeparate Compilationについてのプロポーザルを読み、実装中のRenderer3のコードを読み、ベータ版のcompilerが生成するコードを読み、毎晩毎晩考えを巡らせた結果、ngIvyについてある程度体系的な理解が得られたという錯覚があるので、ここで言語化しておきます。 単なるメモなので、何か伝えたいとかではないです。ng-sake #11 - connpass の話のネタにはなるかもしれません。
また、予め断っておきますが、この内容はAngularの内部処理を理解している上級者向けです。これはブラックボックスの内側です。 これがわからないからといってAngularが使えないわけではないですし、まったく自信を失わなくてよいです。 知らないほうが素直にライブラリを使える可能性のほうが高いです。
いままでのAoTコンパイル
v5までのAoTコンパイルは、以下の流れで greeting.component.ts
をコンパイルします。
- Analysis phase: AoTコンパイルのエントリポイントとなるNgModuleから再帰的にすべての参照をたどり、以下の操作をおこなう。
- それが
.ts
ファイルである場合は、そのファイルからexportされているすべてのシンボルを*.metadata.json
に記録し、関連付ける - それが
.js
ファイルである場合は、隣接する*.metadata.json
を関連付ける
- それが
- Codegen phase: 1でコンパイルに関連付けられたすべての
.metadata.json
について、以下の操作をおこなう.metadata.json
に@Component
や@NgModule
などのAngularデコレータが存在する場合は、それぞれについてNgFactoryのTypeScriptコードを生成する- 生成されたTypeScriptコードをコンパイルし、
.ngfactory.js
を生成するか、型チェックが通らない場合はエラーを出力する
- Compilation phase: 通常の
tsc
の挙動と同じようにすべての.ts
ファイルをコンパイルする
すべての操作が正しく完了すると、AoTコンパイルが成功します。
結果として、GreetingComponent
に対して出力されるファイルは次の3つです。
greeting.component.js
greeting.component.ts
をJavaScriptにコンパイルしたものです。
コンパイル過程で、デコレータの情報は静的フィールドに変換されることがあります。 ( annotationsAs
オプション )
greeting.component.ngfactory.js
greeting.component.js
からexportされるGreetingComponent
を、Angularがコンポーネントとして利用するために機械生成されたコードで、
GreetingComponentNgFactory
クラスをexportします。
GreetingComponentNgFactory
クラスの主な役割は次の2つです。
GreetingComponent
のインスタンス生成: コンストラクタで要求する引数の解決(Dependency Injectionの実行)GreetingComponent
のビュー生成: テンプレートから生成されるビュー組み立て関数の提供
greeting.component.metadata.json
greeting.component.ts
を静的解析した結果得られたメタデータです。公式ドキュメントではある種の抽象構文木(AST)と見ることもできると書かれています。
メタデータはNgFactoryの生成だけでなく、language-serviceによるテンプレートエラー検知にも使われています。
メタデータ中に保存される情報は、TypeScriptの型情報 + Angularデコレータ中の値情報 です。前者だけであれば.d.ts
ファイルで事足りますが、NgFactoryの生成には@Component
などのデコレータに渡される実際の値情報が必要になるため、ngcはこれを*.metadata.json
という形で記録します。
で、ngIvyって何?
ここからが本題で、上述のコンパイル処理はngIvyにより次のように変わります。
ngIvy方式のAoTコンパイルは、以下の流れで greeting.component.ts
をコンパイルします。
- コンパイル対象の
.ts
ファイルについて、以下の操作をおこなう
それぞれのデコレータに対応する定義とメタデータについては DESIGN DOC (Ivy): Separate Compilationを読む - らこらこブログ を参照してください。
手書き定義と脱ngc
ngIvyのコンパイル過程において、「Angularデコレータが存在する場合は、それぞれのデコレータに対応したAngular定義をTypeScriptコード中に生成」と書きましたが、 裏を返せば「Angularデコレータが存在しなければただTypeScriptをコンパイルするだけ」ということです。
ngIvyでは、本来はAngularデコレータから生成されるngComponentDef
やngInjectorRef
のような定義を事前にTypeScript中に記述しておくと、ngcにおけるコード生成過程をスキップできます。
機械生成された定義と手書きの定義は区別されないため、この場合はngc
ではなくtsc
だけでコンパイル可能です。
手書きでなくとも、例えばBabelや他のシステムによってソースコードが変換されたとしても、最終的にngIvyの期待する出力があれば、AngularのAoTコンパイル結果として受け入れられます。 プラグインさえ書けばJSXからAngularコンポーネント定義を生成することも不可能ではないでしょう。
このポイントは、
- ngIvyにより
@Component
などのAngularデコレータは「Angular定義を生成する手段のひとつ」となる。 - サードパーティのツールやマニュアルでの記述により、Angularデコレータを排除したピュアなJavaScriptとしてコンポーネントを作成できるようになる。
実際にngIvyによって、tsc
とrollup
だけで動作するアプリケーションのサンプルはこちらです。
この例ではHelloWorld
コンポーネントは@Component
デコレータを持たず、ngComponentDef
を手書きしているので、コンパイルはtsc
だけで完了します。
分離コンパイル
ngIvyでは従来のAoTコンパイルの非効率性を解決することに重きをおいています。ここでいう非効率性とは、
- NgFactoryの生成はアプリケーションコンパイル時にしかおこなえない(npmライブラリはNgFactoryを含まない
.js
ファイルとNgFactoryの元になる.metadata.json
を提供する必要がある) - NgModuleのスコープ内で何かしらの変更があった場合には、そのNgModuleごと再コンパイルしてmetadata.jsonやNgFactoryを再生成する必要がある
重要なポイントは、このプロポーザルにおいてはこれらを解決するために、ngIvyによるAoTコンパイルは単純にデコレータをもとにコードを変換する仕組みになっていて、アプリケーション自体の整合性を担保する役割は失っていることです。
Ivyにおいては、ランタイムはこれまではコンパイラによって事前計算されたもののほとんどを、実行時に処理することで分離コンパイルを可能にする方法で作られています。
とあるように、アプリケーションが実行可能かどうかのチェックは事前計算ではなくJITでの処理に切り分けられると予想されます。 この場合、Language Serviceとどのように協調するのかは今のところ不明です。
今後
上記仕様はまだ実装されていない(定義の生成部分は見て取れるけど、まだ.metadata.json
の生成が古いように見える)ので、分離コンパイルが最終的にどういう形になるのかはまだ観察が必要です。とはいえ、現状ではngIvyのターゲットはライブラリとアプリケーションだけでパッケージは対象外なので、実質的に.metadata.json
は不要といえば不要なのかもしれません。