アカベコマイリ

HEAR NOTHING SEE NOTHING SAY NOTHING

gulp ありの Web フロントエンド開発

August 06, 2015開発CSS, ES6, gulp, JavaScript, Node, Stylus

gulp なしの Web フロントエンド開発 の追補編として gulp を利用するケースもまとめておく。元記事と同じプロジェクトと機能を gulp で実装した。記事の構成は「gulp なしの〜」を踏襲。各項の説明は package.jsonscripts に代わって gulp タスクとしている。

設計方針

はじめに設計方針を決めておく。

  1. AltJS から JavaScript へのコンパイルに対応する
  2. AltCSS から CSS へのコンパイルに対応する
  3. ファイル監視による AltJS/AltCSS の自動コンパイルに対応する
  4. ユニット テストに対応する
  5. コードド キュメント生成に対応する
  6. Windows 環境を考慮する
  7. 単体 gulpfile.js のみで実装する

方針 1 〜 6 までは「gulp なしの〜」と一緒。方針 7 は環境に関する設定を package.jsongulpfile.js だけにするというもの。

以前 gulp タスクをファイル分割する という記事を書いたときは gulpfile.js をタスク毎にファイル化していたが、環境設定を変更する機会は滅多にないため分割は可搬性を損ねるだけという結論に至った。

ファイル監視と Windows 対応

ファイル監視と Windows 環境は gulp 自体の機能で対応済み。そのため各項では触れない。

検証環境

元記事と同じ。再掲する。

Mac は OS X Yosemite 10.10.4、Windows は 8.1 日本語版で検証。Node は共通で v0.12.7 を採用。

プロジェクト構成

実際に npm run を試せるよう、簡単なプロジェクトを用意する。README.md と LICENSE ファイルは除外している。

/
├ package.json
├ esdoc.json
├ gulpfile.js
├ src/
│ ├ index.html
│ ├ fonts/
│ ├ js/
│ └ stylus/
├ test/
├ dist/
├ esdoc/
└ node_modules/

各種ファイル、ディレクトリについては以下を参照のこと。「gulp なしの〜」からの差分は gulpfile.js のみ。

名前 内容
package.json プロジェクト設定ファイル。
esdoc.json ESDoc 設定ファイル。
gulpfile.js gulp タスク定義ファイル。
src/ 開発用ディレクトリ。
src/index.html エントリー ポイントになる HTML ファイル。
src/fonts/ アイコン フォント置き場。
src/js/ JavaScript 置き場。コンパイル結果は src/ 直下へ出力。
src/stylus Stylus 置き場。コンパイル結果は src/ 直下へ出力。
test/ ユニット テスト置き場。
dist/ リリース用イメージ出力ディレクトリ。動的生成される。
esdoc/ コード ドキュメント出力ディレクトリ。動的生成される。
node_modules/ npm ディレクトリ。動的生成される。

gulpfile.js の共通部分

gulpfile.js 内で共通となる部分を実装。以降の gulp タスク解説ではこれが定義済みであるものとする。

var gulp = require( 'gulp' );
var $    = require( 'gulp-load-plugins' )();

// Common config
var config = {
  src:        './src',
  dest:       './dist',
  isWatchify: false,
  isRelease:  false
};

処理内容について解説する。

1 行目は gulp 定義なので特筆すべきことはない。2 行目は gulp プラグインの呼び出しを簡略化するための定義。gulp-load-plugins は gulp-XXXX という名の npm を統合してくれる。

この定義では gulp-load-plugins を $ としているため、gulp-util なら $.util で参照できる。gulp- 以降の名前にハイフンがある場合は camelCase 化される。例えば gulp-minify-css は minifyCss で呼び出す。

5 〜 12 行目はタスク間で共有される設定。

