はじめに
HTMLの中に if や for をベタ書きしたテンプレートは、書いているうちにどんどん読みにくくなっていきます。条件分岐が増えるたびにマークアップとロジックが絡み合い、半年後に見返すと「このテンプレート、何をしているんだっけ」となった経験がある方も多いのではないでしょうか。
Handlebars.jsは、この問題に対して「テンプレートにロジックを持ち込まない」という一貫した思想で応えてきたテンプレートエンジンです。Mustacheという最小構文のテンプレート言語をベースに、{{#if}} や {{#each}} といったブロックヘルパー、独自ロジックを切り出せるカスタムヘルパー、テンプレートを部品化するパーシャルといった機能を加えることで、複雑な出力にも耐えられる実用性を持たせています。
とはいえ、読むより触った方が早いと思います。入力した名前や商品リストがその場でHTMLに変換される様子を見られるので、先に挙動を見たい方はこちらからどうぞ。
Handlebars.jsとは
Handlebars.jsは、Mustache構文を拡張したJavaScript向けのテンプレートエンジンです。{{変数名}} というプレースホルダにデータを差し込むだけのシンプルな記法をベースに、条件分岐やループなどの制御構造をテンプレート側の「ヘルパー」として提供している点が特徴です。テンプレートの中に生のJavaScript式を書かせない設計により、マークアップとロジックの境界がはっきりします。
主な特徴
- ロジックレス - テンプレート内に任意のJavaScript式を書けない代わりに、
{{#if}}や{{#each}}などの組み込みブロックヘルパーで大半のケースをカバーします - 拡張可能なヘルパー・パーシャル -
Handlebars.registerHelper()で独自の整形処理を追加でき、Handlebars.registerPartial()でテンプレートの一部を部品として再利用できます - プリコンパイルによる高速化 - テンプレートを事前にJavaScript関数へコンパイルしておけるため、本番環境では文字列パースのコストなしにレンダリングできます
インストール
npm install handlebars
yarn add handlebars
ブラウザで直接使う場合は、CDN経由でスクリプトタグを読み込むだけでも動きます。
<script src="https://cdn.jsdelivr.net/npm/handlebars@4.7.9/dist/handlebars.min.js"></script>
Handlebars.jsのサンプルを動かす
下のサンプルは、名前欄に文字を打つとテンプレートの {{name}} 部分がその場で置き換わるフォームです。Handlebars.jsの Handlebars.compile() にテンプレート文字列を渡すとレンダリング関数が得られ、そこにデータオブジェクトを渡すとHTML文字列が返ってきます。
要点だけを抜き出すと、Handlebars.jsの基本的な使い方は次のようになります。
const source = `<p>こんにちは、{{name}}さん!</p>
<ul>
{{#each items}}
<li>{{this}}</li>
{{/each}}
</ul>`
const template = Handlebars.compile(source)
const html = template({
name: '田中',
items: ['りんご', 'みかん'],
})
// => <p>こんにちは、田中さん!</p><ul><li>りんご</li><li>みかん</li></ul>
実際に動かせるものが下です。名前欄を空にしたり、商品リストの行を増やしたりすると、{{#each}} ブロックの出力がすぐに反応するのを確認できます。
このように、Handlebars.compile() は一度呼べばあとは何度でも再利用できる関数を返すため、同じテンプレートに異なるデータを流し込んでレンダリングし直す、という使い方が基本になります。
基本的な使い方
もっとも単純な例は、変数を1つ差し込むだけのテンプレートです。
import Handlebars from 'handlebars'
const template = Handlebars.compile('<h1>{{title}}</h1>')
console.log(template({ title: 'Handlebars.js入門' }))
// => <h1>Handlebars.js入門</h1>
{{title}} のような二重波括弧はデフォルトでHTMLエスケープされるため、<script> タグなどが混入したデータを渡してもそのまま実行されることはありません。エスケープしたくない場合は {{{title}}} と三重波括弧を使いますが、これはユーザー入力を直接渡す用途には向かないため注意が必要です。
実践的なユースケース
ブロックヘルパーで条件分岐と繰り返しを表現する
Handlebars.jsには {{#if}} {{#unless}} {{#each}} といった組み込みのブロックヘルパーが用意されており、テンプレート側にJavaScriptを書かなくても条件分岐やループを表現できます。在庫数によって表示を切り替えるようなケースで特に威力を発揮します。
ボタンを押すたびに inStock の真偽値が反転し、{{#if}}...{{else}}...{{/if}} の出力が切り替わります。テンプレート側の条件式には手を触れず、渡すデータだけで表示が変わる点がロジックレス設計の狙いです。
カスタムヘルパーで独自の整形ロジックを追加する
組み込みヘルパーだけでは足りない処理(日付の整形や大文字変換など)は、Handlebars.registerHelper() で自作のヘルパー関数として登録します。テンプレート内からは通常のヘルパーと同じ {{ヘルパー名 引数}} の記法で呼び出せます。
金額欄の数値を書き換えると、自作した yen ヘルパーがカンマ区切りの金額表記に整形し直します。registerHelper に渡す関数は普通のJavaScript関数なので、toLocaleString のような標準APIをそのまま使い回せるのが利点です。
パーシャルでテンプレートを部品として再利用する
一覧表示のように同じ構造を繰り返すテンプレートは、Handlebars.registerPartial() で部品化しておくと見通しが良くなります。呼び出し側では {{> パーシャル名}} と書くだけで、その部分に部品テンプレートのレンダリング結果が挿入されます。
「商品を追加」を押すたびに items 配列が増え、{{#each}} の中で card パーシャルが繰り返し呼ばれます。カードのマークアップを変更したいときは registerPartial の1箇所を直せば済むため、同じ構造をあちこちにコピーせずに済みます。
まとめ
Handlebars.jsは、テンプレートにロジックを書かせないという制約を軸に、ブロックヘルパー・カスタムヘルパー・パーシャルという3つの拡張手段で実用性を確保してきたテンプレートエンジンです。{{#if}} {{#each}} による基本的な制御から、registerHelper による独自ロジックの切り出し、registerPartial によるテンプレートの部品化まで、この記事で紹介したパターンを組み合わせれば、静的なHTML生成からメール本文の組み立てまで幅広く対応できます。まずは手元の小さなテンプレートをHandlebars.jsに置き換えてみて、ロジックレスな書き方の感覚を掴んでみてください。
