ちゃんと理解するWebpack5。3:HTMLのバンドルとPUGでのページ作成
技術スタック JavascriptwebpackHTML

ちゃんと理解するWebpack5。3:HTMLのバンドルとPUGでのページ作成

2021.10.30 2021.10.30

こんにちはjunです。前回の記事ちゃんと理解するWebpack5。2:Babel、画像の処理と複数バンドルの続きの記事です。前回は画像のバンドル、Bableのトランスコンパイル、そして複数バンドルを行いました。今回は残りのHTMLの取り扱い方と、テンプレートエンジンと呼ばれるPUGを用いてHTMLでページをガンガン作成していこうと思います。

シチュエーションとしては、

  • サイト制作で上がってきたデザインからHTML・CSS・JSのテンプレートファイルを作る。
  • モックとしてサービスのweb部分を作ってみる

といった感じです。とりあえず「デザイン通りに見た目と動きつくってちょ!」というような依頼が来たと思ってください。

webpakcでHTMLを扱うには

webpackには「html-loader」というhtmlファイルを扱うloaderがあります。ちゃんと理解するWebpack5。1では最初ということもあり、distに直接置いていましたが、ローダーを使用することでHTMLもsrc配下に置いてバンドルできます。複数対応ももちろん可能です。

今回はまず素のHTMLを扱う方法とテンプレートエンジンという効率的にHTMLを生成できるpugを用いた2つのバンドル方法を今回はお伝えします。

単純HTMLファイルをバンドルする

前回の構成に加えて

.
├── 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.htmldetail.htmlを作成しました。目標はこの2つのファイルがdist配下に配置されることです。

html扱いに必要なloaderとpluginを入れる

最初にHTMLファイルを扱うために必要なhtml-loaderHtmlWebpackPluginをインストールします。

npm install -D html-loader html-webpack-plugin

ruleとloaderを追加

そしてwebpack.config.jsにhtmlに関する。記述を追加します。まずはrulesにhtmlファイルのルールを追加します。

webpack.config.js
// 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;

html-webpack-pluginの設定

とりあえずこれでwebpackはhtmlファイルを扱えるようになりました。次はentryでjsファイルを指定していたように、バンドル対象のhtmlをwebpackに読み込ませるためにhtml-webpack-pluginを使用します。

webpack.config.js
// ファイル冒頭
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.htmldetail.htmlが出力されるはずです。

inject:'body'がある場合、htmlにはバンドル対象のcss/jsを読み込む為のscriptやlinkを記述する必要はありません。自動的に挿入されます。

画像パスの解決

HTML編の最後に画像パスの解決を行います。html-loadersrcなどロード可能な属性を見つけるとそのパスなどの解決を行おうとします。たとえば以下のようなタグある場合

<!-- バンドル前 -->
<img src="image.png"/>

<!-- バンドル後 -->
<img src="./image.png"/>

このように自動的にパスの調整を行います。相対パスだと階層が深い時大変ですので、scssではエイリアスを用いてsrcを指定できましたが、htmlは残念ながらできません。

<!-- バンドル前 -->
<img src="~/img/image.png"/>

<!-- Module not found -->

しかし対処法はあります。webpack.config.jsresolveにrootsプロパティーを記述します。

webpack.config.js
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の場合はこのようにして画像を指定します。

pugでHTMLファイルをバンドルする

ひとまず以上の設定でhtmlファイルが使用できるようになりました。src/html配下で必要なページ分だけのHTMLを作成して、スタイルはscss、jsも一つにまとめられてスマートに見えます。しかし、繰り返しの記述をしたりテンプレートを作成してより効率的に描きたい時もあると思います。そんな時、テンプレートエンジンと呼ばれるものを使用することでより効率よくマークアップができるようになります。今回はpugを用います。(他の候補としてEJSなどがある)

pugの使い方

今回は詳しい説明は省略しますが、概要的に伝えます。pugは以下のような記述でhtmlのマークアップが可能です。

レイアウトテンプレートファイル

layout/default.pug
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に展開)

pages/index.pug
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を扱う

それではwebpackでpugを扱えるようにしましょう。以下のloaderとpluginを入れます。

npm install -D pug-loader html-webpack-plugin

html-webpack-plugin はHTML編でここでは入っていれば入りません。

まずはrulesにpugのruleとloaderを追加します。

webpack.config.js
// 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;

次に

webpack.config.js
// ファイル冒頭
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は使えるようになります。

pugでの画像パスの解決

HTMLではresolverootsを指定していました。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は以下の通りとなります。参考にどうぞ。

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;
Copyright © 2021 jun. All rights reserved.