webpackについてまとめる②

webpackについてまとめる②

はじめに

前回の記事で webpack を利用したフロントエンド開発環境の構築手順や、 webpack を使う理由についてまとめました。

また、実際に JavaScript をバンドルするところまで構築したので今回は CSS や画像を扱うところまで進めて行きたいと思います。

CSS を読み込むためのローダーをインストールする

まずは webpack で CSS や SASS を読み込めるようにローダーをインストールします。

$ yarn add -D sass sass-loader css-loader postcss-loader autoprefixer mini-css-extract-plugin

また、CSS フレームワークを読み込んだときの確認用に Bootstrap をインストールしておきます

$ yarn add bootstrap

インストールしているプラグインの用途について表にまとめました。

プラグイン名用途
sasssass-loader を利用するために必要なパッケージ。Dart Sassとも呼ばれている。
sass-loadersass を css にコンパイルするローダー
css-loadercss を JavaScript で使用できる形に変換するローダー
postcss-loaderpostcss を使用するローダー。postcss は JavaScript で CSS を変換するプラグインを作るためのツール。
autoprefixercss にベンダープレフィクスが追加された css を出力するツール。postcss 製のプラグイン
mini-css-extract-pluginJavaScript に取り込んだ css を個別の css ファイルに出力するためのローダー。このプラグインを使用した場合はstyle-loaderは不要。

注意点として、記事によってはsassではなくnode-sassを指定している場合がありますが、node-sassは非推奨のパッケージとなっています。

実際に GitHub の README にも以下のように記述されています。

Warning: LibSass and Node Sass are deprecated. While they will continue to receive maintenance releases indefinitely, there are no plans to add additional features or compatibility with any new CSS or Sass features. Projects that still use it should move onto Dart Sass.

読み込んだローダーやプラグインを追加します。

"webpack.config.dev.js"
const { merge } = require('webpack-merge');
const path = require('path');
const dist = path.resolve(__dirname, 'dist');
const commonConfig = require('./webpack.config.common.js');

+ const MiniCssExtractPlugin = require('mini-css-extract-plugin')

module.exports = merge(commonConfig, {
  mode: 'development',
  devtool: 'inline-source-map',
  devServer: {
    open: true,
    port: 9000,
    static: {
      directory: dist,
    },
  },

  module: {
    rules: [
      {
        test: /\.(sass|scss|css)$/,
        use: [
          MiniCssExtractPlugin.loader,
+         loader: 'css-loader', // css ファイルを JavaScript ファイルに変換する
+         loader: 'postcss-loader', // 変換された css に対してベンダープレフィックスを付与する処理を実行する
+         loader: 'sass-loader', // sass を css に変換する
+         // ↑ローダー末尾に指定した方から順番に実行されます。

          /*
          ローダーごとに個別にソースマップを指定できますが、
          デフォルトでは devtool の値に依存しているので、devtool で設定されていれば指定する必要はありません
          {
            loader: 'css-loader'
            オプションを指定した場合の書き方
            options: {
              sourceMap: true
            },
          },
          {
            loader: 'postcss-loader',
            options: {
              sourceMap: true
            }
          },
          {
            loader: 'sass-loader',
            options: {
              sourceMap: true
            }
          },
          */
        ],
      },
    ],
  },
+ // 各種プラグインを読み込む
+ plugins: [
+   // jsファイルとcssファイルを分割するためのプラグイン
+   new MiniCssExtractPlugin({
+     // outputオプションのfilenameと同じ動作をする
+     filename: `./css/style.[contenthash].min.css`,
+   }),
+ ]
});

ローダーの実行は末尾に指定した方から順番に実行されるので記述の順番に注意する必要があります。

module.rules allows you to specify several loaders within your webpack configuration. This is a concise way to display loaders, and helps to maintain clean code. It also offers you a full overview of each respective loader.

Loaders are evaluated/executed from right to left (or from bottom to top). In the example below execution starts with sass-loader, continues with css-loader and finally ends with style-loader. See “Loader Features” for more information about loaders order.

ローダーで指定できるオプション

各種ローダーで指定できるオプションの一覧は公式ドキュメントから確認できます。

JavaScript 同様、ソースマップを指定することでビルド後のファイルとビルド前のファイルが対応付けられデバッグがしやすくなります。ソースマップの指定は各ローダーでも指定できますが、devtoolで指定することで有効化することも出来ます。

なぜ MiniCssExtractPlugin で CSS を別ファイルに出力するのか?

webpack では CSS ファイルも JavaScript に変換してバンドルされますが、全てのファイルを一つにまとめるとファイルサイズが膨大になって読み込みに時間がかかったり、CSS のキャッシュ対策が出来かったりするデメリットがあります。

