21
21

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Angular 2Advent Calendar 2015

Day 19

Angular 2 の初期化・Component のコンパイル周りのソースを読んでみる

Last updated at Posted at 2015-12-19

年末のイベントで何度か Angular 2 について紹介させていただいたのですが、自分で話しながらよくわかっていないなと感じることしきり。時間ができたらしっかり理解したい!と思っていました。理解するにはソースを読むしかない!ということで Angular 2 のソースを読んでみます。

使い方については本家ドキュメントを始めいろいろなところに書かれているので、内部構造について調べます。Change Detection, Dependency Injection 周りは @laco0416 さんによってカバーされている/される予定(12/23)なので、初期化・Component のコンパイル周りに挑戦してみたいと思います。

AngularConnect のキーノートでは、Angular 1 ではランタイムで何度も HTML のコンパイルが走っていたが、Angular 2 では初期化時に一度走るだけになり、高速化につながっているという話がありました。このあたり実際にどうなっているのか興味がありますね。

バージョンは beta.0 リリース直後の 2a2f9a9a196cf1502c3002f14bd42b13a4dcaa53 です。基本的に beta.0 としてリリースされたバージョンと同じと考えていいと思います。

TL;DR

  • Angular 2 はプラットフォームごとに様々なコンポーネントを切り替えられるようになっている。
  • Component のコンパイルの結果、複数の Command ができる。画面を描画するための命令群のようなもの。
  • RuntimeCompiler がトップレベル Component 型を DOM 関係なくコンパイルして ProtoViewRef を返す。View を作るための雛形のようなもの。
  • AppViewManagaer が ProtoViewRef(コンパイルされた Component のメタ情報)をもとに Component のインスタンスを作り、DOM要素に紐付ける。

ソースを読む前に

まず前提として Angular 2 のレポジトリは GitHub の angular/angular です。modules というディレクトリに angular2_material などいくつかのプロジェクトと一緒に入っています。本体のソースは modules/angular2 です。

本体のソース以外で役に立つのは modules/playground のサンプル群と modules/angular2/src/test です。両方共かなり充実しているので、ソースだけでは理解するのが難しい使い方や詳細な仕様が確認できます。playground は HTML に $SCRIPTS$ などの特殊な文字が入っていてビルド時に埋まる感じになっているので、よくわからない場合は angular/angular をチェックアウトしてビルドしてみましょう。angular/angular 本体は Node 4, npm 2 系までしか対応していないことに要注意です。

ソースコードを読むのには Visual Studio Code を使いました。TypeScript の型情報を使って定義にジャンプしたり、参照を検索したりできて便利。

bootstrap を読んでみる

アプリを初期化する bootstrap から始めましょう。以下に出てくるパスは全て modules 以下の話です。ブラウザ向けの bootstrap のある angular2/platfrom/browser.ts を見てみます。

angular2/platfrom/browser.ts
export function bootstrap(
    appComponentType: Type,
    customProviders?: Array<any /*Type | Provider | any[]*/>): Promise<ComponentRef> {
  reflector.reflectionCapabilities = new ReflectionCapabilities();
  let appProviders =
      isPresent(customProviders) ? [BROWSER_APP_PROVIDERS, customProviders] : BROWSER_APP_PROVIDERS;
  return platform(BROWSER_PROVIDERS).application(appProviders).bootstrap(appComponentType);
}

bootstrap にはトップレベルの Component の型と(必要あれば)独自の providers を渡します。以下のような処理の流れですね。

  1. platform: BROWSER_PROVIDERS から PlatformRef を作る。
  2. PlatformRef#application: BROWSER_APP_PROVIDERS + 独自の providers を渡して ApplicationRef を作る。
  3. ApplicationRef#bootstrap: トップレベルの Component の型を渡してアプリケーションを開始する。

ではこれらのメソッドを見ていきましょう。

ちなみに BROWSER_PROVIDERS はプラットフォーム共通の provider に DOM アダプタの初期化処理を加えたものの模様。

angular2/src/platform/browser_common.ts
/**
 * A set of providers to initialize the Angular platform in a web browser.
 *
 * Used automatically by `bootstrap`, or can be passed to {@link platform}.
 */
