chlonolog

web, digital gadgets, and more.

現在のgulpfile.jsを公開します

※この記述は古くなりました。最新の内容をどうぞ


気づけばgulpでCSS/JSのminify&gzip圧縮を書いてから数日経ち、gulpfile.js にもいろいろと処理が溜まってきました。
せっかくなので、メモがてら内容を公開したいと思います。
一応正常に動いているものですが、間違っていたり、無駄な処理が入っていたりするかもしれません。

ディレクトリの構造

hexo ┬ hexo-project-1/ (hexoのプロジェクト)
   ├ hexo-project-2/ (〃)
   ├ hexo-project-3/ (〃)
   ├ node_modules/   (gulpで使用しているモジュール)
   └ gulpfile.js

ディレクトリ内に複数のHexoプロジェクトが存在している関係上、各所でアスタリスクを用いたパスの指定やループ処理を行っています。
また複数プロジェクトの環境を極力共通にするため、

[hexo-project-name] ┬ node_modules/
          ├ scaffolds/
          └ themes ─ [theme-name] ┬ languages/
                       ├ layout/_widget/
                       ├ scripts/
                       └ source ┬ _scss/
                            ├ css/
                            ├ fonts/
                            └ js/

は hexo-project-1 から hexo-project-2・hexo-project-3 の該当する場所へシンボリックリンクを貼ることで対応しています。(テーマはすべて共通のものをベースとしています)

package.json

{
  "name": "hexo",
  "version": "1.0.0",
  "description": "",
  "main": "gulpfile.js",
  "dependencies": {
    "gulp": "^3.9.1",
    "gulp-autoprefixer": "^5.0.0",
    "gulp-clean-css": "^3.9.4",
    "gulp-concat": "^2.6.1",
    "gulp-gzip": "^1.4.2",
    "gulp-load-plugins": "^1.5.0",
    "gulp-image-resize": "^0.13.0",
    "gulp-imagemin": "^4.1.0",
    "gulp-jsmin": "^0.1.5",
    "gulp-newer": "^1.4.0",
    "gulp-notify": "^3.2.0",
    "gulp-plumber": "^1.2.0",
    "gulp-uglify": "^3.0.0",
    "gulp-rename": "^1.3.0",
    "imagemin-mozjpeg": "^7.0.0",
    "imagemin-pngquant": "^6.0.0",
    "run-sequence": "^2.2.1"
  },
  "devDependencies": {
  }
}

gulpfile.js

var gulp = require('gulp');
var $ = require('gulp-load-plugins')({
  pattern: [
    'gulp-*',
    'imagemin-*',
  ]
});

var path  = '/themes/bootstrap-blog/source';
var target_js  = [
  '**'  + path + '/js/*.js',
  '!**' + path + '/js/_*.js',
  '!**' + path + '/js/*.min.js'
];
var target_css  = [
  '**'  + path + '/*.css',
  '!**' + path + '/_*.css',
  '!**' + path + '/*.min.css'
];
var base_path = 'hexo-project-1' + path;
var img_folders = [
    { path: 'hexo-project-1' + path + '/images/', size: 400 },
    { path: 'hexo-project-1' + path + '/images/foo/', size: 400 },
    { path: 'hexo-project-1' + path + '/images/bar/', size: 400 },
    { path: 'hexo-project-2' + path + '/images/', size: 150 },
    { path: 'hexo-project-3' + path + '/images/', size: 300 },
  ];

var imagemin_options = {
    optimizationLevel: 7
  };

//--------------------------------------------------

/**
 * パスからファイル名を取得
 */
function getfilename(path) {
  var f = path.split('/');
  var fname = f[f.length - 1];
  var fpath = path.replace(fname, '');
  return [fpath, fname];
}

//--------------------------------------------------

/**
 * 起動時処理
 */
gulp.task('default', ['watch']);

/**
 * ファイル監視
 */
