シンプルだけどシッカリ働く最新のECMAScript開発環境

どうも、最近スマホゲー界隈からコンバートしてきた新米フロントエンド戦士です。
数年前はブラウザゲー界隈にいたんですが…完全に浦島太郎状態です。

ここ数ヶ月で色々学ばせてもらいつつ、自分なりのNode開発環境が整ったので公開しちゃいます。  

[完成した環境はこちらのv1.0](https://github.com/zprodev/wds-template)（masterの最新は[こちらでクロスプラットフォーム対応を行った](https://qiita.com/zprodev/items/940a381251b97ac6b072)ものになります）

#### 2018/8/28 追記

本日 Babel7がリリースされました！

それに伴いパッケージ名が変更されていました。
`babel-core` -> `@babel/core`
`babel-preset-env` -> `@babel/preset-env`

また

> babel-preset-env2.0でbabel-polyfillは不要になるらしい

の件が実験的機能として導入され、以下のように`useBuiltIns:usage`と設定すると必要に応じて自動でpolyfillしてくれるようになりました。

```javascript:webpack.config.jsから抜粋
  presets: [
    [
      '@babel/preset-env', { 'useBuiltIns': 'usage', ... }
    ]
  ]
```


 

## 目指す開発環境

以下のような環境を目指しました。

 - 分かりやすくシンプルに
 - 最新のECMAScriptを使える
 - ローカルサーバーをサクッと立てる
 - プログラムも静的ファイルも変更監視で自動リロード

 各項目について説明しいきます。


## 分かりやすくシンプルに

いろんなNodeモジュールが公開されていて色々試したくなりますが、目指すところは極力シンプル!  
使うモジュールはこんな感じです。  

```json:package.jsonから抜粋
  "dependencies": {
    "babel-polyfill": "^6.26.0"
  },
  "devDependencies": {
    "cpx": "^1.5.0",
    "mkdirp": "^0.5.1",
    "rimraf": "^2.6.2",
    "babel-core": "^6.26.0",
    "babel-loader": "^7.1.2",
    "babel-preset-env": "^1.6.1",
    "webpack": "^3.5.6",
    "webpack-dev-server": "^2.7.1"
  }
```

ザックリ分類すると

 - ファイル/ディレクトリ操作系：`cpx` `mkdirp` `rimraf` 
 - ビルドツールのwebpack系：`webpack` `webpack-dev-server`
 - トランスパイラのbabel系：`babel-core` `babel-loader` `babel-preset-env` `babel-polyfill`

タスクランナー系は使わず、npm-scriptでなんとかする方針でいきます。

そしてディレクトリ構造はこんな感じにしました。

```
root/
  ├─ src/ ビルド前のindex.htmlやプログラム類を置く
  ├─ assets/ ビルド前の画像などのリソース類を置く
  └─ dist/ ビルド後のindex.htmlやプログラム類とassets配下のコピーを置く
```

ビルド前のプログラムやリソースと、ビルド後の生成物をきっちり分けられるようにしました。
distディレクトリはビルド毎にまるっと削除して作り直します。

### モジュールの説明

#### mkdirpとrimraf  
バンドル後のコードや画像など、最終的な精製物を配置するdistディレクトリの初期化に使います。
クロスプラットフォームなディレクトリ作成/削除モジュールです。
下記コマンドでdistディレクトリの削除と作り直しができます。

```
rimraf dist/ && mkdirp dist/
```

#### cpx
画像など素材をdistディレクトリにコピーするために使います。
さらに、コピーした元ファイルの**変更監視**もできます。
~~こちらもクロスプラットフォームだと見た記憶がありますが、未確認です。（知ってる方いたら教えてください）~~クロスプラットフォームで使えます。（[こちら](https://qiita.com/zprodev/items/940a381251b97ac6b072)で確認しました）
下記コマンドでsrc配下のindex.htmlとassets配下のリソース類をdist配下に配置できます。

```
cpx src/index.html dist/
cpx 'assets/**/*' dist/
```

#### webpack + babel-core + babel-loader

ES2015構文のimport/exportを使ってプログラムをモジュール化するために使います。
webpackでビルドするとモジュールが結合されて1つのJavaScriptファイルになります。

#### babel-polyfill
babel-loaderの**構文変換**だけでは補えない**新機能(Promiseとか)**を補ってくれます。

#### babel-preset-env
非推奨のbabel-preset-es2015やbabel-preset-es2017に代わるpresetで、**最新ECMAScript構文**が使えるようになります。
また、ビルドターゲットがサポートしていない構文のみ無駄なく変換してくれます。

## 最新のECMAScriptを使える

import/exportだけでなく最新のECMAScript構文を使うために、`babel-polyfill`と`babel-preset-env`を設定します。

### babel-polyfillの設定
```javascript:webpack.config.jsから抜粋
  entry: [
    'babel-polyfill',
    path.resolve('src', 'index.js')
  ],
```

entryにindex.jsと共に書いておけば、一緒にバンドルしてくれます。
ただし、これだとbabel-polyfillで用意している全機能がバンドルされ、ビルド後のファイルが少し大きくなってしまいます。
index.jsファイル内の先頭行でimportしてbabel-loaderのuseBuiltInsオプションを付けることで使用する機能のみバンドルすることも出来ますが、babel-preset-env2.0でbabel-polyfillは不要になるらしいので、それまではシンプルさ重視にしました。

### babel-preset-envの設定
```javascript:webpack.config.jsから抜粋
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: path.resolve('node_modules'),
        loader: 'babel-loader',
        options: {
          presets: [
            ['env', {
              'targets': {
                'node': 'current',             // 動かしてるPCのNodeバージョン
                'browsers': 'last 2 versions'　// 各種ブラウザの直近2バージョン
              }
            }]
          ]
        }
      }
    ]
  },
```
babel-loaderのpresetとして`env`を設定します。
targetsとして開発端末のNodeバージョンと、各種ブラウザの直近2バージョンで動くコードにトランスパイルするよう設定しています。

## ローカルサーバーをサクッと立てる

ローカルサーバーは`webpack-dev-server`でサクッと立てます。

### webpack-dev-serverの設定
```javascript:webpack.config.jsから抜粋
  devServer: {
    port: 8000,  // http://localhost:8000として立てる
    open: true   // ビルド後にブラウザでhttp://localhost:8000を開く
  }
```

上記設定を追加し、`webpack`の代わりに`webpack-dev-server`をコマンド実行すればビルド後のアプリケーションが[http://localhost:8000](http://localhost:8000)で立ち上がります。


## プログラムも静的ファイルも変更監視で自動リロード

ビルド対象のindex.jsが依存関係を持つプログラムに関しては、`webpack-dev-server`を使うだけで変更があったら自動的にビルド & ブラウザリロードしてくれます。
しかし、任意のタイミングで読み込みたい画像などの静的ファイルは、別途工夫が必要です。

### webpack-dev-serverの設定追加

```javascript:webpack.config.jsから抜粋
  devServer: {
    port: 8000,
    open: true,
    contentBase: path.resolve('dist'), //追加
    watchContentBase: true             //追加
  }
```
distディレクトリをwatchする追加を設定しました。これでdist配下のファイルはプログラム以外も変更監視の対象になります。  
しかし、静的ファイルはassetsディレクトリで管理してビルド時にdist配下にコピーする方針なので、dist配下で直接変更は行われません。
そこでdist配下へのコピー時にcpxの変更監視機能を使用します。

### cpxの変更監視設定

```
cpx src/index.html dist/ -w
cpx 'assets/**/*' dist/ -w
```
`-w`オプションを付けることで、コピー元に変更があったらdist配下に自動的にコピーされるようになります。
dist配下にコピーされると、`webpack-dev-server`が変更検知してブラウザリロードを行ってくれるという寸法です。  

## npm-scriptについて

最後に、npm-scriptの説明も軽くしておきます。

```json:package.jsonから抜粋
  "scripts": {
    "start": "npm run mkdist && npm run cpassets -- -w & npm run cphtml -- -w & webpack-dev-server",
    "build": "npm run mkdist && npm run cpassets && npm run cphtml && webpack --optimize-minimize",
    "mkdist": "rimraf dist/ && mkdirp dist/",  // startとbuildから呼ばれる
    "cpassets": "cpx 'assets/**/*' dist/",     // startとbuildから呼ばれる
    "cphtml": "cpx src/index.html dist/"       // startとbuildから呼ばれる
  }
```

### npm run start(npm startでもOK)
dist掃除〜webpack-dev-server起動を行います。
mkdistでdistを掃除完了後、cpassetsとcpassetsに`-w`オプションを付けつつ、webpack-dev-serverと`&`で繋ぎ並列実行させています。

### npm run build
dist掃除〜ビルド&圧縮してdistに配置を行います。
監視が必要ないのでstartとは異なり、mkdist cpassets cphtmlを直列で行った後、ファイル圧縮の`--optimize-minimize`オプション付きでwebpackを実行します。