タスクランナーの利用を見直してみた

header

CSS ビルドに使っていたタスクランナーの Gulp の利用をやめてみたのでそのときに考えたことをまとめてみました。


タスクランナーとは何だったのか?

タスクランナーはその名のごとく処理 = タスクを実行します。たとえば

  • Sass の変換をしたい
  • Babel を使って JS を変換したい
  • 画像の最適化を行いたい

などの処理をタスクとして予め定義しておき、タスクを組み合わせて自動で実行できるような仕組みです。

メリット

タスクランナーで処理をタスクとして実行できるように定義することには下記のようなメリットが存在します。

  • そのプロジェクトに必要な処理をコード化し、形式知にできる
  • 形式知とすることで、処理の実行について属人性を排除できる
  • 処理の自動化で人的工数を削減が見込める

デメリット

上記のようなメリットが有る一方で、下記のようなデメリットもあります。

  • タスクランナーの設定を書くには、タスクランナーでの作法を熟知しておく必要がある
  • タスクランナーやそれから使用するツールの仕様変更にプラグインが対応するのを待つ必要がある
  • 上記により npm のパッケージを気軽に更新できなくなる可能性が出てくる

Gulp の近況について

gulp-heroimage

ここで、Gulp の最近のコミット状況を見てみましょう。
Gulp は Grunt と並んでよく使われている Node.js で動作するタスクランナーです。

gulp-contributes

2014 年は非常に活発に開発がされているのですが、ここのところはほぼ平坦で、新規機能が実装されるような大きな変化は見られません。 好意的に解釈するならタスクランナーとしての実装が枯れてきたということなのでしょうか。

また、Gulp v4 が開発中というアナウンスが 2018-01-01 にされてから現在に至るまで正式なリリースはまだされていません。 なお Gulp v4 を使用する場合は npm install gulp@4 でインストール可能です。

余談ではありますが、v4 へのプラグインも対応が進んでいるので Gulp を継続して使うのであれば、 バージョンアップすることを検討してもよいのではないかと思います。

Gulp から移行してみよう

現状やっていることの確認

まずは何が定義されているかを確認しました。 処理の大枠としては、下記のような仕組みでした。

  • 古い成果物、中間生成ファイルを掃除する
  • Sass をビルドする
  • Autoprefixer を適用する
  • CSSnano を適用する
  • SourceMap の生成
  • 実行結果の生成物のサイズを画面に表示する
  • ファイルの変更を検知したら一連のビルド処理を実行する

gulpfile 的にはこんな感じです。(一部のみ抜粋)

const path = require('path')
const gulp = require('gulp')
const gulpSass = require('gulp-sass')
const gulpAutoprefixer = require('gulp-autoprefixer')
const gulpCssnano = require('gulp-cssnano')
const gulpSize = require('gulp-size')
const gulpSourcemaps = require('gulp-sourcemaps')

gulp.task('build:style', () => {
  return gulp
    .src('./src/**/*.scss')
    .pipe(gulpSourcemaps.init())
    .pipe(gulpSass())
    .pipe(gulpAutoprefixer())
    .pipe(gulpCssnano())
    .pipe(gulpSize())
    .pipe(gulpSourcemaps.write('.'))
    .pipe(gulp.dest('./dist'))
})

gulp.task('watch:style', () => {
  gulp.watch('./src/**/*.scss', gulp.parallel(['build:style']))
})

gulp.task('default', () => {
  gulp.watch('./src/**/*.scss', gulp.parallel(['build:style', 'watch:style']))
})

意外とやってることは少なかったですね。これであれば node-sass と postcss の CLI を直接使用して、 追加のパッケージを入れれば Gulp と周辺のプラグインは使わなくても、 npm に組み込まれている機能とファイル監視などの単機能のパッケージを追加すれば事足りるのではないかと思いました。

まず廃止したもの

移行を始める前に、ほぼ使われていない機構を削除することにしました。 断捨離は不要な依存を減らすという点で大事です。