JavaScript でバンドルしたものを CSS ファイルに出力することで個別にキャッシュ対策などが出来るようになります。

MiniCssExtractPlugin のオプション

MiniCssExtractPluginプラグインで指定しているfilenameですが、webpack のエトリーポイントを設定した箇所の output.filenameオプションと同様の動作をします。

output.filenameオプションでは出力先のファイル名を指定しますが、ディレクトリ構造を作成することもでき、./js/bundle.mim.jsのように指定することも可能です。

また、出力先のディレクトリはoutput.pathで指定した先に出力されます。webpack.config.common.jsでは以下のように指定していました。

"webpack.config.common.js"
const path = require('path');
const dist = path.resolve(__dirname, 'dist');
const src = path.resolve(__dirname, 'src');

module.exports = {
  entry: './src/js/index.js',
  output: {
    path: dist, // 出力先ディレクトリ(絶対パスで指定)
    filename: './js/bundle.mim.js',
  },

  // ~ 中略 ~
}

そのため、JavaScript ファイルでインポートしたsassファイルは、distディレクトリ配下のcssディレクトリに出力されます。

出力先のファイル名に[contenthash]と指定することで、ブラウザのキャッシュ対策としてランダムな文字列を指定することが出来ます。ソースファイルに変更がされ、ビルドされるたびに新しいハッシュ値が指定されたファイルが出力されます。

sass ファイルを読み込む

実際に sass ファイルを読み込んでバンドルするところまで行っていきます。前回、src/scss/ディレクトリ配下に作成したstyle.scssファイルを修正します。

"src/scss/style.scss"
@import "~bootstrap/scss/bootstrap";

h1 {
    color: red;
}

index.jsで sass ファイルを読み込みます。

+ import '@scss/style.scss'

// console.log(a);

webpack.config.common.jsでエイリアスを指定しているので、@scssと指定するだけでソースファイルのディレクトリまでパスが割り当てられます。

テンプレートの HTML ファイルも以下のように修正します。

"src/html/index.html"
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div class="container">
        <h1>Hello! webpack!</h1>
    </div>
</body>
</html>

開発用サーバを起動して css ファイルが出力され、スタイリングが適応されていることを確認します。

add-style

また、ハッシュ値が記述された css ファイルが読み込まれていることが分かります。

read-bundle-css01

ソースとなる sass ファイルの内容が変わったときに、ハッシュ値も変化しているのか確認してみます。style.sassを以下のように修正します。

"src/scss/style.scss"
@import "~bootstrap/scss/bootstrap";

h1 {
    color: red;
+   text-align: center;
}

ブラウザをリロードして読み込まれている css ファイルを確認します。

read-bundle-css02

バンドルされた css のハッシュ値が変わっていることが分かります。

ソースマップを確認する

JavaScript ファイルでもソースマップを確認しましたが、sassからcssに変換されたファイルのソースマップを確認します。

デベロッパーツールからHello! webpack!という赤文字にフォーカスを当てます。

source-map-css01

style.scss:3をクリックすると、元ファイルの何行目に書かれたスタイリングなのか確認することが出来ます。

_reboot.scss:93というのは読み込んだ Bootstrap で使用されているsassファイルになります。

devtool: 'inline-source-map'falseに変更すると、JavaScript のときと同様にバンドルされたファイルを参照するようになるので、CSS フレームワークを使用していた場合に自分のソースファイルと区別がしづらくなってしまいます。

出力先のフォルダをクリーンアップする

ソースファイルのstyle.scssが変更される度にハッシュ値が割り当てられてたバンドルファイルが出力されることを確認しましたが、yarn run devを実行する度に以前のファイルが残った状態になっています。

remain-css-file

一度出力先のディレクトリに残っているファイルを削除してから新しいファイルを出力するようにclean-webpack-pluginを導入します。

$ yarn add -D clean-webpack-plugin

webpack.config.common.jsを編集します。

"webpack.config.common.js"
// ~ 中略~

+ const { CleanWebpackPlugin } = require('clean-webpack-plugin');

module.exports = {

  // ~ 中略 ~

  // 各種プラグインを読み込む
  plugins: [
    // HTMLのテンプレートファイルからバンドルされたモジュールを読み込んだHTMLファイルを出力する
    new HtmlWebpackPlugin({
      template: './src/html/index.html',
      filename: 'index.html',
    }),
    // 指定した出力ディレクトリ内のファイルをクリーンアップ(削除)する
+   new CleanWebpackPlugin(),
  ],
}

new CleanWebpackPlugin()で引数を指定しない場合は、デフォルトで出力先で指定したディレクトリ配下の全てのファイルが削除されます。

distディレクトリ配下で使用するjsonファイルなど、削除したくない場合のファイルがある場合は以下のように引数にオブジェクトを指定します。

