【Angular2入門】TypeScriptをコンパイルからのWatchifyでファイル結合、ブラウザで動作確認するところまで - Gulpで作るwebフロントエンド開発環境

正式版もリリースされたことだし、そろそろ Angular2 を始めてみませんか?

angular2_top

2016年9月15日に Angular2 の正式版がしれっとリリースされました。前バージョンの AngularJS 1.x は独自の路線で HTML や Web フロントエンドを拡張するといった志向のフレームワークでしたが、2系は 1.x系の魅力でもある豊富な機能は引き継ぎつつ、より web 標準に寄り添った実装となっているのが特徴です。

これまでは β版の期間が続いていたためになかなか手を出せずにいましたが、ここにきてようやく正式版もリリースされたことですしそろそろ腰を据えて学習してみたいと思います。

今回のゴール

Angular2 を使って Web アプリケーションを快適に開発するための環境を構築してみます。シンプルな方法であれば公式サイトや他の方々のブログなどでも既に紹介されていますが、当エントリでは中 ~ 大規模な web アプリケーション開発を想定して Gulp をベースとしたビルド環境の構築を目指します。

サンプルコードはこちらから取得できます。

前提条件

  • Mac OS Sierra
  • TypeScript v2.0.1
  • Angular2 v2.0.0
  • Node.js v6.7.0
  • npm v3.10.3

logo-typescript

Angular2 自体が TypeScript で記述されており、プロダクションコードの記述も TypeScript が推奨されていることから当エントリでも TypeScript を前提として進めていきます。もちろん JavaScript や DartFlow などでもプロダクションコードは記述出来ますが、型定義の解決や言語が提供する機能などを考慮すると TypeScript が現時点で適切ではないかと思われます1)あくまで wakamsha 個人の感想です。

Gulp で実行する主なタスク

  1. TypeScript コードを JavaScript コードにコンパイル
  2. Browserify で JavaScript ファイル間の依存関係を解決し、単一のファイルとしてビルドし出力する
  3. コードを編集すると自動で差分を検出してコンパイルからビルドまでを行う
  4. web サーバを起動してブラウザ上で動作確認を出来るようにする

おおまかですがこういったタスクを定義していきます。勿論やりようによっては Gulp を使わずとも npm run などを駆使することでこれらと同様のタスクを定義することは可能ですが、複数タスクを並列で実行することでパフォーマンスの向上が見込めたり、SCSSPug ( Jade ) のコンパイルタスクなどを後から容易に追加できる、チェーンメソッド風に記述することができるので処理の流れが追いかけやすいといったメリットが Gulp にはあります。

プロジェクト用ディレクトリを作成

今回の環境構築に必要な Node パッケージをインストールします。任意のディレクトリ ( 今回はhello-angular2とします ) を作成し、npm プロジェクトを作成します。

# ディレクトリを作成
$ mkdir -p path/to/your/directory/hello-angular2
# ディレクトリに移動
$ cd path/to/your/directory/hello-angular2

次に package.json を作成します。サンプルなので細かい項目は入力せず全て初期値のままにしてしまいましょう。

