tohokuaikiのチラシの裏

技術的ネタとか。

gulp4を使ってCache bustingする方法(その1・環境構築編)

CSS/JavaScript/画像ファイルが更新されたらブラウザキャッシュの対策をしたいのです。

で、それらのMD5値をQueryStringに付ければいいんじゃない?と思ったので、それを自動でやる方法。…について調べてたら、色々とやりたくなった。

そのために、CSSJavaScript、画像が更新された際にQueryStringを付けて呼び出すようにする。

前提条件

  • CSSJavaScriptはライブラリも含めて1ファイルにしてしまう。
  • SCSSやTypeScriptのプリプロセッサは別物が動作しても良いようにする。もちろん、gulpでプリプロセスしてもO.K
  • 私が使っている環境はWindows10 / cygwin

gulpを使えるようにする。

Node.jsをインストール

適当なディレクトリを掘って移動

$ mkdir gulp_sample
$ cd gulp_sample

package.jsonを作る

$ npm init -y

gulpをpackage.jsonにも記載する形でインストール。

$ npm i -D gulp

gulpを実行

$ npx gulp

npxを通してgulp実行することでこのプロジェクト内でのnodeライブラリのバージョンで実行できる。npxはRailsのbundle execみたいな感じ。
npm 5.2.0の新機能! 「npx」でローカルパッケージを手軽に実行しよう - Qiita
が参考になった。

ファイル構成

こんな感じ

$ tree
.
├── gulpfile.js
├── htdocs
│   ├── css
│   ├── index.html
│   └── js
├── package.json
├── package-lock.json
└── src
    ├── index.html
    ├── javascripts
    │   ├── main.js
    │   └── vendor
    │       └── jquery-3.4.1.js
    └── scss
        ├── _base.scss
        ├── _footer.scss
        ├── _header.scss
        ├── _mixin.scss
        ├── _vars.scss
        ├── main.scss
        └── vendor
            ├── lity.scss
            ├── media-queries.scss
            ├── normalize.scss
            └── slick.scss

gulpfile.jsを作る

var gulp = require('gulp');

gulp.task('html', function (cb) {
  gulp.src('./src/*.html')
    .pipe(gulp.dest('./htdocs'));
  cb();
});
 
gulp.task('default', gulp.series('html'));

"html"というタスクを作り、そのタスクは、src/*htmlというファイルをhtdocsに出力するというだけのタスク。別にタスク名はhtmlでもhtml-compileでも構わない。

$ npx gulp

コンパイル?してくれる。

cb()の必要性

これが無いとエラーになる。
qiita.com

watchで自動gulp実行

gulp.task('html', function (cb) {
    gulp.src('./src/*.html')
      .pipe(gulp.dest('./htdocs'));
    cb();
});

gulp.task('watch', function(cb){
    gulp.watch(['./src/*.html'],
               gulp.parallel('html'));
    cb();
});

gulp.task('default', gulp.series('html', 'watch'));

taskに'watch'というのを加えて、これを実行するようにしている。
実行内容は、'./src/*.html' というファイルを監視して、それに変更があった場合にparallelでタスク"html"を実行する。タスク1個だけ指定なのでparallelの意味は無いけど。

ちなみに、最後の、「gulp.task('default', gulp.series('html', 'watch'));」に'html'を入れているのは最初の1回はファイル監視対象外になってしまうので1回は初期時に入れる必要がある。seriesは直列実行、parallelは並列実行。

ブラウザのlive reloadを利用したい

gulpでサーバーを立てて、ファイルを監視してファイルが変更あったら自動的にリロードするやつ。

gulp-connectをインストールしてgulpfile.jsを記述

$ npm i -D gulp-connect
// connectを使えるようにして
var connect = require('gulp-connect');
// タスクを登録
gulp.task('connect', function(done) {
  connect.server({
    root: 'htdocs',
    livereload: true
  }, function () { this.server.on('close', done) });
});
// initに加える
gulp.task('default', gulp.series('html', 'watch', 'connect'));

デフォルトポートは8080なので、http://localhost:8080/ でアクセスできるようになる。しかし、livereloadが効かない。

livereloadを効かす

なんか、Gulp4とgulp-connectの相性が悪いらしい。
github.com で、この方のコメントを参考に直してみる。

要は、srcディレクトリを監視して、リロードするというタスクを加える。

var gulp = require('gulp');
var connect = require('gulp-connect');

gulp.task('html', function (cb) {
    gulp.src('./src/*.html')
      .pipe(gulp.dest('./htdocs'));
    cb();
});

gulp.task('watch', function(cb){
    gulp.watch(['./src/*.html'],
               gulp.parallel('html'));
    cb();
});

gulp.task('connect', function(done) {
  connect.server({
    root: 'htdocs',
    livereload: true
  }, function () { this.server.on('close', done) });
 
  gulp.watch(['htdocs/*'], gulp.parallel('live-reload'));
});
gulp.task('live-reload', function(done){
    gulp.src('htdocs/*').pipe(connect.reload());
    done();
});

gulp.task('default', gulp.series('html', 'watch', 'connect', 'live-reload'));

なんか、 gulp.watch(['htdocs/'], gulp.parallel('live-reload'));とgulp.src('htdocs/')とで同じパスを指定しないといけないのが辛いなー。参照元の人は、filepathをonchangeで手に入れてるのが何とかならんかな…

ということで、ちょっと見直してこんな感じ。

var gulp = require('gulp');
var connect = require('gulp-connect');

gulp.task('html', function (cb) {
    gulp.src('./src/*.html')
      .pipe(gulp.dest('./htdocs'));
    cb();
});

gulp.task('watch', function(cb){
    gulp.watch(['./src/*.html'],
               gulp.parallel('html'));
    gulp.watch('htdocs').on('change', (filepath)=>{
        gulp.src(filepath, { read: false }).pipe(connect.reload());
    });
    cb();
});

gulp.task('connect', function(done) {
  connect.server({
    root: 'htdocs',
    livereload: true
  }, function () { this.server.on('close', done) });
});

gulp.task('default', gulp.series('html', 'watch', 'connect'));

いい感じになった。

JavaScriptの圧縮

JavaScriptをMinifyするgulp-uglifyと、複数ファイルをまとめるgulp-concat、JavaScript/SCSSの誤りでwatchが停止しないようgulp-plumber

$ npm i -D gulp-uglify gulp-plumber gulp-concat

gulpfile.jsに追加

var uglify      = require("gulp-uglify");
var plumber     = require("gulp-plumber");
var concat      = require("gulp-concat");
var sourcemaps  = require("gulp-sourcemaps");

gulp.task('javascript', function(cb){
    gulp.src(['./src/javascripts/vendor/**/*.js', './src/javascripts/*.js'])
      .pipe(plumber())
        .pipe(concat('dist.js'))
          .pipe(uglify())
            .pipe(gulp.dest("htdocs/js/"));
    cb();
});
// (略)
gulp.task('watch', function(cb){
    gulp.watch(['./src/**/*.html', './src/js/main.js'],
               gulp.parallel('html', 'javascript'));
    gulp.watch('htdocs').on('change', (filepath)=>{
        gulp.src(filepath, { read: false }).pipe(connect.reload());
    });
    cb();
});
// (略)
gulp.task('default', gulp.series('html', 'javascript', 'watch', 'connect'));

