はじめに
「ボタンを押したらサーバーからデータを取ってきて表示する」──たったこれだけの機能のために、ReactやVueといった大きなフレームワークを導入し、状態管理を設計し、数百KBのバンドルを配信していませんか?
もちろんフル機能のSPAが必要な場面はあります。しかし、実際のWebサイトには「一部だけ動的にしたい」というケースが驚くほど多いものです。そんなときに便利なのが、今回紹介する HMPL です。HTMLのマークアップの中に「サーバーへのリクエスト」を直接書き込めるテンプレート言語で、わずか数KBのサイズでサーバー駆動の動的UIを実現できます。
HMPLとは
HMPL(hmpl-js)は、「Fetch HTML, render it safely(HTMLをフェッチして、安全にレンダリングする)」をコンセプトにした、サーバー指向のテンプレート言語です。テンプレート内に {{#request}} というブロックを書くと、その場所にサーバーから取得したHTMLが差し込まれる、という仕組みで動作します。
「HTMLに属性を書くだけでAjaxを実現する」という点ではHTMXに近い発想ですが、HMPLはJavaScriptから compile() して使うテンプレート関数という形を取っており、リクエストのカスタマイズをJavaScript側で柔軟に行えるのが特徴です。
主な特徴
- 超軽量 - バンドルサイズは数KB程度。SPAフレームワークを入れるまでもないページに最適です
- サーバー指向 - UIのHTMLはサーバー側で生成し、クライアントは受け取って表示するだけ。ロジックをサーバーに寄せられます
- 安全なレンダリング - 取得したHTMLはDOMPurifyでサニタイズされるため、XSS対策が組み込まれています
- Fetch APIベース - リクエストはFetch APIをベースにしており、メソッドやcredentialsなどを自由にカスタマイズできます
- JSON5対応 - テンプレート内のオブジェクト記法にJSON5を採用しており、柔軟に記述できます
インストール
npmを使う場合は次の1行だけです。
npm i hmpl-js
ビルド環境なしでCDNから読み込む場合は、依存ライブラリのJSON5とDOMPurifyも一緒に読み込みます。
<script src="https://unpkg.com/json5/dist/index.min.js"></script>
<script src="https://unpkg.com/dompurify/dist/purify.min.js"></script>
<script src="https://unpkg.com/hmpl-js/dist/hmpl.min.js"></script>
基本的な使い方
HMPLの基本は「テンプレート文字列を compile() して、テンプレート関数を呼び出し、返ってきた response をDOMに追加する」という3ステップです。
まずは、ボタンをクリックするたびにサーバーからクリック数を取得して表示する例を見てみましょう。
import hmpl from "hmpl-js";
const templateFn = hmpl.compile(
`<div>
<button id="btn">Click!</button>
<div>
Clicks: {{#request src="/api/clicks" after="click:#btn"}}{{/request}}
</div>
</div>`
);
const clicker = templateFn();
document.querySelector("#app").append(clicker.response);
ポイントは {{#request}} ブロックの属性です。
src- リクエスト先のエンドポイントafter- リクエストを発火させるトリガー。click:#btnは「#btnがクリックされたら」という意味です
サーバー側は /api/clicks に対して、表示したいHTML片(例えば <span>10</span>)を返すだけで構いません。JSONをパースしてDOMを組み立てる、といったクライアント側のコードは一切不要です。
ローディング表示を追加する
リクエスト中の表示は、{{#indicator}} ブロックをネストして定義できます。
const templateFn = hmpl.compile(
`<div>
{{#request src="/api/user"}}
{{#indicator trigger="pending"}}
<p>読み込み中...</p>
{{/indicator}}
{{/request}}
</div>`
);
trigger="pending" はリクエスト実行中に表示され、完了するとサーバーからのHTMLに置き換わります。スピナーやエラー表示を宣言的に書けるので、isLoading のような状態変数を自前で管理する必要がありません。
実践的なユースケース
フォーム送信と結果の表示
実務でよくある「フォームを送信して、結果をその場に表示する」パターンを実装してみましょう。テンプレート関数には初期化関数を渡すことができ、Fetch APIのオプション(bodyやcredentialsなど)をリクエスト時に組み立てられます。
import hmpl from "hmpl-js";
const templateFn = hmpl.compile(
`<div>
<form id="registration" onsubmit="return false;">
<label for="name">名前:</label>
<input type="text" id="name" name="name" required />
<button type="submit">登録</button>
</form>
<p>
{{#request
src="/api/register"
after="submit:#registration"
method="post"
}}{{/request}}
</p>
</div>`
);
const registration = templateFn(({ request: { event } }) => ({
body: new FormData(event.target),
credentials: "same-origin"
}));
document.querySelector("#app").append(registration.response);
after="submit:#registration" によってフォーム送信がトリガーになり、初期化関数の中で FormData をそのままリクエストボディに載せています。サーバーは登録結果のHTML(「登録が完了しました」など)を返すだけです。バリデーションメッセージの出し分けもサーバー側のテンプレートで完結できます。
ポーリングによる自動更新
interval 属性を使うと、一定間隔でリクエストを繰り返す自動更新UIも宣言的に書けます。ダッシュボードのステータス表示などに便利です。
const templateFn = hmpl.compile(
`<div>
サーバー状態: {{#request src="/api/status" interval=5000}}{{/request}}
</div>`
);
document.querySelector("#dashboard").append(templateFn().response);
setInterval とfetchとDOM操作を組み合わせた定型コードが、テンプレート1行に収まりました。また memo 属性を有効にすると、同じレスポンスが返ってきた場合の再レンダリングを抑制するキャッシュ機構も利用できます。
HTMXとの使い分け
同じサーバー指向のアプローチとして有名なHTMXと迷う方も多いはずです。目安としては次のように考えると良いでしょう。
- HTMX - 既存のHTMLに属性を足していくスタイル。サーバーサイドレンダリング中心のアプリに後付けしやすい
- HMPL - テンプレートをJavaScriptで
compile()して使うスタイル。リクエストのカスタマイズ(ヘッダー、credentials、FormDataなど)をJavaScript側で細かく制御したい場合や、DOMPurifyによるサニタイズを標準で効かせたい場合に向いています
「JavaScriptから使えるHTMX」のような立ち位置と捉えると、導入イメージが湧きやすいと思います。
まとめ
HMPLは、マークアップの中にサーバーリクエストを直接書き込むという発想で、動的UIを驚くほど少ないコードで実現してくれるテンプレート言語です。
{{#request}}ブロックで、宣言的にサーバーからHTMLを取得・表示できますafterやinterval、{{#indicator}}などの機能で、トリガー・ポーリング・ローディング表示まで完結します- DOMPurifyによるサニタイズが組み込まれており、安全性にも配慮されています
- バンドルサイズは数KBなので、「一部だけ動的にしたい」ページへの導入コストが非常に低いです
SPAフレームワークを持ち出すほどではないけれど、素のfetchとDOM操作を書くのは面倒──そんな場面に出会ったら、ぜひHMPLを試してみてください。公式ドキュメント(hmpl-lang.dev)にはさらに多くの例が掲載されているので、そちらもあわせてチェックすることをおすすめします。