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


こんにちはsekitakaです。
使ってみようと以前から思っていたGulpを使ってみました。最近では脱Gulp/Gruntの動きもあるようですが、さくっと導入して便利さを感じてみるにはGulpが早そうに思えたので、Gulpを使うことにしました。

使い方自体は簡単ですが実際に開発とデプロイのフローにのせるためには、試行錯誤が必要でした。
この記事は初めてGulpを使う方向けに、実運用するまでに必要になりそうなプラグインやハマった点などを共有したいと思います。

# やったこと
主な作業内容は以下の通りです。細々他にもありますが、本文中で説明していきます。
* javascriptの結合&最小化
* cssの結合&最小化
* デバッグ用とリリース用で処理を分ける(ソースマップの作成など)
* ローカルでの開発時はファイルの監視を行い変更を検知してビルド




# プロジェクト構成
プロジェクトの構成は以下を例にとって説明していきます。
srcにビルド前のプログラムやリソースがあり、distディレクトリにgulpが生成した公開用のプログラムやリソースを配置します。

```bash
PROJECT_ROOT
├── dist
└── src
    ├── css
    │   ├── fuga.css
    │   └── hoge.css
    ├── index.html
    └── js
        ├── fuga.js
        └── hoge.js
```

# 実践
いざ実践です。

## インストール
NodeJSをインストールすると、パッケージマネージャであるnpmがインストールされるので、npm経由でGulpをインストールします。

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

```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をインストールします。

```bash
npm install --save-dev gulp-concat gulp-uglify gulp-rename
```

インストールしたらgulpfile.jsの先頭にパッケージの読み込みを追記しておきましょう。

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


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

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

こうなります。

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

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

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

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

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

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

```bash
npm install --save-dev gulp-plumber
```
インストールしたらjsタスクを以下のようにしましょう。

```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でも、最小化前のソースコードでデバッグすることが可能になります。

```bash
npm install --save-dev gulp-sourcemaps
```

インストールしたらjsタスクを編集します。

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

```bash
npm install --save-dev gulp-if minimist
```

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

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

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

```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`を使って除去できます。
 
```bash
npm install --save-dev gulp-strip-debug
```

jsタスクはこうなります。

```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`を使用します。

```bash
npm install --save-dev gulp-cssmin
```

cssタスクを以下のように作成しましょう。

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

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


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

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

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

```js: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同様にコピーするだけにしましょう。

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

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

```js: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でのデプロイ時に古いファイルをデプロイしないように。

```bash
npm install --save-dev del
```

```js:gulpfile.js
gulp.task('clean', function(){
  del(['dist'])
    .then(function(paths){
      console.log('deleted. ' + paths);
    });
});
```

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

## 最終的なgulpfile.js
最終的なgulpfile.jsは以下のようになりました。

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