gulp.task('watch', function() {
  gulp.watch(target_css, function(event) {
    var fileargs = getfilename(event.path);
    if (event.type == 'added' ||
        event.type == 'changed') {
      compress_css(fileargs[0], fileargs[1]);
    }
  });
  gulp.watch(target_js, function(event) {
    var fileargs = getfilename(event.path);
    if (event.type == 'added' ||
        event.type == 'changed') {
      compress_js(fileargs[0], fileargs[1]);
    }
  });
});

//--------------------------------------------------

/**
 * 主要JSファイルの結合
 * 結合結果は /js/default.js
 */
gulp.task('js', function () {
  gulp.src([
    base_path + '/js/_jquery-3.3.1.min.js',
    base_path + '/js/_bootstrap.min.js',
    base_path + '/js/_fastclick.js',
    base_path + '/js/_slidebars.js',
    base_path + '/js/_lity.min.js',
    base_path + '/js/_lazysizes/ls.unveilhooks.min.js',
    base_path + '/js/_lazysizes/lazysizes.min.js'
  ])
  .pipe($.plumber({errorHandler: $.notify.onError('<%= error.message %>')}))
  .pipe($.concat('default.js'))
  .pipe(gulp.dest(base_path + '/js/'))
  .pipe($.filelog());
});

/**
 * 主要CSSファイルの結合
 * 結合結果は /css/default.css
 */
gulp.task('css', function () {
  gulp.src([
    base_path + '/css/_slidebars.css',
    base_path + '/css/_lity.min.css',
    base_path + '/css/_styles.css'
  ])
  .pipe($.plumber({errorHandler: $.notify.onError('<%= error.message %>')}))
  .pipe($.autoprefixer({
    browsers: ['last 2 version', 'iOS >= 8.1', 'Android >= 4.4']
  }))
  .pipe($.concat('default.css'))
  .pipe(gulp.dest(base_path + '/css/'))
  .pipe($.filelog());
});

/**
 * 画像ファイルの圧縮
 * 本処理の前にサムネイル生成処理とアイキャッチ画像の作成処理を実行する
 * ついでに画像サイズを最大1000pxにリサイズしている
 */
gulp.task('images', ['thumbnail', 'eyecatch'], function(){
  img_folders.map(function (folder) {
      var resize_options = {
        width: 1000, //最大1000pxで十分だよね?
        height: 1000,
        crop: false,
        upscale: false,
        imageMagick: true
      };
    var source_path = folder.path.replace('/images/', '/_images/');
    return gulp.src([source_path + '*.+(jpg|jpeg|png|gif|svg)'])
      .pipe($.newer(folder.path))  //追加されたファイルのみ対象とする
      .pipe($.imageResize(resize_options))
      .pipe($.imagemin([  //画像圧縮処理
      $.imagemin.gifsicle(),
      $.imageminMozjpeg({ quality: 80 }),
      $.imageminPngquant(),
      $.imagemin.svgo()
    ], {
      verbose: true
    }))
    .pipe($.imagemin())  //メタ情報を再度削除
    .pipe(gulp.dest(folder.path))
    .pipe($.filelog());
  });
});

/**
 * 画像のサムネイル作成
*/
gulp.task('thumbnail', function(){
  img_folders.map(function (folder) {
    if (folder.size > 0) {
      var resize_options = {
        width: folder.size,
        height: folder.size,
        crop: false,
        upscale: false,
        imageMagick: true
      };
      var source_path = folder.path.replace('/images/', '/_images/');
      gulp.src([source_path + '*.+(jpg|jpeg|png|gif|svg)'])
        .pipe($.newer(folder.path + 'thumbnail/'))  //追加されたファイルのみ対象とする
        .pipe($.imageResize(resize_options))
        .pipe($.imagemin(imagemin_options))
        .pipe(gulp.dest(folder.path + 'thumbnail/'))
        .pipe($.filelog());
    }
  });
});