export const BROWSER_PROVIDERS: Array<any /*Type | Provider | any[]*/> = CONST_EXPR([
  PLATFORM_COMMON_PROVIDERS,
  new Provider(PLATFORM_INITIALIZER, {useValue: initDomAdapter, multi: true}),
]);

platform

import/export を追っていくと platformangular2/src/core/application_ref.ts にあることがわかります。ちなみに platform の引数は BROWSER_PROVIDERS です。ざっくり言うと _createPlatform というのを呼んでいるだけです。

angular2/src/core/application_ref.ts
function _createPlatform(providers?: Array<Type | Provider | any[]>): PlatformRef {
  _platformProviders = providers;
  let injector = Injector.resolveAndCreate(providers);
  _platform = new PlatformRef_(injector, () => {
    _platform = null;
    _platformProviders = null;
  });
  _runPlatformInitializers(injector);
  return _platform;
}

新規に Injector を作って、それを使って PlatformRef_ なるものを作った後、プラットフォームごとの初期化処理をした後、PlatformRef_ を返しています。

PlatformRef_

PlatformRef_ を見てみましょう。

angular2/src/core/application_ref.ts
constructor(private _injector: Injector, private _dispose: () => void) { super(); }

引数をメンバ変数としてとっておいているだけですね。第二引数は、アプリ終了時の処理のようです。

PlatformRef_#bootstrap

bootstrap 内で呼ばれている application メソッドを見てみましょう。ここでいう providers は BROWSER_APP_PROVIDERS + 独自 providers です。

angular2/src/core/application_ref.ts
application(providers: Array<Type | Provider | any[]>): ApplicationRef {
  var app = this._initApp(createNgZone(), providers);
  return app;
}

NgZone を作って _initApp に渡しています。

angular2/src/core/application_ref.ts
private _initApp(zone: NgZone, providers: Array<Type | Provider | any[]>): ApplicationRef {
  var injector: Injector;
  var app: ApplicationRef;
  zone.run(() => {
    providers = ListWrapper.concat(providers, [
      provide(NgZone, {useValue: zone}),
      provide(ApplicationRef, {useFactory: (): ApplicationRef => app, deps: []})
    ]);


    var exceptionHandler;
    try {
      injector = this.injector.resolveAndCreateChild(providers);
      exceptionHandler = injector.get(ExceptionHandler);
      zone.overrideOnErrorHandler((e, s) => exceptionHandler.call(e, s));
    } catch (e) {
      if (isPresent(exceptionHandler)) {
        exceptionHandler.call(e, e.stack);
      } else {
        print(e.toString());
      }
    }
  });
  app = new ApplicationRef_(this, zone, injector);
  this._applications.push(app);
  _runAppInitializers(injector);
  return app;
}

ApplicationRef_ なるものが作られ、返されています。その途中で(?) BROWSER_PROVIDERS が入っている injector を継承して BROWSER_APP_PROVIDERS + 独自 providers を足した子 injector が作られているのがわかります。

TODO: NgZone#run とは?

BROWSER_APP_PROVIDERS

ちなみに BROWSER_APP_PROVIDERS は、ブラウザ上で動くアプリに必要な provider たちの模様。BROWSER_APP_COMMON_PROVIDERSCOMPILER_PROVIDERS が分かれているのは、オフラインコンパイルへの布石?

angular2/platform/browser.ts
/**
 * An array of providers that should be passed into `application()` when bootstrapping a component.
 */
export const BROWSER_APP_PROVIDERS: Array<any /*Type | Provider | any[]*/> = CONST_EXPR([
  BROWSER_APP_COMMON_PROVIDERS,
  COMPILER_PROVIDERS,
  new Provider(XHR, {useClass: XHRImpl}),
]);

BROWSER_APP_COMMON_PROVIDERS は以下のような感じです。

angular2/src/platform/browser_common.ts
/**
 * A set of providers to initialize an Angular application in a web browser.
 *
 * Used automatically by `bootstrap`, or can be passed to {@link PlatformRef.application}.
 */
