GulpによるHTML圧縮・JS難読化・リビジョン付与の自動化フロー

電脳備忘録

本記事のソースコードの利用によって生じた損害について、当方は一切の責任を負いません。ご自身の判断と責任のもとで参照・ご利用ください。

自動化の目的

静的サイトの納品や公開において、ソースコードの圧縮や難読化は避けて通れない工程です。これまでは公開のたびに手動でデータを圧縮し、難読化ツールを通していました。しかし、この単純作業は精神的リソースを無駄に浪費します。

人間がやるべきではない反復作業に人生の時間を奪われることは非効率です。そこで、Gulpを導入し、開発環境(srcディレクトリ)から本番環境(distディレクトリ)へのデプロイフローを自動化しました。

実装仕様

今回構築したフローの仕様は以下の通りです。srcディレクトリでの開発完了後、コマンド一つでdistディレクトリへ成果物を生成します。

  • HTML/CSSの圧縮(Minify): 余計な空白や改行を削除し、ファイルサイズを削減。
  • JavaScriptの難読化(Obfuscator): コードの解析を困難にする処理を実行。
  • キャッシュ対策(Revisioning): ファイル名にハッシュ値を付与し、ブラウザキャッシュによる表示トラブルを回避。
  • クリーンアップ: ビルド実行時にdistディレクトリを一度削除し、常に最新の状態で再生成。
  • サーバーアップロード: サーバー上の既存データを削除した上で、distの内容をアップロードする運用を前提とする。

gulpfile.jsの構成

自動化のために実装した gulpfile.js の構成を記録します。

依存パッケージ

必要なnpmパッケージは以下の通りです。HTML圧縮には gulp-htmlmin、JS難読化には gulp-javascript-obfuscator、ファイル名のハッシュ化と書き換えには gulp-rev および gulp-rev-rewrite を使用しています。

コード内容

具体的な処理コードは以下の通りです。

const gulp = require('gulp');
const htmlmin = require('gulp-htmlmin');
const obfuscator = require('gulp-javascript-obfuscator');
const rev = require('gulp-rev').default;
const cleanCSS = require('gulp-clean-css');
const { deleteAsync } = require('del');
const fs = require('fs');
const path = require('path');
const through = require('through2');

// distディレクトリをクリーン
function clean() {
  return deleteAsync(['dist']);
}

// JavaScriptの難読化とハッシュ化
function scripts() {
  return gulp.src('src/assets/js/**/*.js', { allowEmpty: true })
    .pipe(obfuscator({
      compact: true,
      controlFlowFlattening: true,
      stringArray: true
    }))
    .pipe(rev())
    .pipe(gulp.dest('dist/assets/js'))
    .pipe(rev.manifest('js-manifest.json'))
    .pipe(gulp.dest('dist'));
}

// CSSの圧縮とハッシュ化
function styles() {
  return gulp.src('src/assets/css/**/*.css', { allowEmpty: true })
    .pipe(cleanCSS())
    .pipe(rev())
    .pipe(gulp.dest('dist/assets/css'))
    .pipe(rev.manifest('css-manifest.json'))
    .pipe(gulp.dest('dist'));
}

// manifestファイルをマージ
function mergeManifests(done) {
  const jsManifestPath = path.join(__dirname, 'dist', 'js-manifest.json');
  const cssManifestPath = path.join(__dirname, 'dist', 'css-manifest.json');
  const mergedManifestPath = path.join(__dirname, 'dist', 'rev-manifest.json');
  
  let merged = {};
  
  if (fs.existsSync(jsManifestPath)) {
    Object.assign(merged, JSON.parse(fs.readFileSync(jsManifestPath, 'utf8')));
  }
  
  if (fs.existsSync(cssManifestPath)) {
    Object.assign(merged, JSON.parse(fs.readFileSync(cssManifestPath, 'utf8')));
  }
  
  fs.writeFileSync(mergedManifestPath, JSON.stringify(merged, null, 2));
  console.log('Manifests merged:', merged);
  done();
}

// カスタムrev-rewrite関数
function customRevRewrite(manifest) {
  return through.obj(function(file, enc, cb) {
    if (file.isNull()) {
      return cb(null, file);
    }
    
    if (file.isBuffer()) {
      let contents = file.contents.toString();
      
      // manifestの各エントリに対して置換
      for (const [original, hashed] of Object.entries(manifest)) {
        const regex = new RegExp(`assets/(css|js)/${original}`, 'g');
        contents = contents.replace(regex, `assets/$1/${hashed}`);
      }
      
      file.contents = Buffer.from(contents);
    }
    
    cb(null, file);
  });
}

// HTMLの圧縮とファイル参照の書き換え
function html() {
  const manifestPath = path.join(__dirname, 'dist', 'rev-manifest.json');
  let manifest = {};
  
  if (fs.existsSync(manifestPath)) {
    try {
      manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
      console.log('Manifest loaded for HTML:', manifest);
    } catch (err) {
      console.error('Error parsing manifest:', err.message);
    }
  }

  // src直下とサブディレクトリのHTMLファイルすべて
  return gulp.src('src/**/*.html', { base: 'src' })
    .pipe(customRevRewrite(manifest))
    .pipe(htmlmin({
      collapseWhitespace: true,
      removeComments: true,
      minifyCSS: true,
      minifyJS: true
    }))
    .pipe(gulp.dest('dist'));
}

// 画像やその他のファイルをコピー
function assets() {
  return gulp.src(['src/assets/images/**/*', 'src/assets/fonts/**/*'], { 
    base: 'src',
    allowEmpty: true,
    encoding: false // バイナリファイルのため
  })
    .pipe(gulp.dest('dist'));
}

// ファイル監視
function watchFiles() {
  gulp.watch('src/assets/js/**/*.js', gulp.series(scripts, mergeManifests, html));
  gulp.watch('src/assets/css/**/*.css', gulp.series(styles, mergeManifests, html));
  gulp.watch('src/**/*.html', html);
  gulp.watch(['src/assets/images/**/*', 'src/assets/fonts/**/*'], assets);
}

// ビルドタスク
const build = gulp.series(
  clean,
  gulp.parallel(scripts, styles, assets),
  mergeManifests,
  html
);

// 開発用タスク
const dev = gulp.series(
  build,
  watchFiles
);

// タスクをエクスポート
exports.clean = clean;
exports.scripts = scripts;
exports.styles = styles;
exports.mergeManifests = mergeManifests;
exports.html = html;
exports.assets = assets;
exports.watch = watchFiles;
exports.build = build;
exports.dev = dev;
exports.default = build;

各処理において、スクリプト(JS)とスタイル(CSS)はそれぞれ処理後にマニフェストファイルを出力し、それらを mergeManifests 関数で統合しています。最終的にHTMLタスク内でファイルパスの書き換えを行うロジックとしています。

実行コマンド

package.json に以下のスクリプトを定義し、状況に応じて使い分けます。

"scripts": {
    "build": "gulp build",
    "dev": "gulp dev",
    "watch": "gulp watch",
    "clean": "gulp clean"
  },
  • npm run build: 本番用ビルドの実行(dist生成)
  • npm run dev: ビルド実行に加え、ファイル変更の監視を開始
  • npm run watch: ファイル変更の監視のみ実行
  • npm run clean: distディレクトリの削除

所感

いちいち手作業で行っていた工程をスクリプト化したことで、作業手順が明確化されました。単純作業の繰り返しによるミスの誘発を防ぎ、開発そのものにリソースを集中させる環境が整いました。

0%