/**
 * アイキャッチ画像の作成
 * 830px × 580px で決め打ち
 */
gulp.task('eyecatch', function(){
  img_folders.map(function (folder) {
    if (folder.size > 0) {
      var resize_options = {
        width: 830,
        height: 580,
        crop: true,         //大きい画像はクロップする
        gravity: 'Center',  //クロップ位置の指定
        upscale: true,      //幅が830pxに満たない場合は引き伸ばす
        cover: true,        //カバー画像作成モード
        imageMagick: true
      };
      var source_path = folder.path.replace('/images/', '/_images/');
      gulp.src([source_path + '*.+(jpg|jpeg|png|gif|svg)'])
        .pipe($.newer(folder.path + 'eyecatch/'))  //追加されたファイルのみ対象とする
        .pipe($.imageResize(resize_options))
        .pipe($.imagemin(imagemin_options))
        .pipe(gulp.dest(folder.path + 'eyecatch/'))
        .pipe($.filelog());
    }
  });
});

//--------------------------------------------------

/**
 * Minify & gzip圧縮ファイル作成: CSS
 */
function compress_css(strpath, strfile) {
  gulp.src(strpath + strfile)
    .pipe($.plumber({errorHandler: $.notify.onError('<%= error.message %>')}))
    .pipe($.autoprefixer({
      browsers: ['last 2 version', 'iOS >= 8.1', 'Android >= 4.4']
    }))
    .pipe($.rename({
      prefix: '_',
      suffix:'.dest'
    }))                        //_style.dest.cssにリネーム
    .pipe(gulp.dest(strpath))  //autoprefixつけた状態で保存
    .pipe($.cleanCss())        //Minify
    .pipe($.rename({
      basename: strfile.replace('.css', ''),
      suffix: '.min',
      extname: '.css'
    }))                        //style.min.cssにリネーム
    .pipe(gulp.dest(strpath))  //*.min.cssを保存
    .pipe($.filelog())
    .pipe($.gzip())            //gzip圧縮
    .pipe(gulp.dest(strpath))  //*.min.css.gzを保存
    .pipe($.filelog());
  return true;
}

/**
 * Minify & gzip圧縮ファイル作成: JS
 */
function compress_js(strpath, strfile) {
  gulp.src(strpath + strfile)
  .pipe($.plumber({errorHandler: $.notify.onError('<%= error.message %>')}))
  .pipe($.uglify())                  //Minify
  .pipe($.rename({suffix: '.min'}))  //*.min.jsにリネーム
  .pipe(gulp.dest(strpath))          //*.min.jsを保存
  .pipe($.filelog())
  .pipe($.gzip())                    //gzip圧縮  
  .pipe(gulp.dest(strpath))          //*.min.js.gzを保存
  .pipe($.filelog());
  return true;
}

処理の詳細

では、各部について説明を入れていきます。

モジュール読み込み

var gulp = require('gulp');
var $ = require('gulp-load-plugins')({
  pattern: [
    'gulp-*',
    'imagemin-*',
  ]
});

gulp-load-pluginsを使用することで、requireをすっきりさせることができました。

gulp-filelogを用いて、要所でコンソールへ対象ファイル名を出力しています。
エラーのキャッチにはgulp-plumber、通知の出力にgulp-notifyを使用しています。

変数定義

var path  = '/themes/bootstrap-blog/source';
var target_js  = [
  '**'  + path + '/js/*.js',
  '!**' + path + '/js/_*.js',
  '!**' + path + '/js/*.min.js'
];
var target_css  = [
  '**'  + path + '/*.css',
  '!**' + path + '/_*.css',
  '!**' + path + '/*.min.css'
];
var base_path = 'hexo-project-1' + path;