export const BROWSER_APP_COMMON_PROVIDERS: Array<any /*Type | Provider | any[]*/> = CONST_EXPR([
  APPLICATION_COMMON_PROVIDERS,
  FORM_PROVIDERS,
  new Provider(PLATFORM_PIPES, {useValue: COMMON_PIPES, multi: true}),
  new Provider(PLATFORM_DIRECTIVES, {useValue: COMMON_DIRECTIVES, multi: true}),
  new Provider(ExceptionHandler, {useFactory: _exceptionHandler, deps: []}),
  new Provider(DOCUMENT, {useFactory: _document, deps: []}),
  new Provider(EVENT_MANAGER_PLUGINS, {useClass: DomEventsPlugin, multi: true}),
  new Provider(EVENT_MANAGER_PLUGINS, {useClass: KeyEventsPlugin, multi: true}),
  new Provider(EVENT_MANAGER_PLUGINS, {useClass: HammerGesturesPlugin, multi: true}),
  new Provider(DomRenderer, {useClass: DomRenderer_}),
  new Provider(Renderer, {useExisting: DomRenderer}),
  new Provider(SharedStylesHost, {useExisting: DomSharedStylesHost}),
  DomSharedStylesHost,
  Testability,
  BrowserDetails,
  AnimationBuilder,
  EventManager
]);

ApplicationRef_

今度は ApplicationRef_ を見てみましょう。

angular2/src/core/application_ref.ts
constructor(private _platform: PlatformRef_, private _zone: NgZone, private _injector: Injector) {
  super();
  if (isPresent(this._zone)) {
    ObservableWrapper.subscribe(this._zone.onTurnDone,
                                (_) => { this._zone.run(() => { this.tick(); }); });
  }
  this._enforceNoNewChanges = assertionsEnabled();
}

NgZone#onTurnDone

angular2/src/core/zone/ng_zone.ts
/**
 * Notifies subscribers immediately after Angular zone is done processing
 * the current turn and any microtasks scheduled from that turn.
 *
 * Used by Angular as a signal to kick off change-detection.
 */
get onTurnDone() { return this._onTurnDoneEvents; }

だそうで、AJAX や DOM などのイベント発生した後に呼ばれるのかなと。tick() の中では変更検知をキックしているようです。

TODO: Zone.js の詳細

ApplicationRef_#bootstrap

そして最後の bootstrap です。

angular2/src/core/application_ref.ts
bootstrap(componentType: Type,
          providers?: Array<Type | Provider | any[]>): Promise<ComponentRef> {
  var completer = PromiseWrapper.completer();
  this._zone.run(() => {
    var componentProviders = _componentProviders(componentType);
    if (isPresent(providers)) {
      componentProviders.push(providers);
    }
    var exceptionHandler = this._injector.get(ExceptionHandler);
    this._rootComponentTypes.push(componentType);
    try {
      var injector: Injector = this._injector.resolveAndCreateChild(componentProviders);
      var compRefToken: Promise<ComponentRef> = injector.get(APP_COMPONENT_REF_PROMISE);
      var tick = (componentRef) => {
        this._loadComponent(componentRef);
        completer.resolve(componentRef);
      };


      var tickResult = PromiseWrapper.then(compRefToken, tick);


      // THIS MUST ONLY RUN IN DART.
      // This is required to report an error when no components with a matching selector found.
      // Otherwise the promise will never be completed.
      // Doing this in JS causes an extra error message to appear.
      if (IS_DART) {
        PromiseWrapper.then(tickResult, (_) => {});
      }


      PromiseWrapper.then(tickResult, null,
                          (err, stackTrace) => completer.reject(err, stackTrace));
    } catch (e) {
      exceptionHandler.call(e, e.stack);
      completer.reject(e, e.stack);
    }
  });
  return completer.promise.then(_ => {
    let c = this._injector.get(Console);
    let modeDescription =
        assertionsEnabled() ?
            "in the development mode. Call enableProdMode() to enable the production mode." :
            "in the production mode. Call enableDevMode() to enable the development mode.";
    c.log(`Angular 2 is running ${modeDescription}`);
    return _;
  });
}

