史上最悪の Htmx を作ろう

2026/07/31 14:16

史上最悪の Htmx を作ろう

RSS: https://news.ycombinator.com/rss

要約

Japanese Translation:

記事は、人気のある Web フレームワークの小型な「クローン」を構築するためのシリーズを継続しており、本日は htmx というライブラリに焦点を当てています。htm x はバックエンド開発者が最小限の JavaScript でインタラクティブなアプリを構築することを可能にするライブラリです。当初は Intercooler から進化したものであり、宣言的な属性(例:

hx-get
hx-post
)を使用して、イベントと DOM 更新を HTML に直接接続します。このプロジェクトでは、
fetch()
API などの標準的なブラウザ機能のみを使用して htmx の軽量なオープンソース JavaScript クローンを実装しており、10 行で核心的な挙動を再現し、宣言的な AJAX リクエストや DOM スワップといった複雑な機能をサポートしています。実装では、基本的なクローンを強化するために、サニタイズ化、ユーザー指定のターゲット、各種のスワップモード(例:
outerHTML
none
)、複数の HTTP メソッドおよびトリガーを処理する堅牢な
send()
関数を含んでいます。また、
X-Request
などのカスタムヘッダーをサポートし、イベントリスナーを通じてロジックをバインドすることを実現しており、「changed」や「once」などの修飾子付きの強化されたトリガー構文を実装しています。重要なのは、クローンがサーバー側ディレクティブ(例:
HX-Trigger
HX-Redirect
)を完全にサポートし、リクエストコンテキスト管理のためにカスタムイベントを発行することです。現在の実装は「SSS」API(scan、send、swap)を維持しており、バリデーションや SSE などの機能に対してプラグインをサポートしていますが、限定的な点として、失敗したリクエストに対するエラーハンドリングの改善、Promise ベースのキャンセル、モダンなビュー遷移の実装が必要です。完全なコードは https://github.com/zserge/x で入手でき、記事は 2026 年 7 月 27 日に公開されました。

本文

htmx のミニチュア版構築:宣言的記述からプラグインアーキテクチャまで

はじめに

本稿では、人気 Web フレームワークの「ミニチュア版」を構築する一連の記事の続編として、htmx に焦点を当てます。JavaScript 関心を伴わないバックエンド開発者向けのこのライブラリは、フロントエンドの複雑さを裏側で統合し、HTML テンプレートだけで強力なアプリケーションを構築可能にします。

htmx の基本動作

htmx はイベント、AJAX リクエスト、DOM 更新といった処理を隠蔽し、以下のような宣言的な記述で動作します。

<button hx-post="/clicked"
    hx-trigger="click"
    hx-target="#parent-div"
    hx-swap="outerHTML">
    Click Me!
</button>

動作の概要:

  • ボタンクリック時に
    /clicked
    POST リクエスト を送信
  • レスポンスを受け取り、
    #parent-div
    要素をレスポンスの内容で置換(スワップ)

これにより、JavaScript コードなしに動的な UI を実現できます。

コア機能の実装:メソッド、トリガー、ターゲット、スワップ

htmx は

fetch()
で URL を叩き、得られたコンテンツを指定した要素へ更新します。以下はこれを JavaScript で単純化して実装する例です。

単純なフェッチャ

まずは最も基本的な挙動(40 行程度)を実装します。

const attr = (el, name) => el.closest(`[${name}]`)?.getAttribute(name);

// スワップモードの定義
const SWAP = {
  outerHTML: (t, f) => t.replaceWith(f),
  beforebegin: (t, f) => t.before(f),
  afterbegin: (t, f) => t.prepend(f),
  beforeend: (t, f) => t.append(f),
  afterend: (t, f) => t.after(f),
  delete: t => t.remove(),
  none: () => {},
};

// 共通のスワップ処理
const swap = (mode, target, html) => {
  const tpl = document.createElement('template');
  tpl.innerHTML = html;
  // モードに応じた動作実行
  (SWAP[mode] || (() => t.replaceChildren(f)))(target, tpl.content);
};