var img_folders = [
    { path: 'hexo-project-1' + path + '/images/', size: 400 },
    { path: 'hexo-project-1' + path + '/images/foo/', size: 400 },
    { path: 'hexo-project-1' + path + '/images/bar/', size: 400 },
    { path: 'hexo-project-2' + path + '/images/', size: 150 },
    { path: 'hexo-project-3' + path + '/images/', size: 300 },
  ];

var imagemin_options = {
    optimizationLevel: 7
  };

複数のHexoプロジェクトをまたいでwatchさせるため、パスにはアスタリスクを含めています。
ファイル結合はシンボリックリンクを貼っているディレクトリが対象なので、base_pathには大元である hexo-project-1 のディレクトリを設定しています。

target_jstarget_cssは、Minifyとgzip圧縮処理を行うファイルの場所です。配列で指定することにより、「*.css は対象」「_*.css、*.min.cssは対象外」といった動作をさせています。
書き方については、以下の記事を参考にさせていただきました。

img_foldersは、画像処理の際に使用しています。ディレクトリごとにサムネイルのサイズを指定することで、場所によって違う大きさのサムネイルを生成できるようにしています。
本来であればファイルパスにアスタリスクを用いてループで処理したいところですが、指定したいパスがまちまちだったりするため、スマートでない書きかたをしている次第です……。

汎用関数

/**
 * パスからファイル名を取得
 */
function getfilename(path) {
  var f = path.split('/');
  var fname = f[f.length - 1];
  var fpath = path.replace(fname, '');
  return [fpath, fname];
}

getfilename()gulp.task('watch')内で呼んでいます。ファイルのフルパスをパスとファイル名に分割し、配列に格納して返すだけの単純なものです。

起動時処理・ファイル監視

/**
 * 起動時処理
 */
gulp.task('default', ['watch']);

/**
 * ファイル監視
 */
gulp.task('watch', function() {
  gulp.watch(target_css, function(event) {
    var fileargs = getfilename(event.path);
    if (event.type == 'added' ||
        event.type == 'changed') {
      compress_css(fileargs[0], fileargs[1]);
    }
  });
  gulp.watch(target_js, function(event) {
    var fileargs = getfilename(event.path);
    if (event.type == 'added' ||
        event.type == 'changed') {
      compress_js(fileargs[0], fileargs[1]);
    }
  });
});

'watch’内でCSSファイルとJavaScriptファイルに対し監視を行い、新しい/更新されたファイルが見つかった場合、Minifyとgzip圧縮の処理を行っています。
実処理はcompress_css()compress_js()に分けてあります。
タスクは他にも記述してありますが、監視は行っていません。これ以外はすべて手動で実行しています。

/**
 * Minify & gzip圧縮ファイル作成: CSS
 */
function compress_css(strpath, strfile) {
  gulp.src(strpath + strfile)
    .pipe($.plumber({errorHandler: $.notify.onError('<%= error.message %>')}))
    .pipe($.autoprefixer({
      browsers: ['last 2 version', 'iOS >= 8.1', 'Android >= 4.4']
    }))
    .pipe($.rename({
      prefix: '_',
      suffix:'.dest'
    }))                        //_style.dest.cssにリネーム
    .pipe(gulp.dest(strpath))  //autoprefixつけた状態で保存
    .pipe($.cleanCss())        //Minify
    .pipe($.rename({
      basename: strfile.replace('.css', ''),
      suffix: '.min',
      extname: '.css'
    }))                        //style.min.cssにリネーム
    .pipe(gulp.dest(strpath))  //*.min.cssを保存
    .pipe($.filelog())
    .pipe($.gzip())            //gzip圧縮
    .pipe(gulp.dest(strpath))  //*.min.css.gzを保存
    .pipe($.filelog());
  return true;
}

/**
 * Minify & gzip圧縮ファイル作成: JS
 */
