脱webpack : ejs編

## はじめに

現在フロントエンド開発環境のうち、一部のwebpack依存処理をnode.jsスクリプトに置き換えています。
本記事はその結果のうち、ejsをnode.jsスクリプトで変換する方法を共有するためのものです。

## ejsとは

[ejs](http://ejs.co/)は、html内にjavascriptの構文を埋め込めるjavascriptテンプレートエンジンです。

* htmlの一部分を外部ファイル化してincludeできる
* 外部変数を読み込んで複数のhtmlを生成できる
* 配列を利用してDOMの繰り返しを生成できる

などの利点があります。構文についてはこちらの記事に詳しく情報が載っています。

[テンプレートエンジンEJSで使える便利な構文まとめ](https://qiita.com/y_hokkey/items/31f1daa6cecb5f4ea4c9)

### なぜejsを選ぶのか

同種のjavascriptテンプレートエンジンとして[haml](http://haml.info)や[Pug](https://pugjs.org/api/getting-started.html)などがあります。これらのテンプレートエンジンはejsよりもさらに踏み込んで、htmlタグ自体を独自記法に変更してより少ない記述でファイルを生成できます。

これらのテンプレートエンジンは、新規のプロジェクトの立ち上げ時には大きな威力を発揮します。しかし、独自記法を採用しているため既存のプロジェクトに徐々に注入していくのには向きません。ejsはhtmlの記述に関しては手をいれていないため、既存のhtmlファイルに徐々にejsの構文を混ぜていくことができます。

## webpackでのejs変換

ejsのwebpackを利用した変換方法については、こちらの記事に詳細な手順があります。

[webpackで静的サイトジェネレータ(EJS編)](https://qiita.com/kn1cht/items/d3fcd3376ab3461bf05a)

この方法ですと、最低限必要となる依存モジュールは以下の通りとなります。

*▼package.json*

```json
"devDependencies": {
    "ejs-html-loader": "^3.1.0",
    "html-loader": "^0.5.5",
    "html-webpack-plugin": "^3.2.0"
}
```

## 脱webpack

これらの依存モジュールのうち[ejs-html-loader](https://github.com/mcmath/ejs-html-loader)の更新が~~2017年7月からありません。~~

ejs-html-loaderの依存モジュール設定は

*▼package.json*

```json
"peerDependencies": {
  "webpack": "2.x - 3.x",
  "ejs": "2.x"
}
```

となっており、webpack4で利用すると警告が表示されるようになりました。機能的には問題なく動作するのですが、今後更新が途絶える可能性も考えejsの変換処理をwebpackからnode.jsのスクリプトに切り替えます。

**2019/02/12追記**

2019/01/25にejs-html-loaderの更新がありました。webpack4でも警告なく動作します。

## nodeモジュールを利用したejs変換

`ejs-html-loader`や`gulp-ejs`などのモジュールのDependenciesをのぞき込むと、共通の`ejs`というモジュールを利用していることがわかります。

[ejs](https://github.com/mde/ejs)はejsテンプレートをnode.jsで実装したモジュールになります。ejsに関連するモジュールはすべてこのejsモジュールに依存しています。一方ejsモジュールにはDependenciesがありません。依存モジュールの問題で悩む必要はありません。

node.js用のAPIが公開されているので、このejsモジュールを直接利用してejsファイルの変換を行います。

### ディレクトリ構成

```console
root
 ┝ dist
 │　　└ index.html
 ┝ src 
 │　　└ ejs
 │　　　　┝ index.ejs
 │　　　　└ _component.ejs
 ┝ task
 │　　└ ejs.js
 └ package.json
```

`src/ejs/*.ejs`を読み込んで、ディレクトリ構造を維持したまま`dist/*.html`を出力します。
includeして利用するコンポーネントファイルは、ファイル名に`_`をつけて区別しています。
node.js用スクリプト`ejs.js`はtaskディレクトリに格納します。

### package.json

```json
"devDependencies": {
    "ejs": "^2.6.1",
    "glob": "^7.1.2",
    "make-dir": "^1.3.0",
    "onchange": "^4.1.0",
},
"scripts": {
    "ejs": "node task/ejs.js",
    "watch:ejs": "onchange './src/**/*.ejs' -- npm run ejs",
},
```

依存モジュールは``ejs``を筆頭に、対象ファイルを収集する``glob``とファイル更新監視の``onchange``、複数のdirを一気に掘る``make-dir``を使用しています。
scriptsは``ejs.js``を呼び出すタスクと、srcフォルダー内のejsファイルが更新されたら動作するwatchタスクを定義します。

### nodeスクリプト

*▼task/ejs.js*

```js
"use strict";

const fs = require("fs");
const path = require("path");
const ejs = require("ejs");
const glob = require("glob");
const makeDir = require("make-dir");

const srcDir = `${process.cwd()}/src/ejs`;
const distDir = `${process.cwd()}/dist`;

glob(
    `**/*.ejs`,
    {
        cwd: srcDir,
        ignore: `**/_*.ejs`,
    },
    (er, files) => {
        for (let fileName of files) {
            convert(fileName, srcDir, distDir);
        }
    }
);

const convert = (fileName, srcDir, distDir) => {
    ejs.renderFile(path.resolve(srcDir, fileName), (err, str) => {
        if (err) {
            console.log(err);
            return;
        }

        const distPath = path.resolve(distDir, fileName);
        makeDir(path.dirname(distPath)).then(() => {
            const htmlPath = path.format({
                dir: path.dirname(distPath),
                name: path.basename(distPath, ".ejs"),
                ext: ".html",
            });
            fs.writeFile(htmlPath, str, () => {});
        });
    });
};
```

大まかな作業手順は

* ``glob``で対象となるファイルを収集する
* 対象ファイル群を1ファイルごとに``ejs``で変換を行う
* 変換に成功したら対象ディレクトリを``make-dir``で掘る
* ファイルを``fs``で書き込み

となります。

#### node.js標準モジュール

node.jsには基本的な機能を実現するモジュールが標準でインストールされています。これらのモジュールはpackage.jsonで指定しなくても利用できます。

##### fs

[fs](https://nodejs.org/api/fs.html)はファイルシステムモジュールです。このモジュールは基本的なファイルの読み書き機能を実現します。ファイルを読み込むreadFile、書き込むwriteFileなどの関数があります。基本的には関数は非同期で動作しますが、readFileSyncやwriteFileSyncなどのSync関数を利用すると同期で動作します。
なお、fsは存在しないディレクトリに対する操作はすべてエラーを返します。存在しないディレクトリにファイルを書き込もうとする場合、ディレクトリを1層ずつ掘る必要があります。その処理を肩代わりしてくれるのが``make-dir``モジュールです。

##### path

[path](https://nodejs.org/api/path.html)はファイルパスモジュールです。node.jsは複数のプラットフォームで動作することを前提にしていますので、環境によってファイルパスの表記が変化します。pathモジュールは実行環境の情報を読み取って、パスを適切に処理してくれます。パスの連結、ファイルパスからディレクトリ部分だけの取り出し、ファイル名と拡張子の取り出しなどができます。

##### process

[process](https://nodejs.org/api/process.html)はグローバルオブジェクトです。モジュールではないのでrequireで読み込む必要はありません。
node.jsの実行環境に関する情報をprocessオブジェクトから読み取ることができます。node.jsが実行されているディレクトリのパスを読み取るprocess.cwd()、実行時の引数を取得するprocess.argv、エラー発生時にプロセスを終了させるprocess.exit()などが利用できます。

## 個人的な感想

今回はejs変換をnode.jsのスクリプトで処理してみました。脱webpackにはnode.jsの標準モジュールや定番のモジュールの再学習が必要になり、それなりに学習コストがかかります。

しかし、脱webpackをしてしまえばプラグインやローダーの更新停止という事態は避けられます。フロントエンドのビルドプロセスはほとんどの機能が標準モジュールと少数の定番モジュールで実現できますので、一度学習をしてしまえばその知識は使い回しがきき、生産性は向上します。モジュール更新停止の危険性と学習コストを天秤にかけた場合、悪くない投資だと思います。

一方、webpackと相性のいい処理も見えてきました。AltJSのトランスパイルです。1つのエントリーファイルから依存関係を判断し、複数のファイルを変換しつつバンドル、最終的に1つのファイルに出力するというのはwebpackの性質によくマッチしています。この分野に関しては今後もwebpackを使い続けることになると思います。

以上、ありがとうございました。