// メインの送信処理
const send = async (el, method, url) => {
  const sel = attr(el, 'x-target');
  // ターゲットが指定されていない場合は要素そのものをターゲットとする
  const target = sel ? document.querySelector(sel) : el; 
  const mode = attr(el, 'x-swap') || 'innerHTML';
  
  const opts = { method: method.toUpperCase(), headers: { 'X-Request': 'true' } };
  if (el.matches('form')) opts.body = new URLSearchParams(new FormData(el));
  
  const res = await fetch(url, opts);
  // スワップ実行
  swap(mode, target, await res.text());
};

// イベントリスナーの追加(トリガーはクリックをデフォルトとする)
const scan = (root = document.body) =>
  ['get', 'post', 'put', 'patch', 'delete'].forEach(m =>
    root.querySelectorAll(`[x-${m}]`).forEach(el => {
      if (el.$hx) return; // 既対応スキップ
      el.$hx = true;
      
      const evt = attr(el, 'x-trigger') || 'click';
      el.addEventListener(evt, e => {
        e.preventDefault();
        send(el, m, el.getAttribute(`hx-${m}`));
      });
    })
  );

scan();

パフォーマンス改善:DOM 再スキャンの最適化

各スワップ後に全体をスキャンするのは非効率です。新たに追加されたコンテンツのみを対象とした MutationObserver を使用します。

new MutationObserver(ms => {
  for (const m of ms) 
    m.addedNodes.forEach(n => { if (n.nodeType === 1) scan(n); });
}).observe(document.body, { childList: true, subtree: true });

高度な機能:トリガー、ターゲット、イベントの拡張

より優れたトリガー(Trigger)

カンマ区切りのリストやオプションを柔軟に扱えるようにします。

const parseTriggers = (s) => {
  const triggers = s.split(',').map(s => s.trim());
  return triggers.map(trigger => {
    const [event, ...rest] = trigger.split(' ');
    const options = {};
    rest.forEach(opt => {
      const [key, value = true] = opt.split(':');
      options[key] = value;
    });
    return { event, options };
  });
}

使用例:

x-trigger="load, click one, change changed delay:500"

より優れたターゲット(Target)

querySelector
だけでなく、親要素や兄弟要素への相対的参照をサポートします。

const resolve = (el, sel) => {
  if (!sel) return el;
  // 特殊なキーワード対応
  if (sel === 'this') return el;
  if (sel === 'next') return el.nextElementSibling;
  if (sel === 'previous') return el.previousElementSibling;
  if (sel === 'document') return document;
  
  // CSS セレクターによる検索
  if (sel.startsWith('closest ')) return el.closest(sel.slice(8));
  if (sel.startsWith('find ')) return el.querySelector(sel.slice(5));
  return document.querySelector(sel);
};

より優れたイベント(Event)

リクエスト・スワップフローを制御するための 4 つのカスタムイベント を発火します。

  • x:beforeRequest
    : リクエスト前の処理
  • x:afterRequest
    : リクエスト後の処理
  • x:beforeSwap
    : スワップ前の処理
  • x:afterSwap
    : スワップ後の処理

これらに加え、サーバー側から届くヘッダーに基づいて動作を変更します。

  • HX-Trigger
    : カスタムイベントを発火
  • HX-Redirect
    : ブラウザのリダイレクト
  • HX-Refresh
    : ページ更新
  • HX-Retarget
    : 動的なターゲット変更
  • HX-Reswap
    : 動的なスワップモード変更

以下は、これらの機能を統合した最終的な

send()
関数の一部です。

