やること
ということで、CSSとJavaScriptは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したところ
上手く動かなかった。 パッと思ったのは、無限ループになってるな…と。出力したCSSをwatchして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'));
しかし、これ重くならないかな?