ちょっとがんばるWebサイト開発 on Webpack5 - EJSを導入して効率的にHTMLファイルを生成する

# 前書き

## 本記事のゴール

この記事では、**EJS** テンプレートを利用することで、HTMLのコーディングの効率を向上させる方法を理解することを目指します。

## この記事で解説する内容

- EJSの概要について説明します
- EJSをプロジェクトに導入する方法について説明します
- EJSを利用した簡単な実例を紹介します

## この記事で解説しない内容

- EJSの詳細な文法については説明しません
- EJSテンプレートで外部JSONデータを活用する方法については説明しません

> [参考] EJSの文法に関する詳細説明
> - [EJS公式サイト](https://ejs.co/)
> - [EJS公式サイト - ドキュメント](https://ejs.co/#docs)
> - [EJSまとめ - Qiita](https://qiita.com/engineerhikaru/items/9b274ea3662f381e2266)

<!--
> [参考] 外部JSONデータを活用して複数のHTMLファイルを生成する方法
> - [*執筆中の記事*](#)
-->

## シリーズ記事

この記事はシリーズ **「ちょっとがんばるWebサイト開発 on Webpack5」** の記事です。
シリーズ全体の概要は [以下のリンク先記事](https://qiita.com/Sharkkii/items/df707cb68f3729493800) にまとめられています。

https://qiita.com/Sharkkii/items/df707cb68f3729493800

:::note info
この記事では **Webpack5** (version5.X.X) を対象としています。
:::

# 背景知識

## EJSとは何か

**[EJS (Embedding JavaScript template)](https://ejs.co/)** は、HTMLファイルを生成するテンプレートを記述するための言語仕様です。HTML形式をベースに記述されており、そこにJavaScriptの構文を挿入することができます。JavaScriptの構文が利用できるため、条件分岐やループ等を用いてHTML要素の宣言をコントロールすることができます。

https://ejs.co/

EJSファイルは、HTMLファイルを生成するための「テンプレート」として利用します。EJSファイルそれ自体はWebサーバにアップロードせず、コンパイルした結果得られるHTMLファイルを公開するという形で利用します。

この記事ではWebpackを利用してプロジェクトにEJSを導入するところに焦点を当てるため、EJSの詳細には立ち入りません。

> [参考] EJS関連記事
> - [EJSまとめ - Qiita](https://qiita.com/engineerhikaru/items/9b274ea3662f381e2266)
> - [EJSの基本的な書き方](https://qiita.com/miwashutaro0611/items/36910f2d784ff70a527d)

## EJSを使うと何が良いのか

HTML形式には文書構造を宣言するための限られた構文しか用意されていないため、生のHTMLファイルを記述する際に不都合が生じることは多いと思います。

例えば、[GitHubユーザのレポジトリ一覧](https://github.com/orgs/webpack/repositories) に見られるように、ほとんど同一の構造を持つHTML要素を多数並べたいという状況が考えられます。[^1] こうした状況において、各種プログラミング言語で実装されている「ループ（JavaScriptにおけるfor文）」の機能を活用して記述できるとすれば、より効率的で保守性の高いコーディングができるでしょう。

生のHTML/CSS/JavaScript[^2] のみで静的Webサイトを開発した経験がある人ならば誰しも一度は「HTMLのコーディングでもプログラミング言語のような記述をしたい」と考えたことがあると思いますが、EJSはそれを実現する1つのアプローチとなっています。

## EJSを使う必要があるのか

EJS以外の技術でEJS同様の処理を実行することについて考えてみます。

1つ目は既存のHTMLテンプレートエンジンを利用する方法です。

EJS同様に広く利用されているHTMLテンプレートエンジンとしては **[Pug](https://pugjs.org/api/getting-started.html)** というものがあるようです。[JavaScriptテンプレートエンジンのPug（Jade）とEJSの比較 - Qiita](https://qiita.com/otsukayuhi/items/8556b014ea363eabe11f) 等の記事で確認できるように、EJSとPugの記法は大きく異なるため、自分の好みに合わせてテンプレートエンジンを選ぶと良いでしょう。

> [参考] Pug関連記事
> - [JavaScriptテンプレートエンジンのPug（Jade）とEJSの比較 - Qiita](https://qiita.com/otsukayuhi/items/8556b014ea363eabe11f)
> - [[Pug] HTMLを効率よくコーディングする - Qiita](https://qiita.com/yun_bow/items/d21586b51f87d781a857)
> - [Webpackを頑張って設定して、すごい静的サイトジェネレータとして使おう - Qiita](https://qiita.com/toduq/items/2e0b08bb722736d7968c)

2つ目はプロジェクトに特化したHTMLテンプレートエンジンを自作する方法です。

EJSのコンパイラがやっていることは「部分的にJavaScriptコードが挿入されたHTMLファイルをEJSの言語仕様に則って生のHTMLファイルに変換する」という作業です。ということは、適当な言語仕様とコンパイラさえ用意することさえできれば、EJS同様の処理を実行することは難しくありません。「言語仕様（ルール）」「コンパイラ（変換器）」といっても、自身のプロジェクトで必要な最低限のルールと任意のプログラミング言語で実装された変換器が用意できれば十分でしょう。
この意味で、「EJSを導入するほど複雑な処理を行わない」「EJSでは実現できない特殊な処理を行いたい」という場合には自分で実装するというのも1つの選択肢になるかと思います。

3つ目はHTMLテンプレートエンジン以外の技術を利用する方法です。

最近では **[Vue](https://jp.vuejs.org/index.html)**, **[React](https://ja.reactjs.org/)**, **[Angular](https://angular.jp/)** 等のJavaScriptフロントエンドフレームワークを用いる場合が増えてきているようです。これらのフレームワークは通常HTML要素を *動的に* 生成するため、HTMLファイルを *静的に* 生成するEJSの技術とは一線を画す存在となっています。[^3] これらの技術の差異について理解しておくと後々役に立つような気がします。

:::note info
「HTML要素/ページを *静的に* 生成する」とは、ローカル環境においてEJS等のソースファイルをコンパイルすることで事前にHTML要素/ファイルを生成しておくことを指しています。一方で「HTML要素/ページを *動的に* 生成する」とは、Webページがアクセスされた際にサーバにおいてHTML要素/ファイルを生成することを指しています。どちらのアプローチをとるべきかは、Webサイトの特性等を考慮して決定します。
:::

> [参考] JavaScriptフロントエンドフレームワーク
> - [Vue.js公式サイト](https://jp.vuejs.org/index.html)
> - [React公式サイト](https://ja.reactjs.org/)
> - [Angular公式サイト](https://angular.jp/)

# 方法

:::note info
サンプルコードをGitHubで公開しています。記事と併せてご覧ください。
**[/website/website-dev-on-webpack5/ejs-on-webpack5](https://github.com/Sharkkii/sample-codes/tree/develop/website/website-dev-on-webpack5/ejs-on-webpack5)**
:::

:::note info
実装例の説明では、ファイル/フォルダパスを *ルート相対パス* の形式で記述しています。ここでルートはプロジェクトルートまたはWebサイトのドキュメントルートに相当します。
:::

## 準備

WebpackおよびEJS等のnpmパッケージの利用には [Node.js](https://nodejs.org/ja/) が必要です。
インストールが未実施の場合には、以下のリンクからインストールしておいてください。

https://nodejs.org/ja/

また、動作確認を行うための Node.js プロジェクトを作成しておきます。
適当な名前のディレクトリ（ここでは `my-ejs-project` ）を用意した上で、そのディレクトリにおいて `npm init` コマンドを実行することで Node.js プロジェクトを新規作成することができます。

```shell:ターミナル
# my-ejs-projectディレクトリの作成
$ mkdir /path/to/my-ejs-project
$ cd /path/to/my-ejs-project

# Node.jsプロジェクトの作成
$ npm init
```

`npm init` コマンドを実行するとプロジェクトに関する情報を入力するよう促されますが、今回は適当な設定にしておいて問題ありません。今後必要に応じて適宜変更しましょう。
コマンド実行完了後、 `/package.json` という設定ファイルが新規作成されていることを確認したら事前準備完了です。

## 手順概要

1. [npmパッケージを導入する](#手順1-npmパッケージを導入する)
EJSからHTMLを生成するのに必要なnpmパッケージ　`webpack`　`webpack-cli` `html-webpack-plugin` `html-loader` `template-ejs-plugin` をプロジェクトに追加します。

2. [Webpackの設定ファイルを記述する](#手順2-webpackの設定ファイルを記述する)
EJSからHTMLを生成する手順やルールを設定ファイル `/webpack.config.js` に記述します。

3. [EJSファイルを作成する](#手順3-ejsファイルを作成する)
HTMLファイルのテンプレートとなるEJSファイル `/src/source.ejs` を作成します。

4. [EJSファイルをコンパイルしてHTMLファイルを生成する](#手順4-ejsファイルをコンパイルしてhtmlファイルを生成する)
Webpackを実行することでEJSファイルのコンパイルを実行し、HTMLファイル `/dist/target.html` を生成します。

### [手順1] npmパッケージを導入する

今回は [Webpack](https://webpack.js.org/) を利用してEJSファイルのコンパイルを行います。

まず初めに、Webpackを利用するのに必要な　`webpack` `webpack-cli` パッケージをインストールします。
Webpack5（version5.X.X）をインストールするため、バージョン指定を行っています。

https://webpack.js.org/

```shell:ターミナル
npm install --save-dev webpack@5 webpack-cli@5
```

次に、EJSファイルをコンパイルするのに必要な　`html-webpack-plugin` `html-loader` `template-ejs-loader` パッケージをインストールします。

https://www.npmjs.com/package/html-webpack-plugin

https://www.npmjs.com/package/html-loader

https://www.npmjs.com/package/template-ejs-loader

```shell:ターミナル
npm install --save-dev html-webpack-plugin html-loader template-ejs-loader
```

### [手順2] Webpackの設定ファイルを記述する

Webpackは、適切に設定を記述することで様々な処理を実行することができます。
ここではEJSファイル　`/src/source.ejs` をコンパイルして `/dist/target.html` を生成するための必要最小限の設定を示します。

```js:/webpack.config.js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = function() {
  return {

    mode: "development",

    entry: {},

    output: {
      /**
       * OUTPUT_DIRECTORY = /dist
       */
      path: path.resolve(__dirname, "dist"),
    },

    module: {
      rules: [
        {
          test: /\.ejs$/,
          use: [
            /**
             * HtmlLoader -> TemplateEjsLoader
             */ 
            "html-loader",
            "template-ejs-loader",
          ],
        },
      ]
    },
    
    plugins: [
      new HtmlWebpackPlugin({
        template: path.resolve(__dirname, "src", "source.ejs"),
        filename: "target.html",
        minify: {
          collapseWhitespace: true,
          preserveLineBreaks: true,
        },
      }),
    ],

  }
}
```

上記のWebpack設定には同シリーズ別記事 [「ちょっとがんばるWebサイト開発 on Webpack5 - HTMLをコピー＆ペーストする」](https://qiita.com/Sharkkii/items/033e3d8119bfe60f520e) と共通する部分が多数あります。共通部分に関する説明として、同シリーズ別記事の説明を一部改変して引用したものを載せておきます。

> 上記の設定を要約すると、
**Webpackのプラグイン設定箇所に、適切な設定を含む `HtmlWebpackPlugin({...})` オブジェクトを追加する**
となります。特に注目すべきパラメータは以下の通りです。
>
> - `template: path.resolve(__dirname, "src", "source.html")`
> 入力HTMLファイルの *絶対パス* を指定する
>
> - `filename: "target.html"`
> 出力HTMLファイルの *相対パス* を指定する

**注目すべき最も大きな相違点はWebpackのルール設定です**。

Webpackは、ルール設定で指定された内容に従って最適化/変換処理等を行います。
`{ test: /\.ejs$/ }` および　`{ use: [ "html-loader", "template-ejs-loader" ] }` という設定を記述することで、拡張子 `*.ejs` のファイルに対して `ejs-template-loader` の変換処理と `html-loader` の変換処理をこの順で適用することができます。
大雑把に言えば、 `template-ejs-loader` はEJSテンプレートをコンパイルしてHTMLページを生成するもので、 `html-loader` は `template-ejs-loader` 等の出力結果をWebpackのビルドプロセスで取り扱えるようにするものです（この記事では詳細を割愛します）。

:::note info
HtmlWebpackPluginプラグインでは、デフォルトでEJSローダーが適用されることになっているため、より簡潔な記述でもEJSテンプレートファイルを取り扱うことができます。例えば、 `webpack.config.js` を以下のように記述することができます（折りたたみ要素の中にあります）。

<details>
<summary>/webpack.config.js の設定内容例</summary>

```js:webpack.config.js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = function() {
  return {

    mode: "development",

    entry: {},

    output: {
      path: path.resolve(__dirname, "dist"),
    },
    
    plugins: [
      new HtmlWebpackPlugin({
        template: path.resolve(__dirname, "src", "source.ejs"),
        filename: "target.html",
        minify: {
          collapseWhitespace: true,
          preserveLineBreaks: true,
        },
      }),
    ],

  }
}
```
</details>

例において `webpack` `webpack-cli` `html-webpack-plugin` プラグインは必要ですが、 `html-loader` `template-ejs-loader` プラグインは不必要であるため、インストールの手間を削減することができます。

ただし、 **デフォルトのEJSローダーはEJSの構文すべてをサポートしているわけではないことに注意してください** 。EJSの構文すべてを扱うためには、追加プラグイン `html-loader` `ejs-template-plugin` の導入および追加設定を行います。詳細はHtmlWebpackPluginの公式ドキュメントに詳しいです。

> [参考]
> - [html-webpack-plugin/template-option.md](https://github.com/jantimon/html-webpack-plugin/blob/main/docs/template-option.md)
:::

> [参考] Webpack設定
> - [Webpack - Concepts](https://webpack.js.org/concepts)
> - [Webpack - Plugins](https://webpack.js.org/concepts/plugins/)
>
> [参考] html-webpack-plugin / html-loader / template-ejs-loader
> - [html-webpack-plugin - npm](https://www.npmjs.com/package/html-webpack-plugin)
> - [html-loader - npm](https://www.npmjs.com/package/html-loader)
> - [template-ejs-loader - npm](https://www.npmjs.com/package/template-ejs-loader)

### [手順3] EJSファイルを作成する

手順2の設定に従ってEJSファイルを作成します。
上記設定ファイル `/webpack.config.js` では入力EJSファイルパスを　`/src/source.ejs` に指定したので、その位置に `source.ejs` ファイルを作成しましょう。
ファイル内容はどんなものでも構いませんが、ここでは例として以下のように記述することにします（折りたたみ要素の中にあります）。

<details>
<summary> /src/source.ejs の入力内容例</summary>

```html:/src/source.ejs
<!doctype html>
<html>
  <head>
    <title>My EJS Project</title>
  </head>
  <body>
    <h1>Hello, Webpack!</h1>
    <ul>
      <% let plugins = [ "html-webpack-plugin", "html-loader", "template-ejs-loader" ]; %>
      <% for (let idx = 0; idx < plugins.length; idx++) { %>
        <li><p>Hello, <%= plugins[idx] %>!</p></li>
      <% } %>
    </ul>
  </body>
</html>
```
</details>

### [手順4] EJSファイルをコンパイルしてHTMLファイルを生成する

手順1〜3でEJS→HTML変換のための準備が完了しました！
それではWebpackを用いて `/src/source.ejs` から `/dist/target.html` を生成してみましょう。
ターミナルで `npx webpack` コマンドを実行することでビルド作業が開始します。

```shell:ターミナル
# ディレクトリ移動
cd /path/to/my-ejs-project

# Webpackコマンド実行
npx webpack
```

ビルドが正常に完了すれば、HTMLファイル `/dist/target.html` が生成されていることを確認できると思います。ファイルの内容を確認すると、EJSで記述された部分が展開されてHTMLになっているはずです（折りたたみ要素の中にあります）。

<details>
<summary>　/dist/target.html の出力内容例</summary>

```html:/dist/target.html
<!doctype html>
<html>
<head>
<title>My EJS Project</title>
</head>
<body>
<h1>Hello, Webpack!</h1>
<ul>
<li><p>Hello, html-webpack-plugin!</p></li>
<li><p>Hello, html-loader!</p></li>
<li><p>Hello, template-ejs-loader!</p></li>
</ul>
</body>
</html>
```
</details>

:::note info
上記手順では、Webpackを利用してEJSをコンパイルする方法を示しましたが、Webpackを利用しないで実現する方法もあります。
[EJS公式サイト](https://ejs.co/) にも書かれているように、npmパッケージ `ejs` を導入した上で `npx ejs` コマンドを実行することで所望の結果を得ることができます。

```shell:ターミナル
# EJSの導入
npm install --save-dev ejs

# EJSの実行
npx ejs src/source.ejs -o dist/target.html
```

EJSを単体で利用したい場合にはこの方法でも十分ですが、その他にも様々な処理を一挙に行いたい場合にはWebpackを利用すると便利です。一方で、様々な処理が可能なWebpackはその分だけ学習コストが高いという側面もあります。プロジェクトの開発環境やコスト等を考慮しながら導入を検討すると良いと思います。
:::

# 後書き

**サンプルコード**
**[/website/website-dev-on-webpack5/ejs-on-webpack5](https://github.com/Sharkkii/tech-blog-code/tree/develop/website/website-dev-on-webpack5/ejs-on-webpack5)**

[^1]: 実際には、GitHubのWebサイトでは各ページのコンテンツが動的に生成されていると思いますが、ここではそういったコンテンツが事前に用意できるような場合を考えていると思ってください。
[^2]: 一般に「生の」というフレーズは「無加工の」「比較的低レベルの」というような意味で使用されますが、ここでは「Webサイトに直接アップロードするそのままの」というような意味も含んでいます。実装するソースファイルの内容は必ずしもWebサイトにアップロードして公開するHTML/CSS/JavaScriptファイルと同一でなくても良いので、実装の際にはより便利な記法やプログラミング言語を使用できると嬉しいという気持ちが暗に込められています。
[^3]: ここで紹介したフレームワークには「HTMLファイルを静的に生成する」ためのツールがあるらしく、HTMLテンプレートエンジンと同様の使い方ができるかもしれません（詳細はあまり把握していません）。