こんにちはsekitakaです。
使ってみようと以前から思っていたGulpを使ってみました。最近では脱Gulp/Gruntの動きもあるようですが、さくっと導入して便利さを感じてみるにはGulpが早そうに思えたので、Gulpを使うことにしました。
使い方自体は簡単ですが実際に開発とデプロイのフローにのせるためには、試行錯誤が必要でした。
この記事は初めてGulpを使う方向けに、実運用するまでに必要になりそうなプラグインやハマった点などを共有したいと思います。
やったこと
主な作業内容は以下の通りです。細々他にもありますが、本文中で説明していきます。
- javascriptの結合&最小化
- cssの結合&最小化
- デバッグ用とリリース用で処理を分ける(ソースマップの作成など)
- ローカルでの開発時はファイルの監視を行い変更を検知してビルド
プロジェクト構成
プロジェクトの構成は以下を例にとって説明していきます。
srcにビルド前のプログラムやリソースがあり、distディレクトリにgulpが生成した公開用のプログラムやリソースを配置します。
PROJECT_ROOT
├── dist
└── src
├── css
│ ├── fuga.css
│ └── hoge.css
├── index.html
└── js
├── fuga.js
└── hoge.js
実践
いざ実践です。
インストール
NodeJSをインストールすると、パッケージマネージャであるnpmがインストールされるので、npm経由でGulpをインストールします。
cd proj_dir # gulpを導入したプロジェクトのディレクトリ
npm install --save-dev gulp
Gulpのインストールはこれだけ。
--save-dev
オプションはインストールしたパッケージをpackages.json
に保存するオプションです。別の環境で実行する際もこのpackage.json
があれば、npm install
を実行するだけで同じNodeJsのパッケージをダウンロードすることができます。
Gulp自体も含めてnpm install
する場合は、このオプションを使用するとハマりが少なくなると思います。
gulp実行
まずはhello world的にgulpを実行することから始めましょう。
gulpfile.jsという名前のファイルをプロジェクト直下に作成してください。
'use strict';
var gulp = require('gulp');
// デフォルトタスク 今は何もしない
gulp.task('default',function(){
console.log('Hello Gulp World!');
});
コンソールでgulp
と実行してみましょう。Hello Gulp World
と表示されれば成功です。defaultのタスクは必ず必要なのでとりあえず今はこのまま先に進みましょう。
Javascript最小化
まずjavascriptを結合と最小化するためにgulp-concatとgulp-uglifyをインストールします。
npm install --save-dev gulp-concat gulp-uglify gulp-rename
インストールしたらgulpfile.jsの先頭にパッケージの読み込みを追記しておきましょう。
var concat = require('gulp-concat');
var uglify = require('gulp-uglify');
var rename = require('gulp-rename');
よくあるサンプルだと以下のように書いてあります。
gulp.task('js',function(){
gulp.src('src/js/**/*.js')
.pipe(concat('all.js')) // Javascriptを結合
.pipe(uglify()) // all.jsを最小化
.pipe(rename('all.min.js')) // all-min.jsにリネーム
.pipe(gulp.dest('dist/js/'));
});
しかしこの方法だとJavascriptの読み込みの順序に依存関係があった場合に、意図したとおりに最小化されません。
そこで今回は結合するファイルを明示的に順序も含めて指定することにしました。
こうなります。
var jsFiles = [
'src/js/hoge.js',
'src/js/fuga.js'
];
gulp.task('js',function(){
gulp.src(jsFiles)
.pipe(concat('all.js')) // Javascriptを結合
.pipe(uglify()) // all.jsを最小化
.pipe(rename('all.min.js')) // all-min.jsにリネーム
.pipe(gulp.dest('dist/js/'));
});
jsというタスク名でタスクを追加します。gulpfile.jsの編集が終わったらgulp js
とコンソールで実行し、jsタスクを実行しましょう。
dist/js/all.min.js
ができており、hoge.js,fuga.jsの順番で結合され最小化されていることが確認できます。
監視
さてJavascriptの最小化ができるようになりましたが、jsを編集する度にgulp js
コマンドを実行するのは煩わしいですよね。
そこでファイルの変更を監視し、対象のファイルが変更されたら自動でタスクを実行させるようにしたいと思います。
watch
タスクを次のように追加します。
gulp.task('watch', function() {
gulp.watch([jsFiles],['js']); // jsFilesのファイルが変更されたら、jsタスクを実行する
});
gulp watch
をコンソールで実行してみましょう。今までのようにすぐにコマンドが終了せず、待機状態になります。
ここでhoge.jsを編集して保存してみましょう。all.min.js
がすぐに生成され、編集内容が適用されていることがわかります。これでファイルを変更する度に手動でgulpコマンドを実行する必要がなくなりました。
タスク実行中にエラーが発生した場合
監視ができるようになりましたが、タスクの実行中にエラーが発生した場合、監視自体が止まってしまいます。
hoge.js
に以下のようなコードを適用すると監視していたタスクが終了してしまいますので、実験してみてください。
(funct(){
})();
記述ミスの度にgulp watch
をするのも煩わしいです。エラーが発生しても監視を続けるようにしましょう。
npm install --save-dev gulp-plumber
インストールしたらjsタスクを以下のようにしましょう。
gulp.task('js',function(){
gulp.src(jsFiles)
.pipe(plumber()) // エラーが発生しても無視
.pipe(concat('all.js')) // Javascriptを結合
.pipe(uglify()) // all.jsを最小化
.pipe(rename('all.min.js')) // all-min.jsにリネーム
.pipe(gulp.dest('dist/js/'));
});
plumber
を実行することでエラーが発生した場合もタスク自体は停止せず、その後hoge.jsを正常にすることで、all.min.jsは正しく生成されます。
ソースマップ
jsを最小化したは良いですが、これではデバッグがしにくいですよね。
ソースマップを生成しておくことでchromeで最小化されたjsでも、最小化前のソースコードでデバッグすることが可能になります。
npm install --save-dev gulp-sourcemaps
インストールしたらjsタスクを編集します。
gulp.task('js',function(){
gulp.src(jsFiles)
.pipe(plumber()) // エラーが発生しても無視
.pipe(sourcemaps.init()) // ソースマップ初期化
.pipe(concat('all.js')) // Javascriptを結合
.pipe(uglify()) // all.jsを最小化
.pipe(rename('all.min.js')) // all-min.jsにリネーム
.pipe(sourcemaps.write('./')) // ソースマップ出力
.pipe(gulp.dest('dist/js/'));
});
タスクを実行するとdist/js/all.min.js.map
という名前のマップファイルができます。これでall.min.jsを読み込んでいるhtmlをchromeの開発者ツールで開くとhoge.jsやfuga.jsの最小化前のコードでデバッグを行うことができます。
結構記事が長くなってきましたが、続けます!
開発時とリリース時の場合分け
ソースマップ出力できたけど本番環境用のビルドでは出力させたくないですよね。
そんな場合は gulp-if
とminimist
パッケージを使うことでうまくいきます。
npm install --save-dev gulp-if minimist
インストールしたらgulpfile.jsのパッケージ読み込み直後辺りに以下を追記します。
var options = minimist(process.argv.slice(2)); // コマンドライン・オプション読み込み
var isProduction = options.env == 'production'; // --env=productionと指定されたらリリース用
var parentTaskName = process.argv[2] || 'default'; // 指定されたタスク名
var isWatch = parentTaskName == 'watch'; // watchタスクで起動されたか
本番か判定するためのフラグisProduction
とwatch
タスクで起動されたか判別するisWatch
の2つのフラグを生成しました。
本番のビルド時はソースマップを生成しないようにし、watchモードの場合のみエラーを無視するようにします。
というわけでjsタスクは以下のようになります。
gulp.task('js',function(){
gulp.src(jsFiles)
.pipe(gulpIf(isWatch,plumber())) // watchタスクの場合、エラーが発生しても無視
.pipe(gulpIf(!isProduction,sourcemaps.init())) // 本番以外はソースマップ初期化
.pipe(concat('all.js')) // Javascriptを結合
.pipe(uglify()) // all.jsを最小化
.pipe(rename('all.min.js')) // all-min.jsにリネーム
.pipe(gulpIf(!isProduction,sourcemaps.write('./'))) // 本番以外はソースマップ出力
.pipe(gulp.dest('dist/js/'));
});
dist
ディレクトリを一度削除して、gulp js --evn=production
を実行してみましょう。ソースマップが出力されないことがわかると思います。
またwatch
タスク以外でタスク中にエラーが発生した場合echo $?
を実行すれば1
になっていることがわかります。
たいぶ実運用で使えるイメージが近づいてきましたね。
console.logの除去
うっかりconsole.logを多用してしまっていました。開発中はほしいけどリリース版には入れたくないなぁというときは、gulp-strip-debug
を使って除去できます。
npm install --save-dev gulp-strip-debug
jsタスクはこうなります。
gulp.task('js',function(){
gulp.src(jsFiles)
.pipe(gulpIf(isWatch,plumber())) // watchタスクの場合、エラーが発生しても無視
.pipe(gulpIf(!isProduction,sourcemaps.init())) // 本番以外はソースマップ初期化
.pipe(concat('all.js')) // Javascriptを結合
.pipe(gulpIf(isProduction,stripDebug())) // 本番でははconsole.logを除去
.pipe(uglify()) // all.jsを最小化
.pipe(rename('all.min.js')) // all-min.jsにリネーム
.pipe(gulpIf(!isProduction,sourcemaps.write('./'))) // 本番以外はソースマップ出力
.pipe(gulp.dest('dist/js/'));
});
css最小化
cssを最小化するには追加でgulp-cssmin
を使用します。
npm install --save-dev gulp-cssmin
cssタスクを以下のように作成しましょう。
var cssFiles = [
'src/css/hoge.css',
'src/css/fuga.css'
];
gulp.task('css',function(){
gulp.src(cssFiles)
.pipe(gulpIf(isWatch,plumber())) // watchタスクの場合、エラーが発生しても無視
.pipe(gulpIf(!isProduction,sourcemaps.init())) // 本番以外はソースマップ初期化
.pipe(concat('all.css'))
.pipe(cssmin())
.pipe(rename('all.min.css')) // all-min.jsにリネーム
.pipe(gulpIf(!isProduction,sourcemaps.write('./'))) // 本番以外はソースマップ出力
.pipe(gulp.dest('dist/css/'));
});
gulp css
と実行するとdist/css/all.min.css
が生成されます。
ついでに監視対象に含めましょう。
gulp.task('watch', function() {
gulp.watch([jsFiles],['js']); // jsFilesのファイルが変更されたら、jsタスクを実行する
gulp.watch([cssFiles],['css']); // cssFilesのファイルが変更されたら、cssタスクを実行する
});
html
*.htmlファイルはsrcディレクトリからdistディレクトリに階層構造を維持してコピーするだけです。順番も気にしなくてよいのでワイルドカード指定します。
var htmlFiles = [
'src/**/*.html'
];
gulp.task('html',function(){
gulp.src(htmlFiles)
.pipe(gulp.dest('dist/'));
});
gulp html
でdist/index.html
が生成されれば成功です。
これも監視対象にします。
gulp.task('watch', function() {
gulp.watch([jsFiles],['js']); // jsFilesのファイルが変更されたら、jsタスクを実行する
gulp.watch([cssFiles],['css']); // cssFilesのファイルが変更されたら、cssタスクを実行する
gulp.watch([htmlFiles],['html']); // htmlFilesのファイルが変更されたら、htmlタスクを実行する
});
画像
最後に画像です。html同様にコピーするだけにしましょう。
// images
var imageFiles = [
'src/**/*.png',
'src/**/*.gif',
'src/**/*.svg',
];
gulp.task('image',function(){
gulp.src(imageFiles)
.pipe(gulp.dest('dist/'));
});
監視項目にも追加します。
gulp.task('watch', function() {
gulp.watch([jsFiles],['js']); // jsFilesのファイルが変更されたら、jsタスクを実行する
gulp.watch([cssFiles],['css']); // cssFilesのファイルが変更されたら、cssタスクを実行する
gulp.watch([htmlFiles],['html']); // htmlFilesのファイルが変更されたら、htmlタスクを実行する
gulp.watch([imageFiles],['image']); // imageFilesのファイルが変更されたら、imageタスクを実行する
});
クリーン
distディレクトリを削除するclean
タスクも準備しておきましょう。
jenkinsでのデプロイ時に古いファイルをデプロイしないように。
npm install --save-dev del
gulp.task('clean', function(){
del(['dist'])
.then(function(paths){
console.log('deleted. ' + paths);
});
});
gulp clean
でdistディレクトリが削除されることを確認します。
最終的なgulpfile.js
最終的なgulpfile.jsは以下のようになりました。
'use strict';
var gulp = require('gulp');
var concat = require('gulp-concat');
var uglify = require('gulp-uglify');
var rename = require('gulp-rename');
var plumber = require('gulp-plumber');
var sourcemaps = require('gulp-sourcemaps');
var minimist = require('minimist');
var gulpIf = require('gulp-if');
var stripDebug = require('gulp-strip-debug');
var cssmin = require("gulp-cssmin");
var options = minimist(process.argv.slice(2)); // コマンドライン・オプション読み込み
var isProduction = options.env == 'production'; // --env=productionと指定されたらリリース用
var parentTaskName = process.argv[2] || 'default'; // 指定されたタスク名
var isWatch = parentTaskName == 'watch'; // watchタスクで起動されたか
console.log('task: ' + parentTaskName);
console.log('is production: ' + isProduction);
console.log('is watch: ' + isWatch);
// js
var jsFiles = [
'src/js/hoge.js',
'src/js/fuga.js'
];
gulp.task('js',function(){
gulp.src(jsFiles)
.pipe(gulpIf(isWatch,plumber())) // watchタスクの場合、エラーが発生しても無視
.pipe(gulpIf(!isProduction,sourcemaps.init())) // 本番以外はソースマップ初期化
.pipe(concat('all.js')) // Javascriptを結合
.pipe(gulpIf(isProduction,stripDebug())) // 本番でははconsole.logを除去
.pipe(uglify()) // all.jsを最小化
.pipe(rename('all.min.js')) // all-min.jsにリネーム
.pipe(gulpIf(!isProduction,sourcemaps.write('./'))) // 本番以外はソースマップ出力
.pipe(gulp.dest('dist/js/'));
});
// css
var cssFiles = [
'src/css/hoge.css',
'src/css/fuga.css'
];
gulp.task('css',function(){
gulp.src(cssFiles)
.pipe(gulpIf(isWatch,plumber())) // watchタスクの場合、エラーが発生しても無視
.pipe(gulpIf(!isProduction,sourcemaps.init())) // 本番以外はソースマップ初期化
.pipe(concat('all.css'))
.pipe(cssmin())
.pipe(rename('all.min.css')) // all-min.jsにリネーム
.pipe(gulpIf(!isProduction,sourcemaps.write('./'))) // 本番以外はソースマップ出力
.pipe(gulp.dest('dist/css/'));
});
// html
var htmlFiles = [
'src/**/*.html'
];
gulp.task('html',function(){
gulp.src(htmlFiles)
.pipe(gulp.dest('dist/'));
});
// images
var imageFiles = [
'src/**/*.png',
'src/**/*.gif',
'src/**/*.svg',
];
gulp.task('image',function(){
gulp.src(imageFiles)
.pipe(gulp.dest('dist/'));
});
gulp.task('watch', function() {
gulp.watch([jsFiles],['js']); // jsFilesのファイルが変更されたら、jsタスクを実行する
gulp.watch([cssFiles],['css']); // cssFilesのファイルが変更されたら、cssタスクを実行する
gulp.watch([htmlFiles],['html']); // htmlFilesのファイルが変更されたら、htmlタスクを実行する
gulp.watch([imageFiles],['image']); // imageFilesのファイルが変更されたら、imageタスクを実行する
});
// デフォルトタスク 今は何もしない
gulp.task('default',function(){
console.log('Hello Gulp World!');
});
// Clean
gulp.task('clean', function(){
del(['dist'])
.then(function(paths){
console.log('deleted. ' + paths);
});
});
サンプルプロジェクト
今回使用したプロジェクトは以下で公開しています。
https://github.com/sekitaka/first_gulp
npm install
後に使えば動作すると思います。
まとめ
いかがでしたでしょうか。Gulpを触る前に疑問に思っていた「これってできるのかな?」的な点を中心に解説していきました。
長文呼んで頂きありがとうございます!お疲れ様でした!