内容
src 開発用ディレクトリのパス。
dest リリース用ディレクトリのパス。
isWatchify JavaScript のファイル監視と自動コンパイルの開始フラグ。
isRelease リリース用 JavaScript/CSS コンパイル実行フラグ。Uglify/Minify 実行、Source Maps 生成スキップ、コンパイル結果の dest 出力がおこなわれる。

JavaScript コンパイルとファイル監視

AltJS には ES6 を採用。JavaScript のコンパイルと依存解決は gulp-watchify が担当する。Transpiler は babelify を採用。これは ES6 だけでなく React JSX も解釈できる。

// JavaScript ( ES6, React JSX )
gulp.task( 'js', $.watchify( function( watchify ) {
  var buffer    = require( 'vinyl-buffer' );
  var formatter = require( 'pretty-hrtime' );
  var time      = process.hrtime();

  return gulp.src( [ config.src + '/js/App.js' ] )
    .pipe( $.plumber() )
    .pipe( watchify( {
      watch: config.isWatchify,
      basedir:   './',
      debug:     true,
      transform: [ 'babelify' ]
    } ) )
    .pipe( buffer() )
    .pipe( $.if( !( config.isRelease ), $.sourcemaps.init( { loadMaps: true } ) ) )
    .pipe( $.if( config.isRelease, $.uglify() ) )
    .pipe( $.rename( 'bundle.js' ) )
    .pipe( $.if( !( config.isRelease ), $.sourcemaps.write( './' ) ) )
    .pipe( $.if( config.isRelease, gulp.dest( config.dest ), gulp.dest( config.src ) ) )
    .on( 'end', function() {
      var taskTime = formatter( process.hrtime( time ) );
      $.util.log( 'Bundled', $.util.colors.green( 'bundle.js' ), 'in', $.util.colors.magenta( taskTime ) );
    } );
} ) );

gulp-watchify により Browserifywatchify を統合。これらを直に利用する場合、gulp ストリーム化や watch 時のエラー処理がけっこう面倒。しかし gulp-watchify ならすべて gulp として対応できる。詳しくは gulp-watchify を試す を参照のこと。

Source Maps ファイル生成、Uglify (Minify)、コンパイル結果の出力先を変更するため gulp-if を利用している。これは第一引数が true なら第二引数、false であれば第三引数を gulp ストリームに対して実行する。第三引数は省略可能で、その場合は第一引数が true のときだけ第二引数を実行する。この仕組を利用するとフラグ変更する gulp タスクを定義して、その実行により gulp ストリーム処理を分岐できる。10 行目で実施しているファイル監視の分岐もフラグ操作タスクで実現させた。

gulp-watchify には問題もある。このプラグインをインストールすると Browserify/watchify も同時にインストールされるのだけどバージョンが古い。2015/8/6 時点の最新 Browserify は 11.0.1。gulp-watchify では 9.0.8 になってしまう。package.json を修正して最新版をインストールすると gulp-watchify がバージョン互換のエラーになる。

よって最新版を利用したいなら greypants/gulp-starterwatchify を試す のように自前で処理する。いつか更新されるだろうと期待して gulp-watchify を採用しているけど Browserify はメジャー バージョン部分で数世代も差が開いたため、そろそろ自前処理に戻そうかと迷っている。

CSS コンパイルとファイル監視

AltCSS には Stylus を採用。コンパイルは gulp-stylus が担当。

// CSS ( Stylus )
gulp.task( 'css', function() {
  return gulp.src( [ config.src + '/stylus/App.styl' ] )
    .pipe( $.plumber() )
    .pipe( $.if( !( config.isRelease ), $.sourcemaps.init() ) )
    .pipe( $.stylus() )
    .pipe( $.rename( 'bundle.css' ) )
    .pipe( $.if( config.isRelease, $.minifyCss() ) )
    .pipe( $.if( !( config.isRelease ), $.sourcemaps.write( '.' ) ) )
    .pipe( $.if( config.isRelease, gulp.dest( config.dest ), gulp.dest( config.src ) ) );
} );

