Angular2のリリース用ソースをgulp + webpack + inlineNg2Templateで1ファイル+αに圧縮してみる

Angular2の勉強中。
まずはビルド環境を整えつつAngular2の仕組みを理解してみます。

最初に一言、**めっちゃ苦労しました。マジで。**
サラっと簡単に書いてますが、ほぼ半日格闘しました（苦笑）
間違っている点等あればご指摘いただければ幸いです。

いろいろ妥協した部分もありますが、十分実用的になったので今後はこれで進めます。

## 今回やりたいこと

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. ビルドされたリリース用ソースをローカルサーバーで動作させたい
10. ~~開発時はwebpack化せずに、編集後数秒以内に確認できるようにしたい~~

<font color="Red">10. 開発時はwebpack化せずに、編集後数秒以内に確認できるようにしたい</font>、は[次回の記事](http://qiita.com/silverfox/items/ec649b2eb1d9e6b4e188)で書きます。

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

#### 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する場合は不要。次回のローカル開発時で利用する。
```

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

**app以下のファイルはすべてbundle用js(main.bunlde.*.js)にひとまとめになります。**
node_modules内のライブラリも、利用しているものは全部ひとまとめにするため必要としません。
※読み込むライブラリが増えすぎるとファイルサイズが1Mを超えてくる可能性がありますが、その時また考えます()

### 補足

angular-cliでの構築だと一発で行えそうでしたが、Windows10 + node.jsだとバグがあったため見送りました。
また、gulpで行っていた方が後々カスタマイズしやすいということもありgulpを採用しています。

=> [※Windows10での不具合についてはこちら](https://github.com/angular/angular-cli/issues/3102)（最新のコメントを見るにnode.js v7.2.0で直っているかもしれません）


## 前提条件

過去の記事「[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

### 今回devDependenciesとして追加したパッケージ一覧

|パッケージ名|用途|コマンド|
|---|---|---|
|gulp-ejs|開発用とリリース用でhtmlの記述が異なる箇所の分岐をgulpから注入するために利用|npm i gulp-ejs --save-dev|
|gulp-inline-ng2-template|コンポーネントの外部htmlとcssをインライン化するために利用|npm i gulp-inline-ng2-template --save-dev|
|gulp-minify-css|cssの最小化に利用|npm i gulp-minify-css --save-dev|
|gulp-minify-html|htmlの最小化に利用|npm i gulp-minify-html --save-dev|
|gulp-plumber|tsやejsのビルド失敗でgulpのwatchが停止しないようにするために利用|npm i gulp-plumber --save-dev|
||npm install --save-dev gulp-plumber|
|gulp-postcss|Future CSS syntaxを使いたいワケではなく、ブラウザ依存のprefix自動付与が目的|npm i gulp-postcss --save-dev|
|gulp-sass|styleの管理はsassで統一したいため|npm i gulp-sass --save-dev|
|gulp-sourcemaps|mapファイル生成用|npm i gulp-sourcemaps --save-dev|
|gulp-symlink|ローカル開発時にnode_modulesへのシムリンクを張るため。※次回の記事で利用|npm i gulp-symlink --save-dev|
|gulp-typescript|typescriptをcommonjsにビルドするため|npm i gulp-typescript --save-dev|
|gulp-webpack|今回の主役。ビルドファイルを1つにするため|npm i gulp-webpack --save-dev|
|gulp-webserver|ビルド後すぐにローカルで確認をするため|npm i gulp-webserver --save-dev|
|postcss-cssnext|postcssで利用するため|npm i postcss-cssnext --save-dev|
|run-sequence|gulpのtaskの直列実行を行うため|npm i run-sequence --save-dev|
|ts-loader|webpackでtypescriptをビルドするために利用|npm i ts-loader --save-dev|

## コーディング開始

### package.json

必要なパッケージはすべてdevDependenciesに記述しているため、このpackage.jsonをコピーして`npm install`すれば必要なパッケージは全て揃います。
nmpコマンドでの実行は行わないため、scripts部分は空にしてます。

```json:package.json
{
  "name": "angular-webpack-sample",
  "version": "1.0.0",
  "licenses": [
    {
      "type": "MIT",
      "url": "https://github.com/angular/angular.io/blob/master/LICENSE"
    }
  ],
  "dependencies": {
    "@angular/common": "^2.1.2",
    "@angular/compiler": "^2.1.2",
    "@angular/core": "^2.1.2",
    "@angular/forms": "~2.1.1",
    "@angular/http": "~2.1.1",
    "@angular/platform-browser": "^2.1.2",
    "@angular/platform-browser-dynamic": "^2.1.2",
    "@angular/router": "~3.1.1",
    "@angular/upgrade": "~2.1.1",
    "angular-in-memory-web-api": "~0.1.13",
    "core-js": "^2.4.1",
    "reflect-metadata": "^0.1.8",
    "rxjs": "^5.0.0-beta.12",
    "systemjs": "0.19.39",
    "zone.js": "^0.6.26"
  },
  "devDependencies": {
    "@types/core-js": "^0.9.34",
    "@types/node": "^6.0.45",
    "concurrently": "^3.0.0",
    "core-js": "^2.4.1",
    "del": "^2.2.2",
    "gulp": "^3.9.1",
    "gulp-ejs": "^2.2.1",
    "gulp-inline-ng2-template": "^4.0.0",
    "gulp-minify-css": "^1.2.4",
    "gulp-minify-html": "^1.0.6",
    "gulp-plumber": "^1.1.0",
    "gulp-postcss": "^6.2.0",
    "gulp-sass": "^2.3.2",
    "gulp-sourcemaps": "^2.2.0",
    "gulp-symlink": "^2.1.4",
    "gulp-typescript": "^3.1.3",
    "gulp-webpack": "^1.5.0",
    "gulp-webserver": "^0.9.1",
    "postcss-cssnext": "^2.8.0",
    "run-sequence": "^1.2.2",
    "rxjs": "^5.0.0-rc.4",
    "ts-loader": "^1.2.2",
    "typescript": "^2.0.3",
    "zone.js": "^0.6.26"
  }
}
```

```:コンソールで実行
npm install
```

### tsconfig.json

TypeScriptのファイルをjsファイルに変換する際の`tsc`コマンドで読み込まれる設定です。
基本的にgulp側から実行するのでoutDirはどこでもいいです。

```ts:tsconfig.json
{
    "compilerOptions": {
        "target": "es5",
        "module": "commonjs",
        "moduleResolution": "node",
        "outDir": "dest/",
        "sourceMap": true,
        "emitDecoratorMetadata": true,
        "experimentalDecorators": true,
        "removeComments": false,
        "noImplicitAny": true
    },
    "include": [
        "src/**/*"
    ],
    "exclude": [
        "node_modules"
    ]
}
```

### webpack.config.js

**今回の汚点その1。entryポイントのディレクトリ名が./srcではなく./tmpになっています。**
詳細は後述しますが、理由としては`inline-ng2-template`で「外部ファイルのインライン化（目的4と5)」が行われたtsファイルが./tmpに展開されているためです。

jsをUglifyによって圧縮・最適化行うためにpluginの指定を行っています。
また、node_modules内の依存パッケージもbundleするために、resolveで指定しています。

```js:webpack.config.js
const webpack = require("webpack");

module.exports = {
    entry: {
        main: './tmp/app/main.ts'
    },
    output: {
        filename: '[name].bundle.[hash].js'
    },
    devtool: 'source-map',
    plugins: [
        new webpack.optimize.UglifyJsPlugin()
    ],
    resolve: {
        root: ['./node_modules'],
        extensions: ['', '.webpack.js', '.web.js', '.ts',  '.js']
    },
    module: {
        loaders: [
            { test: /\.ts$/, loader: 'ts-loader' },
        ]
    }
};
```

### gulpconfig.json

わざわざ外部ファイル化する記述量でもありませんが、パス情報なので一応外出ししておきます。
※localDirは次回の記事で利用します

```json:gulpconfig.json
{
  "srcDir": "./src",
  "localDir": "./local",
  "inlineDir": "./tmp",
  "distDir": "./dist"
}
```

### gulpfile.js

今回の主役です。
**今回の汚点その2、sassのcss化はsrcディレクトリ配下で行っています。**
理想は「コンポーネントでは.sassを読み込み、ビルド時にcss化&インライン化がメモリ上で行われる」でしたが、試行錯誤したもののうまくいかず諦めて今回の形になっています。
<font color="red">そのため、各コンポーネントで読み込んでいるstyleUrlsはsassではなくビルド後のcssです。</font>

やっていることとしては以下です。
1. [build-clean] クリーニング処理。tmpディレクトリ、distディレクトリの削除。src配下の.cssの削除。
2. [sass] sassのビルド。src配下の.sassをビルドし、同階層に最適化済みcssファイルを作成します。（汚点2)
3. [build-inline] コンポーネントの外部ファイル（template, style）をインライン化した.tsファイルを./tmp以下に生成（汚点1)
4. [build-webpack] webpack化の実施。対象が./srcではなく3で生成された./tmpになっています。
5. [build-statics] 4と並列実行。共通cssやjsファイルを ./dist配下に同じ階層でコピーする。
6. [build-ejs] 後述します。.ejsファイルを.htmlとしてビルドし、./dist配下に同じ階層でコピーします。
7. [build-server] ./distをrootとしたhttpサーバーを起動。ブラウザを自動で立ち上げます。
8. [build-after] ビルド後のお掃除。./tmpディレクトリの削除をします。

```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('build-ejs', function () {
  return gulp.src(conf.gulp.srcDir + '/**/*.ejs')
    .pipe(
      ejs({
        type: "build",
        bundleFileList: fs.readdirSync(conf.gulp.distDir + "/js")
          .filter(function (file) {
            // dist/js 以下の.jsファイルはすべて埋め込む
            if( /\.js$/.exec(file)){ return true; }
          })
        },
        {"ext": ".html"}
    ))
    .pipe(minifyhtml())
    .pipe(gulp.dest(conf.gulp.distDir));
});

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('build-inline', function () {
  return gulp.src(conf.gulp.srcDir + '/**/*.ts', {base: conf.gulp.srcDir})
    .pipe(inlineNg2Template({ UseRelativePaths: true, indent: 0, removeLineBreaks: true, base: conf.gulp.srcDir}))
    .pipe(gulp.dest(conf.gulp.inlineDir));
});

gulp.task('build-clean', function (cb) {
  return del([conf.gulp.inlineDir,
      conf.gulp.distDir,
      conf.gulp.srcDir + '/**/*.css',
    ], cb);
});

gulp.task('build-after', function (cb) {
  return del([conf.gulp.inlineDir,
    conf.gulp.srcDir + '/**/*.css'], cb);
});

gulp.task('build-webpack', function () {
  return gulp.src(conf.webpack.entry.main)
    .pipe(webpack(conf.webpack))
    .pipe(gulp.dest(conf.gulp.distDir + '/js'));
});

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

gulp.task('build-server', function () {
  gulp.src(conf.gulp.distDir)
    .pipe(webserver({
        host: 'localhost',
        port: 8100,
        open: true,
        livereload: false //自動リロードはしない
    }));
});

gulp.task('build', function (cb) {
  return runSequence(
    'build-clean',
    'sass',
    'build-inline',
    ['build-webpack', 'build-statics'],
    'build-ejs', //ejsで埋め込むjsのパスはwebpackに依存しているのでこの位置
    'build-server',
    'build-after',
    cb
  );
});
```

### index.ejs

前述のgulpでのbuid-ejsタスクが何をやっているのか意味不明だと思いますのでそちらの解説です。
今回のwebpack版ではindex.htmlでは生成されたjsを1つ読み込むだけで良いのですが、**このビルドは30秒以上かかります。**
<font color="Red">開発時にビルドで毎回30秒もかかっていたら使い物になりません。</font>
そのため、開発時はbundleせずに通常のビルドを行う形でローカル環境の構築を行っています。（これは[次回の記事](http://qiita.com/silverfox/items/ec649b2eb1d9e6b4e188)ネタ）

ただし、SystemJsを利用している場合、webpack化する場合としない場合で多少起動時の記述を変えています。
※これは筆者が試行錯誤して諦めただけなので、何か良い方法があるはず
その異なる記述部分をejsの分岐を利用することでgulp側から制御を行っています。
（typeの値をgulpから渡している）

今回利用しているindex.htmlの元になるejsファイルが以下です。

```html:index.ejs
<html>
  <head>
    <title>Angular QuickStart</title>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <% 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>
      <% } %>
    <% } %>

    <link href="/css/common.css" media="all" rel="stylesheet" />
  </head>
  <!-- 3. Display the application -->
  <body>
    <my-app>Loading...</my-app>
    <my-app2>aa</my-app2>
  </body>
