フロントエンドのビルド周りを触った

半年前になるが、年末から年始にかけて業務でフロントエンドのビルド周りを初めてがっつり触ったのでメモ。最新技術みたいな話は特に無いのであしからず。

(この「フロントエンドのビルド周り」を表す言葉って無いんだろうか)

今回書くこと

今回書いてないけどやったこと

  • vue component のビルド
  • svg を Web フォントにビルド
    • あと Cache Busting

簡単に用語の説明

Babel ?

Web ブラウザは ECMAScript という規格に準拠して JavaScript の動作環境を提供していますが、 ECMAScript にはバージョンがあり、ブラウザによってどのバージョンまで対応しているかが異なります。代表的なところでは、ES6 は Chrome / Firefox / Edge / Safari などメジャーなブラウザでは動作しますが Internet Explorer 11 では動作しません。当然新しいバージョンに従って記述できたほうが開発者としてメリットがあるわけですが、できれば多くのブラウザをカバーしたい。そういう場合に、新しいバージョンで記述された JavaScript を古いバージョンでも解釈できるように変換 (transpile) してあげるツールが Babel です。

運用としては、開発時は新しいバージョンで記述された JavaScript をソース管理し、デプロイ時に何かしら自動化ツールによって Babel をかけたものを public なディレクトリに配置する、という流れになります。

SCSS ?

CSS は辛い。その辛さを軽減させた言語です。具体的には変数が使えたり関数っぽいものを書けたり子孫セレクタをネストさせて書けたり等。Babel を使用する場合のように .scss ファイルをバージョン管理してデプロイ時に .css に変換します。似た言語として LESS や Stylus などがありますが、いまのところは Sass 一強 のようです。

Cache Busting ?

JavaScriptCSS などの静的ファイルは、無駄なリクエストを減らすためにクライアントのブラウザにキャッシュさせる戦略をとることが多いです (Web サーバーやブラウザーの設定に依存します) 。このとき、サーバー側でファイルの中身を変更したのにクライアント側にキャシュが残っており、表示が崩れたり JavaScript でエラーが発生する、ということが発生します。これを防ぐため、ファイルが変更されたときにキャッシュを破棄することを Cache Busting と呼びます。よくやる手段 (というか他の手段を知らないのですが) は、ファイル名やクエリ文字列に日付や revision などを付与して URI を変えることで、クライアントに新しいファイルであると認識させてあげる、というものです。

Build System / Task Runner

上述のように、これらを実行するためには開発したものを変換してからデプロイしてあげる必要があります。これを実現するためのツールは Gulp や Grunt などがあり、ビルドシステムやタスクランナーなどと呼ばれています (この呼び名の差異がよくわかってない、、) 。 Grunt の方は開発が止まっており (なぜか 2018/02/07 に約2年ぶりに小さな update がありましたが) 、これから始めるのであれば Gulp の方がよさそうです。ただし、近年は「そもそもタスクランナーいらなくね? npm scripts で十分じゃね?」という動きもあり、要注意です。

あらかじめ変換のためのスクリプトを記述しておき、コマンドから gulp などで実行します。あるいは gulp --watch としておくと都度コマンドを実行せずともファイルを保存したときにスクリプトが走るので、開発時はこちらを使用したりします。

JavaScript でやったこと

gulpfile.js で以下のようにします。 gulp-babelgulp-rev を通しています。これにより、 script にいた .js ファイルが変換されて public/scripts に配置されます。各種プラグインnpm -i --save-dev で入れときます。

const gulp = require('gulp');
const plumber = require('gulp-plumber');
const babel = require('gulp-babel');
const rev = require('gulp-rev');

gulp.task('babel', function () {
    return gulp.src('./scripts/*.js')
        .pipe(plumber({ errorHandler: makeErrorHandler(err => err.toString()) }))
        .pipe(babel())
        .pipe(rev())
        .pipe(gulp.dest('public/scripts'))
        .pipe(rev.manifest())
        .pipe(gulp.dest('public/scripts'));
});

Babel の設定はルートの .babelrc に書いておくと勝手に読み込まれます。

{
  "presets": ["es2015"]
}

gulp-rev は何をしてくれているのかというと、 [元のファイル名]-[ハッシュ値].js といった名前に変換し、そしてその対応表を rev-manifest.json として生成してくれます。ハッシュ値は実行の度に変わります。json の中身はこんなかんじ。

{
    "js/unicorn.js": "js/unicorn-273c2c123f.js"
}

これをサーバー側で読み込んで、変換後のファイルを拾います。たとえば Node.js であれば

let revision;
app.all('/*', function(req, res, next) {
    if(!revision) revision = JSON.parse(fs.readFileSync('rev-manifest.json', 'utf-8'));
    
    res.locals.revision = revision;
    
    next();
});

としておいて、view (ejs) で

<script src="/scripts/<%= revision['index.js'] %>"></script>

のようにします。

CSS でやったこと

CSS は Gulp -> Webpack -> SCSS トランスパイル という感じにしました。

......なんでこうしたか覚えてないんですけどね。。たしか js も Webpack にしたかったけどエントリーポイントを複数に指定しようとしてうまくいかなかったとか、そんな感じだったと思います。

Webpack

Webpack はいろんな機能があるので他のツールと並べて説明しづらいのですが、たとえば複数の css ファイルを一つにまとめる (bundle) 、スペースや改行などを削除してファイルサイズを小さくする (minify) 、さらにプラグインを入れて Babel や SCSS トランスパイルをかける、といったことも可能です。設定は JSON で記述します。コマンドから実行もできますし Gulp から呼ぶこともできます。

gulpfile.js で以下のようにします。やはり gulp-rev で Cache Busting を図ります。

const webpackConfig = require('./webpack.config');
const webpackStream = require('webpack-stream');
const rev = require('gulp-rev');

gulp.task('ws', function () {
    return webpackStream(webpackConfig, webpack)
        .on('error', makeErrorHandler(err => undefined))
        .pipe(rev())
        .pipe(gulp.dest('public/bundles'))
        .pipe(rev.manifest())
        .pipe(gulp.dest('public/bundles'));
});

webpack.config.js で以下のようにします。 gulpfile.js の中に直接 JSON を書いても構いませんがファイルの肥大化は避けたいところ。 style-loader css-loader sass-loadernpm i --save-dev で入れておきます。

var glob = require('glob');

module.exports = {
    entry: {
        css: glob.sync('./scss/*.scss')
    },
    output: {
        path: `${__dirname}/bundles/`,
        filename: '[name].bundle.js'
    },
    module: {
        rules: [
            {
                test: /\.scss$/,
                loaders: [
                    'style-loader',
                    'css-loader',
                    'sass-loader'
                ]
            }
        ]
    }
}

gulp ws を実行すると、 /scss.scss ファイルが CSS にトランスパイルされ、更にバンドルされて css-[ハッシュ値].bundle.js として 1 ファイルにまとまります。

ざざっと書いてきましたが、時間が無い中ゴリ押しで進めたプロジェクトだったので、技術選定にあまり時間を割けなかったのが正直なところ。かつ、このへんの情報を Web で調べようとすると、個別の技術については見つかるのですが横断的にまとまってる記事が少なく、いろいろ案件こなしてアンチパターン踏んでいかないと感覚が身につかないだろうなぁと思っています。