tohokuaikiのチラシの裏

技術的ネタとか。

Gulp4で画像のCacheBusterみたいな

今更だけど。

ファイル構造はこんな感じ。

gulpfile.js
gulp-functions.js
package.json
package-lock.json
htdocs/
├── css
│   └── images
├── images
└── js
src
├── javascripts
│   └── vendor
└── scss

src/scss => htdocs/css
src/javascripts => htdocs/js
へとコンパイルされる。

package.json

{
  "name": "bt-gulpfile",
  "version": "1.0.0",
  "description": "",
  "main": "gulpfile.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "private": true,
  "devDependencies": {
    "cheerio": "^1.0.0-rc.3",
    "compass-mixins": "^0.12.10",
    "event-stream": "^4.0.1",
    "gulp": "^4.0.2",
    "gulp-autoprefixer": "^6.1.0",
    "gulp-concat": "^2.6.1",
    "gulp-connect": "^5.7.0",
    "gulp-ejs": "^4.1.1",
    "gulp-htmlmin": "^5.0.1",
    "gulp-if": "^2.0.2",
    "gulp-plumber": "^1.2.1",
    "gulp-postcss": "^8.0.0",
    "gulp-sass": "^4.0.2",
    "gulp-sourcemaps": "^2.6.5",
    "gulp-uglify": "^3.0.2",
    "minimist": "^1.2.0",
    "postcss-cachebuster": "^0.1.6"
  },
  "dependencies": {},
  "browserslist": [
    "last 1 version",
    "ie 9",
    "Android 4.2"
  ]
}

gulpfile.js

var gulp        = require('gulp');
var gulpF       = require('./gulp-functions.js');
var connect     = require('gulp-connect');
var ejs         = require('gulp-ejs');
var uglify      = require("gulp-uglify");
var plumber     = require("gulp-plumber");
var concat      = require("gulp-concat");
var sourcemaps  = require("gulp-sourcemaps");
var autoprefixer= require('gulp-autoprefixer');
var sass        = require('gulp-sass');
var htmlmin     = require('gulp-htmlmin');
var minimist    = require("minimist");
var gulpif      = require('gulp-if');
var postcss     = require('gulp-postcss');
var cachebuster = require('postcss-cachebuster');
const fs        = require('fs');

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

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

var packageJson = JSON.parse(fs.readFileSync('./package.json'));

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/*.scss', './src/scss/vendor/*.scss'])
      .pipe(plumber())
        .pipe(gulpif(config.sourceMaps, sourcemaps.init()))
          .pipe(concat('style.css'))
            .pipe(sass({
              outputStyle: 'compressed',
            }))
              .pipe(autoprefixer({
                cascade: false
              }))
                .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', '!./src/**/_*.html'])
      .pipe(plumber())
        .pipe(ejs())
          .pipe(gulpF.htmlrev())
            .pipe(htmlmin({
              collapseWhitespace : false,
              removeComments : true
            }))
              .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) });
});

// only-compile
gulp.task('compile', gulp.series(
    'html', 'javascript', 'sass'));

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

gulp-functions.js

var eventstream = require('event-stream');
const cheerio   = require('cheerio');
const path      = require('path');
const fs        = require('fs');
const crypto    = require('crypto');

module.exports = {
    // htmlファイルのimg/script/linkのファイルにmd5値でリビジョンを付ける
    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('[ERROR] revision attach failed: ' + e.message);
                        }
                    }
                }
            });
            file.contents =
              new Buffer.from($.root().html());
            cb(null, file);
        });
    }
}

実行する

npxで実行

$ npx gulp watch

でファイル監視の開発モード

$ npx gulp compile

コンパイルしてくれる。

PHPでCSVファイルを書き出す時にBOMを付けて改行コードをCRLFにする

Streamってあんまり使わない…

こんな感じ。

<?php
use App\Helpers\CrlfFilter;
# Streamを開く
$stream = fopen('php://output', 'w');
 # BOMを付ける
fputs($stream, pack('C*',0xEF,0xBB,0xBF));
# 改行コード変換フィルターを登録
stream_filter_register('CrlfFilter', CrlfFilter::class); 
# 登録したフィルターを適用
stream_filter_prepend($stream,'CrlfFilter', STREAM_FILTER_WRITE);
#データ書き込み
fputcsv($stream, [1,2,3];
# 終了
fclose($stream);

stream_filterって初めて使った。

ちなみにCrlfFilterは

こちらからコピペした

<?php
namespace App\Helpers;

/**
 * Class CrlfFilter
 * @package App\Helpers
 * @brief 改行コードをLFからCRLFにする
 */
class CrlfFilter extends \php_user_filter
{
    public function filter($in, $out, &$consumed, $closing) {
        while ($bucket = stream_bucket_make_writeable($in)) {
            $bucket->data = preg_replace("/\n$/", '', $bucket->data);
            $bucket->data = preg_replace("/\r$/", '', $bucket->data);
            $bucket->data = $bucket->data . "\r\n";
            $consumed += $bucket->datalen;
            stream_bucket_append($out, $bucket);
        }
        return PSFS_PASS_ON;
    }
}

webpackでSASSとJavaScriptをそれぞれ別々のファイルにコンパイルしたかっただけの人生だった

今まで、SASSを使うときは1つの.scssファイルにまとめて

$ ./node_modules/node-sass/bin/node-sass ./scss/foo.scss ./htdocs/css/foo.css --output-style compressed --source-map true &

とかやってたんだけど、JavaScriptも一緒にコンパイルしたいなと。TypeScriptじゃなくてJavaScriptなんだけど、npm installとかしたやつを一緒にコンパイルしたくなったんです。今までは、Laravel mix使ってたんで素で使うっというのが無くて…

とりあえず、package.json

{
  "name": "myproject-sass",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "prod": "NODE_ENV=production webpack --mode=production",
    "dev": "NODE_ENV=development webpack --mode=development --watch"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "css-loader": "^5.1.2",
    "mini-css-extract-plugin": "^1.3.9",
    "node-sass": "^5.0.0",
    "sass-loader": "^11.0.1",
    "webpack": "^5.25.1",
    "webpack-cli": "^4.5.0",
    "webpack-fix-style-only-entries": "^0.6.1"
  },
  "dependencies": {
    "bs-custom-file-input": "^1.3.4"
  }
}