</html>
```

仮にwebpackのことをだけを考え、jsファイル名のハッシュ化も行わないのであれば本来は以下でもOKです。

```html:最小構成のindex.html
<html>
  <head>
    <title>Angular Webpack Sample</title>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <script src="/js/bundle.js"></script>
    <link href="/css/common.css" media="all" rel="stylesheet" />
  </head>
  <body>
    <my-app>Loading...</my-app>
  </body>
</html>
```

### main.ts

最後にもう一つ、main.tsの記述にもコツが必要です。
[Angular2でHello world！](http://qiita.com/silverfox/items/3ef3e20966343604a921) の際にはcore-js等の依存ライブラリはscriptタグで読み込んでいましたが、今回はそれらもbundleしたいためimportにする必要があります。

**さらに重要なポイントとして、bootstrapModuleへのセットはDOMContentLoaded発火時に行う必要があります。**
これによりSystem.importをしなくてもAngularが実行されてくれると思います。

```ts: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);
```

## 実行

### ここまでのディレクトリ構成

以下のようになっていればOKです。
（app以下は自由です。angularとして動いていればなんでもOK。）

```text:最終ディレクトリ
├ node_modules/
│   └─ ... 
├ src/
│   ├─ app/
│   │    ├─ bank-table
│   │    │   ├─ bank-table.component.html
│   │    │   ├─ bank-table.component.sass
│   │    │   └─ bank-table.component.ts
│   │    │
│   │    ├─ _setting.sass
│   │    ├─ app.component.ts
│   │    ├─ app.module.ts
│   │    └─ main.ts
│   │
│   ├─ css/
│   │    ├─ _reset.sass
│   │    ├─ _base.sass
│   │    └─ common.sass
│   │
│   ├─ index.ejs
│   └─ systemjs.config.js
│
├ gulpconfig.json
├ gulpfile.js
├ package.json
├ tsconfig.json
└ webpack.config.js
```

### 実行

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

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

```text:コンソールログ
[01:10:44] Using gulpfile  C:\works\angular\qiita\gulpfile.js
[01:10:44] Starting 'build'...
[01:10:44] Starting 'build-clean'...
[01:10:44] Finished 'build-clean' after 26 ms
[01:10:44] Starting 'sass'...
[01:10:45] Finished 'sass' after 1.17 s
[01:10:45] Starting 'build-inline'...
[01:10:45] Finished 'build-inline' after 392 ms
[01:10:45] Starting 'build-webpack'...
[01:10:46] Starting 'build-statics'...
ts-loader: Using typescript@2.0.10 and C:\works\angular\qiita\tsconfig.json
[01:10:50] Finished 'build-statics' after 4.9 s
[01:11:13] Version: webpack 1.13.3
                                  Asset     Size  Chunks             Chunk Names
    main.bundle.3d13a2d7934cdf2b279e.js   881 kB       0  [emitted]  main
main.bundle.3d13a2d7934cdf2b279e.js.map  5.79 MB       0  [emitted]  main

～webpackで色々warningが出ますが無視～

[01:11:14] Finished 'build-webpack' after 28 s
[01:11:14] Starting 'build-ejs'...
[01:11:14] Finished 'build-ejs' after 40 ms
[01:11:14] Starting 'build-server'...
[01:11:14] Webserver started at http://localhost:8100
[01:11:14] Finished 'build-server' after 16 ms
[01:11:14] Starting 'build-after'...
[01:11:14] Finished 'build-after' after 50 ms
[01:11:14] Finished 'build' after 30 s
```

現状でビルド時間30秒もかかっているので、プロジェクトが肥大化すれば3～5分程度になりそうです。
それよりも問題は、すでにファイルサイズが881KBもあることですね笑

これは今後の状況に合わせてよりよい方法を探していく予定です。
まあgulpなのでその辺りは自分次第、ですね。


では、明日は「[Angular2開発をTypeScript+Sass+ejsで行う & gulpでsrcと別ディレクトリに構築](http://qiita.com/silverfox/items/ec649b2eb1d9e6b4e188)」を更新予定です。（長い…）

～ではまた次回～