$ npm init -y
Wrote to path/to/your/directory/hello-angular2/package.json:
{
  "name": "hello-angular2",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

Angular2 本体と依存するライブラリをインストール

2016年9月現在の Angular2 はcore-jszone.jsrxjs という外部ライブラリに依存しています。それぞれのライブラリの概要は以下の通り。

core-js ECMAScript2015 の Promise、Symbol、Typed Array といった機能を Polyfill として提供するなど、ブラウザ間の差異を吸収する
rxjs 非同期処理の一種である Observable パターンを提供する
zone.js 非同期処理関連のユーティリティ。Promise や async / await をより使いやすくする

それではこれらのライブラリと Angular2 をインストールしてきます。ターミナルから以下のコマンドを実行して npm 経由でインストールします。

$ npm install --save \
  core-js rxjs zone.js \
  @angular/{core,common,compiler,platform-browser,platform-browser-dynamic}

Angular2 は 1.x 系のように単一のファイルとしてではなくモジュールとして機能ごとに分割されており、利用者が必要とするモジュールだけを組み合わせて使う仕組みとなっております。Angular はフルスタックフレームワークと言われるだけあって非常に機能が豊富ですが、用途によっては不要な機能もあるでしょう。使わない機能まで含められた単一ファイルとして提供されるとその分ファイルサイズが肥大化してしまいますが、モジュールとして分割提供されていればそういった懸念もなくなります。

今回は最低限必要となる5つのモジュールをインストールしました。

  • @angular/core
  • @angular/common
  • @angular/compiler
  • @angular/platform-browser
  • @angular/platform-browser-dynamic

Angular2 でのプロダクションコードの書き方については今回は割愛します。Hello Worldを表示するサンプルコードは先程の Github にアップしておりますので、ぜひともご参照ください。

当サンプルプロジェクトのディレクトリ構成は以下のようになっております。

.
├── gulpfile.js
├── node_modules/
├── package.json
├── public/
│   └── assets/
│       ├── index.html
│       ├── scripts/
│       │   └── app.js
│       └── styles/
├── src/
│   ├── scripts/
│   │   ├── app.component.ts
│   │   ├── app.module.ts
│   │   ├── hero/
│   │   │   ├── hero-detail.component.ts
│   │   │   └── hero.ts
│   │   └── main.ts
│   ├── styles/
│   └── templates/
│       └── index.html.ts
└── tsconfig.json

1). TypeScript を JavaScript にコンパイル

Gulp およびTypeScript コンパイラをインストールします。


インストールが完了したらgulpfile.js に Gulp タスクを記述します。

const gulp       = require('gulp');
const tsc        = require('gulp-typescript');
const tsProject  = tsc.createProject('./tsconfig.json');
const sourcemaps = require('gulp-sourcemaps');
/** Compile TypeScript sources */
gulp.task('script:compile', () => {
    return gulp.src('src/scripts/**/*.ts')
        .pipe(sourcemaps.init())
        .pipe(tsProject())
        .js
        .pipe(sourcemaps.write())
        .pipe(gulp.dest('build'));
});

TypeScript のコンパイルオプションなどは tsconfig.json に定義し、これを元にしてコンパイル処理を実行します。tsconfig.json の中身は以下の通り。

{
  "compilerOptions": {
    "target": "es6",
    "module": "commonjs",
    "moduleResolution": "node",
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "removeComments": false,
    "noImplicitAny": false,
    "declaration": false,
    "outDir": "build"
  },
  "files": [
    "src/scripts/main.ts",
    "node_modules/typescript/lib/lib.es6.d.ts"
  ]
}

ポイントは target に指定した es6。2016年9月現在、gulp-typescript を使って es5 形式にコンパイルしようとすると大量の型未定義エラーが発生してしまいます2)ただしコンパイル自体は通るので、エラーを気にしないというのであればそれでも大丈夫です ( 多分 ) 。。通常の typescript-cli であればそのような症状は発生しないのであくまで推測ですが、gulp-typescript は型定義ファイルの参照先が異なるか何かしらの不備があるのではないかと考えられます。このあたりはまだ詳細を追えていないので、どなたかご存じの方がいらっしゃればぜひともご教示いただければ幸いです。

タスクが定義出来たら早速実行してみます。

$ gulp script:compile
[18:05:18] Using gulpfile ~/path/to/your/directory/hello-angular2/gulpfile.js
[18:05:18] Starting 'script:compile'...
[18:05:21] Finished 'script:compile' after 2.81 s

build/ というディレクトリにコンパイルされたJavaScriptファイルが生成されているのが分かるかと思います。

build/
├── app.component.js
├── app.module.js
├── hero/
│   ├── hero-detail.component.js
│   └── hero.js
└── main.js

