🍅

【複数ディレクトリ対応】HtmlWebpackPlugin を自動で追加させる

2025/02/25に公開

先ずは結論

とりあえず試してやろうじゃねーか!て人は以下の手順を参考に

前提

  • ejsを使用
  • src/html 配下のディレクトリ構成をそのまま public に出力させる
フォルダ構成(一部省略)
.
└── webpackSample
    ├── public // ファイルの出力先
    ├── src // リソースファイル
    │   └── html
    │       ├── article
    │       │   └── index.ejs
    │       └── index.ejs
    └── webpack.js

1. 宣言を追加

const path = require('path'); const fs = require('fs'); はすでに読み込んでいる場合は追加しなくてよい。

webpack.js:変数宣言を追加
/* 宣言
----------------------------- */
const path = require('path');
const fs = require('fs');
// ビルド対処のhtmlパスを記入
const htmlDirectoryList =
  [
    '/html/',
    '/html/article/',
  ];

2. 関数を追加

webpack.js:関数定義を追加
/* 関数
----------------------------- */
/**
 * [createHtmlData]
 * 引数で受け取ったファイルパスを基に HtmlWebpackPlugin の自動生成に必要な情報をオブジェクトで返す
 * @param {Array} directoryList - 調査対象のパスが記入された配列
 * @returns {Array} - HtmlWebpackPlugin の生成に必要なオブジェクトが格納された配列
 */
const createHtmlData = (directoryList) => {
  return directoryList.map((directory)=>{
    const filePath = path.resolve(__dirname, `src/${directory}`);
    const allNames = fs.readdirSync(filePath); // ディレクトリのファイル名を配列で取得
    let fileName = '';

    allNames.forEach(name => {
      const searchPath = filePath + '/' + name;
      const stats = fs.statSync(searchPath);
      if(stats.isFile()) fileName = path.basename(name, path.extname(name)); // ファイルの場合、拡張子を取り除いた名前を変数に格納
    });

    return {
      output: directory.replace('/html', ''), // '/html/' を '/' に変換
      input: filePath,
      fileName: fileName
    }
  });
}

/**
 * [htmlEntries]
 * HtmlWebpackPlugin の記述を自動生成して配列で返す
 * @param {Array} htmlDirectoryList - 調査対象のパスが記入された配列
 * @return {Array} - new HtmlWebpackPlugin({}) で返却されたオブジェクトが格納された配列
 */
const htmlEntries = (htmlDirectoryList) => {
  const htmlData = createHtmlData(htmlDirectoryList);

  return htmlData.map((dataItem) => {
    return new HtmlWebpackPlugin({
      filename: `${dataItem.output}${dataItem.fileName}.html`,
      template: htmlWebpackPluginTemplateCustomizer({
        htmlLoaderOption: {
          sources: false,
          minimize: false,
        },
        templatePath: `${dataItem.input}/${dataItem.fileName}.ejs`,
      }),
      inject: false,
      minify: false,
    });
  });
}

3. webpack.js の plguins で関数を実行

webpack.js:plguins 内で関数実行
module.exports = {
  // 省略
  plugins: [
    ...htmlEntries(htmlDirectoryList),
  ]
  // 省略
}

HtmlWebpackPlugin の憂い

webpackで HtmlWebpackPlugin てよく使うと思うんだけど、これ普通に使うとページ1つに付き1個ずつ new HtmlWebpackPlugin({}) をしなければならない、、

いやそれはさっすがにダル過ぎの鎌足やん?となったのでこれを自動化しようと思い立った

実装方針

実装の際は以下の方針を決めた

  • 将来的に別の人が修正することを想定して、なるべく記述をシンプルにする
  • ビルド対象のディレクトリを柔軟に対応できるようにする
  • 手間をなるべく少なくする為にページを追加する際は1か所のみ修正すれば動くようにする

実装過程

細かい内容は読むのが辛いと思うので、簡潔にまとめる

1. 実装にあたって HtmlWebpackPlugin に必要な情報は以下の3つ

  1. ビルド先のパス
  2. HTMLテンプレートのパス
  3. 拡張子を取り除いたファイル名

これらを最終的に以下のオブジェクト構造を配列で受け取れるような関数を組む

HtmlWebpackPlugin 自動化に必要な情報オブジェクト
{
    output: ビルド先のパス, // 1
    input: HTMLテンプレートのパス // 2
    fileName: 拡張子を取り除いたファイル名 // 3
}

補足として、何故拡張子を取り除く必要があるかと言うと
new HtmlWebpackPlugin の設定において、templatePath: で指定するファイルが .ejs となる場合、拡張子が付いてないファイル名の方がそのまま filename: の値をとして使いまわせるからだ。(要はビルドした際に index.ejs.html みたいなファイルを出さないようにするため!)