function compress_js(strpath, strfile) {
  gulp.src(strpath + strfile)
  .pipe($.plumber({errorHandler: $.notify.onError('<%= error.message %>')}))
  .pipe($.uglify())                  //Minify
  .pipe($.rename({suffix: '.min'}))  //*.min.jsにリネーム
  .pipe(gulp.dest(strpath))          //*.min.jsを保存
  .pipe($.filelog())
  .pipe($.gzip())                    //gzip圧縮  
  .pipe(gulp.dest(strpath))          //*.min.js.gzを保存
  .pipe($.filelog());
  return true;
}

CSSの処理にはgulp-clean-cssgulp-autoprefixer、JavaScriptの処理にはgulp-uglify、双方でgulp-gzipを使用しています。
gulp-pleeeaseを使って大元のSCSSから処理を繋げようかと試行錯誤したのですが、@importの処理でつまづいてしまい、結局あきらめました。

ファイル結合

CSSとJavaScriptのファイル数が増えたため、結合したファイルを<head>内で読み込むよう指定しています。
一度結合してしまえば、あとは使用するファイルに変更があった時ぐらいしか使わない処理です。
処理にはgulp-concatを使用しています。

/**
 * 主要JSファイルの結合
 * 結合結果は /js/default.js
 */
gulp.task('js', function () {
  gulp.src([
    base_path + '/js/_jquery-3.3.1.min.js',
    base_path + '/js/_bootstrap.min.js',
    base_path + '/js/_fastclick.js',
    base_path + '/js/_slidebars.js',
    base_path + '/js/_lity.min.js',
    base_path + '/js/_lazysizes/ls.unveilhooks.min.js',
    base_path + '/js/_lazysizes/lazysizes.min.js'
  ])
  .pipe($.plumber({errorHandler: $.notify.onError('<%= error.message %>')}))
  .pipe($.concat('default.js'))
  .pipe(gulp.dest(base_path + '/js/'))
  .pipe($.filelog());
});

/**
 * 主要CSSファイルの結合
 * 結合結果は /css/default.css
 */
gulp.task('css', function () {
  gulp.src([
    base_path + '/css/_slidebars.css',
    base_path + '/css/_lity.min.css',
    base_path + '/css/_styles.css'
  ])
  .pipe($.plumber({errorHandler: $.notify.onError('<%= error.message %>')}))
  .pipe($.autoprefixer({
    browsers: ['last 2 version', 'iOS >= 8.1', 'Android >= 4.4']
  }))
  .pipe($.concat('default.css'))
  .pipe(gulp.dest(base_path + '/css/'))
  .pipe($.filelog());
});

画像ファイルの圧縮・サムネイル作成・アイキャッチ画像作成

こちらを参考にさせていただきました。

配列img_foldersをループさせて処理しています。
サムネイル画像とアイキャッチ画像は生成する際に圧縮処理も行っているため、gulp.task('images')で対象にしていません。

アイキャッチ画像は、リサイズオプションを

      var resize_options = {
        width: 830,
        height: 580,
        crop: true,         //大きい画像はクロップする
        gravity: 'Center',  //クロップ位置の指定
        upscale: true,      //幅が830pxに満たない場合は引き伸ばす
        cover: true,        //カバー画像作成モード
        imageMagick: true
      };

と設定しているのがミソ。小さい画像は引き伸ばした上で、指定したサイズにクロップしてくれます。

画像の圧縮処理にgulp-imageminimagemin-mozjpegimagemin-pngquant、リサイズ処理にはgulp-image-resizeを使用しています。
追加されたファイルのみを処理対象とするために、gulp-newerを使用しています。
gulp-image-resize の実行には ImageMagick か GraphicsMagick が必要となります。自分の環境には、ImageMagick をインストールしてあります。

ディレクトリはこんな感じ。

処理対象となる画像      : `img_folders[n].path` に対し`/images/`を`/_images/`に置換したもの
結果を保存する場所      : `img_folders[n].path`
サムネイル画像を保存する場所 : `img_folders[n].path + 'thumbnail/'`
アイキャッチ画像を保存する場所: `img_folders[n].path + 'eyecatch/'`

