[初心者向け]ES2015で書いたJavaScriptをES5にトランスパイルする


ES2015(ES6)の対応が進みつつある今日この頃。ただブラウザや環境によって実装状況にバラつきがあって、使用できる機能・そうでない機能はcompatibility tableを確認しないと分からない状況です。

環境を気にせずに完全なES2015でコーディングしつつES5にトランスパイルして配布すれば、スムーズに作業が進むのではないかと思って環境構築を試してみました。

> **参考** ECMAScript6 compatibility table
> http://kangax.github.io/compat-table/es6/

# こういう環境を作りたい

- 依存はnpmでインストール
- JavaScript（ES2015）でコーディング
- webpackでビルドする
- ビルドプロセスでbabelが呼ばれてES5にトランスパイルされる
- テストはkarma & jasmine
- ソースコードを監視してローカルでブラウザをリロードして欲しい
- ソースコードを監視してテストを実行して欲しい

> **npm** https://www.npmjs.com/
> **webpack**  https://webpack.github.io/
> **babel** https://babeljs.io/
> **karma** https://karma-runner.github.io/1.0/index.html
> **jasmine** http://jasmine.github.io/

# 前提

node & npmがインストールされているものとします。
使用したバージョンは`node v6.0.0` `npm v3.8.6`です。
また動作確認のブラウザは`Chrome v53.0`を使用しています。

# 1) package.jsonを作る

```
$ mkdir my_project; cd $_;
$ npm init
```

# 2) webpackを動かす

### インストール

ローカルサーバーも一緒にインストールしておきます。

```
$ npm i -D webpack webpack-dev-server
```

> **補足** -Dは`devDependencies`に保存するオプションです。

### 必要なファイルの準備

エントリポイント（最初に実行されるファイル）を作ります。

```
$ mkdir src
$ echo 'console.log("test");' > src/index.js
```

設定ファイルを作ります。

```
// webpack.conf.js
module.exports = {
  entry: './src/index.js',
  output: {
    path: './dest',
    filename: 'bundles.js'
  }
};
```

> **補足** この設定では`index.js`をエントリポイントとして読み込み、`require`などで参照された別モジュールを全てまとめて`dest/bundles.js`として出力します。

ブラウザで動作確認するためのhtmlファイルを作ります。

```
// index.html
<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="content-type" content="text/html; charset=utf-8">
    <title>test</title>
    <script src="dest/bundles.js"></script>
    <!--webpackで出力したファイルを参照-->
  </head>
  <body>
    <h3>test</h3>
  </body>
</html>
```

ローカルサーバーを起動するためのスクリプトを追加します。

```
// package.json
"scripts": {
  "build": "webpack",
  "server": "npm run build; webpack-dev-server"
  ...
```

### スクリプトの実行

現在のフォルダ構成はこのようになっています。

```
├── index.html
├── package.json
├── node_modules
├── src
│   └── index.js
└── webpack.config.js
```

スクリプトを実行してみましょう。

```
$ npm run server

> test@1.0.0 server /Users/.../test
> npm run build; webpack-dev-server
> test@1.0.0 build /Users/.../test
> webpack

Hash: ...
Version: ...
Time: ...
 http://localhost:8080/webpack-dev-server/
```

スクリプトを実行するとビルド結果が表示され、URLが記載された部分があります。ここをコピーしましょう。

### ブラウザで開く

ビルド結果のURLは自分でブラウザに入力して開きます。