gulp-stylus は Minify 機能を持つのだが stylus と異なり Source Maps 生成できない。README のサンプルでも gulp-sourcemaps を利用している。JavaScript タスクと同じような処理にしたいので、gulp-stylus は Stylus の CSS コンパイルに徹するように設計した。そのため gulp-if による分岐も JavaScript コンパイルと同様の仕組みとなっている。

Web サーバー起動

Web ブラウザの制限 ( Chrome などでローカル ファイルからの Ajax や Web Storage が抑止される問題 ) を回避して Web サーバー経由の動作確認をおこなうための定義。これは「gulp なしの〜」と同様に browser-sync を採用。

// Local web server
gulp.task( 'server', function() {
  var browser = require( 'browser-sync' ).create();
  browser.init( {
    server: {
      baseDir: config.src
    }
  } );
} );

このタスクは JavaScript/CSS のファイル監視と一緒に使用されることを想定。タスク間の同期は不要なので gulp.task のコールバックは指定せず gulp ストリームも返していない。gulp から起動された場合でも CLI と同様に localhost/IP アドレスの URL をコンソール出力してくれる。ただし gulp プラグインではないためインデントがずれてしまう。

[20:18:11] Starting 'server'...
[20:18:11] Finished 'server' after 513 ms
[20:18:11] Bundling App.js (watch mode)
[BS] Access URLs:
 ------------------------------------
       Local: http://localhost:3000
    External: http://XXX.XXX.XXX.XXX:3000
 ------------------------------------
          UI: http://localhost:3001
 UI External: http://XXX.XXX.XXX.XXX:3001
 ------------------------------------
[BS] Serving files from: ./src
[20:18:12] Finished 'css' after 1.28 s

指定されたディレクトリをルートとする Web サイトのホストと Web ブラウザの自動起動も CLI と同様に機能する。

開発用タスクの統合

JavaScript と CSS のファイル監視と自動コンパイルを走らせつつ Web サーバー経由で動作確認、という感じで開発したいのでタスクを統合。

// Enable wachify
gulp.task( 'watch:js', function( done ) {
  config.isWatchify = true;
  done();
} );

// Watch files
gulp.task( 'watch', [ 'watch:js', 'js', 'css', 'server' ], function () {
  gulp.watch( [ config.src + '/stylus/**/*.styl' ], [ 'css' ] );
} );

// Default task
gulp.task( 'default', [ 'watch' ] );

watch:js タスクは JavaScript コンパイルをファイル監視モードで実行するためのフラグを立てる。これを watch タスクの依存設定で js より前に指定することでフラグを立ててから js タスクを実行。結果として watchify を起動することになる。

タスク依存からの実行は非同期となる。そのため確実に順番保証したいなら js タスクの内容を関数としてくくりだして watch:js へ依存するものとしないもので呼び分けるとよいだろう。以下は参考例。

// JavaScript ( ES6, React JSX )
function compileJS() {
  // gulp-watchify を利用した処理
  // 必ず gulp ストリームを return すること!!
}

// Normal build
gulp.task( 'js', function() {
  return compileJS();
} );

// Watch files
gulp.task( 'watch:js', function() {
  config.isWatchify = true;
  return compileJS();
} );

フラグ変更ぐらいなら神経質になる必要もないが、きっちり順番保証したい!という考えも理解できるので代替を提案しておいた。

ユニット テストとコード ドキュメント

これらは gulp を利用しないので割愛。詳しくは gulp なしの Web フロントエンド開発 を参照のこと。

リリース用イメージ生成

Web フロントエンド部分をデプロイするためのイメージ生成。複数のタスクを組み合わせて実装する。

// Clean release image
gulp.task( 'release:clean', function( done ) {
  var del = require( 'del' );
  del( config.dest, done );
} );

