htmx 逆引きレシピ
履歴キャッシュを調整するには?
公開日:
最終更新日:
履歴キャッシュを調整すると、戻る/進むの体感速度や安全性を同時に改善できます。
このページでは hx-history / hx-history-elt / hx-preserve / hx-swap を使い分ける3パターンを整理します。
使用するhtmx属性
タグ:hx-history / hx-history-elt / hx-preserve / hx-swap
hx-history:履歴スナップショットへ保存するかを制御します(例:hx-history="false")。hx-history-elt:履歴に保存する範囲を絞り込みます(例:#MAINだけ保存)。hx-preserve:差し替え時にID付き要素を保持し、入力途中の値を守ります。hx-swap:差し替え方式を固定して、戻る/進む時の見え方を安定させます。
利用シーン
- 戻る/進むが重い:一覧⇄詳細などの往復が多い画面で、再取得を減らして“戻る/進む”を体感サクサクにしたい
- フォーム値を残したい:入力途中で別画面を見に行って戻っても、入力欄の内容だけは消さずに作業を継続したい
- 機密データを履歴に残したくない:トークン/個人情報/ワンタイム情報は履歴復元させず、戻る時は再取得やマスク表示にしたい
共通PHP(全文)
エスケープ関数・表示切替・フルHTML出力を _history_cache_data.php に集約しています。
全表示値は共通関数経由で htmlspecialchars を適用します。
PHP(_history_cache_data.php)
/htmx/demo/_history_cache_data.php
<?php
// 型を厳密に扱う
declare(strict_types=1);
// HTML特殊文字を安全化する
function history_cache_h(string $value): string
{
// エスケープ済み文字列を返す
return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
}
// リクエスト文字列を安全に取得する
function history_cache_request_text(array $source, string $key, string $fallback): string
{
// 生値を文字列として取得する
$raw = trim((string)($source[$key] ?? ''));
// 空文字なら既定値を返す
if ($raw === '') {
// 既定値を返す
return $fallback;
}
// 入力値を返す
return $raw;
}
// 許可値の中から1つを選ぶ
function history_cache_pick(string $value, array $allowed, string $fallback): string
{
// 入力値が許可されるか判定する
if (!in_array($value, $allowed, true)) {
// 許可外なら既定値へ補正する
return $fallback;
}
// 許可値を返す
return $value;
}
// DEMO番号を安全に決める
function history_cache_request_demo(array $source): string
{
// demo値を取得する
$demo = history_cache_request_text($source, 'demo', '1');
// 許可されたdemo値へ補正する
return history_cache_pick($demo, ['1', '2', '3'], '1');
}
// DEMO1用view値を安全に決める
function history_cache_request_demo1_view(array $source): string
{
// view値を取得する
$view = history_cache_request_text($source, 'view', 'a');
// 許可されたview値へ補正する
return history_cache_pick($view, ['a', 'b'], 'a');
}
// DEMO2用panel値を安全に決める
function history_cache_request_demo2_panel(array $source): string
{
// panel値を取得する
$panel = history_cache_request_text($source, 'panel', 'list');
// 許可されたpanel値へ補正する
return history_cache_pick($panel, ['list', 'detail'], 'list');
}
// DEMO2用item値を安全に決める
function history_cache_request_demo2_item(array $source): string
{
// item値を取得する
$item = history_cache_request_text($source, 'item', '101');
// 許可されたitem値へ補正する
return history_cache_pick($item, ['101', '102', '103'], '101');
}
// DEMO3用view値を安全に決める
function history_cache_request_demo3_view(array $source): string
{
// view値を取得する
$view = history_cache_request_text($source, 'view', 'public');
// 許可されたview値へ補正する
return history_cache_pick($view, ['public', 'secret'], 'public');
}
// DEMO2の案件一覧を返す
function history_cache_demo2_items(): array
{
// 一覧配列を返す
return [
['id' => '101', 'title' => '会員ランク更新バッチ', 'owner' => 'tanaka', 'detail' => '毎日 03:00 に昇格判定を実行する。'],
['id' => '102', 'title' => 'メール本文テンプレ見直し', 'owner' => 'sato', 'detail' => '件名のABテスト配信条件を調整する。'],
['id' => '103', 'title' => 'API鍵ローテーション手順', 'owner' => 'kato', 'detail' => '環境ごとの切替時刻と監視手順を明文化する。'],
];
}
// DEMO2の対象案件を返す
function history_cache_demo2_find_item(string $itemId): array
{
// 一覧データを取得する
$items = history_cache_demo2_items();
// 先頭データを既定値にする
$fallback = $items[0];
// 1件ずつID一致を確認する
foreach ($items as $row) {
// 行IDを文字列化する
$id = (string)($row['id'] ?? '');
// 一致したらこの行を返す
if ($id === $itemId) {
// 該当行を返す
return $row;
}
}
// 見つからない場合は既定行を返す
return $fallback;
}
// 一意な生成番号を返す
function history_cache_generation_id(): string
{
// 高精度時刻文字列を取得する
$seed = (string)microtime(true);
// 8桁の短縮ハッシュを返す
return strtoupper(substr(sha1($seed), 0, 8));
}
// 現在時刻ラベルを返す
function history_cache_now_label(): string
{
// 現在日時を返す
return date('Y-m-d H:i:s');
}
// DEMO1メイン領域を描画する
function history_cache_render_demo1_main(string $selfPath, string $view): string
{
// 重い画面を疑似的に再現する
$started = microtime(true);
// 疑似待機を入れる
usleep(320000);
// 描画時間をmsで計測する
$elapsedMs = (int)round((microtime(true) - $started) * 1000);
// サーバ時刻を取得する
$serverTime = history_cache_h(history_cache_now_label());
// 生成番号を取得する
$generation = history_cache_h(history_cache_generation_id());
// view表示文字列を作る
$viewLabel = history_cache_h('view=' . $view);
// view=aリンクを作る
$linkA = history_cache_h($selfPath . '?view=a');
// view=bリンクを作る
$linkB = history_cache_h($selfPath . '?view=b');
// 表示タイトルを決める
$title = ($view === 'a') ? 'ダッシュボードA(重い集計)' : 'ダッシュボードB(重い可視化)';
// 表示説明を決める
$desc = ($view === 'a')
? '注文集計を再計算する想定で、サーバ側に待機を入れています。'
: '在庫可視化を再計算する想定で、サーバ側に待機を入れています。';
// タイトルを安全化する
$titleEsc = history_cache_h($title);
// 説明を安全化する
$descEsc = history_cache_h($desc);
// HXヘッダを安全化する
$hxHeader = history_cache_h((string)($_SERVER['HTTP_HX_REQUEST'] ?? '(none)'));
// HTMLバッファを開始する
ob_start();
?>
<main id="MAIN" class="SECTION" hx-history-elt>
<h2><?= $titleEsc ?>(<?= $viewLabel ?>)</h2>
<p class="HTMX-NOTE"><?= $descEsc ?></p>
<div class="FORM-RESULT is-ok">
<p><strong>server time:</strong> <?= $serverTime ?></p>
<p><strong>generation #:</strong> <?= $generation ?></p>
<p><strong>render cost:</strong> 約 <?= history_cache_h((string)$elapsedMs) ?>ms(疑似)</p>
<p><strong>HX-Request:</strong> <?= $hxHeader ?></p>
</div>
<nav class="NAV MT1rm">
<a
class="BTN"
href="<?= $linkA ?>"
hx-get="<?= $linkA ?>"
hx-select="#MAIN > *"
hx-target="#MAIN"
hx-swap="innerHTML"
hx-push-url="true"
>view=a</a>
<a
class="BTN"
href="<?= $linkB ?>"
hx-get="<?= $linkB ?>"
hx-select="#MAIN > *"
hx-target="#MAIN"
hx-swap="innerHTML"
hx-push-url="true"
>view=b</a>
</nav>
<section class="CARD MT1rm">
<h3>確認ポイント</h3>
<ul class="HTMX-LIST">
<li>まず view=a→view=b と遷移して履歴を作る</li>
<li>ブラウザの「戻る」で view=a に戻る</li>
<li>時刻/生成番号が変わらなければ履歴キャッシュ復元の目安</li>
</ul>
</section>
</main>
<?php
// 生成したHTMLを返す
return (string)ob_get_clean();
}
// DEMO2メイン領域を描画する
function history_cache_render_demo2_main(string $selfPath, string $panel, string $itemId): string
{
// 一覧データを取得する
$items = history_cache_demo2_items();
// 対象案件を取得する
$current = history_cache_demo2_find_item($itemId);
// 一覧リンクを作る
$linkList = history_cache_h($selfPath . '?panel=list&item=' . rawurlencode($itemId));
// 詳細リンクを作る
$linkDetail = history_cache_h($selfPath . '?panel=detail&item=' . rawurlencode($itemId));
// 現在時刻を安全化する
$serverTime = history_cache_h(history_cache_now_label());
// HTMLバッファを開始する
ob_start();
?>
<main id="MAIN" class="SECTION">
<h2>案件メモ画面(panel=<?= history_cache_h($panel) ?>)</h2>
<p class="HTMX-NOTE">
下の入力欄は <code>id="DEMO2_MEMO" + hx-preserve</code> です。<br>
一覧/詳細の部分更新と、戻る/進むの両方で入力値保持を確認できます。
</p>
<div class="FORM-RESULT">
<p><strong>server time:</strong> <?= $serverTime ?></p>
<p><strong>hint:</strong> 入力欄に文字を打った後でタブ切替し、値が残ることを確認してください。</p>
</div>
<form class="FORM MT1rm" onsubmit="return false;">
<label for="DEMO2_MEMO">作業メモ(保持対象)</label>
<input
id="DEMO2_MEMO"
type="text"
value=""
placeholder="ここに入力してから一覧/詳細を切り替える"
hx-preserve
>
</form>
<nav class="NAV MT1rm">
<a
class="BTN"
href="<?= $linkList ?>"
hx-get="<?= $linkList ?>"
hx-select="#MAIN > *"
hx-target="#MAIN"
hx-swap="innerHTML"
hx-push-url="true"
>一覧</a>
<a
class="BTN"
href="<?= $linkDetail ?>"
hx-get="<?= $linkDetail ?>"
hx-select="#MAIN > *"
hx-target="#MAIN"
hx-swap="innerHTML"
hx-push-url="true"
>詳細</a>
</nav>
<?php if ($panel === 'list'): ?>
<section class="CARD MT1rm">
<h3>案件一覧</h3>
<ul class="HTMX-LIST">
<?php foreach ($items as $row): ?>
<?php $id = (string)($row['id'] ?? ''); ?>
<?php $title = (string)($row['title'] ?? ''); ?>
<?php $owner = (string)($row['owner'] ?? ''); ?>
<?php $itemLink = history_cache_h($selfPath . '?panel=detail&item=' . rawurlencode($id)); ?>
<li>
<a
class="BTN"
href="<?= $itemLink ?>"
hx-get="<?= $itemLink ?>"
hx-select="#MAIN > *"
hx-target="#MAIN"
hx-swap="innerHTML"
hx-push-url="true"
><?= history_cache_h($id) ?></a>
<?= history_cache_h($title) ?>(担当: <?= history_cache_h($owner) ?>)
</li>
<?php endforeach; ?>
</ul>
</section>
<?php else: ?>
<section class="CARD MT1rm">
<h3>案件詳細 #<?= history_cache_h((string)($current['id'] ?? '')) ?></h3>
<p><strong>タイトル:</strong> <?= history_cache_h((string)($current['title'] ?? '')) ?></p>
<p><strong>担当:</strong> <?= history_cache_h((string)($current['owner'] ?? '')) ?></p>
<p><strong>内容:</strong> <?= history_cache_h((string)($current['detail'] ?? '')) ?></p>
</section>
<?php endif; ?>
</main>
<?php
// 生成したHTMLを返す
return (string)ob_get_clean();
}
// DEMO3メイン領域を描画する
function history_cache_render_demo3_main(string $selfPath, string $view): string
{
// 現在時刻を安全化する
$serverTime = history_cache_h(history_cache_now_label());
// 生成番号を安全化する
$generation = history_cache_h(history_cache_generation_id());
// 公開ビューリンクを作る
$linkPublic = history_cache_h($selfPath . '?view=public');
// 機密ビューリンクを作る
$linkSecret = history_cache_h($selfPath . '?view=secret');
// HTMLバッファを開始する
ob_start();
?>
<main id="MAIN" class="SECTION">
<h2>機密情報の履歴制御(view=<?= history_cache_h($view) ?>)</h2>
<div class="FORM-RESULT">
<p><strong>server time:</strong> <?= $serverTime ?></p>
<p><strong>generation #:</strong> <?= $generation ?></p>
<p><strong>policy:</strong> 機密領域は戻る時に再取得/再計算させる</p>
</div>
<nav class="NAV MT1rm">
<a
class="BTN"
href="<?= $linkPublic ?>"
hx-get="<?= $linkPublic ?>"
hx-select="#MAIN > *"
hx-target="#MAIN"
hx-swap="innerHTML"
hx-push-url="true"
>公開情報</a>
<a
class="BTN"
href="<?= $linkSecret ?>"
hx-get="<?= $linkSecret ?>"
hx-select="#MAIN > *"
hx-target="#MAIN"
hx-swap="innerHTML"
hx-push-url="true"
>機密情報</a>
</nav>
<section class="CARD MT1rm">
<h3>公開サマリ</h3>
<p>この領域は通常どおり履歴キャッシュ対象です。</p>
</section>
<?php if ($view === 'secret'): ?>
<?php $oneTime = history_cache_h(substr(hash('sha256', (string)microtime(true)), 0, 6)); ?>
<section class="CARD MT1rm" hx-history="false">
<h3>ワンタイム表示(履歴非保存)</h3>
<p><strong>one-time code:</strong> <?= $oneTime ?></p>
<p class="HTMX-NOTE">
このカードは <code>hx-history="false"</code> を指定しています。<br>
戻る時は履歴復元せず、サーバ再取得で再計算されます。
</p>
</section>
<?php else: ?>
<section class="CARD MT1rm">
<h3>機密カードは未表示</h3>
<p>「機密情報」を開いた時だけワンタイム情報を表示します。</p>
</section>
<?php endif; ?>
</main>
<?php
// 生成したHTMLを返す
return (string)ob_get_clean();
}
// DEMO種別ごとのメイン領域HTMLを返す
function history_cache_render_main(string $selfPath, string $demo, string $view, string $panel, string $itemId): string
{
// DEMO1なら専用描画を返す
if ($demo === '1') {
// DEMO1のHTMLを返す
return history_cache_render_demo1_main($selfPath, $view);
}
// DEMO2なら専用描画を返す
if ($demo === '2') {
// DEMO2のHTMLを返す
return history_cache_render_demo2_main($selfPath, $panel, $itemId);
}
// DEMO3のHTMLを返す
return history_cache_render_demo3_main($selfPath, $view);
}
// フルページを出力する
function history_cache_emit_page(string $selfPath, string $demo, string $view, string $panel, string $itemId, bool $withDemo): void
{
// コンテンツタイプを返す
header('Content-Type: text/html; charset=UTF-8');
// メインHTMLを作る
$mainHtml = history_cache_render_main($selfPath, $demo, $view, $panel, $itemId);
// 自己URLを安全化する
$selfEsc = history_cache_h($selfPath);
// DEMOパラメータ付きURLを作る
$demo1Link = $withDemo ? history_cache_h($selfPath . '?demo=1&view=a') : history_cache_h('/htmx/demo/_history_cache_1.php?view=a');
// DEMO2リンクを作る
$demo2Link = $withDemo ? history_cache_h($selfPath . '?demo=2&panel=list&item=101') : history_cache_h('/htmx/demo/_history_cache_2.php?panel=list&item=101');
// DEMO3リンクを作る
$demo3Link = $withDemo ? history_cache_h($selfPath . '?demo=3&view=public') : history_cache_h('/htmx/demo/_history_cache_3.php?view=public');
?>
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title><?= history_cache_h('history-cache demo ' . $demo) ?></title>
<style>
body{margin:0;padding:0;font-family:sans-serif;line-height:1.5;background:#fafafa}
.HEADER{padding:1rem;background:#fff;border-bottom:1px solid #ddd}
.LAYOUT{display:grid;grid-template-columns:260px 1fr;gap:1rem;padding:1rem}
.ASIDE{background:#fff;border:1px solid #ddd;border-radius:8px;padding:1rem}
.SECTION{background:#fff;border:1px solid #ddd;border-radius:8px;padding:1rem}
.NAV{display:flex;flex-wrap:wrap;gap:.5rem}
.BTN{display:inline-block;padding:.45rem .75rem;border:1px solid #777;border-radius:6px;text-decoration:none;color:#222;background:#fff}
.FORM{display:grid;gap:.5rem}
.FORM input{padding:.5rem;border:1px solid #bbb;border-radius:6px}
.FORM-RESULT{margin-top:.75rem;padding:.75rem;border:1px dashed #bbb;border-radius:8px;background:#fcfcfc}
.FORM-RESULT.is-ok{border-color:#2e7d32;background:#f3fff4}
.CARD{margin-top:.75rem;padding:.75rem;border:1px solid #e2e2e2;border-radius:8px;background:#fff}
.HTMX-NOTE{color:#555}
.HTMX-LIST{padding-left:1.2rem}
.MT1rm{margin-top:1rem}
.FOOTER{padding:1rem;text-align:center;color:#666}
@media (max-width:800px){.LAYOUT{grid-template-columns:1fr}}
</style>
<script src="/htmx/htmx.min.js" defer></script>
</head>
<body>
<header class="HEADER">
<h1>DEMO<?= history_cache_h($demo) ?>: history cache の制御</h1>
<p class="HTMX-NOTE">戻る/進む最適化と安全性のバランスをデモ別に確認します。</p>
</header>
<div class="LAYOUT">
<aside class="ASIDE">
<h2>デモ切替</h2>
<ul class="HTMX-LIST">
<li><a href="<?= $demo1Link ?>">DEMO1</a></li>
<li><a href="<?= $demo2Link ?>">DEMO2</a></li>
<li><a href="<?= $demo3Link ?>">DEMO3</a></li>
</ul>
<p><a class="BTN" href="<?= $selfEsc ?>">初期状態へ</a></p>
</aside>
<?= $mainHtml ?>
</div>
<footer class="FOOTER">
<p>history cache demo footer</p>
</footer>
</body>
</html>
<?php
}
PHP(_history_cache_page.php)
/htmx/demo/_history_cache_page.php
<?php
// 型を厳密に扱う
declare(strict_types=1);
// 共通関数を読み込む
require_once(__DIR__ . '/_history_cache_data.php');
// demo値を取得する
$demo = history_cache_request_demo($_GET);
// DEMO1用view既定値を用意する
$view = history_cache_request_demo1_view($_GET);
// DEMO2用panel既定値を取得する
$panel = history_cache_request_demo2_panel($_GET);
// DEMO2用item値を取得する
$itemId = history_cache_request_demo2_item($_GET);
// DEMO3ならviewを上書きする
if ($demo === '3') {
// DEMO3のview値を取得する
$view = history_cache_request_demo3_view($_GET);
}
// DEMO2ならviewは固定値にする
if ($demo === '2') {
// DEMO2ではviewを固定する
$view = 'a';
}
// フルページを出力する
history_cache_emit_page('/htmx/demo/_history_cache_page.php', $demo, $view, $panel, $itemId, true);
① 戻る/進むが重い → 履歴キャッシュで速くする
疑似的に重いレンダリング(待機)を入れた画面で view=a/b を往復します。
サーバ時刻・生成番号が変わらなければ、戻る時に履歴キャッシュから即復元された目安です。
HTML(view全文)
_demo_htmx_1.php
<div class="DEMO">
<h4>DEMO1: 戻る/進むが重い画面を履歴キャッシュで最適化</h4>
<p class="HTMX-NOTE">
重い画面(疑似待機あり)を <code>view=a/b</code> で往復し、戻る時の復元速度を確認します。<br>
<a class="BTN" href="/htmx/demo/_history_cache_1.php?view=a" target="_blank" rel="noopener">別タブで開く</a>
</p>
<div class="CARD">
<iframe
src="/htmx/demo/_history_cache_1.php?view=a"
title="DEMO1: 履歴キャッシュで戻る/進むを高速化"
style="width:100%;min-height:660px;border:1px solid #ddd;border-radius:8px;background:#fff;"
loading="lazy"
></iframe>
</div>
</div>
PHP(_history_cache_1.php)
/htmx/demo/_history_cache_1.php
<?php
// 型を厳密に扱う
declare(strict_types=1);
// 共通関数を読み込む
require_once(__DIR__ . '/_history_cache_data.php');
// DEMO1のview値を取得する
$view = history_cache_request_demo1_view($_GET);
// DEMO2のpanel既定値を用意する
$panel = 'list';
// DEMO2のitem既定値を用意する
$itemId = '101';
// DEMO1ページを出力する
history_cache_emit_page('/htmx/demo/_history_cache_1.php', '1', $view, $panel, $itemId, false);
デモ
② フォーム値を残したい → hx-preserve を使う
一覧/詳細を部分更新で切り替えながら、id 付き入力欄を hx-preserve で保持します。
戻る/進むと部分更新の両方で、入力中の値が残る動きです。
HTML(view全文)
_demo_htmx_2.php
<div class="DEMO">
<h4>DEMO2: 一覧/詳細の部分更新でも入力値を保持(hx-preserve)</h4>
<p class="HTMX-NOTE">
一覧と詳細を行き来しても、<code>id</code> 付き入力欄だけ値を保持します。<br>
<a class="BTN" href="/htmx/demo/_history_cache_2.php?panel=list&item=101" target="_blank" rel="noopener">別タブで開く</a>
</p>
<div class="CARD">
<iframe
src="/htmx/demo/_history_cache_2.php?panel=list&item=101"
title="DEMO2: hx-preserveで入力値を保持"
style="width:100%;min-height:700px;border:1px solid #ddd;border-radius:8px;background:#fff;"
loading="lazy"
></iframe>
</div>
</div>
PHP(_history_cache_2.php)
/htmx/demo/_history_cache_2.php
<?php
// 型を厳密に扱う
declare(strict_types=1);
// 共通関数を読み込む
require_once(__DIR__ . '/_history_cache_data.php');
// DEMO1のview既定値を用意する
$view = 'a';
// DEMO2のpanel値を取得する
$panel = history_cache_request_demo2_panel($_GET);
// DEMO2のitem値を取得する
$itemId = history_cache_request_demo2_item($_GET);
// DEMO2ページを出力する
history_cache_emit_page('/htmx/demo/_history_cache_2.php', '2', $view, $panel, $itemId, false);
デモ
③ 機密データを履歴に残したくない → hx-history を無効化
ワンタイムコード表示カードに hx-history="false" を付け、履歴キャッシュ保存を抑止します。
戻る時は再取得/再計算させ、機密値のスナップショット残存を避ける構成です。
HTML(view全文)
_demo_htmx_3.php
<div class="DEMO">
<h4>DEMO3: 機密カードだけ履歴保存を止める(hx-history="false")</h4>
<p class="HTMX-NOTE">
ワンタイム情報を表示する領域を履歴キャッシュに残さず、戻る時は再取得させます。<br>
<a class="BTN" href="/htmx/demo/_history_cache_3.php?view=public" target="_blank" rel="noopener">別タブで開く</a>
</p>
<div class="CARD">
<iframe
src="/htmx/demo/_history_cache_3.php?view=public"
title="DEMO3: hx-history falseで機密情報を保持しない"
style="width:100%;min-height:720px;border:1px solid #ddd;border-radius:8px;background:#fff;"
loading="lazy"
></iframe>
</div>
</div>
PHP(_history_cache_3.php)
/htmx/demo/_history_cache_3.php
<?php
// 型を厳密に扱う
declare(strict_types=1);
// 共通関数を読み込む
require_once(__DIR__ . '/_history_cache_data.php');
// DEMO3のview値を取得する
$view = history_cache_request_demo3_view($_GET);
// DEMO2のpanel既定値を用意する
$panel = 'list';
// DEMO2のitem既定値を用意する
$itemId = '101';
// DEMO3ページを出力する
history_cache_emit_page('/htmx/demo/_history_cache_3.php', '3', $view, $panel, $itemId, false);
デモ
次に読むオススメレシピ
参考リンク
このページの著者
経験:Webアプリ/業務システム
得意:PHP・JavaScript・MySQL・CSS
個人実績:フォーム生成基盤/クイズ学習プラットフォーム 等
詳しいプロフィールはこちら! もちもちみかんのプロフィール