JavaScript
laravel
webpack
Laravel-Mix

Laravel-mixで、importした画像ファイルをディレクトリ構造を維持してコンパイルする

いろいろ試して、やっとできたのでメモ。
これよりいい方法があるよ!て方、ぜひ教えてください・・・。

環境

OS
- Windows10

バージョン
- node:v8.9.3
- npm:5.6.0
- laravel-mix:2.1.11

なぜ、ディレクトリ構造を維持したい?

デフォルトの設定のままだと、publicディレクトリ内にimagesディレクトリが作られ
そこに画像がフラットに展開されます。
Vue.jsで開発してるのですが、一つのコンポーネント内に
ディレクトリ違いで同じファイル名の画像ファイルをimportしてコンパイルすると
片方の画像しか表示されないのに気づいて、どうにかしたいな・・・と思ったのがきっかけ。

// これが
import logo1 from './resources/assets/images/hoge/fuga.gif'

// こうなるのを
--public
  --images
    --fuga.gif

// こうしたい!
// *images配下のディレクトリ構造を維持させたい
--public
  --images
    --hoge
      --fuga.gif

なぜ、画像がフラットに展開される?

laravel-mixの2.1系ではwebpack-rules.jsで画像ファイルのコンパイル設定を行ってて
ここでコンパイル後の画像のパスが指定されています。
webpack-rules.js

webpack-rules.jsの12行目から24行目
        // only include svg that doesn't have font in the path or file name by using negative lookahead
        test: /(\.(png|jpe?g|gif)$|^((?!font).)*\.svg$)/,
        loaders: [
            {
                loader: 'file-loader',
                options: {
                    name: path => {
                        if (!/node_modules|bower_components/.test(path)) {
                            return (
                                Config.fileLoaderDirs.images +
                                '/[name].[ext]?[hash]'
                            );
                        }

どうしよう?

webpack-rules.jsの設定には26行目から36行目で
npm installしたライブラリーの画像に対する処理をしてるので
この設定をまるごと無しにはしたくありませんでした。

なのでimportしてる画像を置いてあるディレクトリをデフォルトの処理から除外しつつ、
ディレクトリ構造を維持してコンパイルする設定をくわえることに。

こうしました!

ああでもない、こうでもないと色々試してたら、githubでこれだ!と思えるやり方を発見。
extendメソッドを使って、既存の設定を上書きしたり設定の追加ができるっぽい。
特定のディレクトリをコンパイルの際の除外にするにはwebpackのexcludeを利用します。

Feature/override and merge rules
laravelのextend
webpackのexclude
リンクの内容を参考にwebpack.mix.jsに下記のコードを追加。
※importする画像はresources\assets\imagesにまとめています。

webpack.mix.js
const path = require('path')

/**
 * ビルド後にpublicフォルダ配下におくファイルのパスを生成
 * @param {string} filePath resouce配下のファイルのフルパス
 */
const makeDistPath = (filePath) => {
  let replacedFilePath = filePath.replace(/\\/g, '/')
  let regExpDir = new RegExp(__dirname.replace(/\\/g, '/') + '/resources/assets/', 'g')
  replacedFilePath = replacedFilePath.replace(regExpDir, '')
  return replacedFilePath + '?[hash]'
}

// このプロジェクトで使用している画像ファイルに対する処理をカスタマイズ。
// resources/assets/images配下のディレクトリ構成を引き継いでpublicに出力されるようにする
// customImagesConfigを実行することで適用されます。
mix.extend('customImagesConfig', webpackConfig => {
  // コンパイル時の設定を取得
  const { rules } = webpackConfig.module
  // laravel-mixでデフォルトで設定されている画像ファイルに対する処理からresources/assets/images配下のファイルを対象外にする
  // laravel-mixのバージョンが上がりこの正規表現が変わった場合、それに合わせる必要があります。
  rules.filter(rule => {
  // webpack-rules.jsの13行目を参考に画像ファイルに対する正規表現で該当の設定を見つける
    if (rule.test.toString() === /(\.(png|jpe?g|gif)$|^((?!font).)*\.svg$)/.toString()) {
  // excludeの設定
      rule.exclude = path.resolve(__dirname, './resources/assets/images')
    }
  })

  rules.unshift({
    test: /(\.(png|jpe?g|gif)$|^((?!font).)*\.svg$)/,
    include: [
      path.resolve(__dirname, 'resources/assets/images')
    ],
    loaders: [
      {
        loader: 'file-loader',
        options: {
          name: filePath => makeDistPath(filePath),
        }
      },
      {
        loader: 'img-loader',
        options: {
          enabled: true,
          gifsicle: {},
          mozjpeg: {},
          optipng: {},
          svgo: {}
        }
      }
    ]
  })
})
// customImagesConfigを実行
mix.customImagesConfig()

//**************
// このあとmix.js()やmix.sass()を実行
//**************