gulp タスクをファイル分割する
gulpfile.js の分割方法について。
gulp タスクが増えてくると gulpfile.js
が長くなってメンテナンスしづらい。また、タスク間で共通したい設定があるとして、それを gulpfile.js
内で定義しはじめると更にファイルは伸びてゆく。この問題を解決するため、以下のプロジェクトを参考にタスクを個別ファイルとして定義してみる。
ディレクトリ構成
プロジェクトのディレクトリ構成は以下のようになる。
/
├ package.json
├ gulpfile.js
├ gulp/
│ ├ config.js
│ ├ tasks/
│ │ └ gulp タスクごとのファイル
│ └ util/
│ └ gulp タスクで利用する便利モジュール
├ bower_components
│ └ アプリから利用する Bower モジュール
├ node_modules/
│ └ アプリや gulp から利用する Node.js モジュール
└ src/
└ 開発用リソース
gulpfile.js
をルート直下に配置して関連ファイルは gulp ディレクトリ内にまとめている。記事冒頭にあげた gulp-starter の構成を参考にしている。
require-dir と gulp file.js
別階層にファイル分割したタスクの読み込みには require-dir を利用。gulpfile.js
の処理は以下。
var dir = require( 'require-dir' );
dir( './gulp/tasks', { recurse: true } );
ここには一切、タスクを定義しない。すべて gulp/tasks
にまとめる。require-dir
のオプション recurse
に true
を指定すると再帰的な検索が実行されるため gulp/tasks
内のファイルが増えたら更にサブ ディレクトリを掘ってもよい。
タスクを gulpfile.js
と別の階層に配置することでワーキング ディレクトリが変化するため注意が必要。相対パスなら問題ないが、例えば connect & serve-static を組み合わせてルートに __dirname
を指定していると影響を受ける。
このケースでは path を利用してルート指定を加工する必要がある。path.join
の第一引数へ __dirname
、第二引数にタスクや設定ファイルから gulpfile.js
階層への相対パスを指定することでプロジェクトのルートで取得した場合と同様のパスを得られる。
var root = require( 'path' ).join( __dirname, '../' );
こんな感じでパスを加工しておく。
設定ファイル
タスクで利用するパスやパラメータなどは gulp/config.js
に分割する。タスク側にはなるべく設定を持たせず、ここから参照する方針。設定はモジュールとして実装。
module.exports = {
css: {
src: './src/stylus/*.styl',
dest: './src/css'
},
js: {
src: './src/js/app.js',
dest: './src/js',
bundle: 'bundle.js',
browserify: {
debug: true,
transform: [ 'reactify', 'debowerify' ]
}
},
build: {
depends: [ 'js', 'css' ]
}
};
モジュールは単体のオブジェクトとなり、そこにタスク単位の設定オブジェクトを定義する。
gulp.task
の第二引数に指定する依存タスクは depends
という Array
にしておく。これが定義さたなら対象タスクは別のタスクへ依存していることをあらわす。複数タスクをまとめるだけのものなら、この設定だけが定義されるだろう。
設定をファイル分割することでタスクの再利用性が高まる。例えば開発用リソースのディレクトリ構成が異なるプロジェクト間でタスクを使いまわすとき、設定ファイルだけ変更して運用、というような使い方を想定している。
設定を俯瞰できるのも便利だ。
gulp タスクはプロジェクト構造や設計思想をあらわす情報でもあり、設定はその抽象ともいえる。よってプロジェクト概要を説明するとき、ここを入り口にするとはかどりそう。上記例の js
タスクでいえば src
内の app.js
をエントリーポイントとして Browserify が依存解決し、その結果として bundle.js
が出力される、というようなイメージがわく。
デメリットはタスクと設定が疎結合となること。タスクのサイズが分割するまでもない大きさだと過剰な設計に感じられるかもしれない。その場合はタスクすら分割せず単一の gulpfle.js
で管理したほうがよいだろう。
タスクと設定をひとつのファイルにまとめた場合、例えばプロジェクトのディレクトリ構成を変更したなら影響するタスクのファイルを個別に確認しなければならない。私はこの問題に遭遇して設定ファイルを分割する意義を実感することになった。
gulp タスク
gulp タスク単位のファイルは以下のように定義。
var gulp = require( 'gulp' );
/**
* JavaScript をコンパイルして開発用イメージを scr フォルダに生成します。
*
* @return {Object} gulp ストリーム。
*/
gulp.task( 'js', function() {
var $ = require( 'gulp-load-plugins' )();
var config = require( '../config.js' ).js;
var errorUtil = require( '../util/error' );
var browserify = require( 'browserify' );
var source = require( 'vinyl-source-stream' );
var buffer = require( 'vinyl-buffer' );
return browserify( config.src, config.browserify )
.bundle()
.on( 'error', errorUtil )
.pipe( source( config.bundle ) )
.pipe( buffer() )
.pipe( $.sourcemaps.init( { loadMaps: true } ) )
.pipe( $.uglify() )
.pipe( $.sourcemaps.write( './' ) )
.pipe( gulp.dest( config.dest ) );
} );
config.js
を読み込み、その中から js
タスクの設定を取得 & 利用している。
タスクごとに設定を階層化しておくと、このように必要最小の設定だけ読み込める。ルートからの場合、js
タスク設定を参照するのにいちいち .js
を補完しなければならず面倒。なので読み込み時にタスクを限定してしまうほうが楽ちん。タスク間で共有したい設定があるとしても、それは config.js
内で事前指定しておくほうがよい。
gulp/util
からエラー ハンドリング用のユーティリティ関数を読み込んでいる。
これは gulp-starter
の実装を移植。gulp.watch
でファイル更新があったときに js
タスクを実行しているのだが、ここでコンパイル エラーが発生すると監視が中断されてしまい面倒だ。エラー通知だけして監視は継続されることが理想。
この問題を解決するプラグインとして gulp-plumber
があるけど、これは Browserify には効かない。そのため gulp-starter
では error
イベントのハンドリング関数を定義。処理継続と gulp-notify
によるエラー通知を実現している。
サンプル プロジェクト
昨日の記事のサンプルでタスクのファイル分割をおこなっている。実際の動きをみたい場合は、そちらを参照のこと。
所感
私は gulpfile.js
単体で設定とタスクが完結していることを好ましく感じていた。そのため当初はファイル分割に対して消極的だったが、しかし実際に運用してみたら結構よい感じで気に入った。
gulpfile.js
単体だと規模を小さく保ちたくなり、そのための意識が地味にストレスだった。しかし分割ありきで運用することで規模の管理から開放される。個々のタスクが長くなってもそれは部分の問題であり gulp 全体として意識する必要はない。
設定とタスクを疎結合にすることは関心の分離にもつながる。以前はタスクと設定を一枚岩と認識していたがファイルを分けたことで意識としてもうまく分類された感がある。
これから作成するプロジェクトで gulp を利用するなら、はじめからファイル分割すると思う。