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

webpackでfile-loaderとhtml-loaderを使用しており、バンドルする際に画像と動画で`outputPath`と`publicPath`を分岐させたかった。
それを実現するためにやったことのまとめ。

※ webpackやローダーのインストール方法や説明は特にしていません。

## 実行環境
- macOS Catalina v10.15.7
- node.js v12.18.1
- npm v6.14.5

## webpackなどのバージョン
- webpack v4.43.0
- webpack-cli v3.3.11
- file-loader v6.0.0
- html-loader v1.1.0
- html-webpack-plugin v4.5.1

## やりたいこと
file-uploaderとhtml-loaderを使用して、htmlファイルで使用する画像もwebpackでバンドルしている。
バンドル前は画像と動画でディレクトリを分けていたが、バンドル後には一つのディレクトリにまとめて出力されていた。
これを、バンドル前のディレクトリ構成と同じ状態で出力したい。

**これを**
![webpack-img01.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275589/a607f929-00fa-c872-d30c-969d1ed14164.jpeg)

**こうしたい**
![webpack-img02.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275589/e03d181b-81fb-7ea1-1b3e-15da2ea75cc2.jpeg)

### ディレクトリ構成
以下のように出力したい

```
.
├── package.json
├── public // 出力先
│   ├── index.html
│   ├── js
│   │   └── bundle.js
│   ├──images
│   │   └── 画像ファイル
│   └──videos
│        └─ 動画ファイル
│
├── src // 編集ファイルのディレクトリ
│   ├── index.html
│   ├── js
│   │   └── app.js
│   ├──images
│   │   └── 画像ファイル
│   └──videos
│        └─ 動画ファイル
└── webpack.config.js
```
## 解決法
#### 1.当初のwebpack設定ファイル

```webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin'); // プラグイン読み込み

module.exports = {
    mode: 'development',
    entry: './src/js/app.js', // エントリーポイント
    output: { // 出力設定
        path: path.resolve(__dirname, 'public'), // 絶対パス
        filename: 'js/[name].[contenthash].bundle.js', // 出力するファイル名
    },
    module: { // ローダー設定
        rules: [
            {
                test: /\.(jpe?g|gif|png|svg|mp4)$/, // 処理対象とするファイルの指定
                use: [
                    {
                        loader: 'file-loader',
                        options: {
                            name: '[name].[hash].[ext]',
                            outputPath: 'images', // 出力先
                            publicPath: './images' // htmlから読み込まれる際のパス
                        },
                    },
                    'image-webpack-loader',
                ]
            },
            {
                test: /\.html$/, // 処理対象とするファイルの指定
                loader: 'html-loader',
            },
        ]
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: './src/index.html', // テンプレートとなるHTMLを指定(画像バンドルのため)
        }),
    ]
}
```
file-loaderの設定で`outputPath: 'images'`としているので、出力するとimagesディレクトリに画像ファイルも動画ファイルもまとまって出力される。これを**画像はimagesディレクトリに、動画はvideosディレクトリに分岐させる**ことが今回の目標。

また、`publicPath: './images'`としているので、htmlに挿入される際に、このままでは画像ファイルや動画ファイルへのパスが`./images/ファイル名`となってしまうので、仮に`images`と`videos`ディレクトリに出力を分岐できたとしても、`videos`配下の動画ファイルを読み込目なくなるので、publicPathも調整する必要がある。

#### 2.outputPathとpublicPathの分岐
[webpackの公式ドキュメントのfile-loaderについてのページ](https://webpack.js.org/loaders/file-loader/
)を見ると、`outputPath`と`puclicPath`のとり得る値は、`Type: String|Function`となっているので、関数を使用できることがわかる。
なので、`outputPath`と`publicPath`の値には、**対象のファイルが置かれているディレクトリに応じて、出力先を分ける処理**を追加すればよい。

https://webpack.js.org/loaders/file-loader/

**イメージ**

```js
options: {
    name: '[name].[hash].[ext]',
    outputPath: function() {
        if (処理する画像がimagesディレクトリにある) {
            return 'images'
        } else {
            return 'videos'
        }
    },
    publicPath: function() {
        if (処理する画像がimagesディレクトリにある) {
            return './images'
        } else {
            return './videos'
        }
    },
},

```
#### 2.修正後のwebpack設定ファイル

```webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin'); // プラグイン読み込み

module.exports = {
    mode: 'development',
    entry: './src/js/app.js', // エントリーポイント
    output: { // 出力設定
        path: path.resolve(__dirname, 'public'), // 絶対パス
        filename: 'js/[name].[contenthash].bundle.js', // 出力するファイル名
    },
    module: { // ローダー設定
        rules: [
            {
                test: /\.(jpe?g|gif|png|svg|mp4)$/, // 処理対象とするファイルの指定
                use: [
                    {
                        loader: 'file-loader',
                        options: {
                            name: '[name].[hash].[ext]',
                            outputPath:(url, resourcePath) => {
                                // 処理するファイルの絶対パスに「images」にマッチする文字列があるか判定
                                if (/images/.test(resourcePath)) { 
                                    return 'images/' + url;
                                } else {
                                    return 'videos/' + url;
                                }
                            },
                            publicPath:(url, resourcePath) => {
                                // 処理するファイルの絶対パスに「images」にマッチする文字列があるか判定
                                if (/images/.test(resourcePath)) {
                                    return './images/' + url;
                                } else {
                                    return './videos/' + url;
                                }
                            },
                        },
                    },
                    'image-webpack-loader',
                ]
            },
            {
                test: /\.html$/, // 処理対象とするファイルの指定
                loader: 'html-loader',
            },
        ]
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: './src/index.html', // テンプレートになるHTML
        }),
    ]
}
```
ドキュメントに従って、`outputPath`と`publicPath`の値に関数を記述して、if文でoutputPathとpublicPathを分岐させることができた。
注意点としては、デフォルトでoutputPathやpabulicPathを指定する際には、ディレクトリ名のみを設定すればよいが、今回の場合は、ファイル名まで指定しないと正しく処理されなかった。

ここでの関数は、`url`, `resourcePath`, `context`と3つの引数を取ることができる。

実際に引数を確認してみたところ

- `url` => `ファイル名`
- `resourcePath` => `ファイルまでの絶対パス`
- `context` => `プロジェクトのディレクトリまでの絶対パス`

という内容が渡ってきていた。
この引数を駆使すれば、他にも目的に応じた処理をすることができそう。

##