長いですが結局は、さらに子 injector を作って、ComponentRef なるものを取得し、それを使って _loadComponent して、それで結果の Promiseresolve しているようです。

_loadComponent(ref): void {
  var appChangeDetector = internalView(ref.hostView).changeDetector;
  this._changeDetectorRefs.push(appChangeDetector.ref);
  this.tick();
  this._rootComponents.push(ref);
  this._bootstrapListeners.forEach((listener) => listener(ref));
}

ref.hostView などが生えていることなら、ComponentRef はコンパイル済の Component の模様。いつの間にコンパイルしていたのでしょうか?injector から ComponentRef を取得するところ?

APP_COMPONENT_REF_PROMISE で検索するとありました。

angular2/src/core/application_ref.ts
function _componentProviders(appComponentType: Type): Array<Type | Provider | any[]> {
  return [
    provide(APP_COMPONENT, {useValue: appComponentType}),
    provide(APP_COMPONENT_REF_PROMISE,
            {
              useFactory: (dynamicComponentLoader: DynamicComponentLoader, appRef: ApplicationRef_,
                           injector: Injector) => {
                // Save the ComponentRef for disposal later.
                var ref: ComponentRef;
                // TODO(rado): investigate whether to support providers on root component.
                return dynamicComponentLoader.loadAsRoot(appComponentType, null, injector,
                                                         () => { appRef._unloadComponent(ref); })
                    .then((componentRef) => {
                      ref = componentRef;
                      if (isPresent(componentRef.location.nativeElement)) {
                        injector.get(TestabilityRegistry)
                            .registerApplication(componentRef.location.nativeElement,
                                                 injector.get(Testability));
                      }
                      return componentRef;
                    });
              },
              deps: [DynamicComponentLoader, ApplicationRef, Injector]
            }),
    provide(appComponentType,
            {
              useFactory: (p: Promise<any>) => p.then(ref => ref.instance),
              deps: [APP_COMPONENT_REF_PROMISE]
            }),
  ];
}

TestabilityRegistry はテスト関係だと思うので無視して、DynamicComponentLoader#loadAsRoot がトップレベル Component の型をもらって ComponentRef を作っているようです。

DynamicComponentLoader#loadAsRoot

angular2/src/core/linker/dynamic_component_loader.ts
loadAsRoot(type: Type, overrideSelector: string, injector: Injector,
             onDispose?: () => void): Promise<ComponentRef> {
    return this._compiler.compileInHost(type).then(hostProtoViewRef => {
      var hostViewRef =
          this._viewManager.createRootHostView(hostProtoViewRef, overrideSelector, injector);
      var newLocation = this._viewManager.getHostElement(hostViewRef);
      var component = this._viewManager.getComponent(newLocation);


      var dispose = () => {
        if (isPresent(onDispose)) {
          onDispose();
        }
        this._viewManager.destroyRootHostView(hostViewRef);
      };
      return new ComponentRef_(newLocation, component, type, injector, dispose);
    });
  }
  1. Compiler#compileInHost: Component 型をコンパイルして ProtoViewRef を作ります。
  2. AppViewManager#createRootHostView: ProtoViewRef から Component のインスタンスを作成し、Global View(ブラウザなら Document)内で Component のセレクタ(上書き可)にマッチする最初の要素に結びつけます。返すのは HostViewRef。
  3. AppViewManager#getHostElement:(省略)
  4. AppViewManager#getComponent:(省略)

TODO: HostViewRef と ElementRef の違い?後者は DOM 要素を指していそう。前者は?

Compiler_#compileInHost

いよいよ核心に迫って来ました。

angular2/src/core/linker/compiler.ts
compileInHost(componentType: Type): Promise<ProtoViewRef> {
  var metadatas = reflector.annotations(componentType);
  var compiledHostTemplate = metadatas.find(_isCompiledHostTemplate);

  if (isBlank(compiledHostTemplate)) {
    throw new BaseException(
        `No precompiled template for component ${stringify(componentType)} found`);
  }
  return PromiseWrapper.resolve(this._createProtoView(compiledHostTemplate));
}

