 
  こんにちはjunです。前回の記事ちゃんと理解するWebpack5。2:Babel、画像の処理と複数バンドルの続きの記事です。前回は画像のバンドル、Bableのトランスコンパイル、そして複数バンドルを行いました。今回は残りのHTMLの取り扱い方と、テンプレートエンジンと呼ばれるPUGを用いてHTMLでページをガンガン作成していこうと思います。
シチュエーションとしては、
といった感じです。とりあえず「デザイン通りに見た目と動きつくってちょ!」というような依頼が来たと思ってください。
webpackには「html-loader」というhtmlファイルを扱うloaderがあります。ちゃんと理解するWebpack5。1では最初ということもあり、distに直接置いていましたが、ローダーを使用することでHTMLもsrc配下に置いてバンドルできます。複数対応ももちろん可能です。
今回はまず素のHTMLを扱う方法とテンプレートエンジンという効率的にHTMLを生成できるpugを用いた2つのバンドル方法を今回はお伝えします。
前回の構成に加えて
.
├── dist
├── package-lock.json
├── package.json
├── src
│   ├── imgs
│   ├── html //NEW!!
│   │   ├── index.html //NEW!!
│   │   └── detail.html //NEW!!
│   ├── js
│   │   ├── functions.js
│   │   └── main.js
│   └── sass
│       ├── compnent.scss
│       ├── style.scss
│       └── variable.scss
├── node_modules
└── webpack.config.js
htmlというディレクトリを作成し、バンドル対象のindex.htmlとdetail.htmlを作成しました。目標はこの2つのファイルがdist配下に配置されることです。
最初にHTMLファイルを扱うために必要なhtml-loaderとHtmlWebpackPluginをインストールします。
npm install -D html-loader html-webpack-plugin
そしてwebpack.config.jsにhtmlに関する。記述を追加します。まずはrulesにhtmlファイルのルールを追加します。
// rulesの配列は後で
let rules = [
    // ... 
    // 追加↓
    {
        test: /\.(html)$/,
        use: {
            loader: 'html-loader',
        }
    },
    // ...
]
const buildDefault = = {
    entry: './src/js/main.js',
  
    mode:"development",
    
    module: {
        rules: rules
    },
    rules:rules,
    // ...
}
//...
module.exports = buildDefault;
とりあえずこれでwebpackはhtmlファイルを扱えるようになりました。次はentryでjsファイルを指定していたように、バンドル対象のhtmlをwebpackに読み込ませるためにhtml-webpack-pluginを使用します。
// ファイル冒頭
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const path = require('path');
// この2つを追加
const globule = require('globule');
const HtmlWebpackPlugin = require('html-webpack-plugin');
let rules = [
    // 省略... 
]
const buildDefault = = {
    entry: './src/js/main.js',
  
    mode:"development",
    
    module: {
        rules: rules
    },
    rules:rules,
    // 以下略...
}
// これらを追記
const htmlFiles = globule.find('src/html/*.html');
htmlFiles.forEach((htmlsrc) => {
    const htmlname = htmlsrc.split('/').slice(-1)[0];
    buildDefault.plugins.push(
      new HtmlWebpackPlugin({
        filename: `${path.resolve(__dirname, 'dist')}/${htmlname}`,
        inject:'body',
        template: htmlsrc,
        minify: false
      })
    )
});
module.exports = buildDefault;
詳細の解説をします。まず最初に必要なプラグインとnode.jsのモジュールをインポートします。そして以下の記述はsrc/html配下にあるhtmlファイルを全て取得する処理です。
const htmlFiles = globule.find('src/html/*.html');
配列でhtmlファイルのパスが戻ってきますので、それらをHtmlWebpackPluginにファイル分だけnewします。
htmlFiles.forEach((htmlsrc) => {
    // ファイル名を取得 src/html/index.html → index.html
    const htmlname = htmlsrc.split('/').slice(-1)[0];
    // webpackの設定にある、pluginsに以下のプラグインインスタンスを入れる。
    buildDefault.plugins.push(
      new HtmlWebpackPlugin({
        // distのファイル名。今回はsrcと同じ。
        filename: `${path.resolve(__dirname, 'dist')}/${htmlname}`,
        // 自動的にバンドル対象のjs(main.js)とcss(style.css)を入れる。お節介ならfalseにする。
        inject:'body',
        // 対象のhtmlファイル
        template: htmlsrc,
        // 圧縮するかどうか。defaultはtrue
        minify: false
      })
    )
});
new HtmlWebpackPlugin()では対象のHTMLファイルをwebpackに読み込ませますが、1ファイルづつなのでhtmlが複数ある場合、globuleなどを使用して複数の対象ファイルを取得してforeachで回します。
こうすることでsrc/html配下のhtmlがバンドルされます。適当に内容を書いてnpm run buildしてみましょう。dist配下にindex.htmlとdetail.htmlが出力されるはずです。
inject:'body'がある場合、htmlにはバンドル対象のcss/jsを読み込む為のscriptやlinkを記述する必要はありません。自動的に挿入されます。
HTML編の最後に画像パスの解決を行います。html-loaderはsrcなどロード可能な属性を見つけるとそのパスなどの解決を行おうとします。たとえば以下のようなタグある場合
<!-- バンドル前 -->
<img src="image.png"/>
<!-- バンドル後 -->
<img src="./image.png"/>
このように自動的にパスの調整を行います。相対パスだと階層が深い時大変ですので、scssではエイリアスを用いてsrcを指定できましたが、htmlは残念ながらできません。
<!-- バンドル前 -->
<img src="~/img/image.png"/>
<!-- Module not found -->
しかし対処法はあります。webpack.config.jsのresolveにrootsプロパティーを記述します。
const buildDefault = {
    resolve:{
        extensions: ['.js', '.json', '.scss', '.css'],
        alias: {
            '~': path.resolve(__dirname, 'src'),
        },
        // ↓追加!
        roots: [path.resolve(__dirname, "src")],
    },
}
このrootsプロパティを追加した後、パスは以下のようにします。
<!-- バンドル前 -->
<img src="/img/image.png"/>
<!-- バンドル後 -->
<img src="img/image.png"/>
roots: [path.resolve(__dirname, "src")],によって/img/image.pngのパスをsrc/を基準に探してくれるようになります。HTMLの場合はこのようにして画像を指定します。
ひとまず以上の設定でhtmlファイルが使用できるようになりました。src/html配下で必要なページ分だけのHTMLを作成して、スタイルはscss、jsも一つにまとめられてスマートに見えます。しかし、繰り返しの記述をしたりテンプレートを作成してより効率的に描きたい時もあると思います。そんな時、テンプレートエンジンと呼ばれるものを使用することでより効率よくマークアップができるようになります。今回はpugを用います。(他の候補としてEJSなどがある)
今回は詳しい説明は省略しますが、概要的に伝えます。pugは以下のような記述でhtmlのマークアップが可能です。
レイアウトテンプレートファイル
doctype html
html(lang="ja")
    block head
        include ../components/head_conf
    body
        .body-wrapper
            block header
                include ../components/header
            main.p-main-content
                block content
            block footer
                include ../components/footer
            
            block footerNav 
                include ../components/footerNav