new CleanWebpackPlugin({
  cleanOnceBeforeBuildPatterns: ['**/*', '!*.json', '!directoryToExclude/**'],
});

指定できるパターンも複数種類があるので一度ドキュメントを確認してみて下さい。

ビルドを実行すると、先程複数あったバンドルされた css ファイルが 1 つになっていると思います。

webpack-dev-server 実行時にはバンドルファイルは出力されない

開発用サーバ実行時に起動されたブラウザではバンドルされたファイルを読み込んでいますが、distディレクトリ配下にバンドルされたファイルは出力しないので注意が必要です。

開発用サーバ実行時でもファイルを出力したい場合は、ターミナルを複数開いてビルドコマンドを実行します。

ベンダープレフィックスを付与する

ベンダープレフィックスとはベンダー接頭辞のことで、試験的及び非標準な CSS プロパティに対して付けられます。各ブラウザで css が正しく適応されるようにベンダープレフィックスを付与します。

ベンダープレフィックスの種類

ベンダープレフィックス対応ブラウザ
-webkit-Google Chrome、Safari
-moz-Firefox
-o-古い Opera
-ms-Internet Explorer、Microsoft Edge

postcss.config.js を作成する

postcss-loaderを読み込んだので設定ファイルを追加します。トップディレクトリにpostcss.config.jsを作成して以下の用に記述します。

"postcss.config.js"
module.exports = {
  plugins: [require('autoprefixer')],
};

autoprefixerは指定したブラウザで必要なベンダープレフィックスのみを出力してくれます。ブラウザの指定の仕方は.browserslistrcファイルを作成するか、package.jsonbrowserslistを指定する方法がありますが、今回は.browserslistrcファイルを作成して以下の設定内容を記述します。

".browserslistrc"
last 4 version

これは過去 4 つ前までのバージョンのブラウザをターゲットにしていることを意味します。Browserslistというサイトで構文がどのブラウザターゲットになっているのか確認することが出来ます。

last 4 versionでターゲットになっているブラウザは以下のようになります。

browserslist-suppor01

一度、適当なスタイリングを追加して動作しているか確認します。

"/src/scss/style.scss"
@import "~bootstrap/scss/bootstrap";

h1 {
    color: red;
    text-align: center;
+   user-select: none;
}

user-selectはユーザーがテキストを範囲選択できるかどうかを制御する css プロパティです。開発用サーバを起動してプロパティを確認します。

browserslist-suppor02

ベンダープレフィックスが記述された css プロパティが割り当てられていることが分かります。

not dead の設定もしたほうが良い

last 4 versionで指定したブラウザだと、IE11、IE10、などサポートが終了したブラウザも含まれています。not deadを指定することで、24 ヶ月以上公式サポートやアップデートがないブラウザを除外することができます。

現在は、IE 11、IE Mobile 11、BlackBerry 10、BlackBerry 7、Samsung 4、Opera Mobile 12.1、および Baidu の全バージョンが含まれています。

.browserslistrcを以下のように修正します。

".browserslistrc"
last 4 version and not dead

複数の条件を記述する場合はandで繋げます。not deadを追加した場合のターゲットブラウザは以下のようになります。

browserslist-suppor03

IE などが消えていることが分かります。css プロパティも確認します。

browserslist-suppor04

-ms-というベンダープレフィックスが付与された css プロパティが無くなっていることが確認できました。

.browserslistrcで設定できるターゲットブラウザは様々な範囲を指定できるので、一度Browserslistで確認してみて下さい。

設定項目内容
> 5%全世界で 5% 以上使用されているブラウザ
>= 5% in JP日本で 5%以上使用されているブラウザ
> 0.2% and not dead全世界で 0.2% 以上使用されて且つ更新が停止していないブラウザ

画像ファイルを読み込む

ここからは画像ファイルを読み込めるように設定していきます。

webpack5 からAsset Modulesが導入されました。Asset Modules は、ローダーを追加することなく画像やアイコンなどを利用できるようにするためのモジュールです。

webpack5 以前でファイルを読み込むために使用していたローダーの一覧です。

モジュール名用途
file-loaderファイルを文字列としてインポートする
url-loaderファイルを DataURL としてバンドルする
raw-loaderファイルを出力ディレクトリに出力する

新しく追加されたモジュールを以下の表にまとめました。

モジュール名用途
asset/resource個別のファイルを出力し、その URL をエクスポートする。file-loader に相当する。
asset/inlineアセットのデータ URI をエクスポートする。url-loader に相当する。
asset/sourceアセットのソースコードをエクスポートする。raw-loader に相当する。
assetデータ URI をエクスポートするか、別のファイルを生成するかを自動的に選択する。url-loader に相当する。