あれ?もう Metadata がコンパイルされている前提になっている?何かがおかしい...

探してみると angular2/src/compiler/compiler.ts の中の COMPILER_PROVIDERS で以下のようになっています。

angular2/src/compiler/compiler.ts
export const COMPILER_PROVIDERS: Array<Type | Provider | any[]> = CONST_EXPR([
  // ...
  new Provider(RuntimeCompiler, {useClass: RuntimeCompiler_}),
  new Provider(Compiler, {useExisting: RuntimeCompiler}),
  // ...
]);

Compiler 型の DI では Compiler_ ではなく RuntimeCompiler_ を使うようになっていたわけですね。RuntimeCompiler_Compiler_ を継承しています。

Runtime と名前についているということは Runtime じゃない Compiler もあるはずなので、オフライン用の Compiler も用意されるのかもしれません(AngularConnect のキーノートでもオフラインコンパイルがそのうち実装されるようなことを言っていました)。Compiler_ はオフラインでコンパイルしたテンプレートを使うためのもの?

RuntimeCompiler

気を取り直して RuntimeCompiler を追ってみます。

angular2/src/compiler/runtime_compiler.ts
compileInHost(componentType: Type): Promise<ProtoViewRef> {
  return this._templateCompiler.compileHostComponentRuntime(componentType)
      .then(compiledHostTemplate => internalCreateProtoView(this, compiledHostTemplate));
}
  1. TemplateCompiler#compileHostComponentRuntime: これがまさに Angular 2 のコンパイルの模様!
  2. internalCreateProtoView: 辿ってみると ProtoViewFactory#createHost を呼んでいます。基本的には 1 でできた CompiledHostTemplate を Renderer に登録した後、AppProtoView で包んでいるだけのようです。

TemplateCompiler#compileHostComponentRuntime

TemplateCompiler を見てみます。

angular2/src/compiler/template_compiler.ts
compileHostComponentRuntime(type: Type): Promise<CompiledHostTemplate> {
  var hostCacheKey = this._hostCacheKeys.get(type);
  if (isBlank(hostCacheKey)) {
    hostCacheKey = new Object();
    this._hostCacheKeys.set(type, hostCacheKey);
    var compMeta: CompileDirectiveMetadata = this._runtimeMetadataResolver.getMetadata(type);
    assertComponent(compMeta);
    var hostMeta: CompileDirectiveMetadata =
        createHostComponentMeta(compMeta.type, compMeta.selector);

    this._compileComponentRuntime(hostCacheKey, hostMeta, [compMeta], new Set());
  }
  return this._compiledTemplateDone.get(hostCacheKey)
      .then(compiledTemplate => new CompiledHostTemplate(compiledTemplate));
}
  1. RuntimeMetadataResolver#getMetadata: Component のメタデータを取得していますね。
  2. createHostComponentMeta:(省略)
  3. TemplateCompiler#_compileComponentRuntime: ここがメインのコンパイル処理のようです!

TemplateCompiler#_compileComponentRuntime

なかなかの大物が出て来ました。