const send = async (el, method, url) => {
  let target = resolve(el, attr(el, "x-target"));
  let mode = attr(el, "x-swap") || "innerHTML";
  
  const opts = {
    method: method.toUpperCase(),
    headers: { "HX-Request": "true" },
  };

  // フォーム送信時の処理
  if (el.matches("form")) opts.body = new URLSearchParams(new FormData(el));

  // 送信前イベントフック
  if (!fire(el, "x:beforeSend", { el, url, target, mode, opts }, true)) return;

  const response = await fetch(url, opts);

  // サーバーからの HX ヘッダー処理
  const hdrTrigger = response.headers.get("HX-Trigger");
  if (hdrTrigger) {
    try {
      const data = JSON.parse(hdrTrigger);
      Object.entries(data).forEach(([ev, d]) => fire(document.body, ev, d));
    } catch {
      fire(document.body, hdrTrigger);
    }
  }

  if (response.headers.get("HX-Redirect")) { window.location.href = response.headers.get("HX-Redirect"); return; }
  if (response.headers.get("HX-Refresh") === "true") { window.location.reload(); return; }

  // HX-Retarget と HX-Reswap の適用
  if (response.headers.get("HX-Retarget")) target = resolve(el, response.headers.get("HX-Retarget"));
  if (response.headers.get("HX-Reswap")) mode = response.headers.get("HX-Reswap");

  const html = await response.text();
  
  // イベントフック(送信完了)
  fire(el, "x:afterSend", { el, url, opts, target, mode, response, html });

  const detail = { el, url, target, mode, html, response };

  // スワップ前イベントフック
  if (!fire(el, "x:beforeSwap", detail, true)) return;

  swap(detail.mode, detail.target, detail.html);

  // スワップ完了イベントフック
  fire(el, "x:afterSwap", detail);
  
  // 追加された要素へのリスナー再登録(SSS の一部)
  scan(detail.target); 
};

プラグインアーキテクチャ:コア機能の拡張

htmx は当初は単純な「送信+置換」でしたが、現在は多数の高度な属性がサポートされています。当実装でも SSS(スキャン+送信+置換) の核心を変更せず、プラグインで機能付与を目指します。

例:ブースティング(Boosting)

リンクやフォームを AJAX 化するための「ブースティング」機能は、コアではなく JS スニペットで容易に実装可能です。

document.addEventListener('click', e => {
  const boosted = e.target.closest('[x-boost]');
  if (!boosted) return;
  
  const link = e.target.closest('a');
  if (!link || !link.getAttribute('href') || link.getAttribute('target')) return;

  e.preventDefault();
  window.x.send(boosted, 'get', link.getAttribute('href'));
});

追加プラグインのアイデア

以下のような機能は、数行のコードとイベントフック(主に

x:beforeSend
x:afterSwap
)で実装可能です。

  • x-confirm
    : 送信前に確認ダイアログを表示(キャンセル時は処理を中止)。
  • x-indicator
    : ローディングスピナーを表示・非表示をトグル。
  • x-disable
    : 送信中の要素を無効化し、重複送信を防ぐ。
  • x-headers
    : JSON 属性からリクエストヘッダーを追加。
  • x-vals
    /
    x-include
    : リクエストボディに値を追加。
  • x-select
    : レスポンスの一部だけを置換(バックエンドの簡略化)。
  • x-sync
    : 並列実行中のリクエストを制御(中止・置換・キュー)。
  • x-validate
    : ブラウザのネイティブバリデーションを実行。
  • x-push-url
    /
    x-replace-url
    : ブラウザ履歴を更新。
  • x-sse
    /
    x-ws
    : サーバー送信用ストリーミングメッセージに対応。

今後の課題と互換性への道

完全に htmx と互換性を持たせるには、以下のような拡張が必要です。

  • エラー処理:
    fetch()
    の失敗や非 2xx レスポンスの適切なハンドリング。
  • イベントの増加: より多くのカスタムイベントを発火させる。
  • 共通インターフェース: プラグイン間で共有する
    resolve()
    などの関数の公開。
  • キャンセル機能: Promise ベースの非同期操作(例:確認ダイアログ、ビュー遷移)への対応。

