ECMAScript2015(es6)の基本的な開発環境

#es6の基本的な開発環境を用意する
`Vue.js`や`React.js`等を利用したJSの開発が広まりつつあるが、フレームワークを使わないJSの開発もありえるので、備忘録としてes6の簡易的な開発環境の構築方法についてまとめる。

##node.jsのインスール
`npm`コマンドが必要になるので、[`node.js`](https://nodejs.org/ja/)をインストールする。
インストール方法については、Windows10環境では[こちら](http://qiita.com/taipon_rock/items/9001ae194571feb63a5e)、Macの場合は[こちら](http://qiita.com/akakuro43/items/600e7e4695588ab2958d)が参考になる。

なお、Windows10の場合、`node.js`のコマンドプロンプトで、`chcp 65001`を実行することで、UTF-8テキスト表示ができ、文字化けを防止できる。

##browserifyを利用した環境構築
es6に準拠した書き方で開発すると動作幅広いブラウザで動作しないため、[babelify](https://github.com/babel/babelify)が必要となる。また、`import/export`を利用し（babelifyでrequireに書き換わるため）、複数のJSファイルを一つにまとめるには[browserify](http://browserify.org/)が必要となる。

###browserifyとbabelifyのインスール
es6で開発するルートディレクトリで、以下のように`npm`コマンドを実行する。
`npm init`実行時はすべて質問に対して"Enter"キーのみでスキップした。

```shell-session:モジュールのインストール
$ npm init
$ npm install -g browserify 
$ npm install --save-dev babelify babel-preset-es2015
```

`browserify`はコマンドから頻繁に利用することを想定し、グローバルインストールした。
なお、JSファイルの改行や空白を除去し、minファイルを作成する場合、`uglify`もインストールする。

```shell-session:UglifyJSを利用する場合
$ npm install -g uglify-js
```

###サンプル
JS側でCSVファイルを読み込んで、その内容を描画するサンプルを用意。
具体的には、csvファイルの1列目を読み込みtextareaに表示するサンプル。データがURL形式(http/https)かどうかチェックし、読み込む行数はtextareaのmaxlengthで指定された値に制限する。

####コード
以下のファイルはすべて開発環境のルートディレクトリに配置する。
JSのコードについては、可能な限りES6で追加されているシンタックス（[こちら](http://qiita.com/tuno-tky/items/74ca595a9232bcbcd727)を参考にした）を利用した。

```html:index.html(圧縮前のjsを利用)
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <title>はじめてのES6</title>
</head>
<body>
<div style="margin: 20px" class="js-csv-read">
    <div style="color: red" class="js-message"></div>
    現在<span class="js-csv-count"></span>件登録されています<br>
    <textarea style="height: 30px" class="js-csv-data" readonly maxlength="3"></textarea>
    <br>
    <input type="button" class="js-csv-clear" value="クリア">
    <input type="file" class="js-csv-file">
</div>
<div style="margin: 20px" class="js-csv-read">
    <div style="color: red" class="js-message"></div>
    現在<span class="js-csv-count"></span>件登録されています<br>
    <textarea style="height: 50px" class="js-csv-data" readonly maxlength="5"></textarea>
    <br>
    <input type="button" class="js-csv-clear" value="クリア">
    <input type="file" class="js-csv-file">
</div>
<footer>
<script type="text/javascript" src="bundle.js"></script>
</footer>
</body>
</html>
```
```js:read-csv.js
"use strict";

export default class ReadCsv {
    constructor(targets) {
        this.targets = targets;
        this.init_();
    }

    init_() {
        [].forEach.call(this.targets, (target) => {
            const area   = target.querySelector(".js-csv-data");
            const count  = target.querySelector(".js-csv-count");
            const clear  = target.querySelector(".js-csv-clear");
            const csv    = target.querySelector(".js-csv-file");

            this.clean_(area, count)

            clear.addEventListener("click", (evt) => {
                this.clean_(area, count)
                this.dispalyMessage_(target, "クリアしました");
            });

            csv.addEventListener("change", (evt) => {
                evt.preventDefault();
                this.readCsv_(target, evt, area, count);
            });
        });
    }

    clean_(area, count) {
        area.innerHTML  = "";
        count.innerHTML = "0";
    }

    displayMessage(target, message) {
        const msg = target.querySelector(".js-message");
        msg.innerHTML = message
    }

    appendValue_(target, area, v) {
        const e = document.createTextNode(v + "\n");
        area.appendChild(e);
    }

    readCsv_(target, evt, area, count) {
        const file   = evt.target.files;
        const reader = new FileReader();

        this.clean_(area, count)
        this.displayMessage(target, "");

        reader.onload = (function(obj) {
            const o = obj;
            return function(evt) {
                o.processCsv(target, evt.target.result, area, count);
            };
        })(this);

        reader.onerror = (function(obj) {
            const o = obj;
            return function(evt) {
                o.displayMessage(target, "ファイルの読み込みに失敗しました");
            };
        })(this);

        reader.readAsText(file[0]);
    }

    processCsv(target, csv, area, count) {
        const lines  = csv.split(/\r\n|\n/);
        const maxLen = parseInt(area.getAttribute("maxlength"), 10);

        let c = parseInt(count.innerHTML, 10);

        try {
            lines.forEach((elm, idx) => {
                const data = elm.split(",");
                if (data.length > 0 && /^(https?)(:\/\/[-_.!~*'()a-zA-Z0-9;\/?:@&=+$,%#]+)$/.test(data[0])) {
                    if (c >= maxLen) {
                        throw new Error('上限に達しました');
                    }
                    this.appendValue_(target, area, data[0]);
                    c++;
                }
            });
        } catch (e) {
            console.log(e);
        }
        count.innerHTML = c;
    }
}
```
```js:app.js
"use strict";

import ReadCsv from "./read-csv";

class MyApp {
    static exec() {
        const obj = new MyApp();
        obj.prepareReadCsv();
    }

    prepareReadCsv() {
        const targets = document.querySelectorAll(".js-csv-read");
        if (targets) {
            const ts = new ReadCsv(targets);
        }
    }
}

MyApp.exec();
```

####ビルド
下記のコマンドを実行する。

```shell-session:ビルド
$ browserify app.js -o bundle.js -t [ babelify --presets es2015 ]
```
```shell-session:圧縮する場合
$ uglifyjs bundle.js -c -o bundle.min.js
```

####デモ
#####1. `index.html`にアクセス
<img width="302" alt="スクリーンショット 2017-06-18 12.23.43.png" src="https://qiita-image-store.s3.amazonaws.com/0/135559/e6f50a84-0272-91fb-6187-ba3578f44e64.png">

#####2. 2つの「ファイルを選択」ボタンをそれぞれクリックし、csvファイルを渡す
<img width="236" alt="スクリーンショット 2017-06-18 12.28.45.png" src="https://qiita-image-store.s3.amazonaws.com/0/135559/6f2b5bd4-71c0-0bca-1fd0-ca84c8bd19d6.png">

csvファイルは以下を利用した。

```text:test.csv
http://hogehoge/01
ftp://hogehoge/02
http://hogehoge/03
http:/hogehoge/04
http://hogehoge/05
http://ほげほげ/06
http://hogehoge/07
http://hogehoge/08
http://hogehoge/09
```

##webpackを利用した環境構築
`webpack`は、フロントエンドにおけるビルドツールの1つ。`browserify`のように「複数ファイルを1つのjsファイルにまとめる」ことができ、パッケージ管理もできるので非常に便利。

###インストール
下記コマンドを開発環境のルートディレクトリで実行する。
`npm init`を実行するといくつか質問に答える必要があるが、今回作成時はすべてリターンキーを押下し、デフォルトとした。

```shell-session
$ npm init
$ npm install -g webpack
```

###ビルド
サンブルコードは、「`browserify`を利用した環境構築」と同じコードを利用。
下記を実行することでビルドができる。

```shell-session:実行したコマンド
webpack app.js bundle.js
```
```shell-session:出力結果
Hash: 2bae931e656c8d42ecde
Version: webpack 2.5.1
Time: 72ms
            Asset     Size  Chunks             Chunk Names
bundle.webpack.js  6.05 kB       0  [emitted]  main
   [0] ./read-csv.js 2.69 kB {0} [built]
   [1] ./app.js 364 bytes {0} [built]
```

ただし、これでは複数ファイルをまとめただけなので、複数ブラウザで対応できていないので、`package.json`と`webpack.config.json`を編集し、`babel`を利用する。

###package.jsonとwebpack.config.json
####package.json
`npm init`コマンド実行後に以下のような`package.json`が生成されている。ここでは作業ディレクトリ名が`base`のため`name`キーの値が`base`となっている。

```json:package.json
{
  "name": "base",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}
```

####babel-loader、プリセットのインストール
下記コマンドで`webpack`をローカルインストールし、`babel`をインストールする。

```shell-session
$ npm install --save-dev webpack babel-loader babel-core babel-preset-es2015
```
インストールすると`package.json`も下記のように更新される。

```json:package.json
{
  "name": "base",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "babel-core": "^6.26.0",
    "babel-loader": "^7.1.2",
    "babel-preset-es2015": "^6.24.1",
    "webpack": "^3.5.5"
  }
}
```

####webpack.config.js
webpackコマンド実行時にファイル名を`webpack.config.js`とし、ワーキングディレクトリ直下に配置すると、webpackコマンド実行時に`--config`オプションでconfigファイルを指定する必要がなくなる。
なお、便宜上、出力ファイル名をapp1とした。

```js:webpack.config.js
var webpack = require('webpack');

module.exports = {
  entry: {
    'app1': './app.js',
  },
  output: {
    path: __dirname,
    filename: '[name].bundle.js'
  },
  module: {
    loaders: [
      { 
        test: /\.js$/, 
        exclude: /node_modules/, 
        loader: "babel-loader", 
        query:{
          presets: ['es2015']
        }
      }
    ]
  }
};
```
余談だが、'app1'を'output/app1'とすることで、出力ファイルを指定したディレクトに変更することができるので、entryが複数あり、それぞれ別のディレクトリに出力したいときに便利。

###もう一度ビルド
準備ができたのでもう一度ビルドしてみる。

```shell-session:実行コマンド
$ webpack -p
```
```shell-session:実行結果
Hash: 4417e05b925329407a30
Version: webpack 2.5.1
Time: 1813ms
         Asset     Size  Chunks             Chunk Names
app1.bundle.js  8.37 kB       0  [emitted]  app1
   [0] ./read-csv.js 4.15 kB {0} [built]
   [1] ./app.js 1.49 kB {0} [built]
```

これで複数ブラウザでもJSが動作できる。

####npm runコマンド
`package.json`を以下のように変更することで、`npm`コマンドでもビルドできる。

```json:package.json
{
  "name": "base",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack -p"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "babel-core": "^6.26.0",
    "babel-loader": "^7.1.2",
    "babel-preset-es2015": "^6.24.1",
    "webpack": "^3.5.5"
  }
}
```
変更後、下記コマンドを実行しても同じ結果となる。

```shell-session
$ npm run build
```

###pluginを導入
せっかくなのでpluginも使ってみた。今回は、`UglifyJs`と`BrowserSync`を利用した。
[`Browsersync`](https://www.npmjs.com/package/browser-sync-webpack-plugin)はファイル変更を監視し、自動でブラウザリロードを行う。

####BrowserSyncのインストール
以下を実行する

```shell-session
$ npm install --save-dev browser-sync browser-sync-webpack-plugin
```
`package.json`も更新される。

####UglifyJsのインストール
この場合、グローバルではなくローカルインストール。

```shell-session:
$ npm install --save-dev uglify-js
```

####webpack.config.jsとpackage.jsonの更新
`webpack.config.js`と`package.json`をそれぞれ下記のように更新する。

```js:webpack.config.js
var webpack = require('webpack');
var BrowserSyncPlugin = require('browser-sync-webpack-plugin');

module.exports = {
  entry: {
    'app1': './app.js',
  },
  output: {
    path: __dirname,
    filename: '[name].bundle.js'
  },
  module: {
    loaders: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: "babel-loader",
        query:{
          presets: ['es2015']
        }
      }
    ]
  },
  plugins: [
    new webpack.optimize.UglifyJsPlugin({
      compress: { warnings: false }
    }),
    new BrowserSyncPlugin({
      host: 'localhost',
      port: 3000,
      server: { baseDir: ['./'] } })
  ]
};
```

```json:package.json
{
  "name": "base",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack -p",
    "watch": "webpack  --progress --colors --watch"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "babel-core": "^6.26.0",
    "babel-loader": "^7.1.2",
    "babel-preset-es2015": "^6.24.1",
    "browser-sync": "^2.18.13",
    "browser-sync-webpack-plugin": "^1.2.0",
    "webpack": "^3.5.5"
  }
}
```

####ビルド
```shell-session
$ npm run watch
```

上記コマンドを実行すると、自動的にブラウザ起動し、`http://localhost:3000`にアクセスされて、`index.html`が表示される。
`http://localhost:3001`にアクセスすると以下の画面が表示されるが、`browser-sync-weboack-plugin`について理解度が不足しているのでさらに研究したいと思う。

<img width="1153" alt="スクリーンショット 2017-05-18 3.21.40.png" src="https://qiita-image-store.s3.amazonaws.com/0/135559/607cb5ac-6104-e910-ceda-c33b7c215948.png">

###`package.json`を利用したモジュールのインストール
`es6`の開発環境が既に用意されている場合、`package.json`があるディレクトリ配下で下記コマンドを実行するだけで、必要なpackageがローカルインストールされるので、個別にそれぞれ必要なモジュールをインストールする必要がない。プラグインなどもインストールされる。

```shell-session
$ npm install
```

##webpack-dev-serverについて
前述した`BrowserSync`があるが、[`webpack-dev-server`](https://webpack.js.org/configuration/dev-server/)があり、こちらを利用することが多い。
対象となるファイル（`contentBase`で指定したディレクトリ配下）に修正・変更等があると自動的にビルドされる。

###インストール
```shell-session
$ npm install --save-dev webpack-dev-server
```

###`package.json`と`webpack.config.js`の更新
`webpack.config.js`と`package.json`をそれぞれ下記のように更新する。今回は、`path`を使っている。
`webpack-dev-server`のデフォルトパスは

```json:package.json
{
  "name": "base",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack -p",
    "watch": "webpack  --progress --colors --watch",
    "dev": "webpack-dev-server"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "babel-core": "^6.26.0",
    "babel-loader": "^7.1.2",
    "babel-preset-es2015": "^6.24.1",
    "browser-sync": "^2.18.13",
    "browser-sync-webpack-plugin": "^1.2.0",
    "webpack": "^3.5.5",
    "webpack-dev-server": "^2.7.1"
  }
}
```

```js:webpack.config.js
var webpack = require('webpack');
var BrowserSyncPlugin = require('browser-sync-webpack-plugin');
var path = require('path');

module.exports = {
  entry: {
    'app1': './app.js',
  },
  output: {
    path: __dirname,
    filename: '[name].bundle.js'
  },
  module: {
    loaders: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: "babel-loader",
        query:{
          presets: ['es2015']
        }
      }
    ]
  },
  plugins: [
    new webpack.optimize.UglifyJsPlugin({
      compress: { warnings: false }
    }),
    new BrowserSyncPlugin({
      host: 'localhost',
      port: 3000,
      server: { baseDir: [path.resolve(__dirname)] } })
  ],
  devServer: {
    contentBase: path.resolve(__dirname),
    compress: true,
    port: 9000
  }
};
```

####ビルド
```shell-session
$ npm run dev
```

上記コマンドを実行すると、自動的にブラウザ起動し、`http://localhost:9000`にアクセスされて、`index.html`が表示される。

####備考
`BrowserSync`や`webpack-dev-server`を利用する場合、コンテンツがPHPなど`CGI`で動作する場合、`nginx`でリバースプロキシの設定などを施す必要がある。その場合、`js`が実行されているサーバのURLも`CGI`側で`script`タグで指定するといったイメージ。

##yarnについて
`npm`互換のパッケージマネージャに`yarn`があり、`rails5.1`でサポートされていたり、`React`を利用する際のパッケージマネージャとしてもこちらを利用することが推奨されている。

###yarnのインストール
`Mac`では下記コマンドでインストール可能。

```shell-session
$ brew install yarn --ignore-dependencies
```

###npmとyarnでの簡単なコマンド比較

| npm      |    yarn  |
|:--------:|:-----------:|
| npm init  |   yarn init | 
| npm install  |   yarn install | 
| npm install --save-dev  | yarn add --dev | 
| npm install -g | yarn global add | 
| npm uninstall --save-dev  | yarn remove | 
| npm uninstall --save | yarn remove | 
| npm run | yarn run | 
| npm test | yarn test |