実際に画像を webpack でバンドルして HTML に出力した結果です。フリー画像と webpack の SVG ファイルを読み込んでいます。必要なファイルをsrc/imagesディレクトリに追加しておきます。

asset-modules01

webpack.config.common.jsに Asset Modules に関する記述を追加します。

"webpack.config.common.js"
const path = require('path');
const dist = path.resolve(__dirname, 'dist');
const src = path.resolve(__dirname, 'src');

module.exports = {
  entry: './src/js/index.js',
  output: {
    path: dist, // 出力先ディレクトリ(絶対パスで指定)
    filename: './js/bundle.mim.js',
  },

  module: {
+   rules: [
+     // 画像ファイルをビルドフォルダにコピーする
+     {
+       // 拡張子の大文字も許容するように最後尾に i を加える
+       test: /\.(?:ico|gif|png|jpg|jpeg)$/i,
+        type: 'asset/resource',
+     },
+
+     // フォントSVGファイルなどインライン化するファイル
+     {
+       test: /\.(woff(2)?|eot|ttf|otf|svg|)$/,
+       type: 'asset/inline'
+     },
+   ],
+ },

  // ~ 中略 ~
}

テンプレートの HTML ファイルを修正します

"src/html/index.html"
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
  <div class="container">
    <h1>Hello! webpack!</h1>
+   <div id="root"></div>
  </div>
</body>
</html>

index.jsファイルを画像を読み込む処理と、imgタグなどを作成する記述を追加します。console.logの記述は不要なので削除しておきます。

import '@scss/style.scss'

- // console.log(a);

+ // @imagesはエイリアスで指定したパス
+ import webpackLogo from '@images/webpack.svg'
+ import cat from '@images/cat.jpg'

+ const logo = document.createElement('img');
+ logo.src = webpackLogo;

+ const image = document.createElement('img');
+ image.src = cat;

+ const app = document.querySelector('#root');
+ app.append(logo, image);

開発用サーバを起動後、別のターミナルでビルドを実行します。

$ yarn run start

# 別のターミナルを起動して実行
$ yarn run dev

読み込んだ jpg ファイルがdistディレクトリに出力されて、svg ファイルが Data URI でエクスポートされていることが分かります。

asset-modules02

asset-modules03

asset/inline について

asset/inlineでは、リソースは base64 形式でバンドルされた JavaScript ファイルに出力されるので、SVG などファイルサイズが小さくファイル数が多い場合、JavaScript ファイルにひとまとめにしたほうが通信コストを抑えることが出来ます。

開発モードでバンドルされたファイルを確認してみても、Data URI が読み込まれていることが確認できます。

asset-inline01

asset/resource について

asset/resourceでは特に指定しない限り、デフォルトでは[hash][ext][query]というファイル名で出力ディレクトリ直下に出力されます。

  • [hash]:元ファイルから計算したハッシュ値
  • [ext]:元ファイルの拡張子で、.始まりの値
  • [query]:?始まりのクエリ値

webpack の出力先の指定でassetModuleFilenameを追加するか、asset/resourcegenerator.filenameでモジュールタイプでのみ機能するように指定することが出来ます。

"webpack.config.common.js"
const path = require('path');
const dist = path.resolve(__dirname, 'dist');
const src = path.resolve(__dirname, 'src');

module.exports = {
  entry: './src/js/index.js',
  output: {
    path: dist, // 出力先ディレクトリ(絶対パスで指定)
    filename: './js/bundle.mim.js',
    // こっちに記述してもOK
    // [name]は元のファイル名
    // assetModuleFilename: 'images/[name].[hash][ext][query]'
  },

  module: {
    rules: [
      // 画像ファイルをビルドフォルダにコピーする
      {
        // 拡張子の大文字も許容するように最後尾に i を加える
        test: /\.(?:ico|gif|png|jpg|jpeg)$/i,
        type: 'asset/resource',
+       generator: {
+         // [name]は元のファイル名
+         filename: 'images/[name].[hash][ext][query]'
+       }
      },

      // フォントSVGファイルなどインライン化するファイル
      {
        test: /\.(woff(2)?|eot|ttf|otf|svg|)$/,
        type: 'asset/inline'
      },
    ],
  },

  // ~ 中略 ~
}

ファイルを出力し直してみます。

$ yarn run dev

asset-modules04

指定したディレクトリ構造で出力されました。

asset について

assetを指定するとバンドルするファイルサイズが指定した値より大きければasset/resourceで別ファイルとして出力し、小さければ Data URI として JavaScript ファイルに組み込む動作をします。

ルールの記述方法はRule.parser.dataUrlConditionという項目を参考にしてみて下さい。

おわりに

今回は、webpack で CSS ファイルや画像を読み込む設定についてまとめました。

次回は JavaScript に関するローダーや Linter、コードフォーマッターについてまとめていこうと思います。

参考記事