SCSSのコンパイルと圧縮

よく使われるやつ。

ソースマップを見たいのでgulp-sourcemapsを入れる

$ npm i -D  gulp-sourcemaps

更に、引数でsourcemapsを有効/無効にしたいので、minimistも入れる。あと、引数を元にifするのが意外とすんなりいかなかったのでこの記事を参考に、gulp-ifを使うようにした。gulp-modeもあったんだけど、廃止予定のgulp-utilを含んでいるので使わないようにした。 symfonycasts.com

$ npm i -D  minimist
$ npm i -D  gulp-if

gulpfile.jsに追加(下記はJavaScriptの所だけ)

var minimist    = require("minimist");
var gulpif      = require('gulp-if');

const options = minimist(process.argv.slice(2), {
  string: 'env',
  default: { env: 'production' }
});
const config = {
    production: options.env !== 'dev',
    sourceMaps: options.env === 'dev'
};

gulp.task('javascript', function(cb){
    gulp.src(['./src/javascripts/**/*.js'])
      .pipe(plumber())
        .pipe(gulpif(config.sourceMaps, sourcemaps.init()))
          .pipe(concat('dist.js'))
            .pipe(uglify())
              .pipe(gulpif(config.sourceMaps, sourcemaps.write()))
                .pipe(gulp.dest("htdocs/js/"));
    cb();
});

これで、以下の様にgulpを起動した時だけSourceMapが付く。

$ npx gulp --env dev

ということで、まとめ

package.js

{
  "name": "gulp2",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "gulp": "^4.0.2",
    "gulp-concat": "^2.6.1",
    "gulp-connect": "^5.7.0",
    "gulp-if": "^2.0.2",
    "gulp-plumber": "^1.2.1",
    "gulp-sass": "^4.0.2",
    "gulp-sourcemaps": "^2.6.5",
    "gulp-uglify": "^3.0.2",
    "minimist": "^1.2.0"
  }
}

gulpfile.js

var gulp        = require('gulp');
var connect     = require('gulp-connect');
var uglify      = require("gulp-uglify");
var plumber     = require("gulp-plumber");
var concat      = require("gulp-concat");
var sourcemaps  = require("gulp-sourcemaps");
var sass        = require('gulp-sass');
var minimist    = require("minimist");
var gulpif      = require('gulp-if');


const options = minimist(process.argv.slice(2), {
  string: 'env',
  default: { env: 'production' }
});
const config = {
    production: options.env !== 'dev',
    sourceMaps: options.env === 'dev'
};

gulp.task('javascript', function(cb){
    gulp.src(['./src/javascripts/**/*.js'])
      .pipe(plumber())
        .pipe(gulpif(config.sourceMaps, sourcemaps.init()))
          .pipe(concat('dist.js'))
            .pipe(uglify())
              .pipe(gulpif(config.sourceMaps, sourcemaps.write()))
                .pipe(gulp.dest("htdocs/js/"));
    cb();
});

gulp.task('sass', function (cb) {
    gulp.src(['./src/scss/**/*.scss'])
      .pipe(plumber())
        .pipe(gulpif(config.sourceMaps, sourcemaps.init()))
          .pipe(concat('style.css'))
            .pipe(sass())
              .pipe(gulpif(config.sourceMaps, sourcemaps.write()))
                .pipe(gulp.dest('./htdocs/css/'));
    cb();
});

gulp.task('html', function (cb) {
    gulp.src('./src/**/*.html')
      .pipe(gulp.dest('./htdocs'));
    cb();
});

gulp.task('watch', function(cb){
    gulp.watch(
        ['./src/**/*.html',
         './src/javascripts/**/*.js',
         './src/scss/**/*.scss',
         ],
        gulp.parallel('html', 'javascript', 'sass'));
    gulp.watch('htdocs').on('change', function(filepath){
        gulp.src(filepath, { read: false }).pipe(connect.reload());
    });
    cb();
});

gulp.task('connect', function(done) {
  connect.server({
    root: 'htdocs',
    livereload: true
  }, function () { this.server.on('close', done) });
});

gulp.task('default', gulp.series('html', 'javascript', 'sass', 'watch', 'connect'));

何か色々ととりあえずだけど。