ただ値を埋め込むだけの簡単なテンプレートエンジンをNode.jsで自作する



JavaScriptで `Infrastructure as Code` のツールを開発しています

以前[こちら](https://qiita.com/mjusui/items/419781a06586f38a0f9d)の記事で、サーバからShellScriptで取得してきた値を、HTML表示する機能を紹介しましたが、その際にHTMLに値を埋め込む簡単なテンプレートエンジンを自前で実装したので、その紹介です


# テンプレートエンジンとは?

[Wikipediaのページ](https://ja.wikipedia.org/wiki/テンプレートエンジン)が、ちゃんとあるような専門用語なんですね

固い説明はそちらに任せるとして、ものすごく乱暴に説明すると、 `hoge='fuga'` のとき `I am a {{ hoge }}.` という文字列の `{{ hoge }}` 部分に `I am a fuga.` というように `'fuga'` を埋め込んでくれるようなもののことです

私はAnsibleを本職でよく利用するのでPythonの世界で有名なテンプレートエンジンである `Jinja2` の経験があります

実際のテンプレートエンジンでは `{{ 2 + 3 }}` を `5` というように四則演算ができたり、テンプレートの中に関数を埋め込んだりできるのですが、この記事で紹介するのは、単に変数の値を埋め込む機能だけの簡単なものになります


# npmで適当なテンプレートエンジン提供されてないの?

Node.jsでもいくつかテンプレートエンジンはあるようですが、今回それらを使いませんでした。理由は2つあります

1. 調べた限りでもテンプレートエンジンありすぎて、どれが良いのかよく分からない
2. 可能な限り依存パッケージは減らしたい (というかクライアントサイドでVue.js使った以外はnpmの依存パッケージは0にしました)

# どうやって実装したのか

おおまかな処理の流れは、以下のような関数にまとめられます

1. テンプレートファイルを文字列としてロードする
2. 1を改行コード `/\r\n|\r|\n/ (正規表現)` で分割する
3. 2を1要素(1行)ごとに取り出し、値の埋め込み部分を表す `{{%` と `%}}` で分割する
4. 3の奇数番目の文字列(`{{%` と `%}}` で囲まれた文字列)の両端のスペース ` ` を取り除く(トリミング)
5. 4の結果が引数として渡されたオブジェクトのキーとして存在する場合のみ、値を置換する
6. 全体を 改行コード `\n` でつなげる

※テンプレート内に改行を含む場合は対応しませんでした
※また、別のテンプレートエンジンで使われそうな `{{` `}}` と見分けるために、あえて `{{%` `%}}` にしています

コードとしては以下のようになります

(1) テンプレートファイルを文字列としてロードする

```TemplateEngine.js
// ファイルアクセスのモジュールをロード
const fs=require('fs');

// テンプレートファイルを文字列としてロードする
// テンプレートは関数の外で1回ロードすれば充分
const template=fs.readFileSync(
  `${__dirname}/templates/hoge.templ`
).toString();
```

(2) (1)を改行コード `/\r\n|\r|\n/ (正規表現)` で分割する

```TemplateEngine.js
const render=(props)=>{
  return template
    .split(/\r|\n|\r\n/)
}
```

(3) (2)を1要素(1行)ごとに取り出し、値の埋め込み部分を表す `{{%` と `%}}` で分割する

```TemplateEngine.js
const render=(props)=>{
  return template
    .split(/\r|\n|\r\n/)
  .map(
    line => line.split(/{{%|%}}/);
  );
}
```

(4) (3)の奇数番目の文字列の両端のスペース ` ` を取り除く(トリミング)

```TemplateEngine.js
const render=(props)=>{
  return template
    .split(/\r|\n|\r\n/)
  .map(
    line => {
      const words=line.split(/{{%|%}}/);

      return 1 < words.length
        ? words.map(
            (w, i) => 0 < i%2
              ? w.replace(/^ */, '')
                 .replace(/ *$/, '')
              : w
          ).join('')
        : line
    }
  );
}
```

5. 上記の処理をまとめ、必要な部分に値を埋め込む関数

```TemplateEngine.js
// ファイルアクセスのモジュールをロード
const fs=require('fs');

// テンプレートファイルを文字列としてロードする
// テンプレートは関数の外で1回ロードすれば充分
const template=fs.readFileSync(
  `${__dirname}/templates/hoge.templ`
).toString();


const render=(props)=>{
  return template
    .split(/\r|\n|\r\n/)
  .map(
    line => {
      const words=line.split(/{{%|%}}/);

      return 1 < words.length
        ? words.map(
            (w, i) => 0 < i%2
              ? props[
                  w.replace(/^ */, '')
                   .replace(/ *$/, '')
                ]
              : w
          ).join('')
        : line
    }
  ).join('\n');
}
```

テンプレートを用意して実行するとこんな感じ

```hoge.templ

I am a {{% hoge %}}.

```

```TemplateEngine.js
console.log(
  render({ hoge: 'fuga', })
);
```

```shell-session
$ node TemplateEngine.js

I am a fuga.

```


# おわりに

ただ値を埋め込むだけなら、とても簡単に実装できることが分かります

これなら余計なパッケージのバージョン管理やnodeのバージョンとの互換を気にする必要がなくなるので、独自実装の方が長い目で見ると楽かもしれません