2015年2月17日

webpack babel-loaderを使ってES6でWebを書く

2015-11-21 babel presetsに合わせて更新
そろそろ ES6 で書きたいので環境メモを残しておきます。

はじめに

このエントリでは Node.js を使用してフロントエンドで ECMAScript 6th を書くことを目的にしています。
その際に babel を使います。
babel は grunt, gulp, browserify など多くのプラグインが提供されていますが、ここでは grunt を使っていきます。

以前は RequireJS でモジュール書いて r.js (1ファイルにまとめるやつ) でコンパイル(minify) てやってたのですが、
今回 webpackbabel-loader を使った ES6 環境で進めようと思います。

インストール

Node.js はすでにインストール済み前提とします。
まず適当なところにディレクトリを作って移動して npm init で package.json を作成します。
$ mkdir my_project
$ cd my_project
$ npm init
# いろいろ聞かれるけどとりあえずエンター連打

$ ls
package.json # できてること確認

ファイル構成

ファイル構成は以下のような感じです。
$ tree
.
├── package.json
├── Gruntfile.js        # これから作るgruntタスク
└── public              # 公開ディレクトリ
        ├── index.html  # これから作るhtml
        ├── js          # jsソースを置くディレクトリ
        └── assets      # コンパイル済みのjsなどを置くディレクトリ
まだ package.json しか作ってないので、public ディレクトリと、 その中に js, assets を作ります。

今回使うタスクランナーの grunt をインストールします (-g をつけてグローバルで)。
$ npm install -g grunt-cli

続いて grunt-webpack や grunt-contrib-uglify (圧縮),
grunt-contrib-watch (ファイルの更新を監視) などをインストールします。

本来なら webpack と grunt-webpack だけで済むっぽいですが、
非圧縮ファイルを確認したいときがあるので webpack で1ファイルにまとめてから uglify で圧縮します。
あえて非圧縮ファイルを残します。
$ npm install --save-dev grunt grunt-contrib-uglify grunt-contrib-watch
--save-dev オプションを指定して package.json の devDependencies に追加します。

それから webpack, babel-loaderをインストールします(1回でまとめてインストールしても同じです)。
$ npm install --save-dev grunt-webpack webpack babel-loader babel-preset-es2015

Gruntfile.js

プロジェクトのルート (package.json と同じ階層)に Gruntfile.js を作成します。
// Gruntfile.js
var webpack = require('webpack');

module.exports = function(grunt) {
  var pkg = grunt.file.readJSON('package.json');

  grunt.initConfig({
    // webpackの設定
    webpack: {
      build: {
        progress: true,
        entry: {
          // メインjsファイル (ここでは app.js とする)
          app: './public/js/app.js'
        },
        output: {
          // 出力ファイル (ここでは assets/js に出力)
          path: './public/assets/js',
          filename: 'bundle.js'
        },
        module: {
          loaders: [{
            // .jsに対してbabelを指定してES6で書けるようにする
            test: /\.js$/,
            exclude: /node_modules/,
            loader: 'babel',
            query: {
              presets: ['es2015']
            }
          }]
        },
        resolve: {
          // 読み込む際に拡張子を省略できるようにする
          extensions: ['', '.js']
        }
      }
    },
    // JS圧縮(minify)/最適化
    uglify: {
      options: {
        // Source Mapをつける
        sourceMap: true,
        sourceMapName: './public/assets/js/bundle.map'
      },
      build: {
        files: {
          // 出力ファイル: 元ファイル
          './public/assets/js/bundle.min.js': './public/assets/js/bundle.js'
        }
      }
    },
    // ファイルの変更/更新を監視
    watch: {
      js: {
        files: [
          // js/ 以下を再帰的に監視する
          './public/js/**/*.js',
        ],
        tasks: ['webpack', 'uglify']
      }
    }
  });

  grunt.loadNpmTasks('grunt-webpack');
  grunt.loadNpmTasks('grunt-contrib-uglify');
  grunt.loadNpmTasks('grunt-contrib-watch');

  // デフォルトタスクの登録
  grunt.registerTask('default', ['webpack', 'uglify']);
};

上のコードで Gruntfile.js に書き込んで保存します。

ES6でコードを書く

せっかくなので es6 modules を使って書いてみます。

public/jsapp.js を作成します。
// app.js
import * as math from './lib/math';

// Template Strings
var msg = `2π = ${math.sum(math.pi, math.pi)}`;
alert(msg);

続いて public/js/lib (lib ディレクトリを作成) に math.js を作成します。
// lib/math.js
export function sum(x, y) {
  return x + y;
}
export var pi = 3.141593;

publicindex.html を作成します。
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>webpack es6 babel-loader example</title>
<script src="assets/js/bundle.min.js"></script>
</head>
<body>
  <h1>webpack es6 babel-loader example</h1>
</body>
</html>

Build

各ファイルを作成したら grunt を実行してみます。
単に grunt と実行すると、default のタスクが実行されます。
$ grunt
...
Running "webpack:build" (webpack) task
...
Running "uglify:build" (uglify) task
...
>> 1 file created.

Done, without errors.
みたいに表示されれば成功です。

結果

index.html をブラウザで開くと、2π = 6.283186 とalertで表示されます。

出力

public/assets に以下の3つのファイルが作られています。
  • bundle.js : 非圧縮のファイル
  • bundle.map : Source Mapファイル
  • bundle.min.js : 圧縮済みのファイル

html 内では圧縮されたファイル assets/js/bundle.min.js を読み込んでいますが、
非圧縮の bundle.js を見ると変換されたコードも確認できます。

ファイルの変更を監視

毎回 grunt と入力するのも手間になるので、以降はファイルの変更を監視するようにします。
$ grunt watch
を実行すると、ファイルが更新される度に自動でタスクを実行してくれます。
Ctrl+C で中止できます。

babel-runtimeを含める

Promise などを使う場合、babel の runtime (polyfill) を含めるように指定します。
runtime を使う場合は babel-runtime のインストールが必要です。
$ npm install babel-runtime --save

Gruntfile.js の webpack loader を修正します。
// Gruntfile.js
  ...

    // webpackの設定
    webpack: {
      build: {
        ...
        module: {
          loaders: [{
            // .jsに対してbabel-loaderを指定してES6で書けるようにする
            test: /\.js$/,
            exclude: /node_modules/,
            // ランタイムを含めるよう指定する
            loader: 'babel-loader?experimental&optional=selfContained'
          }]
        },
        ...
      }
    },
  ...

runtime を含めると、それなりにサイズがあるので個別でライブラリ等を使うのもいいかもしれません。

Demo

今回作成した動作デモ:

Source

今回作成したソース:

おわりに

フロントエンド周りは移り変わりが激しいですが、安定したものを使っていきたいところです。
RequireJS 使った r.js の最適化は魅力的なのですが、 あらかじめひとまとめにする前提ってなると webpack が便利に感じます。
最近は CoffeeScript 書くことが多くて Coffee も楽しいなぁと思いつつ、 やっぱり ES6 で書けると嬉しいです (まだまだ勉強中…)。