下記の2つについては、実行されてはいるものの有効に活用されていないものであったため、 必要になったタイミングでまた入れればいいという判断で、ひとまず廃止しました。

npm uninstall でサヨナラを告げます。これにより下記の要素が片付きました。

  • ✅ SourceMap の生成
  • ✅ 実行結果の生成物のサイズを画面に表示する

残ったビルド処理の Webpack への移行

では残ったビルドタスクをタスクランナーである Gulp から切り離して卒業することとします。

今回は元の処理で Sass の変換のあとに autoprefixer と cssnano が適用されているというような単純な処理であるため、 npm-script と 対応する CLI である sass と postcss を組み合わせることでも十分に対応が可能です。 仕組みをシンプルに保つという点においては、必要にならない限りはシンプルなものを選択するのが良いと思います。

しかしながら今回は、Babel や TypeScript でのトランスパイルをおこなったり、 Vue.js のシングルファイルコンポーネントを使ったりする可能性を考慮したため、 この機会に Webpack を導入してしまうことにしました。

webpack-heroimage

Webpack は モジュールバンドラと言われる区分のツールで、 JavaScript や画像、CSS などのファイルを loader と呼ばれる機構を介して、 それぞれが人まとまりになった「バンドル」を生成するツールです。

最たる例は、JavaScript を ES201x で記載し Babel を通してトランスパイルした後に、 周辺ライブラリと結合して1つの JavaScript として仕上げるといったような用途になります。

せっかくなので Webpack の活発さ具合もみてみましょう

webpack-contributes

結構コミット積まれてますね。目盛りの最大が Gulp のときの倍なので Webpack の開発はかなり長期的に活発であると言えるでしょう。

CLIパッケージと npm-scripts の活用

Webpack の活用を始める前に、Webpack を実行するための下準備をまず行います。 Webpack を使用するには当然ながら npm を介してパッケージをインストールする必要があります。

webpack は CLI として使用することが多いので、 よくあるサンプルでは npm install -g webpack webpack-cli などとして、 パッケージをグローバルにインストールする他 npx webpack などとしてパッケージの取得を行いつつ実行するなどの方法があります。

グローバルインストールを要求する CLI パッケージは README に書いて使用を促すという手もあります。 しかしながら package.json に登録しておけば、 npm install するだけで必要なすべてを揃えられるので、 取り回しがしやすいというメリットがあります。

ということで devDependencies に webpack を追加してしまいましょう。

# パッケージのインストール
npm install -D webpack webpack-cli

インストールされたCLIツールについては npm-scripts から使用すると自動でパスを通してもらえるので、そもそもグローバルにインストールする必要がなくなるというわけです。

ref: https://docs.npmjs.com/misc/scripts

{
  "scripts": {
    "webpack": "webpack"
  }
}

このように記載すると下記のコマンドで webpack を使用出来ます。

# webpack を実行する場合
npm run webpack

# 引数も指定できます
npm run webpack --help

webpack は cli が提供されているので npx からも直接使用出来ます。

# package.json に登録された webpack を実行する場合
npx webpack

# こちらも引数を指定できます
npx webpack --help

そのほか npm-scripts は npm で入れたパッケージのほか環境に存在するコマンドを実行することができるので、 ビルド処理を書いたシェルスクリプトや Node.js のスクリプトを用意しておいて、 npm-scripts から使用するというような使い方もできます。

複雑な処理を書く必要がある場合は、まず単一の機能ごとに script として分けて定義して、 それらを組み合わせた script を定義すると見通しが良くなって良さそうです。

