方針
webpackで静的なサイトを生成する需要は一定程度あるようで、既に@toduqさんの大変わかりやすい記事が上がっています。
上記記事(以下、"@toduqさんの記事")では、テンプレートエンジンとしてPugを使っておられます。
ただ、HTMLをそのまま残せるEJSも捨てがたいため、EJSから静的サイトを生成できるようにしてみたいと思います。
環境
$ node -v && npm -v
v9.2.1
5.8.0
"devDependencies": {
"copy-webpack-plugin": "^4.5.1",
"ejs-html-loader": "^3.1.0",
"globule": "^1.2.0",
"html-loader": "^0.5.5",
"html-webpack-plugin": "^3.1.0",
"webpack": "^4.4.1",
"webpack-cli": "^2.0.13"
}
リポジトリ
結果だけ見たいよという方はリポジトリを覗いていただければと思います。
EJSからHTMLへの変換
いきなりですがここで一番ハマりました。EJS webpack
でググると、以下のような記事が出てくるわけです。
両記事では、ejs-compiled-loader
というものが使われています。
これだと、plugins
の中でHtmlWebpackPlugin
のtemplateとしてEJSのファイル名をいちいち書かなければなりません。
これではページの数が増えると大変です。
他のloaderがないか探した結果、ejs-html-loader
というのがありました。
ejs-html-loader!そういうのもあるのか
今のところ日本語情報もなく、DL数も10分の1程度です。
とはいえ、Usageを見る限り他のloaderと同様の書き方ができそうなので使ってみることにしました。
module : {
rules : [{
test : /\.ejs$/,
loader : 'ejs-html-loader'
}]
}
まずはシンプルなEJSで試してみます。
<!DOCTYPE html>
<html lang="ja">
<body>
<p><%= 'Generated with webpack!' %></p>
</body>
</html>
Entrypoint index.html = index.html
[0] ./src/index.ejs 170 bytes {0} [built] [failed] [1 error]
ERROR in ./src/index.ejs
Module parse failed: Unexpected token (1:0)
You may need an appropriate loader to handle this file type.
| <!DOCTYPE html>
| <html lang="ja">
|
エラーで止まりました。
You may need an appropriate loader to handle this file type.
……と言われても、ちゃんとEJSのloader使ってるやん!と思いつつ諸々試してみるも上手くいきません。
正しい使い方
最終的に、以下のissueで謎が解けました。
正しくはこうですね。
module : {
rules : [{
test : /\.ejs$/,
use : [
'html-loader',
'ejs-html-loader'
]
}]
},
plugins : [
new HtmlWebpackPlugin({
filename: 'index.html',
template: 'src/index.ejs'
})
]
issueへの投稿によると、内部ではこのような動作がなされているとのこと。
-
ejs-html-loader
がEJSをHTMLに変換する -
html-loader
がHTMLを解釈し、HTMLを出力するJavaScriptを出力 -
HtmlWebpackPlugin
が最終的なHTMLに変換する
これはwebpackの仕組み上そういうものだと諦めるしかなさそうです。
しかも、HtmlWebpackPlugin
をページの数だけ書くという部分もなくなりませんでした (これは後で手書きしなくてもいいように手を加えます)
EJSのinclude機能を利用する
EJSは、include()
関数によって別のEJSを取り込むことができます。
これを使って、共通部分をテンプレ化して使いまわしてみましょう。
タイトルなど、ページ毎に違う情報はinclude()
の引数に渡せばよいです。
<% const title = 'Generated with webpack!'; %>
<!DOCTYPE html>
<html lang="ja">
<head>
<%- include(`templates/_head`, { title }) -%>
</head>
<body>
<p><%= title %></p>
</body>
</html>
@toduqさんの記事の方法で、ファイル名の頭にアンダーバーを付けて、テンプレートが直接HTMLに変換されないようにしています。
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><%= title %></title>
<link rel="stylesheet" type="text/css" href="css/style.css" media="all">
ついでに、_head.ejs
内でCSSも呼び出してみます。
CSSはCopyWebpackPlugin
で出力ディレクトリにコピーされます。
BODY {
background-color: lightgreen;
}
ビルド結果です。引数で渡したタイトルが表示され、スタイルも反映されました。
サブディレクトリに対応させる
失敗例
about/
というディレクトリを作ってページを増やしてみます。
src
├── about
│ └── index.ejs
├── css
│ └── style.css
├── index.ejs
└── templates
└── _head.ejs
<% const title = 'About Page'; %>
<!DOCTYPE html>
<html lang="ja">
<head>
<%- include(`../templates/_head`, { title }) -%>
</head>
<body>
<p><%= title %></p>
</body>
</html>
@@ -40,6 +40,10 @@ const app = {
filename: 'index.html',
template: 'src/index.ejs'
}),
+ new HtmlWebpackPlugin({
+ filename: 'about/index.html',
+ template: 'src/about/index.ejs'
+ }),
new CopyWebpackPlugin(
[{ from : `${__dirname}/src` }],
{ ignore : Object.keys(targetTypes).map((ext) => `*.${ext}`) }
しかし、これでは上手くいきません。
ビルド結果を画像に示します。
スタイルが適用されませんでした。
これは、_head.ejs
内のCSSへの相対パスがsrc直下からになっている(href="css/style.css"
)ため、サブディレクトリからは見つけられないのが原因です。
対策
_head.ejs
を工夫します。
どの階層から呼んでも正しいリンクが出力されればよいので、include()
の引数としてルートへのパスを与えてやればOKです。
@@ -1,8 +1,8 @@
-<% const title = 'Generated with webpack!'; %>
+<% const rootPath = './'; const title = 'Generated with webpack!'; %>
<!DOCTYPE html>
<html lang="ja">
<head>
- <%- include(`templates/_head`, { title }) -%>
+ <%- include(`templates/_head`, { title, rootPath }) -%>
</head>
<body>
@@ -1,8 +1,8 @@
-<% const title = 'About Page'; %>
+<% const rootPath = '../'; const title = 'About Page'; %>
<!DOCTYPE html>
<html lang="ja">
<head>
- <%- include(`../templates/_head`, { title }) -%>
+ <%- include(`../templates/_head`, { title, rootPath }) -%>
</head>
<body>
テンプレートでは、<%= %>
で囲むと文字列として展開されます。
@@ -3,5 +3,4 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><%= title %></title>
-<link rel="stylesheet" type="text/css" href="css/style.css" media="all">
-
+<link rel="stylesheet" type="text/css" href="<%= rootPath %>css/style.css" media="all">
link・script・a・imgといった要素が登場するたびに<%= rootPath %>
などと書き加えるのは実際面倒です。
ただ、呼び出す側のEJSファイルでは引数を一つ渡せばいいため、メンテナンスは楽になると思います。
ページの数だけHtmlWebpackPluginを増やさなくていいようにする
前述の通り、HtmlWebpackPlugin
はページの数だけ宣言しなければなりません。
手書きは嫌なので、勝手に宣言されるようにwebpack.config.js
を改善します。
準備として、@tuduqさんの方法を参考に、変換元・変換先の拡張子をセットで与えると{ 変換後ファイル名 : 変換前ファイルパス,... }
の形式で一覧を出すgetEntriesList()
関数を作っておきます。
const targetTypes = { ejs : 'html', js : 'js' };
const getEntriesList = (targetTypes) => {
const entriesList = {};
for(const [ srcType, targetType ] of Object.entries(targetTypes)) {
const filesMatched = globule.find([`**/*.${srcType}`, `!**/_*.${srcType}`], { cwd : `${__dirname}/src` });
for(const srcName of filesMatched) {
const targetName = srcName.replace(new RegExp(`.${srcType}$`, 'i'), `.${targetType}`);
entriesList[targetName] = `${__dirname}/src/${srcName}`;
}
}
return entriesList;
}
設定が入ったオブジェクトをexportsする前に、EJSファイルの一覧を取得してHtmlWebpackPlugin
をそれぞれ宣言し、pluginsにpushします。
const app = {
entry : getEntriesList(targetTypes),
// (中略)
};
for(const [ targetName, srcName ] of Object.entries(getEntriesList({ ejs : 'html' }))) {
app.plugins.push(new HtmlWebpackPlugin({
filename : targetName,
template : srcName
}));
}
module.exports = app;
これならページを増減させてもwebpack.config.js
を修正する必要がありません。
おわりに
webpackによる静的サイトジェネレータのメリットは、webpackの設定次第で好みの動作が実現できるということだと思います。
今回は例をなるべくシンプルにするためにSassもBabelも使っていませんが、loaderを入れれば簡単に拡張できるので色々と試してみてはいかがでしょうか。