これらの実装は読者の課題となります。あるいは、すでに完成した htmx をそのまま利用するのが最適かもしれません。

まとめとリソース

今回の実験で構築した全コードは、以下の GitHub リポジトリに公開されています。バグ報告やコントリビュートをご希望の方はぜひご協力ください。

本記事のシリーズ(「Let's make the worst VueJS ever!」など)もご覧ください。

同じ日のほかのニュース

一覧に戻る →

2026/08/01 4:03

Hugging Face の侵入を Tailscale が阻止しなかった

## Japanese Translation: 最近のセキュリティインシデントにより、Hugging Face の AI エージェントが永続的な Tailscale 認証キーを介して侵害され、攻撃者が悪意のあるノード 181 台を生成し、Kubernetes クラスタで root アクセスを取得し、4 日間で秘密管理ストレージにある 136 キーを含むシークレットストアにアクセスできたことが明らかになりました。Tailscale そのものには脆弱性はありませんでしたが、特権の過度に付与されたエージェントが静的認証キーを使用することで、このエスケープが可能になりました。専門家は、これらを**ワークロードアイデンティティ連邦**(署名された OIDC により短期間有効なトークンを生成)または、サポートされている場合にハードウェアバインドのキーを利用するように置き換えることを推奨しています。組織もまた、エージェントがローカルテレメトリを抑制している場合でも異常を検出するために**ネットワークフローログ**を有効にすべきであり、**Tailnet Lock**などの厳格なアドミッション制御を実装する必要があります。Tailscale は文書の改善、危険なアクションに対する UI の警告の追加、デフォルト設定の微調整による将来のインシデントの防止に取り組んでおり、同社はこの点を認識しています。 --- ### 改訂サマリー(欠落していた詳細を統合): 最近のセキュリティインシデントにより、Hugging Face の AI エージェントが永続的な Tailscale 認証キーを使用して侵害される仕組みが暴露されました。攻撃者はこれらの再利用可能な認証情報を利用し、4 日間にわたり悪意のあるノード 181 台を生成し、「秘密管理ストレージの 136 キー」へのアクセスを含むシークレットを窃取しました。これは、静的なキーが「ゼロトラスト」環境であっても深刻なリスクをもたらすことを示しています。Tailscale そのものには脆弱性はありませんでしたが、デフォルトの設定により、特権の過度に付与されたエージェントが Kubernetes クラスタの root アクセスを取得することができました。このケースは、auth keys などの標準的な認証方法の危険性を浮き彫りにしており、これらは一般的ですが、継続的な AI ワークロードには不適切で不安全です。将来のエスケープを防止するため、専門家は静的認証情報を、ワークロードアイデンティティ連邦による短期間有効なトークン(または HSM の発行が利用の妨げにならない場合にハードウェアバインドのキー)に置き換えることを推奨しています。組織はまた、異常を検出するためにネットワークフローログを有効にし、動的な識別子ベースのアクセス制御へと移行する必要があります。さらに、**Tailnet Lock**による厳格なアドミッション制御の実装や、デバイスポスチャーチェックの利用によって、不明瞭なノードをより効果的に孤立させることができます。Tailscale はゼロトラストの期待にもかかわらずインシデントを引き起こしたことを認め、文書の改善、UI のナッジの追加、デフォルト設定の微調整、類似の AI 駆動によるエスケープベクトルに対する構成強化へのエンジニアリングサポートを提供することで対応することを約束しています。

2026/08/01 0:17

エレベーター

## 日本語訳: 歴史的事象シミュレーションによるエレベーターアルゴリズムの比較により、単純な反応型戦略は動的な交通状況において複雑な最適化手法よりも優れたパフォーマンスを発揮することが示されています。SCAN(1961 年に特許出願)はロビーから最上階まで移動した後で方向を反転させ、一方 LOOK は現在の方向の要求が完了する dès à présent で反転を開始し、必ずしも最上階まで到達する必要はありません。両者はどちらも中央スケジューラーに依存し、新しい要求を最も手近な稼働中のエレベーターへ割り当てます。パフォーマンスは、30 秒以内かつ 90 秒以内の到着割合といった待機時間指標で測定されます。これらの研究では、早朝ラッシュ(ロビーから上層への移動)は、一貫して特定の方向の混雑を生じるため、夜間よりも通常より悪い待機時間を引き起こすことが示されています。奥蒂斯の RSR などの高度なプラットフォームは、遅延を処理するために継続的な再最適化(5 秒ごと)を使用し、ETA、車内負荷ペナルティ、同方向への集まる回避ボーナス、方向一致ボーナス、近接アイドルボーナスといった評価要素を活用します。しかし、ベンチマーク結果では、LOOK は高流量(>7 階/分)時や小規模なビルにおいて RSR を上回る可能性があり、そのシンプルなルールが不要な停車を減らすためです。キオスクを使用した目的地割り当てシステムは、通常よりも悪い待時間を生じることが多く、この直感に反する結果は、硬直的なキオスク割り当てと、5 秒ごとの再バランスステップがその窓期内に変化する交通状況に対応できないことに起因します。極めて高層のビルで多数のエレベーターがある場合、キオスクが提供する追加情報が有益である可能性もありますが、一般的なシミュレーション結果では、完璧な効率を追求する重機的な最適化手法よりも、適応可能なルールベースの割り当てシステムを維持することで、より優れた信頼性を確保できると示唆されています。待機時間(<30 秒、<90 秒)、階数、車両数、流量(例:18/分)などの変数を実験するためのシミュレーションツールが用意されています。

2026/08/01 3:04

qm

## Japanese Translation: Quantum(QM)は、スタートアップ向けに開発された安全なマルチプレイヤージェントハネスであり、Slack と Web チャンネルと直接連携しつつ、隔離されたワークスペース内で従業員が安全にコラボレーションすることを可能にする。该平台は、耐久性のあるサンドボックス、スコープされたメモリ、そして個々のユーザーおよび共有ルーム両方に対してファイルおよびキーチェーンビューに対する厳格な制御を提供することで、重要なデータプライバシーの問題に対処しています。オープンソースの原則(MIT ライセンス)に基づいて構築され、Node 上で TypeScript と Fastify を使用して動作するヘッドレスコア API を備えた QM は、Pi、OpenCode、Codex、Claude Code など多様な AI モデルをサポートしながら、ベンダーロックインを引き起こしません。システムは、破壊的なアクションに対して硬い拒否を実装する事前宣言されたコマンドポリシーを含む 3 つの構成可能なポーズ(Strict、Auto default、Dangerous)を通じてセキュリティを確保しています。技術的には、Postgres の永続化レイヤーを利用し、デプロイは特定のディレクトリ構造(`deploy/layers/<org>/`)を介して管理され、バイト識別可能性のあるコアを組織固有のインフラストラクチャとプラグインイメージから分離します。デプロイは `qm init` CLI を使用して開始され、スキルを具現化し、GitHub の標準的なフォーク機能ではなくローカルでリポジトリをフォークすることで、組織がコードベース全体を秘密に保つことを可能にします。さらに、QM は内部データの漏洩を厳格に防止しながらアップストリームの変更をマージする特定のスキル(`update-qm` および `upstream-pr`)を通じて継続的な更新を促進します。また、プラットフォームはカスタム内部 Web アプリ、Git リポジトリから共有可能なスキル、cron を介したバックグラウンドプロセス、および管理制御をサポートしています。ドキュメントは `docs/getting-started.md` などの主要なマークダウンファイルで利用可能です。最終的には、QM はデータの完全性やセキュリティを損なうことなく、スタートアップがプライベートプロジェクトにおける強固なコラボレーションを実現できるようにし、AI を活用する方法を変革します。

史上最悪の Htmx を作ろう | そっか~ニュース