[Node.js] Handlebars Tips

# 概要

テンプレートエンジンであるHandlebarsをいろいろ触ってみた。
Handlebars自体は珍しいライブラリではなく、他に記事があるので、日本語情報が見つからなかった機能を主に紹介する。

https://handlebarsjs.com/
Handlebarsを使う際は、ここを一度は目を通すことをお勧めする。

> ここでは、通常のテンプレートとしての機能は紹介しない。
> 環境は、AWS Lambda Node.js上で動かす事を想定している。

# pertialとhelper

Handlebarsで機能を拡張する方法には、この2種類がある。

* partial : いわゆるサブテンプレート、テンプレートに動的に他のテンプレートを埋め込む事ができる。
* helper : 関数。テンプレートの中でヘルパ関数を実行し、その結果をtemplateに埋め込む事ができる。


```hbs
{{!sample.hbs}}
{{#if hoge }}
{{ sample.sub.hbs hoge }}
{{/if}}
```

例えばこんな感じで、`sample.hbs`というテンプレートに、ある条件に合致したときのみ`sample.sub.hbs`という別のテンプレートを埋め込む。

```hbs
{{!sample.hbs}}
{{>hoge a "aaa"}}
{{/if}}
```

```js
// helperes.js
const hoge = (key1, key2) => {
   return key + ':' + value;
};
Handlebars.registerHelper('hoge', hoge);
```

たとえばこんなことをすると、`a`の値と`":aaa"`文字列を結合した結果を出力してくれる。
こんな単純なHelper関数だけではなく、`{{#if}}`のようなブロックヘルパー（内部に構造を持つヘルパー）を作成することもできる。

# Tips紹介

## precompile

コードの中でreadFileしてコンパイルすることも可能だが、簡単にprecompileできる。

### templateの場合
```hbs
{{!test.hbs}}
{{a}}
```

たとえばあらかじめこんなテンプレートを作成しておき、

```sh
$ handlebars test.hbs -f test.hbs.js
```
コマンドを実行することで、jsファイルにコンパイルされる。

```js
global.Handlebars = require('handlebars')
require('test.hbs')

const template = Handlebars.templates['test.hbs']
console.log(template({a: 1}))
// 1
```

プリコンパイルしたコードを読み込めば、templateを実行することができる。

* 前もってglobalにHandlebarsを設定する必要がある。
> この情報がどこにも書かれていない。サンプルを見ると、precompileしたコードをfileとして読んで、`Handlebars.template`でロードしろと言っているようにも見えるが・・・そんなことをするよりこっちのほうが簡単で便利なきがするのだが・・・（もちろん名前空間の問題を抜きにすればだが）

* テンプレートは、Handlebars.templatesにファイル名をキーに登録される。

### partialの場合
partialもtemplateと同様にプリコンパイルできる。

```hbs
{{!test.hbs}}
{{test.sub.hbs a}} {{! partialにはコンテキストを渡すことができる。}}
```

```hbs
{{!test.sub.hbs}}
{{b}}
```

たとえばあらかじめこんなテンプレート、サブテンプレートを作成しておき、

```sh
$ handlebars test.hbs -f test.hbs.js
$ handlebars test.sub.hbs -p -f test.sub.hbs.js
```
コマンドを実行することで、jsファイルにコンパイルされる。

```js
global.Handlebars = require('handlebars')
require('test.hbs')
require('test.sub.hbs')

const template = Handlebars.templates['test.hbs']
console.log(template({a:{b:1}}))
// 1
```

* partialの扱いもtemplateとほぼ同じ。プリコンパイルオプションに`-p`を設定するだけ。
* partialタグがインデントされていた場合、中身も自動的にインデントされる。

### global.Handlebarsについて

globalにHandlebarsをセットする方法は推奨ではない可能性もある。
とすると以下のようにする必要がある。


```js
const Handlebars = require('handlebars');
Handlebars.partials['sample.hbs'] = Handlebars.template(fs.readFileSync('sample.hbs.js'));
```
> ただ、せっかくprecompileしたコードをわざわざreadFileで読むのか？それとももっといい方法があるのか？

globalに設定する場合、通常はpreloadすることになるだろう。

```js
// setup.js
global.Handlebars = require('handlebars')
```

```js
// package.json
{
  "jest": {
    "verbose": true,
    "setupFilesAfterEnv": [
      "<rootDir>/setup.js"
    ]
  }
}
```
jestでは、これでpreloadされた。

```sh
$ node -r ./setup.js
```
言わずもがなだと思うが、nodeであればこれでpreloadできる。

## Helperについて

### helperから`@root`を取得したい

helperからコンテキストの情報や`@root`が欲しくなった場合、どうすればいいだろうか？

```js
// helpers.js

const getRoot = (key, _) => {
   return _.data.root[key];
};
Handlebars.registerHelper('getRoot', getRoot);
```

```hbs
{{!test.hbs}}
{{#with a}}
   {{b}}
   {{getRoot "c"}}
{{/with}}
```

ヘルパー関数の通常の引数の後ろに、handlebarsの環境情報が全て格納されているからここから取得できる。
> この情報も見つける事ができなかった。APIドキュメントにも記載はなかった。
> 重要な情報だと思うのだが・・・


ちなみに、現在のコンテキストだけならもっと簡単に取れる。

```js
// helpers.js

const getValue = function(key){
   return this[key];
};
Handlebars.registerHelper('getValue', getValue);
```

ラムダ式ではなくfunctionで関数定義すれば、thisでコンテキストを参照できる。

### helperの一括登録

```js
// helpers.js
const helper1 = () => {...}
const helper2 = () => {...}

Handlebars.registerHelper({ helper1, helper2 });
```
複数のHelper関数を一括で登録できる。

### Helperのテスト

helperに対して単体テストを書く場合、

```js
// helpers.test.js
// must preload global.Handlebars
require('helpers.js')
const _ = Handlebars.compile;

describe('test helpers', () => {
  test('getRoot', () => {
    const template = _('{{#with a}}{{b}}{{getRoot "c"}}{{/with}}');
    expect(template({a:{b:1},c:2})).toEqual('12');
  });
});
```
こんな感じで、小さなテンプレートを使ってテストを書いていけばいい。
巨大なproductionテンプレートを使ってhelperのテストを書こうとすると、膨大な量のテストになるだろう。
単体テストをしよう。

# 結論

とりあえず上記により、Handlebarsを使ってやりたかった事はできました。
ただし、正直これが正しい作法かどうかはわかりません。ご意見があれば下さい。

もっと言えば、もうtemplateはReactあたりで書いて、jsx ==> jsにプリコンパイルすればいいんじゃないかという気もしなくもありません。