![スクリーンショット 2016-10-23 18.56.00.png](https://qiita-image-store.s3.amazonaws.com/0/102284/119450d7-2879-e59d-84cd-494432c9c89a.png "スクリーンショット 2016-10-23 18.56.00.png")

### 動作を確認

ブラウザのコンソールウインドウを開くと`test`が出力されます。これは`index.js`に書いた`console.log('test');`が実行された結果です。

Sourcesタブを開くとビルドした後のファイルを見る事ができます。

![スクリーンショット 2016-10-23 18.59.42.png](https://qiita-image-store.s3.amazonaws.com/0/102284/5abb0166-8fe3-3f9d-c36d-cb086dc18948.png "スクリーンショット 2016-10-23 18.59.42.png")

`index.js`には1行しか書いていませんが、webpackは依存を含めた全てのJavaScriptを`bundles.js`として出力するため、いろいろな処理が追加されます。


ブラウザでの動作を確認したら`npm run`コマンドはCtrl+Cで終了しましょう。

# 3) ES2015の構文を書いてみる

サンプルで書いた`console.log()`ではトランスパイルしても何も変わらないので、ES2015の構文を使ったサンプルに書き換えてみます。

```
// src/index.js
let greet = require('./greet');
console.log(greet.greeting('hoge'));
```

```
// src/greet.js
class Greet {
  greeting(name) {
    return `Hello ${name}`;
  }
}

module.exports = new Greet();
```

再度ローカルサーバーを実行します。

```
$ npm run server
```

今回はコンソールウインドウに`Hello hoge`が出力されます。
Sourcesタブでビルドされたファイルを見ると、ES2015の構文がそのまま出力されている事が分かります。

![スクリーンショット 2016-10-23 19.11.17.png](https://qiita-image-store.s3.amazonaws.com/0/102284/1e670fdb-b7fb-855c-9a20-4d88d4dd316f.png "スクリーンショット 2016-10-23 19.11.17.png")

たまたま今見ているブラウザではES2015のclass構文に対応しているようですが、対応していないブラウザで同じページを開くとエラーになりますよね？

# 4) babel

ES2015からES5にトランスパイルするためにbabelをインストールします。

### インストール

```
$ npm i -D babel-core babel-loader babel-preset-es2015
$ npm i babel-polyfill
```

> **補足** polyfillはES2015をサポートしていない環境で代替となるものに置き換えてくれる機能です。ビルドしたbundles.jsとは別に必要になるか判断が付かなかったのでdependenciesに入れました。

### 設定

webpackのモジュールローダーとしてbabelを指定します。

```
// webpack.config.js
module.exports = {
  ...
  module: {
    loaders: [{
      test: /\.js$/,
      exclude: /node_modules/,
      loader: 'babel-loader'
    }]
  }
```

`.babelrc`を書いておくとbabelの実行時に読み込んでくれます。
どのフォーマットのファイルをトランスパイルするかを指定します。

```
$ echo '{ "presets": [ "es2015" ] }' > .babelrc
```

### スクリプトの実行

現在のフォルダ構成はこのようになっています。

```
├── .babelrc
├── dest
│   └── bundles.js
├── index.html
├── package.json
├── node_modules
├── src
│   └── index.js
└── webpack.config.js
```

スクリプトを実行してみましょう。

```
$ npm run server
```

### 動作確認

再びブラウザのSourcesを見ると、今度はES5の構文に置き換わっています。これで（相当古いものでない限り）ほとんどのブラウザで実行できそうです。

![スクリーンショット 2016-10-23 19.25.05.png](https://qiita-image-store.s3.amazonaws.com/0/102284/d28b5468-cfa6-257c-100a-200826f27b98.png "スクリーンショット 2016-10-23 19.25.05.png")


# 5) ローカルサーバーをもうちょっと便利に

### sourcemap

```
// package.json
"scripts": {
  "build": "webpack -d"
```

webpackのオプションに`-d(debug)`を指定すると、ビルド時にソースマップのファイルを出力してくれます。するとコンソールウインドウのSourcesタブに`webpack://`が現れ、ビルド前の元のソースを見る事ができます。（ブレークポイントも設定できます、便利。）

![スクリーンショット 2016-10-23 19.32.51.png](https://qiita-image-store.s3.amazonaws.com/0/102284/8f74951d-8044-7723-de65-4edb8e74663a.png "スクリーンショット 2016-10-23 19.32.51.png")

### ソースコードを監視してローカルでブラウザをリロードして欲しい

これを`webpack-dev-server`でやるのが難しかったので`browser-sync-webpack-plugin`に変更します。

```
$ npm i -D browser-sync-webpack-plugin browser-sync
```

webpackを`-w(watch)`モードで呼び出す必要があるので`server`のスクリプトを以下のように書き換えます。

```
// package.json
"scripts": {
  "server": "npm run build -- -w"
```

webpackのプラグインとして動作するように設定を追加します。

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

module.exports = {
  ...
  plugins: [
    new BrowserSyncPlugin({
      host: 'localhost',
      port: 3000,
      server: {
        baseDir: ['./']
      }
    })
  ]
```

もう一度スクリプトを実行してみます。
今度はブラウザのURLを自分で入力する事なく、自動的にページが開くはずです。

```
$ npm run server
```

この状態でソースを何か変更してみましょう。

```
// src/index.js
console.log(greet.greeting('fuga'));
```

ビルドが再実行され、ブラウザも自動的にリロードされます。

![スクリーンショット 2016-10-23 19.53.08.png](https://qiita-image-store.s3.amazonaws.com/0/102284/b989a7e0-5cae-ca63-f8fd-d067b19cab80.png "スクリーンショット 2016-10-23 19.53.08.png")

`browser-sync-webpack-plugin`にはブラウザのリロード以外にも、ChromeとFireFoxを開いて片方のsubmitボタンを押したらもう一方のブラウザも同期した動作をさせる、などの便利な機能があるようです。

> **browser-sync-webpack-plugin**  
> https://www.npmjs.com/package/browser-sync-webpack-plugin

# 6) テスト

### インストール

```
$ npm i -D jasmine-core karma karma-chrome-launcher karma-jasmine karma-webpack karma-mocha-reporter karma-phantomjs-launcher
```

### スクリプトの追加

`npm run test`でテストが実行されるようにスクリプトを追加します。

```
// package.json
"scripts": {
  ...
  "test", "karma start"
}
```

### webpackの設定を分割

テスト実行時も開発時と同じように、webpackからbabelを呼び出してES2015からES5にトランスパイルします。ただ現在の`webpack.conf.js`は`browser-sync-webpack-plugin`の設定を書いているため、これだとテストの度にブラウザが起動してしまいます。

そこでwebpackの設定をテスト用・開発用それぞれに分ける事にします。

- webpack.config.js --- 共通の設定
- webpack.test.config.js --- テスト用
- webpack.dev.config.js --- 開発用

共通の設定を読み込んでからそれぞれの環境に合わせた設定をマージするため`webpack-merge`を使用します。

```
$ npm i -D webpack-merge
```

#### webpack.conf.js（共通の設定）

```
module.exports = {
  entry: './src/index.js',
  module: {
    loaders: [{
      test: /\.js$/,
      exclude: /node_modules/,
      loader: 'babel-loader'
    }]
  }
};
```

#### webpack.test.config.js（テスト用）

```
const webpackMerge = require('webpack-merge');
const config = require('./webpack.config.js');

module.exports = webpackMerge(config, {
});
```

#### webpack.dev.config.js（開発用）

```
var BrowserSyncPlugin = require('browser-sync-webpack-plugin');
const webpackMerge = require('webpack-merge');
const config = require('./webpack.config.js');

module.exports = webpackMerge(config, {
  output: {
    path: './dest',
    filename: 'bundles.js'
  },
  plugins: [
    new BrowserSyncPlugin({
      host: 'localhost',
      port: 3000,
      server: {
        baseDir: ['./']
      }
    })
  ]
});

```

### karmaの設定

webpackでビルドしてからテストを実行するように設定します。

```
// karma.conf.js
var webpackConfig = require('./webpack.test.config.js');

module.exports = function(config) {
  config.set({
    basePath: './src',
    frameworks: ['jasmine'],
    files: [ { pattern: './spec.js', watched: false } ],
    exclude: [],
    preprocessors: {
      './spec.js': ['webpack']
    },
    webpack: webpackConfig,
    webpackServer: { noInfo: true },
    reporters: ['mocha'],
    mochaReporter: {
      output: 'minimal'
    },
    port: 9876,
    colors: true,
    logLevel: config.LOG_INFO,
    autoWatch: false,
    browsers: ['PhantomJS'],
    singleRun: true,
    concurrency: Infinity
  })
}
```

`files`で指定した`spec.js`はまだ存在しません。このファイルはテスト実行時に`*.spec.js`というネーミングを持つファイルを全て読み込むために作成します。

```
// src/spec.js
var testsContext = require.context("./", true, /\.spec\.js$/);
testsContext.keys().forEach(testsContext);
```

> **補足** `context()`の2番目の引数はサブディレクトリ内も再帰的に検索する事を意味します。

### テストを書く

簡単なテストを書いてみましょう。`src/greet.js`に挨拶を返す`Greet`クラスが存在するので、挨拶の結果を検証します。

```
// src/greet.spec.js
describe('greet', () => {
  var greet = require('./greet');

  it('test', () => {
    expect(greet.greeting('abc')).toEqual('Hello abc');
  });
});
```

現在のフォルダ構成はこのようになっています。

```
├── .babelrc
├── dest
│   └── bundles.js
├── index.html
├── karma.conf.js
├── node_modules
├── package.json
├── src
│   ├── greet.js
│   ├── greet.spec.js
│   ├── index.js
│   └── spec.js
├── webpack.config.js
├── webpack.dev.config.js
└── webpack.test.config.js

```

### テストの実行

テスト用のスクリプトを実行してみましょう。

```
$ npm run test

> test@1.0.0 test /Users/.../test
> karma start

START:
23 10 2016 22:28:52.592:INFO [karma]: Karma v1.3.0 server started at http://localhost:9876/
...
Finished in 0.006 secs / 0.002 secs

SUMMARY:
✔ 1 test completed
```

1つのテストが成功しkarmaが終了します。

### ソースコードを監視してテストを実行して欲しい

karmaのオプションで指定します。

```
// package.json
"scripts": {
  "test": "karma start --auto-watch --no-single-run"
```

> **補足** karma.confにもオプションと同じ設定がありますが、必要でない時にもファイル監視が実行されてしまう可能性があります。監視したくない時、例えば`package.json`で`test:nowatch`スクリプトを定義すれば柔軟に対応ができます。

### sourcemap

ローカルサーバーでも出てきたsourcemapですが、karmaでも使用する事ができます。

```
$ npm i -D karma-sourcemap-loader
```

karmaの`preprocessors`に`sourcemap`を指定します。

```
// karma.conf.js
module.exports = function (config) {
    ...
    preprocessors: {
      './spec.js': ['webpack', 'sourcemap'] // sourcemapを追加
    },
```

webpackのビルドに`sourcemap`の情報を含めます。

```
// webpack.test.config.js
module.exports = webpackMerge(config, {
  devtool: 'inline-source-map' // この行を追加
});
```

テストが落ちるようにして結果を確認しましょう。

```
// src/greet.spec.ts
  it('test', () => {
    expect(greet.greeting('unknown')).toEqual('Hello abc'); // 'unknown'に変更
  });
```

`greet.spec.js`の行数が出力されるようになりました。

```
$ npm run test

FAILED TESTS:
  greet
    ✖ test
      PhantomJS 2.1.1 (Mac OS X 0.0.0)
    Expected 'Hello unknown' to equal 'Hello abc'.
    webpack:///src/greet.spec.js:5:46 <- spec.js:110:47 <--- これ
    loaded@http://localhost:9876/context.js:151:17
```

# おわり

webpack自体あまり使った事がないので、試行錯誤で書いてみた設定です。プロダクション用やCSSを含めたビルドについては全く書いていないので、不完全な状態だと思います。

ここ数年JavaScriptをとりまく環境は進歩が激しいですね。私と同じように時代について行けず何となくwebpackとかbabelとか億劫になっている方がチュートリアル的に一歩ずつ進められて、終わったあとに何となく「分かったような、気がする...」の気分を感じていただけたらと思います。

今回使用したソース：
https://github.com/ringtail003/babel-with-webpack