DESIGN DOC (Ivy): Separate Compilation を読む
ngIvy の Separate Compilation についてのプロポーザルを読み、実装中の Renderer3 のコードを読み、ベータ版の compiler が生成するコードを読み、毎晩毎晩考えを巡らせた結果、ngIvy についてある程度体系的な理解が得られたという錯覚があるので、ここで言語化しておきます。 単なるメモなので、何か伝えたいとかではないです。https://ng-sake.connpass.com/event/80734/ の話のネタにはなるかもしれません。
また、予め断っておきますが、この内容は 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
ファイルについて、以下の操作をおこなう- Angular デコレータが存在する場合は、それぞれのデコレータに対応した Angular 定義を TypeScript コード中に生成し、対応するメタデータを
.metadata.json
に記録する - TypeScript コードをコンパイルし、
.js
を生成するか、型チェックが通らない場合はエラーを出力する
- Angular デコレータが存在する場合は、それぞれのデコレータに対応した Angular 定義を TypeScript コード中に生成し、対応するメタデータを
それぞれのデコレータに対応する定義とメタデータについては 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
は不要といえば不要なのかもしれません。