余白

https://blog.lacolaco.net/ に移転しました

ngIvyメモ

lacolaco.hatenablog.com

ngIvyのSeparate Compilationについてのプロポーザルを読み、実装中のRenderer3のコードを読み、ベータ版のcompilerが生成するコードを読み、毎晩毎晩考えを巡らせた結果、ngIvyについてある程度体系的な理解が得られたという錯覚があるので、ここで言語化しておきます。 単なるメモなので、何か伝えたいとかではないです。ng-sake #11 - connpass の話のネタにはなるかもしれません。

また、予め断っておきますが、この内容はAngularの内部処理を理解している上級者向けです。これはブラックボックスの内側です。 これがわからないからといってAngularが使えないわけではないですし、まったく自信を失わなくてよいです。 知らないほうが素直にライブラリを使える可能性のほうが高いです。

いままでのAoTコンパイル

f:id:lacolaco:20180310123120p:plain

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.tsJavaScriptコンパイルしたものです。 コンパイル過程で、デコレータの情報は静的フィールドに変換されることがあります。 ( 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により次のように変わります。

f:id:lacolaco:20180311131044p:plain

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だけでコンパイル可能です。

f:id:lacolaco:20180311131157p:plain

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