tohokuaikiのチラシの裏

技術的ネタとか。

gulp4を使ってCache bustingする方法(その2・実際にQueryStringを付ける編)

やること

ということでCSSJavaScriptは1枚にまとまったのでHTMLに対してそれぞれをLinkしているタグのsrc/hrefにQueryStringを付けてやる。

<head>
  <link href="css/main.css" rel="stylesheet">
  <script src="js/main.js"></script>
</head>

<body>
  <img src="images/sample.gif">
</body>

<head>
  <link href="css/main.css?v=(main.cssのMD5値)" rel="stylesheet">
  <script src="js/main.js?v=(main.jsのMD5値)"></script>
</head>

<body>
  <img src="images/sample.gif?v=(sample.gifのMD5値)">
</body>

という感じです。

で、そもそもQueryStringを付けてうまくいくのか?

この記事 blog.open.tokyo.jp を読むと、

GETパラメータの付加は推奨されない 前記の対応のうち、GETパラメータにリビジョン番号を追加する方法はあまり推奨されません(参照:High Performance Web Sites)。 この方法は、ブラウザやWebサーバがキャッシュを利用しないため、サイトの負荷につながります。

とあった。んー、と思って調べると stackoverflow.com で同じような質問をしている人が。回答の返信の所だけど、「 stevesouders.com/blog/2008/08/23/ ではQueryString付きだとブラウザやCDNがキャッシュしてくれない」とあるよ…と。

Steve Souders: "To gain the benefit of caching by popular proxies, avoid revving with a querystring and instead rev the filename itself." The full explanation can be found here: stevesouders.com/blog/2008/08/23/… – lao Feb 18 '16 at 13:48 24

に対して「それは古い情報だ。CDNやProxyは対応している」と。CDNが対応しているならブラウザもだろう。Chromeで試すとQueryString付きのCSSは「Provisional headers are shown」と出てるのでキャッシュされている。

That blog post is approaching a decade old now. Do you think that cache providers and CDNs have yet to accommodate it? Squid seems to be able to cache documents with query strings now. – jeteon Mar 9 '16 at 22:44

で、先ほどのブログも、その根拠になっているhttps://www.amazon.co.jp/High-Performance-Web-Sites-Essential/dp/0596529309/は2007年の発行だった。

で、QueryString付きにしてくれるのがgulp-rev-append

だったのだけど、glup-rev-appendはgulp-utilに依存してるのでうーん…とりあえず、gulp-utilのFileライブラリを使ってないのでこれで試したところ、なんかイマイチだった。

というのは、変更したいimg/scriptなどには、?rev=@@hashをつけておく必要があって、「うーん、それを逐一つけるのはなぁ」という感じ。

出力側SCSSを更にCache busterしたところ

上手く動かなかった。 パッと思ったのは、無限ループになってるな…と。出力したCSSwatchしてCacsh busting処理をする…となるとまたそれがwatch対象になる。2つできてしまうのはイマイチだなー。

ということで、SCSSからのコンパイル時に引っかけるようにした。

これをあまりしたくなかった理由は、gulpのSCSSではなくデザイナーが別途SCSSコンパイラ使ってる時*1に困るなーと思ったから。コンパイル済みのCSSに対してCache Bustingしたかったため。

しかし、どう考えてもうまくいかなかったので、gulpでSCSSコンパイルすることにして、こんな感じのgulpファイルになった。

HTMLのcache buster処理はgulp-rev-appendを参考にした。

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');
var eventstream = require('event-stream');
var postcss     = require('gulp-postcss');
var cachebuster = require('postcss-cachebuster');
const crypto    = require('crypto');
const fs        = require('fs');
const path      = require('path');
const cheerio   = require('cheerio');

var options = minimist(process.argv.slice(2), {
  string: 'env',
  default: { env: 'production' }
});

const config = {
    production: options.env !== 'dev',
    sourceMaps: options.env === 'dev'
};

function htmlrev(){
    return eventstream.map(function(file, cb) {
        var $ = cheerio.load(file.contents.toString(),
                             { decodeEntities: false });
        $('link[rel="stylesheet"],script[src],img').each(function(i, ele) {
            let attrname = "";
            let hash;
            switch (ele.name.toLowerCase()){
              case 'link':
                attrname = 'href';
                break;
              case 'img':
              case 'script':
                attrname = 'src';
                break;
            }
            if (attrname) {
                let src = $(ele).attr(attrname);
                if (!src.match(new RegExp('^https?:\/\/'))) {
                    var normPath = path.normalize(src);
                    if (normPath.indexOf(path.sep) === 0) {
                        dependencyPath = path.join(file.base, normPath);
                    }
                    else {
                        dependencyPath = path.resolve(path.dirname(file.path), normPath);
                    }
                    let hasQS = dependencyPath.lastIndexOf("?");
                    if (hasQS >= 0){
                        dependencyPath = dependencyPath.substring(0, hasQS);
                    }
                    // replace src => htdocs
                    dependencyPath = dependencyPath.replace(
                        path.sep+'src'+path.sep,
                        path.sep+'htdocs'+path.sep);
                    try {
                        data = fs.readFileSync(dependencyPath);
                        hash = crypto.createHash('md5');
                        hash.update(data.toString(), 'utf8');
                        $(ele).attr(attrname,
                                    $(ele).attr(attrname) + '?rev=' + hash.digest('hex'));
                    }
                    catch(e) {
                        // fail silently.
                        console.log(e.message);
                    }
                }
            }
        });
        file.contents =
          new Buffer.from($.root().html());
        cb(null, file);
    });
}

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

gulp.task('sass', function (cb) {
    gulp.src(['./src/scss/vendor/*.scss', './src/scss/*.scss'])
      .pipe(plumber())
        .pipe(gulpif(config.sourceMaps, sourcemaps.init()))
          .pipe(concat('style.css'))
            .pipe(sass({
              outputStyle: 'compressed',
            }))
              .pipe(postcss( [
                  cachebuster({
                    type: 'checksum',
                    cssPath : '/htdocs/css',
                  }),
                  ]))
                .pipe(gulpif(config.sourceMaps, sourcemaps.write()))
                  .pipe(gulp.dest('./htdocs/css/'));
    cb();
});

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

gulp.task('watch', function(cb){
    // auto compile
    gulp.watch(
        ['./src/**/*.html',
         './src/javascripts/**/*.js',
         './src/scss/**/*.scss',
         ],
        gulp.series('html', 'javascript', 'sass'));
    
    // browser live reload
    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'));

しかし、これ重くならないかな?

*1:特にCompass使っててCompassライブラリCSSに依存してる時とか