albatrosary's blog

UI/UXとエンタープライズシステム

Ahead-of-Time Compilation in Angular v2

「Angular v2は事前コンパイルすることができる」ということを聞いても何のことだろうと思ってしまう。何故このように事前コンパイルする必要があるかというと「開発したAngular2アプリケーションをより効率の良いもの」にするためです。この「効率」というものを具体的にコードを書いて見ます。サンプルコードはAngular2チュートリアルを使っています。

事前コンパイルはngcコマンドで行いますが、そのモジュールが@angular/compiler-cliから提供されます。これにtsconfig.aot.jsonというファイルを追加することと、package.json"ngc": "ngc -p ./tsconfig.aot.json",というスクリプトを定義すること、そしてbootstrapを定義しているmain.tsに対してAoT(Ahead-of-Time)ファイルmain.aot.tsを定義することです。

// tsconfig.aot.json
{
  "compilerOptions": {
    "target": "es2015",
    "module": "es2015",
    "moduleResolution": "node",
    "declaration": false,
    "removeComments": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "sourceMap": true,
    "pretty": true,
    "allowUnreachableCode": false,
    "allowUnusedLabels": false,
    "noImplicitAny": true,
    "noImplicitReturns": true,
    "noImplicitUseStrict": false,
    "noFallthroughCasesInSwitch": true,
    "outDir": "./.tmp",
    "rootDir": "./src",
    "types": [
      "node"
    ]
  },
  "angularCompilerOptions": {
    "debug": true
  },
  "compileOnSave": false,
  "files": [
    "src/main.ts"
  ],
  "exclude": [
    "node_modules",
    "bin"
  ]
}

package.jsonにはAoT意外にファイルを圧縮するコマンドも追加しています。

// package.json
  "scripts": {
    "ngc": "ngc -p ./tsconfig.aot.json",
    "minify": "uglifyjs bin/bootstrap.bundle.js --screw-ie8 --compress --mangle --output bin/bootstrap.bundle.min.js",
    "minify:aot": "uglifyjs bin/bootstrap.aot.bundle.js --screw-ie8 --compress --mangle --output bin/bootstrap.aot.bundle.min.js",
    "build": "npm run ngc && npm run webpack && npm run minify && npm run minify:aot",
    "start": "concurrently \"node server/app.js\" \"npm run lite\"",
    "webpack": "webpack --config webpack.config.js",
    "tsc": "tsc",
    "tsc:w": "tsc -w",
    "lite": "lite-server -c bs-config.js",
    "test": "karma start karma.conf.js"
  },

main.tsmain.aot.tsを比較します。

// main.ts
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './shared/app.module';
platformBrowserDynamic().bootstrapModule(AppModule);
// main.aot.ts
import { platformBrowser } from '@angular/platform-browser';
import { enableProdMode } from '@angular/core';
import { AppModuleNgFactory } from './shared/app.module.ngfactory';
enableProdMode();
platformBrowser().bootstrapModuleFactory(AppModuleNgFactory);

幾つか違いがあります。platformBrowserplatformBrowserDynamicAppModuleAppModuleNgFactoryなど。特にNgFactory.tsというファイルがngcコマンド実行後に生成されますが、これが最適化されたファイルです。

そして今回はSystemJSではなくWebpackを利用しています。その設定ファイル

// webpack.config.js
'use strict';
let path = require('path');

module.exports = {
  entry: {
    'bootstrap': './src/main.ts',
    'bootstrap.aot': './src/main.aot.ts'
  },

  output: {
    path: './bin',
    filename: '[name].bundle.js'
  },

  module: {
    loaders: [
      {
        test: /\.ts$/,
        loader: 'ts',
        query: {
          configFileName: 'tsconfig.json'
        }
      }
    ]
  },

  resolve: {
    root: [ path.join(__dirname, 'src') ],
    extensions: ['', '.ts', '.js']
  },

  devtool: false
};

実際にngcコマンドを実行しwebpackを使ってjsを出力したファイルのサイズを比較します。

$ ls -lh
total 11600
 1.7M  9 16 18:19 bootstrap.aot.bundle.js
 536K  9 16 18:19 bootstrap.aot.bundle.min.js
 2.6M  9 16 18:19 bootstrap.bundle.js
 813K  9 16 18:19 bootstrap.bundle.min.js

AoTでは特にテンプレートをJavaScriptに事前変換します(それ意外に多くのことを行っています)。例えば、テンプレートを次のように定義します。

@Component({
  selector: 'hero-search',
  template: `
  <div id="search-component">
    <h4>Hero Search</h4>
    <input #searchBox id="search-box" (keyup)="search(searchBox.value)" />
    <div>
      <div *ngFor="let hero of heroes | async"
          (click)="gotoDetail(hero)" class="search-result" >
        <p class="ashiras">{{hero.name}}</p>
      </div>
    </div>
  </div>
  `,

通常は

core_1.Component({
  selector: 'hero-search',
  template: "\n  <div id=\"search-component\">\n    <h4>Hero Search</h4>\n    <input #searchBox id=\"search-box\" (keyup)=\"search(searchBox.value)\" />\n    <div>\n      <div *ngFor=\"let hero of heroes | async\"\n          (click)=\"gotoDetail(hero)\" class=\"search-result\" >\n        <p class=\"ashiras\">{{hero.name}}</p>\n      </div>\n    </div>\n  </div>\n  ",
  styleUrls: ['hero-search.component.css'],
  providers: [hero_search_service_1.HeroSearchService]
}), 

AoTでは完全なJavaScriptに変換されています

function (rootSelector) {
  this._el_0 = this.renderer.createElement(null, 'div', this.debug(0, 5, 6));
  this.renderer.setElementAttribute(this._el_0, 'class', 'search-result');
  this._text_1 = this.renderer.createText(this._el_0, '\n        ', this.debug(1, 6, 60));
  this._el_2 = this.renderer.createElement(this._el_0, 'p', this.debug(2, 7, 8));
  this.renderer.setElementAttribute(this._el_2, 'class', 'ashiras');
  this._text_3 = this.renderer.createText(this._el_2, '', this.debug(3, 7, 27));
  this._text_4 = this.renderer.createText(this._el_0, '\n      ', this.debug(4, 7, 44));
  var disposable_0 = this.renderer.listen(this._el_0, 'click', this.eventHandler(this._handle_click_0_0.bind(this)));
  this._expr_1 = import9.UNINITIALIZED;
  this.init([].concat([this._el_0]), [
    this._el_0,
    this._text_1,
    this._el_2,
    this._text_3,
    this._text_4
  ], [disposable_0], []);
  return null;
};

より詳しい説明は @mgechev が書かれてますので一読下さい。

blog.mgechev.com

公式サイトにもあります。

Ahead-of-Time Compilation - ts