こんにちは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;
コメント
コメント読み込み中..