Help us understand the problem. What is going on with this article?

初めてのGulp!実戦投入まで!

More than 3 years have passed since last update.

こんにちは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という名前のファイルをプロジェクト直下に作成してください。

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の先頭にパッケージの読み込みを追記しておきましょう。

gulpfile.js
var concat = require('gulp-concat');
var uglify = require('gulp-uglify');
var rename = require('gulp-rename');

よくあるサンプルだと以下のように書いてあります。

gulpfile.js
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の読み込みの順序に依存関係があった場合に、意図したとおりに最小化されません。
そこで今回は結合するファイルを明示的に順序も含めて指定することにしました。

こうなります。

gulpfile.js
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タスクを次のように追加します。

gulpfile.js
gulp.task('watch', function() {
  gulp.watch([jsFiles],['js']); // jsFilesのファイルが変更されたら、jsタスクを実行する
});

gulp watchをコンソールで実行してみましょう。今までのようにすぐにコマンドが終了せず、待機状態になります。
ここでhoge.jsを編集して保存してみましょう。all.min.jsがすぐに生成され、編集内容が適用されていることがわかります。これでファイルを変更する度に手動でgulpコマンドを実行する必要がなくなりました。

タスク実行中にエラーが発生した場合

監視ができるようになりましたが、タスクの実行中にエラーが発生した場合、監視自体が止まってしまいます。
hoge.jsに以下のようなコードを適用すると監視していたタスクが終了してしまいますので、実験してみてください。

hoge.js(エラー)
(funct(){
})();

記述ミスの度にgulp watchをするのも煩わしいです。エラーが発生しても監視を続けるようにしましょう。

npm install --save-dev gulp-plumber

インストールしたらjsタスクを以下のようにしましょう。

gulpfile.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タスクを編集します。

gulpfile.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-ifminimistパッケージを使うことでうまくいきます。

npm install --save-dev gulp-if minimist

インストールしたらgulpfile.jsのパッケージ読み込み直後辺りに以下を追記します。

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タスクで起動されたか

本番か判定するためのフラグisProductionwatchタスクで起動されたか判別するisWatchの2つのフラグを生成しました。
本番のビルド時はソースマップを生成しないようにし、watchモードの場合のみエラーを無視するようにします。
というわけでjsタスクは以下のようになります。

gulpfile.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タスクはこうなります。

gulpfile.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タスクを以下のように作成しましょう。

gulpfile.js
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が生成されます。
ついでに監視対象に含めましょう。

gulpfile.js
gulp.task('watch', function() {
  gulp.watch([jsFiles],['js']); // jsFilesのファイルが変更されたら、jsタスクを実行する
  gulp.watch([cssFiles],['css']); // cssFilesのファイルが変更されたら、cssタスクを実行する
});

html

*.htmlファイルはsrcディレクトリからdistディレクトリに階層構造を維持してコピーするだけです。順番も気にしなくてよいのでワイルドカード指定します。

gulpfile.js
var htmlFiles = [
  'src/**/*.html'
];
gulp.task('html',function(){
  gulp.src(htmlFiles)
    .pipe(gulp.dest('dist/'));
});

gulp htmldist/index.htmlが生成されれば成功です。
これも監視対象にします。

gulpfile.js
gulp.task('watch', function() {
  gulp.watch([jsFiles],['js']); // jsFilesのファイルが変更されたら、jsタスクを実行する
  gulp.watch([cssFiles],['css']); // cssFilesのファイルが変更されたら、cssタスクを実行する
  gulp.watch([htmlFiles],['html']); // htmlFilesのファイルが変更されたら、htmlタスクを実行する
});

画像

最後に画像です。html同様にコピーするだけにしましょう。

gulpfile.js
// images
var imageFiles = [
  'src/**/*.png',
  'src/**/*.gif',
  'src/**/*.svg',
];
gulp.task('image',function(){
  gulp.src(imageFiles)
    .pipe(gulp.dest('dist/'));
});

監視項目にも追加します。

gulpfile.js
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
gulpfile.js
gulp.task('clean', function(){
  del(['dist'])
    .then(function(paths){
      console.log('deleted. ' + paths);
    });
});

gulp clean でdistディレクトリが削除されることを確認します。

最終的なgulpfile.js

最終的な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を触る前に疑問に思っていた「これってできるのかな?」的な点を中心に解説していきました。
長文呼んで頂きありがとうございます!お疲れ様でした!

runners
スポーツで世界を良くしたいエンジニアチーム。応援navi、.finisher、run&といった製品開発をしています!
https://www.wantedly.com/projects/167082
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away