具体的には、img_folders[n].path = 「/foo/bar/images/baz/」だった場合

処理対象となる画像      : /foo/bar/_images/baz/
結果を保存する場所      : /foo/bar/images/baz/
サムネイル画像を保存する場所 : /foo/bar/images/baz/thumbnail/
アイキャッチ画像を保存する場所: /foo/bar/images/baz/eyecatch/

となります。

/**
 * 画像ファイルの圧縮
 * 本処理の前にサムネイル生成処理とアイキャッチ画像の作成処理を実行する
 * ついでに画像サイズを最大1000pxにリサイズしている
 */
gulp.task('images', ['thumbnail', 'eyecatch'], function(){
  img_folders.map(function (folder) {
      var resize_options = {
        width: 1000, //最大1000pxで十分だよね?
        height: 1000,
        crop: false,
        upscale: false,
        imageMagick: true
      };
    var source_path = folder.path.replace('/images/', '/_images/');
    return gulp.src([source_path + '*.+(jpg|jpeg|png|gif|svg)'])
      .pipe($.newer(folder.path))  //追加されたファイルのみ対象とする
      .pipe($.imageResize(resize_options))
      .pipe($.imagemin([  //画像圧縮処理
      $.imagemin.gifsicle(),
      $.imageminMozjpeg({ quality: 80 }),
      $.imageminPngquant(),
      $.imagemin.svgo()
    ], {
      verbose: true
    }))
    .pipe($.imagemin())  //メタ情報を再度削除
    .pipe(gulp.dest(folder.path))
    .pipe($.filelog());
  });
});

/**
 * 画像のサムネイル作成
*/
gulp.task('thumbnail', function(){
  img_folders.map(function (folder) {
    if (folder.size > 0) {
      var resize_options = {
        width: folder.size,
        height: folder.size,
        crop: false,
        upscale: false,
        imageMagick: true
      };
      var source_path = folder.path.replace('/images/', '/_images/');
      gulp.src([source_path + '*.+(jpg|jpeg|png|gif|svg)'])
        .pipe($.newer(folder.path + 'thumbnail/'))  //追加されたファイルのみ対象とする
        .pipe($.imageResize(resize_options))
        .pipe($.imagemin(imagemin_options))
        .pipe(gulp.dest(folder.path + 'thumbnail/'))
        .pipe($.filelog());
    }
  });
});

/**
 * アイキャッチ画像の作成
 * 830px × 580px で決め打ち
 */
gulp.task('eyecatch', function(){
  img_folders.map(function (folder) {
    if (folder.size > 0) {
      var resize_options = {
        width: 830,
        height: 580,
        crop: true,
        gravity: 'Center',  //クロップ位置の指定
        upscale: true,      //幅が830pxに満たない場合は引き伸ばす
        cover: true,        //カバー画像作成モード
        imageMagick: true
      };
      var source_path = folder.path.replace('/images/', '/_images/');
      gulp.src([source_path + '*.+(jpg|jpeg|png|gif|svg)'])
        .pipe($.newer(folder.path + 'eyecatch/'))  //追加されたファイルのみ対象とする
        .pipe($.imageResize(resize_options))
        .pipe($.imagemin(imagemin_options))
        .pipe(gulp.dest(folder.path + 'eyecatch/'))
        .pipe($.filelog());
    }
  });
});

作業がかなり簡略化されました

以上の処理を記述したことで、作業が相当楽になりました。
画像のサムネイル作成と圧縮処理は、以前を挙げました。しかしImageOptimは動作が非常に遅く、プレビューを使う方法もgulpの手軽さには及びません。
こういった「たまにやるぐらいならいいけれど、ちょくちょくとなると面倒臭い」処理をコマンド一発で済ませられるのが、gulpの便利なところだなと感じました。

コメント

© 2018 chlono