ご無沙汰してます!
最近はgulpとwebpackの勉強をしてnode.jsで開発環境を作ってました。
EJS、SCSS、JS(ES6)のモジュール化、文法チェック、コード自動修正、コード圧縮、画像圧縮、SVGスプライト・・・とやりたいこと全部入りで作ったらモジュール数が大変ヤバイことになって反省しているんですが、勉強になることも多くありました。
最後に今回作った環境を晒しておくので、コード全文はbitbacketでご覧ください。
今回は、gulpで特定のファイルをpipeから除外するテクニックをご紹介します。
このテクニックを使用すると、本来書き出されるはずだったファイルをなかったことにできます!
どんなときにpipeからファイルを弾きたい?
私がまさにぶち当たった例になるのですが、1つ目は画像圧縮です。
srcに元データを、distに圧縮データを置く想定なんですが、開発環境をスタートするたびに圧縮プロセスが走ってしまうんですね。gulp 4.xにはlastRun()という差分チェック機能があるんですが、開発環境をスタートしたときには差分チェックが効かないみたいです。軽い処理なら好きに走らせとけでいいんですが、画像圧縮は結構負担かかるので処理をスキップできないかと。
2つ目はgulp-svg-spriteでSVGスプライトをCSSスプライトとして使用したいときです。
scssを出力できるんですが、肝心の画像のパスが合ってない!cssを出力する場所を指定してやると正常なパスになります。しかし、そうするとdistにcssが出力されてしまう。なんども書き出してなんども消すのは嫌だし、そもそも書き出さない方法はないのかと。
through2で画像圧縮をスキップした
gulpで独自の関数を実行するを参考に、through2オブジェクトを使って独自関数を書くことにしました。
through2はgulpにバンドルされているのでinstallしなくてもrequireするだけで使うことができます。
以下が画像圧縮をスキップしたコードの抜粋です。
const gulp = require("gulp"); // gulpを動かす const path = require("path"); // 安全にパスを解決する const settings = require(path.join(__dirname, "settings.json")); // 初期設定はsettings.jsonにまとめる const $ = require("gulp-load-plugins")(); // gulp-*プラグインをまとめて読み込む const fs = require("fs"); // ファイルを操作する const through = require("through2"); // through2オブジェクトを使用する gulp.task("imagemin", imagemin); // 画像を圧縮して公開領域に転送する function imagemin (done) { gulp.src(`${settings.img.src}**/*.*`, { since: gulp.lastRun(imagemin) }) .pipe($.plumber({ errorHandler: notifyError() })) .pipe(through.obj((file, enc, callback) => { const syncPath = file.path.replace(path.join(__dirname, settings.img.src), ""); // srcとdistでの共通パス const destPath = `${settings.img.dest}${syncPath}`; // 公開領域側の画像パス const srcStat = file.stat; // 開発領域側の画像ファイルステータス const destStat = getFileStat(destPath); // 公開領域側の画像ファイルステータス // 開発領域側のタイムスタンプが公開領域側のタイムスタンプと同じか古いとき弾く if (destStat && srcStat.mtime <= destStat.mtime) file = null; callback(null, file); })) .pipe($.imagemin()) .pipe(gulp.dest(settings.img.dest)); done(); } function getFileStat(path) { try { return fs.statSync(path); } catch (err) { if (err.code === "ENOENT") { return null; } else { throw err; } } }
付随して色々と処理してますが、重要なのはここですね。
.pipe(through.obj((file, enc, callback) => { const syncPath = file.path.replace(path.join(__dirname, settings.img.src), ""); // srcとdistでの共通パス const destPath = `${settings.img.dest}${syncPath}`; // 公開領域側の画像パス const srcStat = file.stat; // 開発領域側の画像ファイルステータス const destStat = getFileStat(destPath); // 公開領域側の画像ファイルステータス // 開発領域側のタイムスタンプが公開領域側のタイムスタンプと同じか古いとき弾く if (destStat && srcStat.mtime <= destStat.mtime) file = null; callback(null, file); }))
第1引数のfileには今流れてきているファイルのデータが入っています。
それを元にしてsrc側とdist側それぞれの画像ファイルステータスを取得して、タイムスタンプを比較しています。
そして、through2では処理の終わりに必ずcallbackを実行してあげないといけないのですが、callbackの第2引数にfileを渡さないことで流れてきたファイルがなかったことになります!
補足ですが、fs.statSyncは存在しないファイルに対して行うとENOENTエラーになるので、関数を切り出してエラーを握りつぶしてます。もっといい書き方があったら教えてください。
through2でcssファイルを握りつぶした
長くなるのでsvg-spriteの記述部分は割愛してpipeだけ抜粋します。(全文はページ最後のbitbacketでご覧ください)
.pipe(through.obj((file, enc, callback) => { if(/\.css$/.test(file.path)) file = null; // 拡張子がcssだったら弾く callback(null, file); }))
先ほどに比べて簡単です。file.pathの末尾が.cssだったらfileをnullにしています。
svg-spriteでcssの出力先を指定しているので、cssファイルが流れてくるのですが、ここで握りつぶしてしまうので出力されることはありません!
まとめ
- 独自の処理はthrough2オブジェクトを使うと書くことができる。
- through2はgulpにバンドルされているのでrequireするだけで使える。
- through2の第一引数には流れてきているファイルが入っている。
- through2のコールバック第2引数にfileを渡すと次の処理へ進める。
- through2のコールバック第2引数のfileを消してしまえばなかったことにできる。
今回作った環境を晒す
https://bitbucket.org/mtbk4919/nyc_framework/src/master/
反省はしている、後悔はしていない。