ちょっとがんばるWebサイト開発 on Webpack5 - 画像アセットを利用する

# 前書き

## 本記事のゴール

この記事では、Webpack開発環境において画像アセットを含むプロジェクトをビルドする方法を理解することを目指します。

## この記事で解説する内容

- Webサイトにおける画像アセットの利用目的/方法一般論について説明します
- 画像アセットを含むプロジェクトをビルドする方法を説明します
- 簡単な実例を示します

## この記事で解説しない内容

- 画像アセットの種類およびその性質については議論しません
- 画像の作成方法については説明しません

## シリーズ記事



# 背景知識

## 画像アセットはWebサイトにおいてどのように利用されるか？

画像アセットは、Webサイトへの興味喚起およびコンテンツの理解促進といった目的で多くのWebサイトに採用されています。

Webサイトの主要な構成要素はHTML/CSS/JavaScriptですが、画像アセットはHTML/CSS/JavaScriptにおいてそれぞれ以下のように利用されます。

### HTML

`<img>` タグによって生成される画像要素において利用されます。

```html
<img src="image.jpg">
```

> [参考] HTML imgタグ
> - [<img>: 画像埋め込み要素 - HTML: HyperText Markup Language | MDN](https://developer.mozilla.org/ja/docs/Web/HTML/Element/img)

### CSS

`background-image` プロパティによって描画される背景画像として利用されます。

```css
#element {
  background-image: url(image.png);
}
```

> [参考] CSS background-imageプロパティ
> - [background-image - CSS: カスケーディングスタイルシート | MDN](https://developer.mozilla.org/ja/docs/Web/CSS/background-image)

### JavaScript

JavaScript（またはJavaScriptフレームワーク）における画像アセットの利用方法は大きく2つに分類することができます。

1つ目はJavaScriptプログラムによるDOM操作・スタイル操作の中で画像アセットを間接的に利用する方法です。DOM操作において `<img>` タグを新規作成・編集する場合やスタイル操作において `background-image` プロパティを追加・変更する場合等が考えられます。

```js
// DOM操作
let parentElement = document.getElementById("element");
let imageChildElement = document.createElement("img");
imageChildElement.setAttribute("src", "image.jpg");
parentElement.appendChild(imageChildElement);

// スタイル操作
let backgroundImageElement = document.getElementById("background-image");
backgroundImageElement.style.backgroundImage = require("image.jpg");
```

> [参考] JavaScript DOM操作
> - [文書の操作 - ウェブ開発を学ぶ | MDN](https://developer.mozilla.org/ja/docs/Learn/JavaScript/Client-side_web_APIs/Manipulating_documents)

2つ目はJavaScriptフロントエンドフレームワーク（Vue.js等）のJavaScript系ファイル（`*.vue` ファイル等）のHTMLテンプレート・CSSスタイルシートの中で画像アセットを直接的に利用する方法です。実質的には1つ目の方法と等価である場合が多いと思いますが、記述方法が大きく異なります。以下にVueファイルにおける実装例を示します。

```vue
<template>
  <img v-bind:src="require('image.jpg')">
  <div id="background-image"></div>
</template>

<style>
#background-image {
  background-image: url("image.png");
}
</style>
```

## 画像アセットはWebpackビルドにおいてどのように処理されるか？

画像アセットは、基本的にはHTML/CSS/JavaScriptファイルの依存アセットとしてWebpackのビルドプロセスに取り込まれます。

Webpackはプロジェクトに含まれる多数のアセットを1つ以上のモジュールにまとめて指定ディレクトリに出力するという仕事を担っています。最も基本的な仕事は「エントリポイントに指定されたファイル群が依存するファイル群をモジュールにまとめる」ことで、主にエントリポイントとなるJavaScriptファイルとそれが依存するJavaScript系ファイル群を1つのJavaScriptモジュールに統合するために利用されています（適切な設定を加えることで、JavaScript系ファイルだけでなくHTML/CSS等のファイルも利用可能です）。JPEG/PNG等の画像アセットは、HTML/CSS/JavaScriptファイルの依存アセットとして検出されることでWebpackのビルド対象となります[^1]。

さらに、Webpackではビルド対象のファイル群に対して変換処理等を実行することができます。特にJPEG/PNG等の画像アセットに対して、可逆/非可逆圧縮および様々な変換処理を施した上でその結果を出力させることができます。画像アセットの最適化はWebサイトのパフォーマンスを改善するために必要な項目の1つですが、その問題を部分的に解決することができます。

要約すると、画像アセットはWebpackのビルドプロセスにおいて HTML/CSS/JavaScript の依存アセットとして認識され、HTML/CSS/JavaScript とともに指定ディレクトリに出力されます。このとき、適切な設定を記述することで、画像アセットの圧縮や変換を施すこともできます。

# 方法

:::note info
サンプルコードをGitHubで公開しています。記事と併せてご覧ください。
**[/website/website-dev-on-webpack5/image-asset-on-webpack5](https://github.com/Sharkkii/sample-codes/tree/develop/website/website-dev-on-webpack5/image-asset-on-webpack5)**
:::

:::note info
実装例の説明では、ファイル/フォルダパスを *ルート相対パス* の形式で記述しています。ここでルートはプロジェクトルートまたはWebサイトのドキュメントルートに相当します。
:::

## 前提

WebpackおよびSCSS等のnpmパッケージの利用には [Node.js](https://nodejs.org/ja/) が必要です。
インストールが未実施の場合には、以下のリンクからインストールしておいてください。

https://nodejs.org/ja/

また、動作確認を行うためのNode.jsプロジェクトを作成しておきます。
適当な名前のディレクトリ（ここでは `my-image-project` ）を用意した上で、そのディレクトリにおいて `npm init` コマンドを実行することでNode.jsプロジェクトを新規作成することができます。

```shell:ターミナル
# my-image-projectディレクトリの作成
mkdir /path/to/my-image-project
cd /path/to/my-image-project

# Node.jsプロジェクトの作成
npm init
```

`npm init` コマンドを実行するとプロジェクトに関する情報を入力するよう促されますが、今回は適当な設定にしておいて問題ありません。今後必要に応じて適宜変更しましょう。
コマンド実行完了後、 `/package.json` という設定ファイルが新規作成されていることを確認したら事前準備完了です。

## 手順概要

1. [npmパッケージを導入する](#手順1-npmパッケージを導入する)
Webpackの利用に必要なnpmパッケージ　`webpack` `webpack-cli` および動作確認用のHTML/CSSをビルドするのに必要なnpmパッケージ `html-webpack-plugin` `html-loader` `css-loader` `mini-css-extract-plugin` をプロジェクトに追加します。

2. [Webpackの設定ファイルを記述する](#手順2-webpackの設定ファイルを記述する)
JPEG/PNG画像アセットの処理手順やルールを設定ファイル `/webpack.config.js` に記述します。

3. [JPEG/PNGファイルを準備する](#手順3-jpegpngファイルを準備する)
JPEG/PNG画像アセット `/src/assets/images/favicon-[html,css,js].[jpg,png]` を準備します。

4. [HTML/CSS/JavaScriptファイルを作成する](#手順4-htmlcssjavascriptファイルを作成する)
動作確認用のHTMLファイル `/src/source.html` ・CSSファイル `/src/css/source.css` ・JavaScriptファイル `/src/js/source.js` を作成します。

5. [Webpackビルドを実行する](#手順5-webpackビルドを実行する)
Webpackビルドを実行することで、HTMLファイル `/dist/target.html` ・CSSファイル `/dist/css/target.css` ・JavaScriptファイル `/dist/js/target.js` および画像アセットファイル `/dist/images/favicon-[html,css,js].[jpg,png]` を出力します。

## 手順詳細

### [手順1] npmパッケージを導入する

今回はWebpackを利用して画像アセットを含むプロジェクトのビルド作業を行います。

まず初めに、Webpackを利用するのに必要な　`webpack` `webpack-cli` パッケージをインストールします。
Webpack5（version5.X.X）をインストールするため、バージョン指定を行っています。

https://webpack.js.org/

```shell:ターミナル
npm install --save-dev webpack@5 webpack-cli@5
```

次に、動作確認用HTML/CSS/JavaScriptファイルをバンドルするのに必要な `html-webpack-plugin` `html-loader` `css-loader` `mini-css-extract-plugin` パッケージをインストールします。

https://www.npmjs.com/package/html-webpack-plugin

https://www.npmjs.com/package/html-loader

https://www.npmjs.com/package/css-loader

https://www.npmjs.com/package/mini-css-extract-plugin

```shell:ターミナル
npm install --save-dev html-webpack-plugin html-loader css-loader mini-css-extract-plugin
```

### [手順2] Webpackの設定ファイルを記述する

今回は以下の2つの処理を実行するためのWebpackの設定を記述します。

- JPEG/PNG画像アセットを最適化して指定ディレクトリに出力する
  - JPEG/PNG画像 `/src/assets/images/favicon.jpg` `/src/assets/images/favicon.png` をコピー＆ペーストして `/dist/images/favicon.jpg` `/dist/images/favicon.png` に出力します。

- 動作確認用のHTML/CSS/JavaScriptを出力する
  - HTMLファイル `/src/source.html` から `/dist/target.html` を生成します。
  - CSSファイル　`/src/css/source.css` から `/dist/css/target.css` を生成します。
  - JavaScriptファイル `/src/js/source.js` から `/dist/js/target.js` を生成します。
  - HTML/CSS/JavaScriptファイル生成の際、画像アセットのパスを自動的に調整するようにします。

:::note info
実装例において、ソース画像アセットの格納場所を `/src/assets/images/` 、配信用画像アセットの出力場所を `/dist/images/` にしている点に注意してください。このようなディレクトリ構成にする理由は、「ソースHTML/CSS/JavaScriptファイルから見たソース画像アセットの相対パス」と「配信用HTML/CSS/JavaScriptファイルから見た配信用画像アセットの相対パス」が異なるような状況を作り出すためです。

こうした状況においても、Webpack（および関連プラグイン）はアセットの依存関係や相対パスを適切に解決することができるということを実装例で確認します。
:::

これを実現する必要最小限のWebpack設定を以下に示します。

```js:/webpack.config.js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");

module.exports = function() {
  return {

    mode: "development",

    entry: {
      /**
       * /src/css/source.css -> OUTPUT_DIRECTORY/css/target.css
       * /src/js/source.js -> OUTPUT_DIRECTORY/js/target.js
       */
      "css/target": path.resolve(__dirname, "src/css/source.css"),
      "js/target": path.resolve(__dirname, "src/js/source.js"),
    },

    output: {
      /**
       * [path]
       * OUTPUT_DIRECTORY = /dist
       */
      path: path.resolve(__dirname, "dist"),
      
      /**
       * [filename]
       * XXX/YYY.ZZZ -> OUTPUT_DIRECTORY/YYY.js
       */
      filename: "[name].js",

      /**
       * [assetModuleFilename]
       * XXX/YYY.ZZZ -> OUTPUT_DIRECTORY/images/YYY.ZZZ
       */
      assetModuleFilename: "images/[name][ext]",
    },

    module: {
      rules: [
        {
          test: /\.(jpg|png)$/,
          type: "asset/resource",
        },
        {
          test: /\.html$/,
          use: [
            {
              loader: "html-loader",
              options: {
                sources: {
                  list: [
                    {
                      tag: "img",
                      attribute: "src",
                      type: "src",
                    }   
                  ],
                },
              },
            }
          ]
        },
        {
          test: /\.css$/,
          use: [
            /**
             * CssLoader -> MiniCssExtractPluginLoader
             */ 
            MiniCssExtractPlugin.loader,
            "css-loader",
          ],
        },
      ]
    },
    
    plugins: [
      new HtmlWebpackPlugin({
        /**
         * /src/source.html -> OUTPUT_DIRECTORY/target.html
         */
        template: path.resolve(__dirname, "src/source.html"),
        filename: "target.html",
        inject: false,
      }),
      new MiniCssExtractPlugin(),
    ],

  }
}
```

JPEG/PNG画像アセット出力に関する設定で特に注目すべきポイントは以下の2点です。

- モジュール設定（`module.rules`）
- 出力設定（`output`）

Webpackの `module.rules` 設定では、Webpackがビルド対象とするファイルからモジュールを生成するための変換処理等を記述します。ここではJPEG/PNG画像ファイルに対して **[Asset Modules](https://webpack.js.org/guides/asset-modules/)** の設定、HTMLファイルに対して `html-loader` 、CSSファイルに対して `css-loader` を適用するよう定めています。

**Asset Modules** とは、各種アセットファイルを利用するためのWebpackの仕組みです。Webpack4までは画像・フォント等のアセットを利用するために `file-loader` `raw-loader` `url-loader` といったローダを導入する必要がありましたが、Webpack5からはnpmパッケージを追加することなく `module.rules.type` 設定を記述するだけで済むようになりました。

> [参考] raw-loader / url-loader / file-loader
> - [raw-loader | webpack](https://v4.webpack.js.org/loaders/raw-loader/)
> - [url-loader | webpack](https://v4.webpack.js.org/loaders/url-loader/)
> - [file-loader | webpack](https://v4.webpack.js.org/loaders/file-loader/)

Asset Modulesでは、`module.rules.type` 設定として以下の4種類が用意されています。

- `{ type: "asset/resource" }`: アセットをファイルとして出力します
- `{ type: "asset/source" }`: アセットを文字列として出力します
- `{ type: "asset/inline" }`: アセットをインライン化してバンドルに組み込みます
- `{ type: "asset" }`: 上記設定を自動的に決定します

> [参考] Asset Modules
> - [Asset Modules | webpack](https://webpack.js.org/guides/asset-modules/)

今回は「JPEG/PNG画像アセットファイルを指定ディレクトリ `/dist/images` に出力する」ことを目指しているので、 `{ type: "asset/resource" }` を指定しています。

一方、HTML/CSSファイルに対してはそれぞれ `html-loader` / `css-loader` を適用するよう定めています。

`html-loader` `css-loader` は、HTML/CSSファイルを（JavaScriptファイルをベースとする）Webpackのビルドプロセスで取り扱うことができるようにする非常に重要なローダーです。`html-loader` `css-loader` の重要な役割の1つにHTML/CSSファイル中に含まれる依存関係の検出があり、ここではその機能を利用して画像アセットの取り込みを実現しています。

:::note info
`html-loader` `css-loader` は、デフォルト設定の状態ではHTML/CSSのパース＋依存関係の検出を行うようになっています。今回の実装例では、HTMLファイルにおけるCSS/JavaScript読み込みの依存関係は手動で解決することにしているので、 `html-loader` に対して「画像アセット読み込み（`<img src="XXX"`）の依存関係のみ解決してほしい」という命令をしています。`html-loader` `css-loader` のオプションの詳細に関しては公式のGitHubレポジトリをご覧ください。

> [参考] html-loader / css-loader
> - [webpack-contrib/html-loader: HTML Loader #sources](https://github.com/webpack-contrib/html-loader#sources)
> - [webpack-contrib/css-loader: CSS Loader #url](https://github.com/webpack-contrib/css-loader#url)
> - [webpack-contrib/css-loader: CSS Loader #import](https://github.com/webpack-contrib/css-loader#import)
:::

`output` 設定では、ビルド出力結果のファイル名を指定します。 `output` 設定項目は数多くあるため、ここでは最も頻繁に使用する3つについて簡単に紹介します。

- `output.path`：出力ディレクトリを指定する
- `output.filename`：出力ファイル名を指定する
- `output.assetModuleFilename`：（**画像等のアセットの**）出力ファイル名を指定する

[Webpack公式サイトのoutput設定](https://webpack.js.org/configuration/output/#outputassetmodulefilename) に記述されているように、 `output.assetModuleFilename` はAsset Modulesの出力ファイル名を指定するものです。画像アセットを特別なディレクトリで管理する場合に便利だと思います[^2]。
　Webpackの出力設定は最も基本的な設定の1つですが、この記事では詳細を割愛します。詳細な情報に関しては、Webpack公式ドキュメントをご参照ください。

> [参考] Webpackの出力設定
> - [Concepts | webpack #output](https://webpack.js.org/concepts/#output)
> - [Output | webpack](https://webpack.js.org/configuration/output/)

### [手順3] JPEG/PNGファイルを準備する

手順3では、Webサイトに掲載する画像アセットを準備します。
画像変換ソフト等を利用して、2種類の形式（JPEG/PNG）の画像を作成しておきます。
ここでは、HTML/CSS/JavaScriptファイルにおける画像アセット読み込みの動作確認を行うため、オリジナル画像を複製して6枚の画像アセットファイルを作成することにしましょう。6枚の画像の配置場所は以下の通りとします（HTML/CSS/JavaScriptの実装例は手順4で示します）。

- HTML用JPEG画像：`/src/assets/images/favicon-html.jpg`
- HTML用PNG画像：`/src/assets/images/favicon-html.png`
- CSS用JPEG画像：`/src/assets/images/favicon-css.jpg`
- CSS用PNG画像：`/src/assets/images/favicon-css.png`
- JavaScript用JPEG画像：`/src/assets/images/favicon-js.jpg`
- JavaScript用PNG画像：`/src/assets/images/favicon-js.png`

### [手順4] HTML/CSS/JavaScriptファイルを作成する

手順4では、HTML/CSS/JavaScriptから画像アセットを適切に読み込むができるということを確認するために、動作確認用のHTML/CSS/JavaScriptファイルを作成します。
上記設定ファイル `/webpack.config.js` では入力HTML/CSS/JavaScriptファイルパスをそれぞれ　`/src/source.html` `/src/css/source.css` `/src/js/source.js` に指定したので、その位置に `source.html` `source.css` `source.js` ファイルを作成しましょう。

ファイル内容はどんなものでも構いませんが、ここでは例として以下のように記述することにします（折りたたみ要素の中にあります）。

<details>
<summary> /src/source.html の入力内容例</summary>

```html:/src/source.html
<!doctype html>
<html>
  <head>
    <title>My Image Project</title>
    <link rel="stylesheet" href="./css/target.css">
  </head>
  <body>
    <h1>My Image Project</h1>
    <p>&lt;img&gt;要素・PNG画像</p><img src="./assets/images/favicon-html.png" width="100" height="100">
    <p>&lt;img&gt;要素・JPEG画像</p><img src="./assets/images/favicon-html.jpg" width="100" height="100">
    <p>background-image属性・PNG画像</p><div id="png-image" class="image"></div>
    <p>background-image属性・JPEG画像</p><div id="jpeg-image" class="image"></div>
    <script src="./js/target.js"></script>
  </body>
</html>
```
</details>

<details>
<summary> /src/css/source.css の入力内容例</summary>

```css:/src/css/source.css
.image {
  width: 100px;
  height: 100px;
}

#jpeg-image {
  background-image: url(../assets/images/favicon-css.jpg);
  background-size: cover;
}

#png-image {
  background-image: url(../assets/images/favicon-css.png);
  background-size: cover;
}
```
</details>

<details>
<summary> /src/js/source.js の入力内容例</summary>

```js:/src/js/source.js
function createTextElement(text) {
  const textElement = document.createElement("p");
  textElement.textContent = text;
  return textElement;
}

function createPngImgElement() {
  const pngImage = require("../assets/images/favicon-js.png");
  const pngImgElement = document.createElement("img");
  pngImgElement.setAttribute("src", pngImage);
  pngImgElement.width = 100;
  pngImgElement.height = 100;
  return pngImgElement;
}

function createJpegImgElement() {
  const jpegImage = require("../assets/images/favicon-js.jpg");
  const jpegImgElement = document.createElement("img");
  jpegImgElement.setAttribute("src", jpegImage);
  jpegImgElement.width = 100;
  jpegImgElement.height = 100;
  return jpegImgElement;
}

document.addEventListener("DOMContentLoaded", function() {
  const bodyElement = document.body;
  bodyElement.appendChild(createTextElement("JavaScript由来<img>要素・PNG画像"));
  bodyElement.appendChild(createPngImgElement());
  bodyElement.appendChild(createTextElement("JavaScript由来<img>要素・JPEG画像"));
  bodyElement.appendChild(createJpegImgElement());
});
```
</details>

### [手順5] Webpackビルドを実行する

手順1〜4で画像アセット出力およびその動作確認のための準備が完了しました！
それではWebpackを用いて `/src/assets/images/favicon-[html,css,js].[jpg,png]` から `/dist/images/favicon-[html,css,js].[jpg,png]` を生成してみましょう。
ターミナルで `npx webpack` コマンドを実行することでビルド作業が開始します。

```shell:ターミナル
# ディレクトリ移動
cd /path/to/my-image-project

# Webpackコマンド実行
npx webpack
```

ビルドが正常に完了すれば、画像アセットファイル `/dist/images/favicon-[html,css,js].[jpg,png]` が生成されていることを確認できると思います。`/dist/target.html` をWebブラウザ等で確認すると、画像アセットの読み込みが正常に動作している様子を見ることができるはずです。

また、`/dist/target.html` `/dist/css/target.css` `/dist/js/target.js` の内容を見ると、画像アセット読み込みのパスが正しく解決されていることを確認できると思います。例えば、`/dist/target.html` では `<img>` タグが以下のように変更されていることを確認することができます。

```html:変更前（/src/source.html）
<img src="./assets/images/favicon-html.png" width="100" height="100">
```

```html:変更後（/dist/target.html）
<img src="images/favicon-html.png" width="100" height="100">
```

参考までに、`/dist/target.html` `/dist/css/target.css` の出力結果全体を載せておきます（`/dist/js/target.js` の出力結果は非常に長くなるため省略します）。

<details>
<summary> /dist/target.html の出力内容例</summary>

```html:/dist/target.html
<!doctype html>
<html>
  <head>
    <title>My Image Project</title>
    <link rel="stylesheet" href="./css/target.css">
  </head>
  <body>
    <h1>My Image Project</h1>
    <p>&lt;img&gt;要素・PNG画像</p><img src="images/favicon-html.png" width="100" height="100">
    <p>&lt;img&gt;要素・JPEG画像</p><img src="images/favicon-html.jpg" width="100" height="100">
    <p>background-image属性・PNG画像</p><div id="png-image" class="image"></div>
    <p>background-image属性・JPEG画像</p><div id="jpeg-image" class="image"></div>
    <script src="./js/target.js"></script>
  </body>
</html>
```
</details>

<details>
<summary> /dist/css/target.css の出力内容例</summary>

```css:/dist/css/target.css
/*!**********************************************************************!*\
  !*** css ./node_modules/css-loader/dist/cjs.js!./src/css/source.css ***!
  \**********************************************************************/
.image {
  width: 100px;
  height: 100px;
}

#jpeg-image {
  background-image: url(../images/favicon-css.jpg);
  background-size: cover;
}

#png-image {
  background-image: url(../images/favicon-css.png);
  background-size: cover;
}
```
</details>

# 後書き

**サンプルコード**
**[/website/website-dev-on-webpack5/image-asset-on-webpack5](https://github.com/Sharkkii/tech-blog-code/tree/develop/website/website-dev-on-webpack5/image-asset-on-webpack5)**

[^1]: 技術的には、画像アセットをエントリポイントとしてビルド対象に含めたり、Webpackローダー/プラグインを利用して特殊な形でビルドの出力結果に含めたりすることもできます。実際、プロジェクトの事情によってはそのようなアプローチを採用する方が良い場合もあるかもしれません。ただ、「Webサイトにおいて画像アセットはHTML/CSS/JavaScriptファイルが利用する構成要素である」という観点を踏まえると、画像アセットを依存アセット/モジュールとして扱う方が管理しやすいように思います。
[^2]: `module.rules.type` プロパティが `"asset/resource"` 等に設定されているとき、 `module.rules.generator.filename` プロパティでアセットファイルの出力パスを設定することもできます。詳細は [Webpack公式サイトのAsset Modulesの説明](https://webpack.js.org/guides/asset-modules/#custom-output-filename)　をご覧ください。