ES6を使ったフロントエンドの開発環境を作ってみた。概要としては、タスクランナーにgulpを使い、いくつかのソースファイルにわかれたES6のコードをbrowserifyで1つにまとめて、Babelで現行のブラウザで動くJavaScriptコードにトランスパイルする。他にuglifyで難読化やソースマップの出力、HTMLやCSSのビルドも行う。

前提としてnode.js, npmがインストールしてあるものとする。

俺用完成コード:https://github.com/tyfkda/FrontendTemplate (追記:本記事から多少内容が変更されてます) git clone, npm install して使える。

プロジェクトディレクトリの作成

プロジェクト用にディレクトリを作り、その中で環境を構築していく:

$ mkdir ~/gulptest  # プロジェクト用ディレクトリ作成
$ cd ~/gulptest  # 中に移って
$ npm init -y  # npm初期化(-yで質問への回答を省略してデフォルトを使用する)
$ npm install --save-dev gulp  # ローカルにgulpをインストール
$ sudo npm install -g gulp  # グローバルにもgulpをインストール
$ npm install --save-dev babel-preset-es2015  # babel変換用
  • npm install とすることで必要なパッケージが package.json に記録され、他の環境で npm install したときに自動的に反映されるようになる
    • --save-dev で、開発時にのみ必要なモジュールという指定になる
    • -gを指定しない場合にはローカル(package.jsonがあるディレクトリのnode_modules以下)にパッケージがダウンロードされ保存される
  • 実際のところ、npmで使用するpackage.jsonscriptにコマンドを記述してやればgulpをグローバルにインストールする必要はないが、以降の説明の都合上

gulpの設定ファイル作成

gulpを動かすのに設定ファイルgulpfile.jsが必要だが、それもES6で統一して書くためにgulpfile.babel.jsというファイル名にする。これはバッチファイルみたいなもので、ソースの変換などどういう処理をさせるかを記述する:

// gulpfile.babel.js
import gulp from 'gulp'

gulp.task('default', [], () => {
  // ダミー:あとで削除する
  console.log('hoge')
})

ES6を使うためには.babelrcという、Babelに関する設定ファイルに使用するプリセットを記述する:

{
  "presets": ["es2015"]
}

この状態でgulpを実行するとdefaultのタスクが実行される:

$ gulp
[14:24:02] Requiring external module babel-core/register
[14:24:03] Using gulpfile ~/gulptest/gulpfile.babel.js
[14:24:03] Starting 'default'...
hoge
[14:24:03] Finished 'default' after 153 μs

ES6をトランスパイルするタスクの作成

ES6を現行のブラウザ上で動くJavaScriptコードに変換するために、Babelを使う。実際にはbrowserifyのtransformでbabelifyを指定する。

まずシェルで、npmのパッケージをインストールする:

$ npm install --save-dev browserify babelify vinyl-source-stream vinyl-buffer \
    gulp-sourcemaps gulp-uglify browser-sync

gulpにES6を変換するタスクを追加:

// gulpfile.babel.js
const browserSync = require('browser-sync').create()
import babelify from 'babelify'
import browserify from 'browserify'
import buffer from 'vinyl-buffer'
import source from 'vinyl-source-stream'
import sourcemaps from 'gulp-sourcemaps'
import uglify from 'gulp-uglify'

const destDir = './public'  // 出力先

gulp.task('es6', () => {
  browserify({entries: 'src/main.js', debug: true})
    .transform(babelify)
    .bundle()
    .on('error', err => console.log('Error : ' + err.message))
    .pipe(source('main.js'))
    .pipe(buffer())
    .pipe(sourcemaps.init({loadMaps: true}))
    .pipe(uglify())
    .pipe(sourcemaps.write('./'))
    .pipe(gulp.dest(destDir))
    .pipe(browserSync.reload({stream: true}))
})

エントリポイントとなるファイルをsrc/main.jsと指定しているので、テストとして適当にES6のソースファイルを用意して

// src/main.js
import Util from './util'
Util.greeting()
// src/util/util.js
export default class Util {
  static greeting() {
    console.log('Hello, Util')
  }
}

ビルドしてみると

$ gulp es6

requireをまとめてES5に変換され圧縮されたコードが public/main.js にが出力される:

$ cat public/main.js
!function e(r,n,t){function u(i,f){if(!n[i]){if(!r[i]){var a="function"==typeof 
require&&require;if(!f&&a)return a(i,!0);if(o)return o(i,!0);var l=new Error("Ca
nnot find module '"+i+"'");throw l.code="MODULE_NOT_FOUND",l}var c=n[i]={exports
:{}};r[i][0].call(c.exports,function(e){var n=r[i][1][e];return u(n?n:e)},c,c.ex
ports,e,r,n,t)}return n[i].exports}for(var o="function"==typeof require&&require
,i=0;i<t.length;i++)u(t[i]);return u}({1:[function(e,r,n){"use strict";function 
t(e){return e&&e.__esModule?e:{"default":e}}var u=e("./util"),o=t(u);o["default"
].greeting()},{"./util":2}],2:[function(e,r,n){"use strict";function t(e,r){if(!
(e instanceof r))throw new TypeError("Cannot call a class as a function")}var u=
function(){function e(e,r){for(var n=0;n<r.length;n++){var t=r[n];t.enumerable=t
.enumerable||!1,t.configurable=!0,"value"in t&&(t.writable=!0),Object.defineProp
erty(e,t.key,t)}}return function(r,n,t){return n&&e(r.prototype,n),t&&e(r,t),r}}
();Object.defineProperty(n,"__esModule",{value:!0});var o=function(){function e(
){t(this,e)}return u(e,null,[{key:"greeting",value:function(){console.log("Hello
, Util")}}]),e}();n["default"]=o},{}]},{},[1]);
//# sourceMappingURL=main.js.map
  • gulp-sourcemapsでソースマップ出力
  • gulp-uglify でJSコードの圧縮
  • vinyl-source-streamvinyl-buffer はgulpのストリームを操作する?よくわからん
  • browserSync.reload() で、serverで開いたページのリロードをかける(後述)
  • browserifyよりもwebpackの方が新しくて良い?という話だが、ひとまずこれで…

HTMLの変換

HTMLも1つのファイルにすべて書くのではなく、パーツに分けてインクルードしたり圧縮したりしたいので、いろいろモジュールを使う。HTMLのテンプレートエンジンとして、ejsを使い、gulp-htmlminで圧縮する:

$ npm install --save-dev gulp-ejs gulp-htmlmin
// gulpfile.babel.js
import ejs from 'gulp-ejs'
import htmlmin from 'gulp-htmlmin'

gulp.task('html', () => {
  return gulp.src(['src/**/*.html',
                   '!src/**/_*.html'])
    .pipe(ejs())
    .pipe(htmlmin({
      collapseWhitespace: true,
      removeComments: true,
      minifyCSS: true,
      minifyJS: true,
      removeAttributeQuotes: true,
    }))
    .pipe(gulp.dest(destDir))
    .pipe(browserSync.reload({stream: true}))
})
  • アンダースコアで始まるファイル名はインクルードされるファイルという取り決めにして、ビルドから除外する
  • gulp-ejsがテンプレートエンジン
  • gulp-htmlminで圧縮(内部ではhtml-minifierを使ってる
<!-- src/index.html -->
<html>
  <head>
    <link rel="stylesheet" href="default.css">
  </head>
  <body>
    Hello, HTML
    <% include('_footer.html') %>
    <script src="main.js" type="text/javascript"></script>
  </body>
</html>
<!-- src/html/_footer.html -->
<div>
  All rights reserved.
</div>

gulpのタスクを実行すると、インクルードがまとめられ、圧縮されたHTMLがpublic/index.htmlに出力される:

$ gulp html

SASSの変換

CSSはSASSを変換して作るようにする:

// gulpfile.babel.js
import sass from 'gulp-sass'
import cssnano from 'gulp-cssnano'
import plumber from 'gulp-plumber'

const srcSassFiles = './src/**/*.scss'

gulp.task('sass', () => {
  return gulp.src('./src/**/*.scss')
    .pipe(plumber())
    .pipe(sass())
    .pipe(cssnano())
    .pipe(gulp.dest(assetsDir))
    .pipe(browserSync.reload({stream: true}))
})
/* default.scss */
$BG_COLOR: #ddd;

body {
  background-color: $BG_COLOR;
}
  • npm installは推測してください
  • gulp-plumber で、文法エラーが出てもgulpが終了しないようにする

サーバを起動するタスク

// gulpfile.babel.js
gulp.task('server', () => {
  browserSync.init({
    server: {
      baseDir: destDir,
    },
  })
})
  • browser-sync で指定のディレクトリ以下を返すサーバを立ち上げる
    • http://localhost:3000/ にサーバが立ち上がる
  • なぜわざわざサーバを立ち上げるのかは、次の項目を参照

ファイルの変更を監視して自動ビルド

シェルからgulpの呼び出しは起動に結構時間がかかる。gulp.watchでファイルを監視して、変更があったらタスクを実行するようにして、起動時間を気にしなくてよいようにする。

// gulpfile.babel.js
gulp.task('watch', [], () => {
  gulp.watch('./src/**/*.html', ['html'])  // 監視するファイル、処理するタスク                     
  gulp.watch('./src/**/*.scss', ['sass'])
  gulp.watch('./src/**/*.js', ['es6'])
})
  • ES6などのタスクが実行された時にはbrowserSync.reload()が指定されているので、自動的にリロードがかかる

最後に、一番最初に定義したdefaultタスクを修正する:

// gulpfile.babel.js
gulp.task('default', ['server', 'html', 'sass', 'es6', 'watch'])

以上で、シェルからgulpのデフォルトタスクを動かす

$ gulp

でサーバが起動し、ソースに変更があると自動ビルドがかかり、ブラウザで開いているページが自動的にリロードされる。


  • 2016/01/07: gulp-minify-htmlgulp-minify-cssがdeprecatedということなのでgulp-htmlmingulp-cssnanoに変更
  • 2015/12/27: gulpfileをES6で書く - Qiitaを参考にgulpfileもES6で書けるように、babelifyでv7.x系を利用する - to-Rを参考にbabelifyの7系を使うように、またbrowserifyやuglifyで変換したJavaScriptにソースマップを出力するよう修正