main配下のページ内容(上記のテンプレートファイルのblock contentに展開)
extends ../layouts/default.pug
include ../components/badge
    include ../components/_data
        - var recommneds = variables.recommneds
block content
    .p-first-view-content
        .p-sliders.swiper(id="top-slider")
            .p-slider-wrapper.swiper-wrapper
                .c-slider.swiper-slide
                    .c-img-adjuster
                        img(src=require("~/img/sample/top_slider_img.jpg"), alt="スライダーの写真")
                .c-slider.swiper-slide
                    .c-img-adjuster
                        img(src=require("~/img/sample/top_slider_img.jpg"), alt="スライダーの写真")
    
    div.p-fullfilled
        .p-row-container
            .p-row-wrapper
                each val,index in recommneds
                    +badge(val,"recommend-"+index)
for文によるループ、テンプレート、mixinやインポートなどPHPなどバック側で行っていたような、htmlの構築ができます。laravelのbladeみたいな感じです。pugを使うことでhtmlで面倒と思っていたことは大体解消できます。変更にも強いのでpugは使うことをお勧めします。
htmlの時は単にsrc/html配下にファイルを配置するだけでしたが、もう少しpugで管理しやすいように以下のように変更します。
.
├── dist
├── package-lock.json
├── package.json
├── src
│   ├── imgs
│   ├── html
│   │   ├── component
│   │   ├── layout
│   │   └── page
│   ├── js
│   │   ├── functions.js
│   │   └── main.js
│   └── sass
│       ├── compnent.scss
│       ├── style.scss
│       └── variable.scss
├── node_modules
└── webpack.config.js
component,layout,page,というものを追加しました。componentは繰り返し使われるパーツ(ボタンとかカードとか)のpugを格納、layoutはhead,bodyの構成を含めたmainタグ以外の箇所のレイアウトを決めるpugを格納し、pageにバンドル対象の各種ページのpugを配置します。
pageに先ほどのindex.pugと detail.pugを配置して、最終的にhtmlにしてdistに配置します。適宜component、layoutからファイルをインポートして使用します。私は大体のプロジェクトはこれで十分なカバーできる気がします。
それではwebpackでpugを扱えるようにしましょう。以下のloaderとpluginを入れます。
npm install -D pug-loader html-webpack-plugin
html-webpack-plugin はHTML編でここでは入っていれば入りません。
まずはrulesにpugのruleとloaderを追加します。
// rulesの配列は後で
let rules = [
    // ... 
    // 追加↓
    {
        test: /\.pug$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'pug-loader',
            options: {
              pretty: true,
            }
          }
        ]
    }
    // ...
]
const buildDefault = = {
    entry: './src/js/main.js',
  
    mode:"development",
    
    module: {
        rules: rules
    },
    rules:rules,
    // ...
}
//...
module.exports = buildDefault;
次に
// ファイル冒頭
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const path = require('path');
// この2つを追加
const globule = require('globule');
const HtmlWebpackPlugin = require('html-webpack-plugin');
let rules = [
    // 省略... 
]
const buildDefault = = {
    entry: './src/js/main.js',
  
    mode:"development",
    
    module: {
        rules: rules
    },
    rules:rules,
    // 以下略...
}
// これらを追記
const pugFiles = globule.find('src/html/page/*', {
    ignore: [ 'src/html/components/*','src/html.layouts/*' ]
});
pugFiles.forEach((pug) => {
    const html = pug.split('/').slice(-1)[0].replace('.pug', '.html');
    buildDefault.plugins.push(
      new HtmlWebpackPlugin({
        filename: `${path.resolve(__dirname, 'dist')}/${html}`,
        inject:'body',
        template: pug,
        minify: false
      })
    )
});
module.exports = buildDefault;
詳細はHTML編の記述を見てください。HTML編と似ていますが、
const pugFiles = globule.find('src/html/*', {
    ignore: [ 'src/html/components/*','src/html.layouts/*' ]
});
globuleではignoreを指定して全てのpugファイルを拾わないようにします。(今回の構成ならfindするディレクトリを src/html/page/* にしてもいいかもしれません)
基本的にはこれでpugは使えるようになります。
HTMLではresolveでrootsを指定していました。pugではそれらの指定は特に必要なく、以下のように指定します。
//- OK
img(src=require("~/img/sample.png"), alt="")
//- NG
img(src="~/img/sample.png", alt="")
pugではnode.jsやjsの記述が利用できる為、requireを用いてエイリアスと一緒にパスの解決ができます。
以上でwebpackを用いたjs,scss,画像,htmlのバンドルは以上となります。vueやtypecriptの導入を考えるとさらに深い理解は必要そうですが、ひとまずHTMLのマークアップ程度であれば今回の構成を用いれば十分な気がします。vue・typesciptもいずれやってみようと思います。また今回使用したwebpack.config.jsは以下の通りとなります。参考にどうぞ。
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const path = require('path');
const globule = require('globule');
const HtmlWebpackPlugin = require('html-webpack-plugin');
  
let rules = [
    {
        test: /\.(sa|sc|c)ss$/,
        exclude: /node_modules/,
        use: [
            MiniCssExtractPlugin.loader,
            {
                loader: 'css-loader',
                options: { url: true }
            },
            'sass-loader',
        ],
    },
    {
        test: /\.(png|jpg|gif|svg)$/i,
        generator: {
            filename: 'img/[name][ext][query]'
        },
        type: 'asset/resource'
    },
    {
        test: /\.(html)$/,
        use: {
            loader: 'html-loader',
        }
     },
    {
        test: /\.pug$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'pug-loader',
            options: {
              pretty: true,
            }
          }
        ]
    }
]
if(process.env.es5){
    rules.push(
        {
            test: /\.m?js$/,
            exclude: /(node_modules|bower_components)/,
            use: {
              loader: 'babel-loader',
              options: {
                presets: ['@babel/preset-env']
              }
            },
            generator: {
              filename: '[name].js'
            }
        }
    )
}
const buildDefault = {
    entry:['./src/index.js'],
  
    mode:process.env.mode,
    
    module: {
        rules: rules
    },
    resolve:{
        extensions: ['.js', '.json', '.scss', '.css'],
        alias: {
          '~': path.resolve(__dirname, 'src'),
        },
        roots: [path.resolve(__dirname, "src")],
    },
    // ファイルの出力設定
    output: {
        //  出力ファイルのディレクトリ名
        path: `${__dirname}/dist`,
        filename: '[name].js',
    },
    plugins: [
        new MiniCssExtractPlugin({
            filename: '[name].css'
        }),
    ]
};
const pugFiles = globule.find('src/html/*', {
    ignore: [ 'src/html/components/*','src/html.layouts/*' ]
});
pugFiles.forEach((pug) => {
    const html = pug.split('/').slice(-1)[0].replace('.pug', '.html');
    buildDefault.plugins.push(
      new HtmlWebpackPlugin({
        filename: `${path.resolve(__dirname, 'dist')}/${html}`,
        inject:'body',
        template: pug,
        minify: false
      })
    )
});
module.exports = buildDefault;
コメント
コメント読み込み中..