やりたいこと
- webpackでejsをhtmlに変換する
- フッターやヘッダーなどの共通部分をincludeで読み込む
- sassをcssに変換する
- 画像やフォントなどを使用する
想像以上にハマったのでテンプレートを公開する。
TL;DR
githubならココ
https://github.com/kajikentaro/webpack-ejs-html
ソースのzipがほしいならココ
https://github.com/kajikentaro/webpack-ejs-html/archive/refs/tags/1.3.zip
ダウンロード後、必要に応じて以下のコマンドを実行する
npm install #パッケージのインストール
npm run build #ビルドする
npm run watch #差分ビルドする
npm run server #開発用サーバーを起動する
環境
- node 18.6
古すぎるとうまく行かないかも
コマンド解説
npm run install
webpackやwebpackを動かすのに必要なプラグインをインストールする
npm run build
本番環境用に、圧縮されたHTMLやCSSファイルをdistディレクトリに生成する
npm run watch
開発環境用に、普通のビルドよりも高速に差分ビルドを行う。srcディレクトリのファイルが更新されると、それを検知して自動で再ビルドが走る(便利)
npm run server
開発用のサーバーがlocalhost:3000に起動する。dist内のファイルが更新されるとブラウザが自動でリロードされる。
注意
できれば絶対パスを使う
includeを使う際、できれば/srcからの絶対パスで表記するようにする。
なぜなら、もしsrc/sub/index.ejsが別のejsから読み込まれた場合は、「別のejsからの相対パス」に解釈されてしまうため。
<html>
<%- include("/template/_head.ejs") %>
<body>
<%- include("../template/_header.ejs") %>
<div>index.ejs</div>
<img src="/sample.png"/>
<%- include("../template/_footer.ejs") %>
</body>
</html>
<link rel="stylesheet" href="./style/index.scss" />
src/sub/index.ejs
静的ファイル(ejs, sass, js以外)は全てpublicフォルダに置く
画像やフォントなどwebpackで変換が不要なものはpublic
に配置する。CopyWebpackPlugin
がpublicディレクトリをまるごとdist
にコピーしているため
こちらも<img src="/sample.png"/>
のようにできれば絶対パスで記述する。
テンプレートはファイル名先頭に_をつける
拡張子がejsのファイルは全てwebpackのエントリーポイントになるが、
アンダーバー_
をファイル名の先頭につけることで例外になる。
例えば共通フッターを_footer.ejs
ではなくfooter.ejs
のファイル名にするとfooter.html
というファイルが作成されてしまう。
webpack.confing.js
const glob = require("glob");
const fs = require("fs");
const path = require("path");
const CopyWebpackPlugin = require("copy-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const RemoveEmptyScriptsPlugin = require("webpack-remove-empty-scripts");
// srcディレクトリと拡張子を消す
// ex: ./src/index.html -> ./index
const arrangePath = (fullPath) => {
const removedExtension = fullPath.match(/(.+\/.+?).[a-z]+([?#;].*)?$/)[1];
const removedSrcDir = removedExtension.replace(/src\//, "");
return removedSrcDir;
};
// src配下の全てのejsパスを取得する
const getAllEjs = () => {
const ejsList = glob.sync("./src/**/[!_]*.ejs");
return ejsList;
};
const getEntry = () => {
const entry = {};
getAllEjs().forEach((v) => {
entry[arrangePath(v)] = v;
});
return entry;
};
// src配下の全てのejsを、ejsからhtmlに変換するプラグインを作成する
const getHtmlPlugins = () => {
const ejsList = getAllEjs();
const htmlWebpackPlugins = ejsList.map((v) => {
return new HtmlWebpackPlugin({
filename: arrangePath(v) + ".html",
template: v,
chunks: [arrangePath(v)], // デフォルトはallなので全てのcssやjsが挿入されてしまう
});
});
return htmlWebpackPlugins;
};
module.exports = {
entry: getEntry(),
output: {
path: path.resolve(__dirname, "dist"),
filename: "[name].js",
clean: true,
publicPath: "/",
},
module: {
rules: [
{
test: /\.ejs$/,
use: [
{
loader: "html-loader",
options: {
sources: {
// copy-webpack-pluginでpublicをまるごとコピーするためcss, js以外の名前解決は行わない
urlFilter: (attribute, value, resourcePath) => {
if (/\.(scss|sass)$/.test(value)) {
return true;
}
if (/\.(js)$/.test(value)) {
return true;
}
return false;
},
},
minimize: false,
},
},
{
loader: "template-ejs-loader",
options: {
includer: (originalPath, parsedPath) => {
let filename = "";
if (/^\./.test(originalPath)) {
// includeでパスが'.'から始まる場合
filename = parsedPath;
} else if (/^\//.test(originalPath)) {
// includeでパスが'/'から始まる場合
filename = path.resolve(__dirname, "src", "." + originalPath);
} else {
filename = path.resolve(__dirname, "src", originalPath);
}
if (filename && fs.existsSync(filename)) {
return { filename };
}
// ファイルが存在しない場合
throw new Error("Not Found: could not resolve " + originalPath);
},
},
},
],
},
{
test: /\.(scss|sass|css)$/,
use: [
MiniCssExtractPlugin.loader,
{ loader: "css-loader", options: { url: false } },
"sass-loader",
],
},
],
},
resolve: {
modules: [
path.resolve(__dirname, "node_modules"),
path.resolve(__dirname, "src"),
],
roots: [path.resolve(__dirname, "src")],
},
plugins: [
// webpackの仕様上, 余計なjsファイルが生まれるので削除
new RemoveEmptyScriptsPlugin({
extensions: /\.(css|scss|sass|less|styl|ejs|html)([?].*)?$/,
remove: /main\.(js|mjs)$/,
}),
// htmlをdistに出力
...getHtmlPlugins(),
// cssをdistに出力
new MiniCssExtractPlugin({
filename: "[name]-[contenthash].css",
}),
// publicフォルダーをdistにコピー
new CopyWebpackPlugin({
patterns: [
{
from: path.resolve(__dirname, "public"),
to: path.resolve(__dirname, "dist"),
},
],
}),
],
devtool: "source-map",
};
解説
src
フォルダ内の全てのejsのパスをエントリーポイント(webpackが最初に読み込むファイル)にする。
ファイル名先頭が_で始まるものはスキップ。
template-ejs-loader
ejsのパスをHTMLに変換する。includeが使われていたらそれも処理するhtml-loader
HTMLを読み込む。ファイル内でsassや外部JavaScriptが使われていたらsass, 外部JavaScriptのパスをwebpackに渡すHtmlWebpackPlugin
HTMLをファイルに出力する。改行やコメントが使われていたら消す
webpack-remove-empty-scripts
webpackはejsやsassなどの入力ファイルを全てjs(main.jsなど)として出力してしまうので、その余計なファイルを消す
sass-loader
sassのパスをcssに変換するcss-loader
cssを読み込む。ほぼ何もしないで右から左に渡すmini-css-extract-plugin
cssをファイルに出力する
copy-webpack-plugin
src/publicフォルダーをdist/publicにコピーする
package.json
npm run dev
を実行することでdist
フォルダに開発用のビルドされたファイルが生成される
npm run build
を実行することでdist
フォルダに本番用のビルドされたファイルが生成される
npm run watch
を実行することでWebサーバーを起動する。ファイルを編集すると自動でリロードされる
{
"name": "test",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "webpack --mode=development",
"build": "webpack --mode=production",
"watch": "webpack --mode=development --watch",
"server": "browser-sync start -s dist -w dist/**"
},
"author": "",
"license": "ISC",
"devDependencies": {
"browser-sync": "^2.27.10",
"copy-webpack-plugin": "^11.0.0",
"css-loader": "^6.7.1",
"ejs": "^3.1.8",
"file-loader": "^6.2.0",
"glob": "^8.0.3",
"html-loader": "^4.1.0",
"html-webpack-plugin": "^5.5.0",
"mini-css-extract-plugin": "^2.6.1",
"sass": "^1.45.1",
"sass-loader": "^12.4.0",
"template-ejs-loader": "^0.9.3",
"webpack": "^5.73.0",
"webpack-cli": "^4.10.0",
"webpack-remove-empty-scripts": "^0.8.1"
}
}
まとめ
webpackを使用することでejsをhtmlにビルドすることができた。
ejsを用いればヘッダーやフッター、各コンポーネントなどの共通化を行うことができる。
もちろんfor文で同じ処理を繰り返すみたいな事もできるので便利。
変換したファイル群はdist/
に出力されるので、それをS3に保存したりやftpでアップロードすれば公開ができる。
webpackで生成したファイルは、コメントや無駄な空白改行が削除されているので、容量の削減やユーザーの信頼性の向上が期待できる。