Handlebars.jsとは?基本的な使い方からカスタムヘルパーまで

はじめに

こんにちは。DTダイナミクスGrowthチームでmeviyの開発をしております、森です。

meviyでは帳票作成時とメールの文面作成時にHandlebars.jsを使用しています。
Handlebars.jsについての基本的な使い方と活用例を紹介いたします。

Handlebars.jsとは

Handlebars.jsは、JavaScript向けのシンプルで強力なセマンティック・テンプレートエンジンです。その主な目的は、データ(JSONオブジェクトなど)とHTMLテンプレートを組み合わせて、動的なHTMLを生成することです。

基本的な考え方は、HTMLの中に{{variable}}のような特別なプレースホルダー(式)を埋め込み、プログラムから渡されたデータでその部分を置き換える、というものです。これにより、ロジック(JavaScript)とビュー(HTML)をきれいに分離できます。

Handlebars.jsの主な特徴とメリット

  1. シンプルで可読性の高い構文: Mustache構文 ({{ }}) をベースにしており、直感的で学習が容易。HTMLテンプレートがすっきりと保たれ、デザイナーと開発者の共同作業がしやすくなる。
  2. ロジックレス・テンプレート:
    • テンプレート内に複雑なビジネスロジックを記述することを意図的に制限している。これにより、ビューとロジックの関心事が明確に分離され、コードの保守性が向上する。
    • 簡単な条件分岐 ({{#if}}) や繰り返し ({{#each}}) は提供されている。
  3. パフォーマンス:
  4. 再利用可能なテンプレート(パーシャル):
    • {{> partialName}}のように、テンプレートの一部を別のファイル(パーシャル)として切り出し、再利用できる。ヘッダーやフッターなど、共通のUI部品を効率的に管理できる。
  5. 拡張性(ヘルパー):
    • 標準機能だけでは不足する場合、{{myHelper argument}}のようにカスタム関数(ヘルパー)をJavaScriptで定義して、テンプレートから呼び出せる。これにより、日付のフォーマットや特定の計算など、柔軟な処理を追加できる。
  6. サーバーサイドとクライアントサイドの両方で動作:
    • Node.js環境でもブラウザ環境でも同じように動作するため、サーバー側でHTMLを生成(SSR)することも、クライアント側で動的にUIを更新することも可能。

Handlebars.jsのデメリット

  1. ロジックレスの制約:
    • メリットである一方、複雑なUIロジックが必要な場合にはデメリットにもなる。少し凝った条件分岐や計算をしたいだけでも、JavaScriptでカスタムヘルパーを作成する必要があり、かえって手間がかかってしまうこともある。
  2. 双方向データバインディングの欠如:
    • React、Vue、Angularのようなモダンなフレームワークが提供する「双方向データバインディング」(UIの変更が自動的にデータモデルに反映される仕組み)はない。Handlebarsはあくまで一方向のテンプレートエンジンであり、UIの状態管理には別の仕組みが必要。
  3. 学習コスト(ヘルパー作成):
    • 基本的な構文は簡単だが、複雑なカスタムヘルパーを作成するには、HandlebarsのAPIやコンテキスト(this)の挙動について深い理解が必要になり、学習コストが上がる。
  4. モダンなSPA開発では主流ではない:
    • コンポーネントベースの開発が主流となった現在、新規の複雑なWebアプリケーション(SPA)開発でHandlebarsが第一選択肢になることは少なくなった。より宣言的で状態管理に優れたフレームワークが好まれる傾向にある。

Handlebars.jsでよく使う構文

基本的なデータ埋め込み

入力オブジェクト

{
  "userName": "hoge"
}

テンプレート

{{userName}}

出力結果

hoge

ネストされたプロパティへのアクセス

入力オブジェクト

{
  "messages": {
    "message1": "Hello",
    "message2": "World"
  }
}

テンプレート

{{{messages.message1}}} {{{messages.message2}}}

または

{{#with messages ~}}
{{{message1}}} {{{message2}}}
{{/with}}

~を使用すると、その箇所の前後の空白や改行を削除できます。

出力結果

Hello World

条件分岐 (#if)

条件によって表示する内容を切り替えます。

入力オブジェクト

{
  "showDetails": true,
  "message": "詳細情報を表示します。"
}

テンプレート

{{#if showDetails ~}}
  <p>{{{message}}}</p>
{{/if}}

出力結果

<p>詳細情報を表示します。</p>

また、{{else}}を使うことで、条件が偽の場合の表示も指定できます。

入力オブジェクト (falseの場合)

{
  "isLoggedIn": false,
  "successMessage": "ようこそ!",
  "failureMessage": "ログインしてください。"
}

テンプレート

{{#if isLoggedIn}}
  <p>{{{successMessage}}}</p>
{{else}}
  <p>{{{failureMessage}}}</p>
{{/if}}

出力結果

<p>ログインしてください。</p>

繰り返し (#each)

配列の要素を1つずつ取り出して表示します。

入力オブジェクト

{
  "items": [
    {
      "projectName": "プロジェクトA",
      "partsName": "部品X"
    },
    {
      "projectName": "プロジェクトB",
      "partsName": "部品Y"
    }
  ]
}

テンプレート

<ul>
{{#each items ~}}
  <li>プロジェクト名: {{{projectName}}}, 部品名: {{{partsName}}}</li>
{{/each}}
</ul>

出力結果

<ul>
  <li>プロジェクト名: プロジェクトA, 部品名: 部品X</li>
  <li>プロジェクト名: プロジェクトB, 部品名: 部品Y</li>
</ul>

meviyでどのように使用しているか

ここでは、帳票(概算見積書)の出力時を例にご紹介します。

meviyでは概算見積書を作成する時、サーバサイド(Scala)でHandlebars.javaを使用し、
HTMLテンプレート(.hbsファイル)にデータベースから取得した見積もり情報などのデータと組み合わせて
最終的な成果物(PDF化された帳票)を生成しています。

  • 使用しているHandlebarsのライブラリ: com.github.jknack.handlebars

Handlebars.javaは、JavaおよびJVM言語(Scalaなど)向けのHandlebars実装です。 JavaScript版と同様の構文と機能を提供し、JVM環境でテンプレートエンジンとして利用できます。

処理の流れ

帳票シーケンス図

このHTMLテンプレート内で、データベースから取得した日付オブジェクト(ZonedDateTime)を「yyyy/MM/dd」のような特定の書式に整形する必要がありました。
Handlebarsの標準機能だけではこの変換が難しいため、独自の「カスタムヘルパー」を作成して対応しました。その一例がDateFormatヘルパーです。

作成したカスタムヘルパーの例:DateFormat

DateFormatは、Handlebarsテンプレート内で日付・時刻データを指定された書式に整形するためのカスタムヘルパーです。主に、ZonedDateTime型(タイムゾーン情報を持つ日時オブジェクト)のデータを、人間が読みやすい文字列に変換する目的で使用します。

テンプレート内での記述方法

{{formattedDate 日時オブジェクト format="書式" zoneId="タイムゾーン"}}
  • 日時オブジェクト: テンプレートに渡されるZonedDateTime型の変数。
  • format(任意): 出力する日時の書式を指定する(例:"yyyy年MM月dd日")。
  • zoneId(任意): タイムゾーンを指定する(例:"Asia/Tokyo")。省略した場合、デフォルトで"Asia/Tokyo"が使用される。

カスタムヘルパーの実装 (Scala) ヘルパー関数の第二引数option: Optionsには、formatzoneIdのようなイコール記号で指定されたパラメータ(ハッシュパラメータ)が格納されています。これはHandlebarsライブラリがヘルパーを呼び出す際に自動的に生成して渡すものです。

handlebars.registerHelper(
  "formattedDate",
  (obj: Any, option: Options) => {
    val value = option.hash[Any]("format")
    val zoneId = option.hash[Any]("zoneId")
    (obj, value, zoneId) match {
      case (dateTime: ZonedDateTime, format: String, zoneId: String) =>
        toZonedDateTimeStr(dateTime, format, ZoneId.of(zoneId))
      case (dateTime: ZonedDateTime, format: String, _) =>
        toZonedDateTimeStr(dateTime, format, ZoneId.of("Asia/Tokyo"))
      case (dateTime: ZonedDateTime, _, _) => dateTime.toString
      case _ => ""
    }
  }
)

使用例 2025-11-21T00:00:00Zというデータを2025/11/21という形式で出力したい場合。

{{formattedDate dateObject format="yyyy/MM/dd"}}

dateObjectには2025-11-21T00:00:00ZのようなZonedDateTimeオブジェクトが渡されていると仮定します。zoneIdは省略されているため、ヘルパー内部のデフォルト値 "Asia/Tokyo" が使用されます。

おわりに

今回は、Handlebars.jsの概要と、meviyでの活用例を紹介しました。
データとテンプレートを組み合わせてHTMLを生成したい時、ビューに複雑なビジネスロジックが不要なケースであれば、Handlebars.jsは比較的低い学習コストで導入できるツールです。

参考