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に依存してる時とか

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'));

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

aitendoのLEDのスペックの読み方

aitendoでLED買うときに、こんな感じで色とかのスペックが分からなくて非常に困る…

f:id:tohokuaiki:20190518133020p:plain
aitendoのLED選択

ということで、自分なりに解読メモ。

f:id:tohokuaiki:20190518132841p:plain
aitendoのLEDの読み方

頭の形
FL 平型
SH 弾頭型
頭直径
5 5mm
3 3mm
Nがある場合 頭のちょっと引っかかるところ(ハチマキ)が無い
発光色
R
B
Y 黄色
W
WW 暖色の白
G
O オレンジ
PK ピンク
PU
JG ジェードグリーン(ちょっと白みがかった緑)
レンズ色
WC 透明
D 曇りガラス
BWC 青色透明
短足
SP アノード17mm
(表記無し) アノード27mm

参考

平頭型LED(3mm/20個入) - aitendo
平頭型LED(3mm/20個入) - aitendo
平頭型LED(5mm/20個入) - aitendo
砲弾型LED(φ5mm/20個入) - aitendo
砲弾型短足LED(φ3mm/20個入) - aitendo
短足LED(20個入) - aitendo
砲弾型短足LED(φ3mm/20個入) - aitendo
短足LED(20個入) - aitendo

Windowsのコマンドラインからアプリケーションのバージョンを確認する方法

CygwinPHPから使った。

レジストリにアクセスするのはPowershellに任せている。

<?php
// IE
$ie_cmd = 'reg query "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Internet Explorer" /v "svcVersion"';
$r = shell_exec($ie_cmd);
$ie_version = '不明';
if (preg_match('/REG_SZ\s+(.*)/', $r, $m)){
    $ie_version = $m[1];
}
printf("IE: %s\n", $ie_version);

$csv = "";
$output_file = 'apps_output.csv';

$cmd=<<<EOF
Get-ItemProperty HKLM:Software\Microsoft\Windows\CurrentVersion\Uninstall\* | Select-Object DisplayName, DisplayVersion | export-csv  -NoTypeInformation -Encoding Default
Get-ItemProperty HKLM:Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\* | Select-Object DisplayName, DisplayVersion | export-csv  -NoTypeInformation -Encoding Default
Get-AppxPackage | Sort-Object Name | Select  PackageFullName, Version | export-csv  -NoTypeInformation -Encoding Default
Get-WmiObject -Class Win32_Product -ComputerName . | Select-Object Name, Version | Export-Csv -Encoding Default
EOF;

foreach (explode("\n", trim($cmd)) as $c){
    $c = trim($c);
    shell_exec(sprintf('powershell.exe "%s %s"', $c, $output_file));
    $csv .= "\r\n---------------------\r\n".file_get_contents($output_file);
}
file_put_contents($output_file, $csv);

$data = array();
if (($handle = fopen($output_file, "r"))) {
    while (($d = fgetcsv($handle, 1000, ","))) {
        $data[] = $d;
    }
}
// この$dataを調べると良い感じで。

Adobe Flash Professional CS3を久しぶりに使おうとしてインストールしたら意外と面倒だった件

久しぶりというか、多分最後

になるじゃないかなーって思うんだけど、6年くらい前に作ったFlashHTML5にしようと思って、素材を抜き出すために再インストールした。

インストール→Windows10再起動→Flash起動のアクティベーション…ができない。

電話認証もダメ。お客様のお使いの電話番号は現在…

あれー、CS2のアクティベーションサーバーが死んだのは知ってるけど…と思ったらCS3以降ももうだめだったのな。
helpx.adobe.com

ということで、アンインストールして…

こちらの記事に従って、再度インストール helpx.adobe.com

新しいシリアルを発行して、それが有効になるようなインストーラーをくれるみたい。

はー。アクティベーションサーバーの維持の方が大変とは…