JavaScript | |

history.pushStateとpopstateを試してみる

今時のWebサービス使ってると、ページがリロードされてないのにURLがいつの間にか変わってたり、なのになぜかブラウザバックで適切な箇所に戻れたりするアレ。


急にSPAのこと調べてて、次のスライド見てました。

それで、初めて history.pushState() の存在を知りました。これ使うと、ページ遷移しなくてもブラウザの「戻る」の履歴を増やせるらしいです。そして実際に「戻る」が押されたときには、popstate イベントを使ってページの状態をハンドリングできるみたいです。

ググってもよくわからなかったから、↑のページ読みながらサンプル書いてみました。

説明はサンプルページ内に書いてます。よかったら読んでみてください。

.htaccess
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ index.html [L]
HTML
<!DOCTYPE html>
<html lang="ja">
<head>
	<meta charset="UTF-8" />
	<meta name="robots" content="noarchive" />
	<title>pushState / popState サンプル</title>
</head>
<body>
	<div data-screen-name="index" data-screen-title="スタート">
		<h1>スタート</h1>
		<p>このサンプルのリンクを踏むと、リロードなしでページの内容とURLが切り替わります。</p>
		<p>ブラウザで「戻る」「進む」およびリロードもできます。</p>
		<a href="#" data-ref-screen-name="p1">次→</a>
	</div>

	<div data-screen-name="p1" data-screen-title="画面1">
		<h1>画面1</h1>
		<p>今回はあらかじめHTMLに全ての画面の内容を書いておき、リンクを踏んだ時に要素の表示/非表示を切り替えているだけです。</p>
		<p>ここではそれを「画面」と読んでます。</p>
		<p>「画面」要素にはデータ属性 data-screen-name を付与してます。値は「画面名」です。(開発者ツールで見てみてください)</p>
		<p>また、リンク要素にはデータ属性 data-ref-screen-name を付与してます。値は「遷移先の画面名」です。</p>
		<a href="#" data-ref-screen-name="index">←前</a> <a href="#" data-ref-screen-name="p2">次→</a>
	</div>

	<div data-screen-name="p2" data-screen-title="画面2">
		<h1>画面2</h1>
		<p>各リンクには、onclickで pushState を呼び出すイベントを仕掛けてあります。</p>
		<p>pushState を使うと、popstate時 (「戻る」「進む」が押された時) に持ち回れる任意の情報 (state) をセットできます。</p>
		<p>ここでは state に「画面名」を持たせてます。popstate時に「画面名」を取り出して、目的の画面を表示してます。</p>
		<a href="#" data-ref-screen-name="p1">←前</a> <a href="#" data-ref-screen-name="p3">次→</a>
	</div>

	<div data-screen-name="p3" data-screen-title="画面3">
		<h1>画面3</h1>
		<p>リロード対策のため、このディレクトリ以下へのアクセスは、.htaccessを使って全てこのページに集めてます。</p>
		<p>なのでこのページはonload時にURLの最後のスラッシュ以降を見て、最初に表示する画面を切り替えてます。</p>
		<p>試しにURLの最後を p2 に書き換えて飛んでみてください。画面2が表示されると思います。</p>
		<p>ちなみに、URLの最後に aaa とか存在しない画面名を指定すると、「見つかりませんでした」画面が表示されます。</p>
		<p>※ただしここではJSのパスを相対指定してるので、スラッシュを含む名称を入れるとアウトです。すみません。</p>
		<a href="#" data-ref-screen-name="p2">←前</a>
	</div>

	<!-- URLで直接指定された画面名が見つからなかった時に表示 -->
	<div data-screen-name="screen_not_found" data-screen-title="画面が見つからない">
		<p>画面が見つかりませんでした。</p>
		<a href="#" data-ref-screen-name="index">スタートに戻る</a>
	</div>
	
	<script src="script.js"></script>
</body>
</html>
JavaScript
'use strict';
	
/**
 * 画面名を指定して画面を表示する
 * @param {String} screenName - 遷移先の画面名
 * @param {Boolean} push - trueならブラウザの履歴に追加
 */
const showScreen = (screenName, push) => {
	// 全ての画面を非表示にする
	Array.prototype.forEach.call(document.querySelectorAll('[data-screen-name]'), (elem) => {
		elem.style.display = 'none';
	});

	// 対象の画面だけ表示する
	let screenElem = document.querySelector('[data-screen-name="'+ screenName +'"]');
	if (screenElem === null) {
		// 対応する要素が無ければ「見つかりませんでした」画面表示
		screenElem = document.querySelector('[data-screen-name="screen_not_found"]');
	}
	screenElem.style.display = 'block';
	
	// 表示した画面の情報をブラウザの履歴に追加
	if (push) {
		history.pushState(
			{ screenName: screenName },      // 「戻る」「進む」が押されたときに受け取りたい情報
			screenElem.dataset.screenTitle,  // 画面タイトル (※現在未使用。今回は一応セット)
			'./' + screenName                // 画面URL
		);
	}
};


// 「data-ref-screen-name」属性つきaタグのクリック時に画面を切り替えるよう設定
Array.prototype.forEach.call(document.querySelectorAll('a[data-ref-screen-name]'), (elem) => {
	elem.addEventListener('click', (e) => {
		e.preventDefault(); // aタグの本来の挙動をキャンセル
		showScreen(e.target.dataset.refScreenName, true);
	});
});

// URLの最後のスラッシュより後ろを画面名として解釈
let screenName = location.pathname.split('/').pop();
if (screenName === '') {
	screenName = 'index';
}
showScreen(screenName); // 画面を表示する


// ブラウザの「戻る」「進む」が押されたとき
window.addEventListener('popstate', (e) => {
	// e.stateには、pushStateの第1引数にセットした値がそのまま入ってる
	if (e.state === null) {
		showScreen('index', false);
	} else {
		showScreen(e.state.screenName, false); // false = ブラウザの履歴に追加しない
	}
});