angular2/src/compiler/template_compiler.ts
private _compileComponentRuntime(
    cacheKey: any, compMeta: CompileDirectiveMetadata, viewDirectives: CompileDirectiveMetadata[],
    compilingComponentCacheKeys: Set<any>): CompiledComponentTemplate {
  let uniqViewDirectives = removeDuplicates(viewDirectives);
  var compiledTemplate = this._compiledTemplateCache.get(cacheKey);
  var done = this._compiledTemplateDone.get(cacheKey);
  if (isBlank(compiledTemplate)) {
    var styles = [];
    var changeDetectorFactory;
    var commands = [];
    var templateId = `${stringify(compMeta.type.runtime)}Template${this._nextTemplateId++}`;
    compiledTemplate = new CompiledComponentTemplate(
        templateId, (dispatcher) => changeDetectorFactory(dispatcher), commands, styles);
    this._compiledTemplateCache.set(cacheKey, compiledTemplate);
    compilingComponentCacheKeys.add(cacheKey);
    done = PromiseWrapper
               .all([<any>this._styleCompiler.compileComponentRuntime(compMeta.template)].concat(
                   uniqViewDirectives.map(dirMeta => this.normalizeDirectiveMetadata(dirMeta))))
               .then((stylesAndNormalizedViewDirMetas: any[]) => {
                 var childPromises = [];
                 var normalizedViewDirMetas = stylesAndNormalizedViewDirMetas.slice(1);
                 var parsedTemplate = this._templateParser.parse(
                     compMeta.template.template, normalizedViewDirMetas, compMeta.type.name);

                 var changeDetectorFactories = this._cdCompiler.compileComponentRuntime(
                     compMeta.type, compMeta.changeDetection, parsedTemplate);
                 changeDetectorFactory = changeDetectorFactories[0];
                 var tmpStyles: string[] = stylesAndNormalizedViewDirMetas[0];
                 tmpStyles.forEach(style => styles.push(style));
                 var tmpCommands: TemplateCmd[] = this._compileCommandsRuntime(
                     compMeta, parsedTemplate, changeDetectorFactories,
                     compilingComponentCacheKeys, childPromises);
                 tmpCommands.forEach(cmd => commands.push(cmd));
                 return PromiseWrapper.all(childPromises);
               })
               .then((_) => {
                 SetWrapper.delete(compilingComponentCacheKeys, cacheKey);
                 return compiledTemplate;
               });
    this._compiledTemplateDone.set(cacheKey, done);
  }
  return compiledTemplate;
}

CompiledCompoentTemplate というものを作って返しています。

angular2/src/core/linker/template_commands.ts
@CONST()
export class CompiledComponentTemplate {
  constructor(public id: string, public changeDetectorFactory: Function,
              public commands: TemplateCmd[], public styles: string[]) {}
}

changeDetectorFactory の実体, commands, styles は、CompiledComponentTemplate を返してしまった後に埋まるようになっているのが面白いところ。

以下が実際のコンパイルの処理です。

  1. StyleCompiler#compileComponentRuntime: CSS のパース(多分 AST にしている)
  2. TemplateCompiler#normalizeDirectiveMetadata: テンプレート内で使われる Directive たち(ngFor など PLATFORM_DIRECTIVES 内のものや directives で指定したもの)の Metadata を正規化
  3. TemplateParser#parse: 2 を使ってテンプレートをパースして AST に。結果は TemplateAST[]
  4. ChangeDetector のファクトリ関数を作成。
  5. TemplateCompiler#_compileCommandsRuntime: 上で作った AST などの情報から commands(TemplateCmd[]) を作る。中では再帰的に子 Component をコンパイルしていくようです。

どうやらここで作りたかったのは TemplateCmd[] のようです。

Command とは一体何?

angular2/src/core/linker/template_commands.ts
export interface TemplateCmd extends RenderTemplateCmd {
  visit(visitor: RenderCommandVisitor, context: any): any;
}

TemplateCmd の親は・・・

angular2/src/core/render/api.ts
/**
 * Abstract base class for commands to the Angular renderer, using the visitor pattern.
 */
export abstract class RenderTemplateCmd {
  abstract visit(visitor: RenderCommandVisitor, context: any): any;
}

これを継承している抽象クラスには以下がありました。

  • RenderBeginCmd: Command to begin rendering.
  • RenderTextCmd: Command to render text.
  • RenderNgContentCmd: Command to render projected content.
  • RenderBeginElementCmd: Command to begin rendering an element.
  • RenderBeginComponentCmd: Command to begin rendering a component.
  • RenderEmbeddedTemplateCmd: Command to render a component's template.

意外に少ないですね。どうやら Command というのは Rendering のための命令のことのようです。つまるところ Angular 2 は、Component の Template を一回 AST にパースした後、(ブラウザの場合は)HTML を描画するための命令セットに変換していることがわかりました。このことをコンパイルと言っているようです。

終わりに

今回はこのあたりで時間切れですが、(次があれば)次回はコンパイルして作った Command がどうやって使われているかを見ていきたいなと思います。みんなで Angular 2 の内部構造を解析しましょう!

21
21
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
21
21

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?