まずは第一段階成功です。次にこれらの HTML ファイルから読み込みやすくするためにこれらのファイル郡を単一のファイルに結合してみます。

IE11 など ECMAScript2015 に対応していないブラウザもサポートする必要がある場合は、コンパイル後に Babel で ES5 形式にダウンコンパイルすることで対処します。少々面倒ですが、今のところそれが妥当な落とし所ではないでしょうか。

2). Browserify で JavaScript ファイル間の依存関係を解決し、単一のファイルとしてビルドし出力する

生成された JavaScript ファイルはimport 文でお互いのコードを参照しあっていますが ( ※依存関係 )、web ブラウザには import 機能が搭載されていないのとファイル数が多くなるとリクエスト数も多くなってパフォーマンスに悪影響を及ぼしてしまいます。そのため、こういった依存関係を上手い具合に解決した状態で単一の JavaScript ファイルに結合するために Browserify を使います。Browserify につきましては過去のエントリでもご紹介しておりますので、併せて御覧ください。

それでは必要な Node パッケージをインストールします。


インストールが出来たら Gulp タスクを定義します。gulpfile.js に以下のコードを追記します。

const source     = require('vinyl-source-stream');
const browserify = require('browserify');
/** Bundle JavaScript sources by Browserify */
gulp.task('script:bundle', () => {
    const b = browserify({
        cache: {},
        packageCache: {},
        debug: true
    });
    b.add('build/main.js');
    return b.bundle()
        .pipe(source('app.js'))
        .pipe(gulp.dest('public/assets/scripts'));
});

Browserify を Gulp のストリームに乗せるために vinyl-source-stream を併用します。タスクが出来たら実行してみます。

$ gulp script:bundle
[18:35:31] Using gulpfile ~/path/to/your/directory/hello-angular2/gulpfile.js
[18:35:31] Starting 'script:bundle'...
[18:35:35] Finished 'script:bundle' after 4.63 s

public/assets/scripts/ ディレクトリにapp.jsが一つだけ生成されているのが分かります。

public/
└── assets/
    └── scripts/
        └── app.js

非常に大きなファイルサイズとなっていますが、これは Angular2 やその他ライブラリまで全てが結合されているからです。よって HTML からはこのファイル一つだけを読み込めばすべて事足りるというわけです。

3). コードを編集すると自動で差分を検出してコンパイルからビルドまでを行う

通常であればgulp-watchを使って編集を検知してコンパイルなどの Gulp タスクを自動で実行したりします。先程の TypeScript のコンパイルから Browserify を使っての依存関係解決も同様の方法で自動化することは可能です。しかし Broserify はコード内で使用されている全てのモジュールの依存関係を解決したうえで結合するため、コード量が多くなればそれだけ処理に時間がかかります。プロダクションコードだけならまだしも Angular といったライブラリも対象となるため、結構な時間がかかります。事実、今回のサンプルは非常に小規模なものですが、それでも4秒以上かかってしまっています。

そこで Watchify の登場です。変更の差分のみを検知し、その部分だけ依存関係を解決することで処理時間を大幅に短縮することができます。

npm から Watchify をインストールします。


先程の Gulp タスクを以下のように書き換えます。

const source     = require('vinyl-source-stream');
const browserify = require('browserify');
const watchify   = require('watchify');
/** Bundle JavaScript sources by Watchify */
gulp.task('script:bundle', () => {
    const b = browserify({
        cache: {},
        packageCache: {},
        debug: true
    });
    const w = watchify(b);
    w.add('build/main.js');
    const bundle = () => {
        return w.bundle()
            .pipe(source('app.js'))
            .pipe(gulp.dest('public/assets/scripts'));
    };
    w.on('update', bundle);
    return bundle();
});

タスクを実行してみます。

$ gulp script:bundle
[19:04:38] Using gulpfile ~/path/to/your/directory/hello-angular2/gulpfile.js
[19:04:38] Starting 'script:bundle'...
[19:04:43] Finished 'script:bundle' after 4.53 s

