🤪

やっぱりwebpackがわからない(エピソード2)

2022/02/05に公開

はじめに

やっぱりwebpackがわからない(エピソード1)からの続きです。そもそもnpmからわからないも参考にしてください。

ローダー(Loader)とは

webpackは、基本的にはJavaScriptのデータしか扱うことができません。そこで、CSSや画像などをJavaScriptのオブジェクトにして、webpackで扱えるようにするモジュールがローダーです。リソースそれぞれに、ローダーが存在します。

CSSはともかく、画像をJavaScriptにするというのは何だか不思議な話でもありますが、画像も基本的にはコンピューターのデータですので、JavaScriptで使用できるデータに変換することは可能です。

ローダー及びバンドルの注意点

これらのリソースをJavaScriptのデータにしてどうするかですが、webpackはモジュールバンドラーですので、もちろんこれらをJavaScriptのデータとして1つにまとめます。ただし、ここで注意点があるのですが、何でもかんでもバンドルするのは好ましくありません。

確かにデータをまとめて通信回数を減らすことにより、パフォーマンス(表示速度)をあげることができるのですが、バンドルは諸刃の剣であって、代わりにデータ量が増えます。JavaScriptにまとめるので、JavaScriptの容量が増えるのはもちろんですが、バンドルするCSSや画像などのデータは、元の容量より変換後の容量の方が多くなりがちです。 では、なぜバンドルするのかですが、それでも複数回通信するよりかはパフォーマンスがいいからです。だたし、これは場合によりけりとなります。例えば、多数の小さな画像を複数回通信するのなら、JavaScriptにバンドルしたほうがパフォーマンスにいいですが、大きな画像を1つ通信するのにバンドルすると、その1回の通信コストより、データ量のコストの方が大きくなってしまいます。これらを見極めるのが技術者の腕の見せ所でもありますが、webpackである程度設定することも可能です。

CSS

では、代表的なローダーであるcss-loaderを使用して、ローダーの使い方を説明します。

CSSをwebpackで扱えるようにするには、cssをJavaScriptにバンドルするcss-loaderと、バンドルされたCSSをHTMLでスタイルシートとして読み込まれるようにするstyle-loaderが必要になります。

まずは、インストールをしましょう。

$ npm i -D style-loader css-loader

webpack.config.js

moduleプロパティを追加しルールを設定します。

module.exports = {
  module:{
    rules:[
      {
        test:/\.css$/,
        use:['style-loader','css-loader']
      }
   ]
  }
}

前回の続きから記述すると、次になります。

const path  = require('path');

module.exports = {
  context: path.join(__dirname, "src"),

  entry: `./index.js`,

  output: {
    path: path.join(__dirname, "dist"),
    filename: "main.js"
  },

  module:{
    rules:[
      {
        test:/\.css$/,
        use:['style-loader','css-loader']
      }
   ]
  },
};

さて、本コンテンツの題名「やっぱりwebpackがわからない」ですが、わからなくなるのはここからです。 この辺りからwebpack.config.jsの記述が複雑になってきます。

この設定が、どのような構造になっているかがわかりますか?エピソード1でも説明しましたが、webpackの設定はどこまでいってもプロパティに値を設定しているだけとなります。

この例でいうと、以下となります。1つずつじっくりと確認してみてください。

  • moduleオブジェクトのexportsプロパティにmoduleプロパティを設定している。
  • そのmoduleプロパティに配列としてrulesプロパティを設定してオブジェクトを代入し、testプロパティとuseプロパティを設定している。
  • testプロパティには正規表現を設定し、useプロパティには配列を設定している。

つまり、ドット演算子で表現すると以下のようになります。

module.exports.module.rules[0].test   = /\.css$/;
module.exports.module.rules[0].use[0] = 'style-loader';
module.exports.module.rules[0].use[1] = 'css-loader';

随分とわかりやすくなったのではないでしょうか。これが分からない人はJavaScriptのオブジェクトが理解できていないので、そこから学んだほうがいいです。Gulpがどうも読めないという人も、大抵JavaScriptのオブジェクトが理解できていない場合が多いです。

では、各プロパティの説明をします。

  • module
    ローダーなどのモジュールの設定をするプロパティです。
  • module.rules
    各ローダーを設定するプロパティです。配列となっており、その各要素に各ローダーのルールを設定して行きます。
  • rules.test
    プロパティ名からは想像しにくいですが、正規表現などで該当するファイルを指定します。
  • rules.use
    使用するローダーを指定するプロパティです。

