Angular2開発をTypeScript+Sass+ejsで行う & gulpでsrcと別ディレクトリに構築

前回の「[Angular2のリリース用ソースをgulp + webpack + inlineNg2Templateで1ファイル+αに圧縮してみる](http://qiita.com/silverfox/items/825409b4046095e85190)」の続編となります。
前回はリリース用として全てのファイルを1つのjsにまとめましたが、**ビルドに30秒以上かかってしまうため開発時には使い物になりません。**
そのため、ローカル開発用としてビルドの単位を最小限に抑えたgulpのタスクを用意します。

##今回やりたいこと
1. Angular2のソースはTypeScriptで書きたい
2. src用ディレクトリにビルドされたファイル(.jsや.map)を展開したくない
3. styleの指定ではsassとpostcssを使いたい
4. コンポーネントのtemplateやstyleは外部ファイルで保存したい
5. ~~4のファイルはすべてビルド時にインライン化したい~~
6. ~~html, css, js すべて最小化（minity）したい~~
7. templateはejsも利用可能にしたい
8. ~~生成されたスクリプトはファイル名にhash値を含み、index.htmlで取り込みたい~~
9. ビルドされたリリース用 or 開発用ソースをローカルサーバーで動作させたい
10. <font color="red">開発時はwebpack化せずに、編集後数秒以内に確認できるようにしたい</font>

5、6、8は[前回](http://qiita.com/silverfox/items/825409b4046095e85190) の内容となるため、今回は語りません。
今回の主題は10の実現であり、前回の**「リリース時のwebpack化」**と、今回の**「開発時の最小単位のビルド化」**を**gulpのタスクで切り分ける**のが目的です。


### 例：やりたいことをディレクトリ構成で表現

#### gitリポジトリで管理する用のソース（ビルド前）
```text:srcディレクトリ
├ node_modules/
│   └─ ... 全てのnodeライブラリ
├ src/
│   ├─ app/
│   │    ├─ bank-table
│   │    │   ├─ bank-table.component.html
│   │    │   ├─ bank-table.component.sass  -- 該当コンポーネント専用のstyle情報
│   │    │   └─ bank-table.component.ts    -- htmlとcss(.sassではない）を外部ファイル読込している
│   │    │
│   │    ├─ _setting.sass    -- 各コンポーネントで共通利用するようなsassの変数を格納
│   │    ├─ app.component.ts
│   │    ├─ app.module.ts
│   │    └─ main.ts
│   │
│   ├─ css/
│   │    ├─ _reset.sass
│   │    ├─ _base.sass
│   │    └─ common.sass      -- _reset.sassと_base.sassをimportしている共通style情報。
│   │
│   ├─ index.ejs             -- 次回のローカル開発用の分岐等が必要なためejsで記述
│   └─ systemjs.config.js    -- webpackする場合は不要。今回のローカル開発時で利用する。
│
├ gulpconfig.json            -- ※前回の内容と同一なため今回の解説は無し
├ gulpfile.js
├ package.json               -- ※前回の内容と同一なため今回の解説は無し
├ tsconfig.json              -- ※前回の内容と同一なため今回の解説は無し
└ webpack.config.js          -- ※前回の内容と同一なため今回の解説は無し
```

#### リリースをする用のソース（上記ディレクトリのリリース用ビルド後）
※[前回](http://qiita.com/silverfox/items/825409b4046095e85190) の内容なためリリース用環境の説明は今回はありません

```text:distディレクトリ
dist/
  ├─ css/
  │    └─ common.css
  ├─ js/
  │    ├─ main.bundle.6114f73f73ce49e6cfef.js      -- キャッシュ対策でファイル名にハッシュ値を含む
  │    └─ main.bundle.6114f73f73ce49e6cfef.js.map  -- map情報。(htmlからは読み込まない)
  │
  └─ index.html
```

#### 開発をする用のソース（上記ディレクトリのローカル用ビルド後）
<font color="red">今回のメインです。</font>
`.tsは.jsへ、.sassは.cssへ、.ejsは.htmlへ`それぞれビルド済みのものが展開されます。

```text:localディレクトリ
local/
  ├─ app/
  │    ├─ bank-table
  │    │   ├─ bank-table.component.css
  │    │   ├─ bank-table.component.html
  │    │   └─ bank-table.component.js
  │    │   └─ bank-table.component.js.map
  │    │
  │    ├─ app.component.js
  │    ├─ app.component.js.map
  │    ├─ app.module.js
  │    ├─ app.module.js.map
  │    ├─ main.js
  │    └─ main.js.map
  │
  ├─ css/
  │    └─ common.css
  │
  ├─ node_modules          -- 一階層上のnode_modulesへのシムリンクです
  │
  ├─ index.html
  └─ systemjs.config.js    -- webpack時は不要ですが、local実行時には重要な役割を果たします。
```


## 前提条件

前回の「[Angular2のリリース用ソースをgulp + webpack + inlineNg2Templateで1ファイル+αに圧縮してみる](http://qiita.com/silverfox/items/825409b4046095e85190)」の続きです。
また、ファイル構成は過去の記事「[Angular2でHello world！](http://qiita.com/silverfox/items/3ef3e20966343604a921)」で構築した環境をベースにしています。

### 環境を再掲

- Microsoft Surface Pro4 (i5 2.4GHz, Mem 4GB)
- OS:Windows 10 Pro
- node.js v7.1.9
- npm v3.10.9


## コーディング開始

`gulpconfig.json`、`package.json`、`tsconfig.json`は[前回の記事](http://qiita.com/silverfox/items/825409b4046095e85190)と同様のものを利用しますので解説は省きます。

### systemjs.config.js

[前回](http://qiita.com/silverfox/items/825409b4046095e85190)は利用しなかった`systemjs.config.js`ですが、今回は利用します。
ベースとなっているのは「[Angular2でHello world！](http://qiita.com/silverfox/items/3ef3e20966343604a921)」の記事の時のsystemjs.confing.jsです。
今回は`core-js`や`zone.js`といったファイルをindex.htmlのscriptタグで読み込むのではなく、main.ts内のimport指定で読み込んでいるしているため、SystemJsで定義しておきます。

```js:systemjs.config.js
(function (global) {
  System.config({
    paths: {
      'npm:*': 'node_modules/*'
    },
    map: {
      app: 'app',
      '@angular/core': 'npm:@angular/core/bundles/core.umd.js',
      '@angular/common': 'npm:@angular/common/bundles/common.umd.js',
      '@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js',
      '@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js',
      '@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js',
      '@angular/http': 'npm:@angular/http/bundles/http.umd.js',
      '@angular/router': 'npm:@angular/router/bundles/router.umd.js',
      '@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js',
      '@angular/upgrade': 'npm:@angular/upgrade/bundles/upgrade.umd.js',
      'core-js': 'npm:core-js/client/shim.min.js',
      'zone.js': 'npm:zone.js/dist/zone.js',
      'rxjs': 'npm:rxjs',
      'angular-in-memory-web-api': 'npm:angular-in-memory-web-api/bundles/in-memory-web-api.umd.js'
    },
    packages: {
      app: {
        main: 'main.js',
        defaultExtension: 'js'
      },
      rxjs: {
        defaultExtension: 'js'
      }
}
    });
})(this);
```

### src/app/main.tsとsrc/index.ejs

前回と同じ内容ですが、これは敢えて再掲します。
理由は、**この2つが「webpackでのbundle化」と「個別ビルド化」を共存させるためのカギとなる為です。**

webpack化する場合、`webpack.config.js`でのentryとして`main.tsを`を指定すると、main.tsの中身はグローバルに展開されるため、scriptが読み込まれた際にmain.tsの内容は即実行されます。
ですが、headタグ内にscriptタグを置いているとDOMの構築が終わる前にmain.tsのbootstrapが実行されてしまい、「対象のelementが無いというエラー」が出てしまいます。例：`The selector "my-app" did not match any elements`
これを回避するために、webpack化の対応としてbootstrapへの引き渡しはDOMContentLoadedの発火時に行うようにします。
※scriptタグをindex.htmlのbodyの最後に書くことでも回避はできますが、これはダサいのでやりません

```js:
document.addEventListener("DOMContentLoaded", function(){
  platform.bootstrapModule(AppModule)
});
```

が、これを行うと今度はローカル実行時にindex.htmlで`System.import('app');`を行ってもbootstrapが実行されなくなります。
何故かというと、`System.import('app');`はmain.js（`systemjs.config.js`でappとして定義されているjsファイル）を実行しているだけだからです。
main.js内ではbootstrapの実行をDOMContentLoaded発火時に行うようにしましたが、index.html内の`System.import('app');`の内部処理はDOMContentLoaded完了後に実行されます。（SystemJs側の挙動）
そのため`System.import('app');`でmain.jsが実行されるタイミングではDOMContentLoadedが既に完了しており、bootstrapが実行されることはありません。

**なんということでしょう！**

「なんかもうwebpack時と個別ビルド時用のmain.ts分ければ良くね('A`)？」と諦めそうになりましたが、SystemJSの仕様を追っていると、thenメソッドの高階関数の引数にmain.tsのオブジェクトが渡されている事が分かりました。
それを利用したのが以下のmain.tsとindex.ejs（index.html）です。

```ts:src/app/main.ts
import "core-js";
import "zone.js";
import "rxjs/Rx";
  
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app.module';
const platform = platformBrowserDynamic();

export function init(): void {
    platform.bootstrapModule(AppModule);
}
document.addEventListener("DOMContentLoaded", init);
```

```html:src/index.ejs
<html>
  <head>
    <title>Angular QuickStart</title>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link href="/css/common.css" media="all" rel="stylesheet" />

    <% if (type == 'local') { %>
      <script src="/node_modules/systemjs/dist/system.src.js"></script>
      <script src="/systemjs.config.js"></script>
      <script>
          System.import('app').then(function(m){
              m.init();
          }).catch(function(err){
              console.error(err);
          });
      </script>
    <% } else { %>
      <% for (var bundleFilePath of bundleFileList) { %>
        <script src="/js/<%= bundleFilePath %>"></script>
      <% } %>
    <% } %>

  </head>
  <!-- 3. Display the application -->
  <body>
    <my-app>Loading...</my-app>
    <my-app2>aa</my-app2>
  </body>
</html>
```

ejs側が分かりずらいですが、gulpを通して生成されるindex.htmlが以下です。

```html:index.html
<html>
  <head>
    <title>Angular QuickStart</title>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link href="/css/common.css" media="all" rel="stylesheet" />
      <script src="/node_modules/systemjs/dist/system.src.js"></script>
      <script src="/systemjs.config.js"></script>
      <script>
          System.import('app').then(function(m){
              m.init();
          }).catch(function(err){
              console.error(err);
          });
      </script>
  </head>
  <!-- 3. Display the application -->
  <body>
    <my-app>Loading...</my-app>
    <my-app2>aa</my-app2>
  </body>
</html>
```

ここが今回の妙技です。webpackの際はDOMContentLoaded発火時にbootstrapが呼ばれ、個別ビルドの場合はこの記述によりbootstrapが実行されます。

```js:妙技の部分
System.import('app').then(function(m){
  m.init();
})
```

これでこれで全てが出そろいました。
あとはgulpfile.jsを書いて完了です。

### gulpfile.js

オオトリ。
本来は[前回のgulpfile.jsの内容](http://qiita.com/silverfox/items/825409b4046095e85190) も合わせて記述されていますが、記事が長くなるので今回はローカル実行分だけ記載します。
<font color="red">前回と今回の内容を1つのgulpfile.jsに書くことで、`gulp build`でリリース用のビルド、`gulp local`でローカル実行用のビルドが行えるようになります。</red>

やっていることとしては以下です。
1. [local-clean] クリーニング処理。localディレクトリの削除、src配下の.cssの削除。
2. [sass] sassのビルド。src配下の.sassをビルドし、同階層に最適化済みcssファイルを作成します。（汚点)
3. [local-ts][並列] .tsファイルをes5の.jsに変換する処理。./local配下の同じ階層に展開されます。
3. [local-watch][並列] src配下の.sass、.ts、.ejs、.html、.css、.jsファイルの更新を検知して自動ビルド化
5. [local-statics][並列] 共通cssやjsファイルを ./local配下に同じ階層でコピーします。
6. [local-ejs][並列] .ejsファイルを{type:local}として.htmlにビルドし、./local配下に同じ階層でコピーします。
7. [local-nodemodules][並列] 直下にあるnode_modulesへのシムリンクを./local/node_modulesとして貼ります。
8. [local-server] ./localをrootとしたhttpサーバーを起動。ブラウザを自動で立ち上げます。

```js:gulpfile.js
var gulp = require('gulp');
var typescript = require('gulp-typescript');
var plumber = require('gulp-plumber');
var runSequence = require('run-sequence');
var inlineNg2Template = require('gulp-inline-ng2-template');
var webpack = require('gulp-webpack');
var sourcemaps = require('gulp-sourcemaps');
var webserver = require('gulp-webserver');
var del = require('del');
var sass = require('gulp-sass');
var postcss = require('gulp-postcss');
var cssnext = require('postcss-cssnext');
var minifycss = require('gulp-minify-css');
var minifyhtml = require('gulp-minify-html');
var symlink = require('gulp-symlink'); //次回で利用
var ejs = require("gulp-ejs");
var fs   = require('fs');
var path = require('path');

var conf = {
  'gulp' : require('./gulpconfig'),
  'webpack' : require('./webpack.config.js'),
  'ts' : require('./tsconfig')
}

gulp.task('sass', function () {
  var processors = [cssnext()];
  return gulp.src(conf.gulp.srcDir + '/**/*.sass', {base: conf.gulp.srcDir})
    .pipe(sass().on('error', sass.logError))
    .pipe(postcss(processors))
    .pipe(minifycss())
    .pipe(gulp.dest(conf.gulp.srcDir))
});
gulp.task('local-clean', function (cb) {
  return del([
      conf.gulp.srcDir + '/**/*.css',
      conf.gulp.localDir,
    ], cb);
});

gulp.task('local-ts', function () {
  var tsResult = gulp.src(conf.gulp.srcDir + '/**/*.ts', {base: conf.gulp.srcDir})
    .pipe(sourcemaps.init())
    .pipe(plumber())
    .pipe(typescript(conf.ts.compilerOptions));
  tsResult.dts.pipe(gulp.dest(conf.gulp.localDir));

  return tsResult.js.pipe(sourcemaps.write("./"))
    .pipe(gulp.dest(conf.gulp.localDir));
});

gulp.task('local-nodemodules', function () {
  return gulp.src('node_modules')
    .pipe(symlink(conf.gulp.localDir + '/node_modules'));
});

gulp.task('local-ejs', function () {
  return gulp.src(conf.gulp.srcDir + '/**/*.ejs')
    .pipe(plumber())
    .pipe(ejs({type: "local"}, {"ext": ".html"}))
    .pipe(gulp.dest(conf.gulp.localDir));
});

gulp.task('local-statics', function () {
  return gulp.src(
        [ conf.gulp.srcDir + '/**/*.html',
          conf.gulp.srcDir + '/**/*.css',
          conf.gulp.srcDir + '/**/*.js' ],
        { base: conf.gulp.srcDir }
    ).pipe( gulp.dest(conf.gulp.localDir));
});

gulp.task('local-server', function () {
  gulp.src(conf.gulp.localDir)
    .pipe(webserver({
        host: 'localhost',
        port: 8100,
        open: true,
        livereload: false //自動リロードはしない
    }));
});
gulp.task('local', function (cb) {
  return runSequence(
    'local-clean',
    'sass',
    ['local-ts', 'local-watch', 'local-statics', 'local-ejs', 'local-nodemodules'],
    'local-server',
    cb
  );
});

// 自動ビルド
gulp.task('local-watch', function () {
    gulp.watch(conf.gulp.srcDir + '/**/*.sass', ['sass']);
    gulp.watch(conf.gulp.srcDir + '/**/*.ts', ['local-ts']);
    gulp.watch(conf.gulp.srcDir + '/**/*.ejs', ['local-ejs']);
    gulp.watch([ conf.gulp.srcDir + '/**/*.html',
                conf.gulp.srcDir + '/**/*.css',
                conf.gulp.srcDir + '/**/*.js' ], ['local-statics']);
});
```


## 実行

コンソールからgulpを実行します。

```:コンソールから実行
>gulp loacl
```

```text:コンソールログ
gulp local
gulp local
[00:40:34] Using gulpfile C:\works\angular\qiita\gulpfile.js
[00:40:34] Starting 'local'...
[00:40:34] Starting 'local-clean'...
[00:40:34] Finished 'local-clean' after 44 ms
[00:40:34] Starting 'sass'...
[00:40:35] Finished 'sass' after 879 ms
[00:40:35] Starting 'local-ts'...
[00:40:35] Starting 'local-watch'...
[00:40:35] Finished 'local-watch' after 59 ms
[00:40:35] Starting 'local-statics'...
[00:40:35] Starting 'local-ejs'...
[00:40:35] Starting 'local-nodemodules'...
[00:40:36] gulp-symlink:C:\works\angular\qiita\node_modules symlinked to ./local/node_modules
[00:40:36] Finished 'local-nodemodules' after 47 ms
[00:40:36] gulp-symlink:C:\works\angular\qiita\node_modules symlinked to ./local/node_modules
[00:40:36] Finished 'local-ejs' after 67 ms
[00:40:40] Finished 'local-statics' after 4.37 s
[00:40:40] Finished 'local-ts' after 4.76 s
[00:40:40] Starting 'local-server'...
[00:40:40] Webserver started at http://localhost:8100
[00:40:40] Finished 'local-server' after 8.76 ms
[00:40:40] Finished 'local' after 5.7 s
```

初回起動時で5～10秒程度かかりますが、その後のファイル更新における反映はそれなりに速いです。

- tsファイル : 5秒程度
- sass : 1秒程度
- ejs : 0.1秒程度
- html, css, js :5秒程度

プロジェクトが大きくなってくると確実に劣化すると思いますが、tsファイルは差分ビルドにしたり、html, css, jsはwathを分けたりすることで改善は図れると思っています。
gulpなのでその辺は自分次第でいくらでも調整可能、といったところです。

～ではまた次回～