タスクが終了せず監視状態に入りました。この状態で TypeScript を編集してコンパイル処理を実行すると build/ 以下に変更が入り、それを watchify が検知して差分のみ処理を行うようになります。なお、watchify に指定する監視対象ファイルはエントリポイントとなる main.js のみとなっていますが、そこから再帰的に差分のみを検知してくれるのでこれで問題ありません。

4). web サーバを起動してブラウザ上で動作確認を出来るようにする

browser-sync を使って簡易 web サーバを起動し、ブラウザ上で動作確認を出来るようにします。また、コードを編集してビルドしたタイミングでブラウザを自動でリロード出来るようにもします。browser-sync につきましても過去のエントリでご紹介済みですので、こちらも併せて御覧ください。


タスクを定義します。

const browserSync = require('browser-sync');
/** Run Web server */
gulp.task('serve', () => {
    return browserSync.init(null, {
        server: {
            baseDir: 'public/'
        },
        reloadDelay: 1000
    })
});

さらに watchify のタスクにブラウザをリロードする処理を追記します。

/** Bundle JavaScript sources by Watchify */
gulp.task('script:bundle', () => {
    const b = browserify({
        cache: {},
        packageCache: {},
        debug: true
    });
    const w = watchify(b);
    w.add('build/main.js');
    const bundle = () => {
        return w.bundle()
            .pipe(source('app.js'))
            .pipe(gulp.dest('public/assets/scripts'))
            .pipe(browserSync.reload({
                stream: true
            }));
    };
    w.on('update', bundle);
    return bundle();
});

これで全てのタスクが完成しました。TypeScript のビルドが全て完了している状態で以下のタスクを実行すると web ブラウザが起動するはずです。

$ gulp serve
[19:23:38] Using gulpfile ~/path/to/your/directory/hello-angular2/gulpfile.js
[19:23:38] Starting 'serve'...
[19:23:38] Finished 'serve' after 50 ms
[BS] Access URLs:
 -------------------------------------
       Local: http://localhost:3000
    External: http://192.168.2.59:3000
 -------------------------------------
          UI: http://localhost:3001
 UI External: http://192.168.2.59:3001
 -------------------------------------
[BS] Serving files from: public/

%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2016-09-30-19-24-56

[ おまけ ] - AngularJS 1.x とは別モノなの?

Angular2 の登場以降、ネット上では『AngularJS 1.x と全く違うからアップデートするとなると全部作り直しだ!』『後方互換も無いしまたゼロから学習し直すのしんどすぎでしょ…』といった声をチラホラと目にしてきました。まだ公式のチュートリアル ( TUTORIAL, ADVANCED ) を一通り写経した程度ですが、それらネット上での声は 半分は当たっていて、半分はそんなことはない というのが僕個人の感想です。

確かに 1.x系と 2系とでは互換性が無いので、そのままAngular のコードを差し替えただけでは動作しません。これはもうメジャーバージョンから違うということで、腹をくくるしかないですね。ただし、Service の DI や Component ( Directive )、Pipe ( Filter ) を作成し、それらを組み合わせてアプリケーションを構築していくという基本的な発想は変わっていませんし、 ng-click="onClick()"(click)="onClick()" へ、ng-repeat=""*ngFor="" へと記法が変わったものの、それぞれが提供している機能に大きな変更はありません。つまり既に 1.x系 で何かしらアプリケーションを作ったことがある方であれば、2系の学習コストは 1.x系との差分程度だということが言えます。

まぁこの差分は決して小さくはありませんが、それでも 2系は1.x系の正当進化型であり多くの web 標準仕様を取り込んだものになっていますので、これらを学習することで得られるメリットは非常に大きいのではないでしょうか。

脚注

脚注
1 あくまで wakamsha 個人の感想です。
2 ただしコンパイル自体は通るので、エラーを気にしないというのであればそれでも大丈夫です ( 多分 ) 。