[初心者向け]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

> [email protected] server /Users/.../test
> npm run build; webpack-dev-server
> [email protected] build /Users/.../test
> webpack

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

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

ブラウザで開く

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

動作を確認

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

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

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の構文がそのまま出力されている事が分かります。

たまたま今見ているブラウザでは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の構文に置き換わっています。これで(相当古いものでない限り)ほとんどのブラウザで実行できそうです。

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

sourcemap

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

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

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

これを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'));

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

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.jsbrowser-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

> [email protected] 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.jsontest:nowatchスクリプトを定義すれば柔軟に対応ができます。

sourcemap

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

$ npm i -D karma-sourcemap-loader

karmaのpreprocessorssourcemapを指定します。

// 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