gulp+ES6でフロントエンド開発で、ES6で書いたコードをBrowserifyでまとめてuglifyで圧縮する、という開発環境を作ってみたのだが、ソースコードが増えるに従ってビルドの時間が気になるようになってきた。たかだか数秒とはいえ、開発中は何度も修正・ビルド・リロードを繰り返すので少しでも時間が短いほうがよい。

そこで開発時にはES6から変換したJavaScriptのコードをまとめずに個別のままにしておいて、ブラウザ側でそれぞれ個別に読み込んだほうがサイクルとして速くなるんではないかと思って試してみた。

RequireJS

個別に分かれていて、依存関係のあるJavaScriptのスクリプトファイルをブラウザ上で自動的に読み込むには、RequireJSを使ってエントリポイントとなるファイルを指定してやればよい:

<!-- src/html/index.html -->
  <script src="require.js" data-main="main.js"></script>

ES6からRequireJSで使える形式(AMD)への変換

BabelでES6からpresets: 2015で普通に変換すると、CommonJS形式に?変換されてしまい、RequireJSでは使えない。RequireJSで読み込めるようにするにはAMD(Asynchronous Module Definition)という形式にする必要がある。

例:次のようなソースがあったとして:

// src/es6/main.js
import Util from './util'
Util.log('Hello, ES6')

presets:es2015 で変換すると(babel --presets es2015 src/es6/main.js):

'use strict';

var _util = require('./util');

var _util2 = _interopRequireDefault(_util);

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

_util2.default.log('Hello, ES6');

となってしまう。これをAMDという形式に変換するよう指定すると(babel --presets es2015 --plugins transform-es2015-modules-amd src/es6/main.js):

define(['./util'], function (_util) {
  'use strict';

  var _util2 = _interopRequireDefault(_util);

  function _interopRequireDefault(obj) {
    return obj && obj.__esModule ? obj : {
      default: obj
    };
  }

  _util2.default.log('Hello, ES6');
});

となって、RequireJSで使える形式に変換されるようになる。

gulpのタスクにする

以上のことをgulpのタスクにする。まずはパッケージのインストール

$ npm install --save-dev gulp-babel babel-plugin-transform-es2015-modules-amd

gulpfileにES6を変換する開発用のタスクを追加:

// gulpfile.babel.js
import babel from 'gulp-babel'
import browser from 'browser-sync'
import plumber from 'gulp-plumber'
import sourcemaps from 'gulp-sourcemaps'

const srcEs6Dir = './src/es6'
const srcEs6Files = `${srcEs6Dir}/**/*.js`
const destJsDevDir = './public/jsdev'

const buildEs6ForDebug = (glob) => {
  return gulp.src(glob, {base: srcEs6Dir})
    .pipe(plumber())
    .pipe(sourcemaps.init({loadMaps: true}))
    .pipe(babel({
      plugins: ['transform-es2015-modules-amd'],
    }))
    .pipe(sourcemaps.write('./'))
    .pipe(gulp.dest(destJsDevDir))
    .pipe(browser.reload({stream: true}))
}

gulp.task('es6', () => {
  buildEs6ForDebug(srcEs6Files)
})

gulp.task('watch-es6', [], () => {
  gulp.watch(srcEs6Files, (obj) => {
    if (obj.type === 'changed')
      buildEs6ForDebug(obj.path)
  })
})
  • watch-es6で監視するタスクでは変更があったファイルだけをビルドするようにする
  • Lintも同様にする

リリース時には以前の内容と同様に、Browserify+uglifyで1つにまとめてやることにする:

// gulpfile.babel.js
const releaseAssetsDir = 'release/assets'

gulp.task('release', () => {
  // es6
  browserify({entries: `${srcEs6Dir}/main.js`})
    .transform(babelify)
    .bundle()
    .on('error', err => console.log('Error : ' + err.message))
    .pipe(source('main.js'))
    .pipe(buffer())
    .pipe(uglify())
    .pipe(gulp.dest(releaseAssetsDir))
})

リリース時にはRequireJSを使わないようにする

リリース時にはせっかくJavaScriptのファイルを1つにまとめているので、RequireJSを使わないで直接読みこむようにしてやると無駄な読み込みが省けて精神衛生上よい。

HTMLから読み込むファイルを変えるには、HTMLを何らかの方法で変換してやる必要がある。ここではEJSというテンプレートエンジンを使って、分岐で切り替えることにする。開発時には変数buildTarget'debug'を指定して、

// gulpfile.babel.js
  gulp.src([srcHtmlFiles,
            '!' + srcHtmlDir + '/**/_*.html'])
    .pipe(ejs({buildTarget: 'debug'}))
    ...

HTML内で分岐させる:

<!-- src/html/index.html -->
  <% if (buildTarget === 'debug') { %>
    <!-- 開発時にはRequireJSを使って個別のJSファイルを読み込み -->
    <script data-main="jsdev/main.js" src="require.js"></script>
  <% } else { %>
    <!-- リリース時にはまとめられたファイルを直接読み込み -->
    <script src="assets/main.js" type="text/javascript"></script>
  <% } %>

リリース時にはbuildTargetreleaseを指定する。

結果

  • 開発時のビルド時間が短縮された

俺用テンプレートリポジトリ (追記:リポジトリはWebPackを使うよう変更されました)