// Create release directory & copy files
gulp.task( 'release:copy', function() {
  var src = [
    config.src + '/*.html',
    config.src + '/fonts/**'
  ];

  return gulp.src( src, { base: config.src } )
    .pipe( gulp.dest( config.dest ) );
} );

// Release config
gulp.task( 'release:config', function( done ) {
  config.isRelease = true;
  done();
} );

// Build release image
gulp.task( 'release', function( done ) {
  var runSequence = require( 'run-sequence' );
  return runSequence(
    'release:clean',
    'release:copy',
    'release:config',
    [ 'js', 'css' ],
    done
  );
} );

release:clean はリリース用イメージ出力先ディレクトリを del で削除。ディレクトリが存在しない場合でもエラーにはならないので安心して実行できる。

release:copy はリリース用ファイルをコピー。gulp 標準の src/dest を利用するだけで済む。glob 対応しているので条件指定も柔軟に対応可能。gulp を採用するメリットとして、この機能によるファイル操作の強力さが挙げられる。

release:config でリリース用フラグを立てる。以降に jscss タスクを実行すると Source Maps 無効、Uglify/Minify 有効状態でコンパイルされたファイルがリリース用イメージ出力先に生成される。

Source Maps を単体ファイルとして出力しているならそれを削除するだけでもよさそうだが、ファイルがない場合でも JavaScript/CSS 内に参照は残ってしまう。この状態でリリースすると Web ブラウザーが JavaScript/CSS を読み込んだときに Source Maps ファイル用の GET リクエストを発行してしまい無駄な通信となる。そのため避けるべき。この記事を書くまでこの問題に気づかず参照が残ったままリリースしているものがある (例えば本ブログの WordPress テーマ)。後で直したい。

release でリリース系タスクを統合。依存タスクは同期・非同期で実行するものが入り混じるため gulp の依存設定で対応するのは面倒だから run-sequence でまとめて管理している。

release:cleanrelease:config までを同期、jscss は非同期に実行。js で利用している gulp-watchify は出力先ディレクトリが存在しないとファイル生成に失敗するため release:copy を先に処理することでディレクトリの存在を保証。gulp の依存タスクも Array 要素へ直に定義したタスク間を直列、要素が Array ならそのタスク間を並列というようにしてくれると嬉しいのだが。

gulp タスクを scripts で抽象化

npm で依存もタスクも一元化する を参考にして gulp タスクを package.json の scripts で抽象化。この方法には以下のメリットがある。

  • グローバルの gulp インストールが不要
  • コマンド利用側は npm run だけ覚えればよく、gulp の知識は不要
  • gulp を利用しないものと併用しても違和感がない
  • gulp のメリットだけ享受できる

対応は簡単で package.jsonscripts に gulp タスクを対応付けるだけ。

{
  "scripts": {
    "test": "mocha --compilers js:espower-babel/guess test/**/*.test.js",
    "esdoc": "esdoc -c esdoc.json",
    "start": "gulp watch",
    "server": "gulp server",
    "build:css": "gulp css",
    "build:js": "gulp js",
    "release": "gulp release"
  }
}

testesdoc 以外のコマンドはすべて gulp タスク呼び出し。開発中は npm start、リリース時は npm run release を実行する。その他のコマンドは好みに応じて定義する。あまりに多いと管理しにくいため gulp タスクの中でも単体実行したくなりそうなものに限定するとよい。

まとめ

賛否あるものの私は gulp を素晴らしいツールと認識している。前回の記事を gulp 否定として読まれた方もいたようだが、その意図はない。単純に異なる方法を検討してみたくなっただけ。それを踏まえ、同一構成のプロジェクト運用が gulp の有無でどのように変わるのかを示せたら面白いと思って今回の記事を書いてみた。

結果、これまでリリース用イメージに Source Maps 参照を含めていたことの問題に気づくなど gulp 利用時の運用も見直せてよかった。

最後に今回作成したサンプル プロジェクトを公開しておく。これは「gulp なしの〜」で作成したものの gulp 版である。