{
  "scripts": {
    // dist を削除する処理
    "clean": "rm -rf ./dist",
    // シェルスクリプトを使ってビルドタスクを実行する
    "build:by-bash": "./path/to/script.sh",
    // Node.js を使ってビルドタスクを実行する
    "build:by-node": "node ./path/to/script.js",
    // node-sass パッケージの CLI を使って処理を実行する
    "build:style": "sass src/hoge.scss:dist/hoge.css src/fuga.scss:dist/fuga.css",
    // 単機能として定義したスクリプトをまとめて実行する
    "build": "npm run clean && npm run build:by-bash && npm run build:by-node && npm run build:style"
  }
}
# 実行方法 (clean と build:by-bash、build:by-node、build:style が実行される)
npm run build

また npm-scripts には pre<任意の名前> post<任意の名前> などの特定のキーワードをつけてスクリプトを定義すると、 <任意の名前> の実行の前後でそれぞれのコマンドを自動で実行してくれるような機能があります。

今回はこれらの機能を使うことはあえてしませんが、なれてきたら便利に使ってあげると良いでしょう。 その他にも 意外と npm-scripts は奥が深いので暇な時にドキュメントを見てみると発見がありそうです。

その他必要なパッケージのインストール

では Gulpfile の変換処理を Webpack に移していくにあたって、そのほかの必要なパッケージをインストールします。

まずファイルの操作でつかうので glob パッケージを取得します。

npm install -D glob

glob は Gulp でもおなじみの */**.ts のような指定でファイルを検索する事のできるパッケージです。

続いて Sass の変換と PostCSS (autoprefixer, cssnano) の適用を行うため下記のパッケージを追加します。

npm install -D sass-loader node-sass css-loader postcss-loader autoprefixer cssnano

****-loader は Webpack に対して JS 以外のファイルの読み込み、あるいは途中で変換をかけるときに使用します。 sass-loader は Sass の読み込み、css-loader は CSS の読み込み postcss-loader は PostCSS の適用を行うといった具合です。

最後に、結果を JavaScript ではなく CSS として出力してほしいので下記のプラグインをインストールします。

npm install -D mini-css-extract-plugin webpack-fix-style-only-entries

mini-css-extract-plugin は Webpack で処理された CSS を外部ファイルとして出力するための Webpack 公式のプラグインです。 Webpack v3 以前では extract-text-webpack-plugin というプラグインがよく利用されていましたが、 README を確認する CSS の出力に使ってはいけないということでしたので、案内に従う形となりました。

refs:

webpack-fix-style-only-entriesmini-css-extract-plugin をつかって CSS を出力した際に空の JavaScript が生成されてしまう問題を抑制します。 こちらもREADMEに記載されているものとなりますが、Issue として報告されているもののようです。(Webpack v5で修正されるらしい?)

refs:

Webpack でのビルド設定を書く

必要なものは揃ったので早速設定を書いていきます。 Webpack の設定はプロジェクトルートに webpack.config.js として記載して配置します。

ものとしてはコードを見たほうが早いと思いますのでコメントをつけつつ下記に示します。 Webpack の設定は奥が深くて大変なのですが、今回については割とシンプルに収まりました。

const path = require('path')
const glob = require('glob')
const autoprefixer = require('autoprefixer')
const cssnano = require('cssnano')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const FixStyleOnlyEntries = require('webpack-fix-style-only-entries')

// 使用する Postcss の設定
const postcssConfig = {
  plugins: [
    autoprefixer(),
    cssnano(),
  ],
}

// ソースのルートディレクトリ
const srcRoot = 'src'
// 出力先のルートディレクトリ
const distRoot = 'dist'

// src/scss/**/[a-z]*.scss にマッチするファイルを Webpack の entry にする
// entry を出力ファイル名をキーにしたオブジェクトにするとそのとおりに出力される
const entry = {}
for (const i of glob.sync(`./${srcRoot}/scss/**/[a-z]*.scss`)) {
  // 入力元が scss/**.scss なのを 出力先では css/**.css にする
  const name = i.replace(new RegExp(`\./${srcRoot}/scss/(.+?)\.scss`), 'css/$1')
  entry[name] = i
}

