🤢

Webpack5でCSSとJavaScriptで読み込んだ画像のパスが一致しない場合

2021/11/19に公開

まえがき

webpack のかなり古いバージョンから(v2系)一気に最新(v5系)にアップデートしたので、そのときのメモと、

CSS ファイル内で、

background-image:url("./img/me.jpg");

としたときと、 JavaScript ファイル内で、

import image from 'static_img/me.jpg'

と読み込んだ場合に、画像のパスが同じにならず CSS ファイル内の画像が読み込まれない現象が発生したので、それのメモになります。

これは v2系 のときには発生せず v5系 に上げた際に発生したので、大掛かりなアップデートをする方が対象になるかと思います。

hisasann/webpack-v5-with-file-loader

前提条件

npmモジュールのバージョンたち

このようなバージョンで試しています。

react は意図的に v16 を使っていますが、ここは v17 にしても問題ありません。

"@babel/core": "^7.16.0",
"@babel/preset-env": "^7.16.0",
"@babel/preset-react": "^7.16.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.1",
"babel-loader": "^8.2.3",
"babel-plugin-module-resolver": "^4.1.0",
"css-loader": "^6.5.1",
"file-loader": "^6.2.0",
"image-webpack-loader": "^8.0.1",
"react": "^16.13.0",
"react-dom": "^16.13.0",
"react-refresh": "^0.11.0",
"style-loader": "^3.3.1",
"url-loader": "^4.1.1",
"webpack": "^5.64.0",
"webpack-cli": "^4.9.1",
"webpack-dev-server": "^4.4.0"

webpack v2以下のwebpack.config.jsの書き方

webpack v2系 のCSS・JavaScriptの画像パスを解決してくれるサンプル

let config = {
  mode: environment,
  entry: {
    main: ['./src/index.jsx'],
  },
  output: {
    filename: '[name].js',
    path: path.resolve(__dirname, 'public'),
  },
  resolve: {
    extensions: ['.jsx', '.js'],
  },
  module: {
    rules: [
      {
        test: /\.(jpe?g|png|gif|svg)$/i,
        use: [
          {
            loader: 'file-loader',
            options: {
              name: '/images/[name].[hash:7].[ext]',
              limit: 10000,
            },
          },
        ],
      },
      {
        test: /\.css$/,
        use: [
          {
            loader: 'style-loader',
          },
          {
            loader: 'css-loader',
            options: {
              url: true,
            },
          },
        ],
      },
      {
        test: /\.[jt]sx?$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            plugins: [environment === 'development' && require.resolve('react-refresh/babel')].filter(Boolean),
          },
        },
      },
    ],
  },
  plugins: [
  ],
}

webpack v5以下のwebpack.config.jsの書き方

webpack v5系 のCSS・JavaScriptの画像パスを解決してくれるサンプル

const webpack = require('webpack')
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin')
const path = require('path')

const environment = process.env.NODE_ENV || 'development'

process.noDeprecation = true

// 共通の設定
let config = {
  mode: environment,
  entry: {
    main: ['./src/index.jsx'],
  },
  output: {
    filename: '[name].js',
    path: path.resolve(__dirname, 'public'),
  },
  resolve: {
    extensions: ['.jsx', '.js'],
  },
  module: {
    rules: [
      {
        test: /\.(png|jpg|gif)$/i,
        generator: {
          filename: 'images/[name][ext][query]'
        },
        type: 'asset/resource'
      },
      {
        test: /\.css$/,
        use: [
          {
            loader: 'style-loader',
          },
          {
            loader: 'css-loader',
            options: {
              url: true,
            },
          },
        ],
      },
      {
        test: /\.[jt]sx?$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            plugins: [environment === 'development' && require.resolve('react-refresh/babel')].filter(Boolean),
          },
        },
      },
    ],
  },
  plugins: [
  ],
}

// development環境設定
if (environment === 'development') {
  config = Object.assign(config, {
    plugins: [
      ...config.plugins,
      new webpack.HotModuleReplacementPlugin(),
      new ReactRefreshWebpackPlugin(),
    ],
    devServer: {
      // Dev server client for web socket transport, hot and live reload logic
      hot: true,
      historyApiFallback: true,
      static: {
        directory: path.join(__dirname, 'public'),
      },
    },
  })
}

module.exports = config

重要なポイント

webpack v2

v2 では file-loader を使って画像ファイルを特定のディレクトリ images にコピーしていました。