以下参考

webpack.js
new HtmlWebpackPlugin({
      filename: `(ファイルの名前差し込み).html`, // ビルド先のパス
      template: htmlWebpackPluginTemplateCustomizer({
        templatePath: `(ファイルの名前差し込み).ejs`, // テンプレートのパス
      }),
    });

そして実際に組んだのがこれだ

/**
 * [createHtmlData]
 * 引数で受け取ったファイルパスを基に HtmlWebpackPlugin の自動生成に必要な情報をオブジェクトで返す
 * @param {Array} directoryList - 調査対象のパスが記入された配列
 * @returns {Array} - HtmlWebpackPlugin の生成に必要なオブジェクトが格納された配列
 */
const createHtmlData = (directoryList) => {
  return directoryList.map((directory)=>{
    const filePath = path.resolve(__dirname, `src/${directory}`);
    const allNames = fs.readdirSync(filePath); // ディレクトリのファイル名を配列で取得
    let fileName = '';

    allNames.forEach(name => {
      const searchPath = filePath + '/' + name;
      const stats = fs.statSync(searchPath);
      if(stats.isFile()) fileName = path.basename(name, path.extname(name)); // ファイルの場合、拡張子を取り除いた名前を変数に格納
    });

    return {
      output: directory.replace('/html', ''), // '/html/' を '/' に変換
      input: filePath,
      fileName: fileName
    }
  });
}

2. new HtmlWebpackPlugin を量産して配列に格納

1.で生成した配列をループさせて new HtmlWebpackPlugin を繰り返し実行する
1.の処理をこの関数に含めても良くない?と思う人もいるかもだが、個人的に「ループして実行する役」「情報を取得して成型する役」はそれぞれ分けた方が保守しやすいと思ったので関数を分割している

そして実際に組んだのがこれだ

new HtmlWebpackPlugin を量産して配列に格納
/**
 * [htmlEntries]
 * HtmlWebpackPlugin の記述を自動生成して配列で返す
 * @param {Array} htmlDirectoryList - 調査対象のパスが記入された配列
 * @return {Array} - new HtmlWebpackPlugin({}) で返却されたオブジェクトが格納された配列
 */
const htmlEntries = (htmlDirectoryList) => {
  const htmlData = createHtmlData(htmlDirectoryList);

  return htmlData.map((dataItem) => {
    return new HtmlWebpackPlugin({
      filename: `${dataItem.output}${dataItem.fileName}.html`,
      template: htmlWebpackPluginTemplateCustomizer({
        htmlLoaderOption: {
          sources: false,
          minimize: false,
        },
        templatePath: `${dataItem.input}/${dataItem.fileName}.ejs`,
      }),
      inject: false,
      minify: false,
    });
  });
}

3. Htmlのパス情報を持った配列を準備して関数を実行

後はhtmlテンプレートのパスを持った配列を準備して

パス情報を持った配列
const htmlDirectoryList =
  [
    '/html/',
    '/html/article/',
  ];

plguinsでその配列データを関数に渡してあげて、実行&展開してあげればよい

plguins 内で関数実行
module.exports = {
  // 省略
  plugins: [
    ...htmlEntries(htmlDirectoryList),
  ]
  // 省略
}

なんでパスの配列データを使うのか

これは正直とても迷った
わざわざ配列データなんか作らずに src/html 配下すべてを出力してくれるようにした方が楽なんじゃないかとも思ったのだが、以下の理由からそれは止めた

  • src/html 配下全ての情報を取得する記述が複雑になりそうで、他の人が修正する必要が発生した際にコードリーディングや修正コストが高くなりそう
  • src/html 配下で出力したくないファイル・フォルダ(例えばインポート用のcommonファイルとかね)の設定が必要になり、これもまた記述が複雑になりそう、かつ出力しないファイルの設定が関数内に暗黙的に存在してしまう可能性があり不親切なコードになる(これは実装してないから実際にこうなるとは分からんけどね)
  • 配列に出力したいディレクトリを記述することで、他の実装者に明示的に出力したいファイルを提示できる

一見すると全自動は楽だが、状況と言うのは刻一刻と変わる
求められるものが変化しても誰でも出来るだけ柔軟に対応出来るようにコードをの品質を保っておくのが大事という事ですな(出来てるとは言ってない)

終わりに

かなーり殴り書きで書いたのでタイポ等あったら申し訳なき
またソースコードも正直なもっとうまく書ける所とか、そもそもの実装方針がおかしいところもあるかもだがご容赦たのむ

Discussion