node-sass使ってるのは、いにしえのSCSSライブラリcompassを使ってるからである。

当初、sass-loaderさえあればcss-loaderいらんやろ、CSSなんて書いて無いしって思ったんだけど、いるのね。

webpack.config.js

const path = require('path');
const MiniCSSExtractPlugin = require("mini-css-extract-plugin");
const FixStyleOnlyEntries = require("webpack-fix-style-only-entries");
const enabledSourceMap =  process.env.NODE_ENV !== 'production';

module.exports = ( env, argv ) => ({
  entry: {
      "foo_script": './src/js/foo.js',
      "foo_style": './src/scss/foo.scss'
  },
  output: {
    filename: 'js/[name].js',
    path: path.resolve(__dirname, 'htdocs')
  },
  module: {
    rules: [
        {
          test: /\.(sa|sc|c)ss$/,
          use: [
              {
                loader: MiniCSSExtractPlugin.loader,
              },
              {
                loader: "css-loader",
                options: {
                  url: false,
                  sourceMap: enabledSourceMap
                }
              },
              {
                loader: "sass-loader",
                options: {
                  sourceMap: enabledSourceMap
                }
              }
              ]
        }]
  },
  plugins: [
    new FixStyleOnlyEntries(),
    new MiniCSSExtractPlugin({
      filename: "./css/[name].css"
    })
  ],
  devtool: "inline-source-map",
});

ハマりポイント

  • devtoolを付けないと、sourceMapが有効にならなかった。
  • 本当は、foo.js foo.cssってしたかったんだけど、するとentryに同じキーになるのでできなかった。
  • じゃあ、outputのfilenameでfoo.css/foo.jsってそれぞれ指定すればいいじゃんって思ったんだけど、そうするとエラーが出てうまくいかなかった。多分、CSSにする際に一旦JavaScriptを経由してて、そのJavaScriptのファイル名が被ってしまうせいだと思う。
  • 環境変数NODE_ENVは、NODE_ENV=development ってpackage.jsonで指定しないといけなかったのを知らんかった。

これで、npm run prod/devで快適にSCSSとJavaScriptが書ける。・

コンストラクタでPHPのクラスの確認をして、interfaceでなくてもnewできないクラスを作る

interface指定するとプロパティが使えなくなってしまうのが嫌なんですよ。

ということで、selfとget_classを使ってこんな感じ。

<?php
namespace Foo;
class ArgsP // このクラスはインスタンスを作らせたくない。
{
    function __construct(... $args){
        if (self::class === get_class($this)){
            throw new \Exception('クラスは作れません。');
        }
    }
}
class ArgsC extends ArgsP
{
    function __construct(... $args){
        parent::__construct(... $args);
    }
}
$a = new ArgsC(1, 2, 3); // O.K
$b = new ArgsP(1, 2, 3); // 例外発生

PHPの可変長引数について、コンストラクタでそのまま渡せるのか確かめた

要するに、クラスを継承した時に引数の呼び出し方を変えたくないということなんだけど。

親クラスの __constructの引数の実装を知らなくても良いようにしたいということ。

ダメだったケース

<?php
class ArgsP
{
    function __construct(... $args){
        var_dump($args); 
    }
}
class ArgsC extends ArgsP
{
    function __construct(... $args){
        parent::__construct($args);
    }
}
new ArgsC(1, 2, 3);

ってすると、

array(1) {
  [0] =>
  array(3) {
    [0] =>
    int(1)
    [1] =>
    int(2)
    [2] =>
    int(3)
  }
}

ってなってしまった。当たり前やん。

呼び出す時も... を使った可変長引数を使えばO.K

こんな感じ。

<?php
class ArgsP
{
    function __construct(... $args){
        var_dump($args); 
    }
}
class ArgsC extends ArgsP
{
    function __construct(... $args){
        parent::__construct(... $args);
    }
}
new ArgsC(1, 2, 3);

これで結果が

array(3) {
  [0] =>
  int(1)
  [1] =>
  int(2)
  [2] =>
  int(3)
}

ってなる。やった~。