そして、そのパス含めたファイル名が CSS や JavaScript でインポートしたところで書き換えられていました。

{
  test: /\.(jpe?g|png|gif|svg)$/i,
  use: [
    {
      loader: 'file-loader',
      options: {
        name: '/images/[name].[hash:7].[ext]',
        limit: 10000,
      },
    },
  ],
},

webpack v5

v5 では v2 の書き方だと、なぜか CSS 側には file-loader の部分がうまく効かず /images/ というパスがつかない URL が指定されてしまって 404 を返してしまっていました。

(ここには大いにハマりました。)

ので、以下のように v5 での書き方に変更しました。

css-loader に options で url: true で明示的に CSS 内の url()メソッド を取り込んでいます。

ここは default value が true なので、あまり気にしなくてよいですが、 false だと url を require に置き換えてくれなくなるので、画像ファイルのロードができなくなります。

css-loader | webpack

そして type: 'asset/resource' ここを追加することで v2 で file-loader を使って画像ファイルのパスを変更した仕組みが v5 でもちゃんと動くようになります。

Asset Modules | webpack

シンプルになりましたね。

{
  test: /\.(png|jpg|gif)$/i,
  generator: {
    filename: 'images/[name][ext][query]'
  },
  type: 'asset/resource'
},
{
  test: /\.css$/,
  use: [
    {
      loader: 'style-loader',
    },
    {
      loader: 'css-loader',
      options: {
        url: true,
      },
    },
  ],
},

番外編:Fast Refresh

webpack を使っていて、さらに React で JSX を書いていると、なにか変更したら live reload ではなく HMR が走るのがありがたい時代がありました。

Hot Module Replacement | webpack

コードを変更したら自動的にロードしてくれる仕組みを少しまとめると、

  • live reload - 画面全体をリロードする
  • HMR - 変更があったコンポーネントを差し替えてくれる
    • ただし、エントリーポイントや Redux 側にコードを足す必要がある
  • Fast Refresh
    • 実際のコードには何も足さなくてよい、最高!

ということで、 Fast Refresh を試してみました。

react-refresh - npm

How should we set up apps for HMR now that Fast Refresh replaces react-hot-loader? · Issue #16604 · facebook/react

webpack の plugin

pmmmwh/react-refresh-webpack-plugin: A Webpack plugin to enable "Fast Refresh" (also previously known as Hot Reloading) for React components.

const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');

const isDevelopment = process.env.NODE_ENV !== 'production';

module.exports = {
  mode: isDevelopment ? 'development' : 'production',
  module: {
    rules: [
      {
        test: /\.[jt]sx?$/,
        exclude: /node_modules/,
        use: [
          {
            loader: require.resolve('babel-loader'),
            options: {
              plugins: [isDevelopment && require.resolve('react-refresh/babel')].filter(Boolean),
            },
          },
        ],
      },
    ],
  },
  plugins: [isDevelopment && new ReactRefreshWebpackPlugin()].filter(Boolean),
};

こんな感じで実現されるのでかなり便利です。

今回用に用意したリポジトリに実は導入しています。

hisasann/webpack-v5-with-file-loader

仕組みはこちらの zenn のスクラップに書いてありました。

React Fast Refreshを調べる

あとがき

webpack はどうやら v4 で大きく変わり、その後の v5 でも大きく変わったようで、 v2 -> v5 はなかなかググるのが大変でした。

とにかく小さくプロジェクトを作ってみて、いろいろ試してみて、ダメだったら戻すという作業を繰り返して検証しました。

そんなほんの一角の CSS と JavaScript 内の画像ファイルのパスの不一致についてメモしました。

いやはや、ほんとこれは氷山の一角なんですよ。

では、ぼくはまだある webpack の検証に戻りますね。

良い週末を!

参考資料

What is the loader order for webpack? - Stack Overflow

ちゃんと理解するWebpack5。2:Babel、画像の処理と複数バンドル

最新版で学ぶwebpack 5入門 - スタイルシート(CSS/Sass)を取り込む方法 - ICS MEDIA

webpack1からwebpack5に上げた - Qiita

【webpack】file-loaderで画像などの出力先を条件分岐させる方法 - Qiita

【React】Fast Refresh を有効にする - Qiita

wwalpha/react-refresh-typescript-example

Hot Reload から Fast Refresh に移行する - Qiita

Discussion