Marginalia

ngIvyメモ

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をコンパイルします。

  1. Analysis phase: AoT コンパイルのエントリポイントとなる NgModule から再帰的にすべての参照をたどり、以下の操作をおこなう。
    1. それが.tsファイルである場合は、そのファイルから export されているすべてのシンボルを.metadata.jsonに記録し、関連付ける
    2. それが.jsファイルである場合は、隣接する.metadata.jsonを関連付ける
  2. Codegen phase: 1 でコンパイルに関連付けられたすべての.metadata.jsonについて、以下の操作をおこなう
    1. .metadata.json@Component@NgModuleなどの Angular デコレータが存在する場合は、それぞれについて NgFactory の TypeScript コードを生成する
    2. 生成された TypeScript コードをコンパイルし、.ngfactory.jsを生成するか、型チェックが通らない場合はエラーを出力する
  3. 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をコンパイルします。

  1. コンパイル対象の.tsファイルについて、以下の操作をおこなう
    1. Angular デコレータが存在する場合は、それぞれのデコレータに対応した Angular 定義を TypeScript コード中に生成し、対応するメタデータを.metadata.jsonに記録する
    2. TypeScript コードをコンパイルし、.jsを生成するか、型チェックが通らない場合はエラーを出力する

それぞれのデコレータに対応する定義とメタデータについては DESIGN DOC (Ivy): Separate Compilation を読む を参照してください。

手書き定義と脱 ngc

ngIvy のコンパイル過程において、「Angular デコレータが存在する場合は、それぞれのデコレータに対応した Angular 定義を TypeScript コード中に生成」と書きましたが、 裏を返せば「Angular デコレータが存在しなければただ TypeScript をコンパイルするだけ」ということです。

ngIvy では、本来は Angular デコレータから生成されるngComponentDefngInjectorRefのような定義を事前に TypeScript 中に記述しておくと、ngc におけるコード生成過程をスキップできます。 機械生成された定義と手書きの定義は区別されないため、この場合はngcではなくtscだけでコンパイル可能です。

手書きでなくとも、例えば Babel や他のシステムによってソースコードが変換されたとしても、最終的に ngIvy の期待する出力があれば、Angular の AoT コンパイル結果として受け入れられます。 プラグインさえ書けば JSX から Angular コンポーネント定義を生成することも不可能ではないでしょう。

このポイントは、

  • ngIvy により @Component などの Angular デコレータは「Angular 定義を生成する手段のひとつ」となる。
  • サードパーティのツールやマニュアルでの記述により、Angular デコレータを排除したピュアな JavaScript としてコンポーネントを作成できるようになる。

実際に ngIvy によって、tscrollupだけで動作するアプリケーションのサンプルはこちらです。

この例では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は不要といえば不要なのかもしれません。