ちなみにwebpackは、このtestやらuseなどという、プログラミング初心者が遊び半分で取って付けたようなプロパティ名が多いのも、分かりにくくしている要因の1つです。つまり、webpackが分かりにくいというのは、あなたがそこそこプログラミングができるからでもありますので、安心してください。

ビルド

では、実際にファイルを用意してビルドしてみましょう。
index.htmlはビルドしませんので、直接出力先へ設置します。

./src/index.js
import './style.css';
./src/style.css
body {
  color: red;
  font-weight: bold;
}
./dist/index.html
<!doctype html>
<html lang="ja">
<head>
  <script src="./main.js"></script>
</head>
<body>
  <p>TEST</p>
</body>
</html>

これでビルドすると、index.htmlではCSSファイルを読み込んでいませんが、main.jsにバインドされているためスタイルシートが反映されます。

options

sourceプロパティでソースマップ出力の設定をしましたが、各ローダーのソースマップを含めるには、sourceMapプロパティを設定する必要があります。trueで出力、falseまたは設定なしで出力しません。

このような使用するオプションはoptionsプロパティを設置します。次のようにoptionsプロパティを用意して、sourceMapプロパティを設定しましょう。

module: {
  rules: [
    {
      test: /\.css/,
      use: [
        "style-loader",
        {
          loader: "css-loader",
          options: {
            sourceMap: true,
          }
        }
      ]
    }
  ]
},

さて、moduleプロパティにはcss-loader以外にもローダーを設定していくことになるのですが、その都度sourceMapプロパティの設定を行うことになります。したがって、developmentモードではローダーのソースマップは必要ないなどといった場合、各設定を変更しなければなりません。このような場合、様々な方法はありますが、例えば次のような方法でまとめて設定できます。

// MODE変数でmodeの値を設定する。
const MODE = "development";
// MODE変数がdevelopmentならsourceMapStatusをtrueにする。
const sourceMapStatus = MODE === "development";