module.exports = {
  // 実行モードを設定 Webpack v4 から必要な項目
  mode: process.env.NODE_ENV === 'production' ? 'production' : 'development',
  entry,
  output: {
    // 出力先ディレクトリと、ファイル名を設定する
    path: path.join(__dirname, distRoot),
    filename: '[name].js'
  },
  module: {
    // entry に対して適用する処理の定義
    rules: [
      {
        // 拡張子 scss に対する設定
        test: /\.scss/,
        // 適用する loader の設定 (配列の下から順に適用されていきます)
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader',
          { loader: 'postcss-loader', options: postcssConfig },
          'sass-loader',
        ],
      },
    ],
  },
  plugins: [
    // 不要なファイルの出力を抑えるプラグイン (出力が多いので silent: true を有効化)
    new FixStyleOnlyEntries({ silent: true }),
    // CSS を出力するためのプラグイン
    new MiniCssExtractPlugin(),
  ],
  // 出力が多いので下位の処理の出力を止める設定を有効化
  stats: { children: false },
  // ビルドにキャッシュを使用する設定を有効化
  cache: true,
}

ここまで書いたら package.json の scripts を下記のようにします。

{
  "scripts": {
    // dist ディレクトリを削除する
    "clean": "rm -rf ./dist",
    // 本番用にビルドするため webpack を実行する
    "build": "npm run clean && env NODE_ENV=production webpack",
    // 開発用ビルドするため webpack を監視モードで実行する
    "dev": "npm run clean && webpack --watch"
  }
}

あとは下記のコマンドでビルドを実行することが出来ます。

# ビルドだけ行う
npm run build

# 監視モードでビルドを開始する
npm run dev

ということで一気に以下が片付きました

  • ✅ 古い成果物、中間生成ファイルを掃除する
  • ✅ Sass をビルドする
  • ✅ Autoprefixer を適用する
  • ✅ CSSnano を適用する
  • ✅ ファイルの変更を検知したら一連のビルド処理を実行する

あとは不要になった Gulp 周りのパッケージを uninstall すれば移行は完了となります。

その他やったこと

一部 CSS のセレクタ名の変更

.\-xxxxx というような記号始まりのセレクタ名で、Webpack による変換結果が正しくエスケープされない問題に遭遇しました。

css-loader の処理中でエスケープが正しく処理されないようであるという Issue は見つけたのですが、現状打てる手は無いようでした。 幸いにも、使われている箇所が少ないことと、記号始まりの名前をやめれば解決できる話であったので、名前を変更して乗り切りました。

ref: https://github.com/webpack-contrib/css-loader/issues/578

まとめ

今回は npm に備わっている npm-scripts と CLI (今回は webpack) を導入することで Gulp で行っていた処理を置き換えてみました。

依存するパッケージが減り、小回りがきくようになるかなと言うところを目論んでに取り組んだのですが、 今回についてはタスクランナーの Gulp と入れ替えで モジュールバンドラーの Webpack 入れた結果、 Webpack の設定やらプラグインやらを入れる必要が出たので、 それ自体のデメリットはあんま変わらなかったというオチとなってしまいました。

逆を言うと Gulp から Webpack を使うことになっていれば依存がその分増えていたはずですが、今回はそれは回避することが出来ましたし、 Webpack の設定もそれなりに知識が必要なので、Gulp が抜けた分総合的には学習コストを抑えることに成功したと言えるでしょう。

また Webpack を導入することで副次的に、ファイル監視モードでは差分ビルドを使用することができるようになり、 開発中のビルドの高速化が出来たのでこれはラッキーだったと言えるでしょうし、 Gulp 以上の機能を享受できる下準備ができたので結果オーライなのではないかと思います。

タスクランナーをとりあえず入れているだけになっている、もしくはあまり高度に使っていない場合は、 一度考えてもしできればやめてみるという選択をしてみるのもいいかもしれませんね。

ではでは。

関連リンク