module.exports = {
  mode: MODE,
  module: {
    // 省略
    options: {
      sourceMap: sourceMapStatus
    }
};

css-loaderでよく使うオプションをもう1つ、ご紹介しておきます。urlオプションです。urlオプションは、CSS内のurl()の有効無効を設定します。デフォルトではtrueとなっています。

options: {
  url: false,
  sourceMap: sourceMapStatus,
}

Sass

CSSの次はSassですよね。ウェブデザイナーからフロントエンドエンジニアまで、現在スタイルシートのコーディングには、ほとんどの方がSassを使用していると思います。

SassをCSSに変換するローダーにはsass-loaderを使用します。また、コンパイルを行うモジュールとしてsassが必要ですので、共にインストールをします。

$ npm i -D sass-loader node-sass

webpack.config.jsの設定をしていきましょう。とりあえずは、testプロパティをsassとscssファイルが読めるようにし、useプロパティで使用するローダーを設定するだけでOKです。なお、使用するローダーを設定は、後ろから順番に適用されます。

module: {
  rules: [
    {
      test: /\.(sass|scss|css)$/,
      use:['style-loader','css-loader','sass-loader']
    }
  ]
},

では、実行に必要なファイルを用意します。

./src/index.js
import './style.scss';
./src/style.scss
$color: red;
$weight: bold;

body {
  color: $color;
  font-weight: $weight;
}
./dist/index.html
<!doctype html>
<html lang="ja">
<head>
  <script src="./main.js"></script>
</head>
<body>
  <p>TEST</p>
</body>
</html>

これでビルドを行うと style.scssがCSSに変換され、かつmain.jsにバインドされます。

また、CSSで記述したように、ローダーに分けて設定もできます。

module: {
  rules: [
    {
      test: /\.(sass|scss|css)$/,
      use: [
        "style-loader",
        {
          loader: "css-loader",
          options: {
            url: false,
            sourceMap: true,
          }
        },
        {
          loader: "sass-loader",
          options: {
            sourceMap: true,
          }
        }
      ]
    }
  ]
},

この場合も、useプロパティの後ろから設定した順に、適用されて行きます。つまり、次の順番となります。

  1. sass-loader: SassをCSSに変換
  2. css-loader: CSSをJavaScriptにバンドル
  3. style-loader: HTMLのlinkタグにCSSを展開

CSS内の画像をバンドル

これまで、urlプロパティの値をfalseにしていましたので、CSS内のurl()は無効となり、画像は読み込まれませんでした。これをtrueにすると画像もビルドされ、それが読み込まれるのですが、JavaScriptにバンドルさせるには別の設定が必要となります。

画像をJavaScriptにバンドルするということは、画像をJavaScriptで使用可能なデータに変換しなければいけないのですが、そのために画像をbase64という形式に変換します。ここでは詳しく説明しませんが、base64はアルファベットの大文字小文字、数字、記号を含めた64文字で構成されるエンコード方式となります(実際は65文字となる)。JavaScriptは、Base64のエンコードとデコードに対応した関数を持っています。

webpack4までは、画像をBase64にエンコードして埋め込むのにurl-loaderを使用していましたが、webpack5からは搭載されているtypeプロパティで対応できます。

rulesプロパティの配列に新しいエレメントを設けて、testプロパティで対象ファイルを設定、typeプロパティの値をasset/inlineにすることにより、一括で全てのファイルをバンドルします。また、css-loaderurlプロパティをtrueするのを忘れないで下さい。

module: {
  rules: [
    {
      test: /\.(sass|scss|css)$/,
      use: [
        "style-loader",
        {
          loader: "css-loader",
          options: {
            // url()を機能させる。
            url: true,
            sourceMap: true,
          }
        },
        {
          loader: "sass-loader",
          options: {
            sourceMap: true,
          }
        },
      ]
    },
    // 追加
    {
      test: /\.(gif|png|jpg|svg)$/,
      type: "asset/inline",
    }
  ]
},

実行するsassファイルにbackground-imageプロパティを追加して、同じ階層に画像を設置しましょう。

./src/style.scss
$color: red;
$weight: bold;

body {
  color: $color;
  font-weight: $weight;
  background-image: url(img.png);
}

これで実行すると、画像がバンドルされます。

バンドルする画像を分ける

ローダー及びバンドルの注意点では、何でもかんでもバンドルするのは好ましくないと説明しました。例えば、画像をBase64にエンコードした場合、容量は約1.33倍になってしまいます。したがって、通信コストと比較して、それでもバンドルしたほうがいい小さな画像はバンドルして、通信コストより容量の増加コストが高い画像は、そのまま使用したりします。

まず説明したいのが、typeプロパティの値をasset/inlineにしてバンドルをしていましたが、これをasset/resourceにすると、画像は出力されますがバンドルはされません。つまり、画像によってasset/inlineasset/resourceかを切り替えればいいわけです。

まず、typeプロパティの値をassetと、どっちつかずの設定にします。そして、parser.dataUrlCondition.maxSizeでバンドルする最大ファイル値を設定します。つまり、これを超える画像はバンドルされません。

{
  test: /\.(gif|png|jpg|svg)$/,
  type: "asset",
  parser: {
    dataUrlCondition: {
      // これで100KB以上という設定になる。
      maxSize: 100 * 1024,
    },
  },
}

プラグイン(Plugins)

ローダーは、リソースをwebpackで扱えるようにするためのものでした。そのローダーでは実現できない機能を提供するのがプラグインです。

mini-css-extract

mini-css-extractプラグインは、バンドルしたJavaScriptからスタイルシートの箇所をcssファイルとして出力します。つまり、通常のウェブサイト制作のように、CSSファイルをlinkタグで読み込むことができます。

プラグインを使用するには、使用したいプラグインをインストールする必要があります。

$ npm i -D mini-css-extract-plugin

使い方ですが、require()でプラグインを読み込み、ローダーでプラグインを有効にします。そして、プラグインの設定を行います。

以下、わかりやすくするように、他の設定は必要最低限にしています。

// プラグインの読み込み
const MiniCssExtractPlugin = require("mini-css-extract-plugin");

module.exports = {
  entry: `./src/index.js`,

  output: {
    path: `${__dirname}/dist`,
    filename: "index.js"
  },

  module: {
    rules: [
      {
        test: /\.(sass|scss|css)$/,
        use: [
          // CSSファイルを書き出すオプションを有効にする
          {
            loader: MiniCssExtractPlugin.loader,
          },
          {
            loader: 'css-loader',
          },
	  // sassを使用しない場合は不必要
          {
            loader: 'sass-loader',
          }
        ]
      }
    ]
  },

  // プラグインの設定
  plugins: [
    new MiniCssExtractPlugin({
      // 出力先の設定
      filename: './css/[name].css',
    }),
  ],
};

では、よく使用するプラグインを2つ紹介しておきます。

html-webpack-plugin

これまで、出力先のディレクトリ内に直接htmlファイルを置いていました。もちろんですが、このように出力元と先に差がある開発は好ましくありません。そこで、htmlファイルも同じようにビルドしたいのですが、それを行うのがhtml-webpack-pluginです。対象とするhtmlファイルをビルドしてくれます。その際、JavaScriptとCSSの読み込み設定もしてくれます。

インストールします。

$ npm i -D html-webpack-plugin

同じようにrequire()でプラグインを読み込み、プラグインの設定をしています。今回は特にローダー側の設定はしません。

const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const HtmlWebpackPlugin    = require('html-webpack-plugin');

module.exports = {
  entry: `./src/index.js`,

  output: {
    path: `${__dirname}/dist`,
    filename: "index.js"
  },

  module: {
    rules: [
      {
        test: /\.(sass|scss|css)$/,
        use: [
          {
            loader: MiniCssExtractPlugin.loader,
          },
          {
            loader: 'css-loader',
          },
          {
            loader: 'sass-loader',
          }
        ]
      }
    ]
  },

  plugins: [
    new MiniCssExtractPlugin({
      filename: './css/[name].css',
    }),

    // html-webpack-pluginの設定
    new HtmlWebpackPlugin({
      // 対象のテンプレートを設定
      template: `${__dirname}/src/index.html`,
      // 書き出し先
      filename: `${__dirname}/dist/index.html`,
      // ビルドしたjsファイルを読み込む場所。デフォルトはhead
      inject: 'body'
    }),
  ],
};

templateプロパティですが、設定しないと自動でhtmlファイルが出力されます。また、src配下にindex.ejsがあれば、それを使用します。EJSの使い方はここでは説明しませんが、JavaScriptで使用するテンプレートとして、ヘッダーやフッターなどに分割して管理することが可能となります。
EJS

copy-webpack-plugin

copy-webpack-pluginは、指定したファイルをそのままコピーして出力します。これも、出力元と先を合わせるのに役立ちます。

$ npm i -D copy-webpack-plugin

以下、/src/img/内のファイルを全て/dist/img/にコピーします。

const CopyWebpackPlugin   = require('copy-webpack-plugin');

省略

plugins: [
  new CopyWebpackPlugin({
    patterns: [
      {
        from: `${__dirname}/src/img/`,
        to: `${__dirname}/dist/img/`,
      }
    ]
  }),
],

imagemin-webpack-plugin

copy-webpack-pluginはファイルを圧縮します。

$ npm i -D imagemin-webpack-plugin

各ファイル形式に対応したパッケージもインストールします。

// jpg
$ npm i -D imagemin-pngquant
// png
$ npm i -D imagemin-mozjpeg
// gif
$ npm i -D imagemin-gifsicle
// svg
$ npm i -D imagemin-svgo

詳しい説明は省略しますが、次は設定例となります。

const ImageminMozjpeg = require('imagemin-mozjpeg');

省略

plugins: [
  new ImageminPlugin({
    test: /\.(jpe?g|png|gif|svg)$/i,
    pngquant: {
      quality: '70-85'
    },
    gifsicle: {
      interlaced: false,
      optimizationLevel: 9,
      colors: 256
    },
    plugins: [
      ImageminMozjpeg({
        quality: 85,
        progressive: true
      })
    ],
    svgo: {},
  })
]

最後に

エピソード2はとりあえずここで終わりにしておきます。これ以上ここで説明してしまうと、恐らくややこしいまま、モヤモヤした気持ちで終わることになるだろうからです。エピソード2も反響があれば、エピソード3を書きたいと思います。

今までReactやらTypeScriptなどでwebpackの設定を見て「?!っ」となっていた人も、エピソード1、2を読んだ後に再度webpackの設定を見ると、まるで氷が溶けたかのようにスーっと頭の中に内容が入ってくると思います。

最後の最後に、よければ業務ができる中級者になるためのJavaScript入門(文法編)DOM編を公開しておりますので、無料公開ページだけでも読んでやってください。

Discussion