JavaScriptで履歴管理

最近、jsでメニューを開けたり閉じたりした操作や、AJAXJavaScriptによる動的差し替えを行った際に、ブラウザの戻る・進むボタンが使えず、直感的な操作でないような気がしたので、いろいろ調べつつ、クロスブラウザなライブラリを作ってみた。
(とりあえず、現時点でIE6/7・FireFox2・Safari3.1・Opera9.23で検証)
(2008.06.14追記。Opera9.50でも動いた。)

とりあえず、ぐぐったりなんかして、断片的に調べてみたところ、以下のようなことがわかる。

■ページ内のアンカージャンプを利用して履歴を残す。
ページ内に存在しないアンカーに対してジャンプさせることで、ページ移動せずに履歴を残すことができる。
<この方法の問題>
IEの場合、アンカージャンプでは履歴が残らない。


■上との組み合わせ、アドレスの変化を監視して、変更時にアクションを起動。
window.location.watch("href", function(){}) や、setInterval(function(){}, 100) などで、アドレス欄の変化を検出してアクションを起動する。
<この方法の問題>
・window.location.watch() は、OperaSafariではうまくいかない。
・setIntervalは、Operaでは戻るボタンを使った場合、タイマーがとまってしまう。

■iframe内の遷移を利用して、履歴を残す。
iframeのソースを変更することで、履歴を残すことができる。iframeの読み込みが完了した時点でアクションを起動可能。
<この方法の問題>
Safariでは履歴が残らない。
FireFoxの場合、戻るボタンではiframeの再読み込みが起きない。


上記を組み合わせて、うまい具合にいかないかなーと調査。組み合わせのためには、片方で履歴を残しつつ、もう片方は履歴を残さずに遷移することが必要なので、以下を追加調査。

■履歴を残さずに画面を切り替える。
window.location.replace() を利用して、履歴を残さずに遷移することが可能。
<この方法の問題>
Safariにおいて、iframeのソースに対して行った場合、履歴に残る。


というわけで、いろいろ試行錯誤してみたところ、ブラウザによって処理を分けないとどうしようもないということで、仕方がないので分割。IE/Operaと、その他(FireFox/Safari)の場合に場合分けするのがいい感じ。上に書いてないのに、下で結論付けられている部分は、試した結果そうだった、ということで。

IE/Operaの場合
  ・隠しiframeを設定し、読み込みイベントで画面内のアンカージャンプを行う。
  ・iframeの読み込みは、onreadystatechange(IE)・onload(Opera)で検出。

◆その他の場合
  ・画面内のアンカージャンプを行い、インターバルタイマーでアドレス変更を監視。

/**

[概要説明]

JavaScriptを利用した処理で、画面のコンテンツ変更を行った場合、履歴に残らないため、
戻るボタンや、ブックマークからのアクセスで、意図した画面に遷移できない場合がある。
このライブラリを利用することで、ブックマーク・戻る・進むを有効にしつつ、JavaScript
(Ajax)による画面コンテンツ差し替えを実現する。



[使用方法]

JavaScriptでコンテンツを変更しつつ、履歴に残したいHTMLファイル(以下HTMLファイル
と呼ぶ)と同じディレクトリに、空の blank1.html と blank2.html を設置します。

HTMLファイル内で、このjsライブラリを読み込む。

    <script type="text/javascript" src="scripts/history.js"></script>

利用したい画面で、jsライブラリを読み込んだ後で、実行するアクションを
以下のように追記する。このアクションは、ライブラリによる履歴作成が完了
した後(画面内遷移によりURLが書き換わった後)で行われる。

MiniHistory.doAction = function() {
	//実行したいアクション
};


MiniLocation.next(keyword) を利用して、画面内遷移を行う。


@History
  2008.06.13  1st release

@Author
  Takehiro Korai <kochan@kendama.jp>

*/

var MiniLocation = {
  Browser: {
    IE:     !!(window.attachEvent && !window.opera),
    Opera:  !!window.opera,
    WebKit: navigator.userAgent.indexOf('AppleWebKit/') > -1,
    Gecko:  navigator.userAgent.indexOf('Gecko') > -1 && navigator.userAgent.indexOf('KHTML') == -1,
    MobileSafari: !!navigator.userAgent.match(/Apple.*Mobile.*Safari/)
  }
};
MiniLocation.dump = function(obj) {
	var s = "";
	for ( var p in obj ) {
		try {
			if ( s != "" ) { s = s + "<br>"; }
			s = s + p + " : " + obj[p];
		}
		catch (e) {s = s + p + " : " + e;}
	}
	this.debug(s);
};
MiniLocation.dumphistory = function() {
	var s = "";
	for ( var i = 0; i < history.length; i++ ) {
		try {
			if ( s != "" ) { s = s + "<br>"; }
			s = s + "[" + i + "] : " + history[i];
		}
		catch (e) {s = s + "[" + i + "] : " + e;}
	}
	this.debug(s);
};
MiniLocation.debug = function(msg) {
	
	var div = document.getElementById('debugmsg');
	if ( !!!div ) {
		div = document.createElement("div");
		div.name = "debugmsg";
		div.id = "debugmsg";
		div.style.position = "relative";
		document.body.appendChild(div);
	}
	div.innerHTML = div.innerHTML + "<br><br>" + msg;
};

var MiniHistory = {
	IFrameName: "minihistory",
	oldhash: "",
	initialized: false
};
MiniHistory.doAction = function() {};
MiniHistory.checkSearchChange = function() {
	try {
		var fr = MiniHistory.getIFrame();
		var cl = fr.contentWindow.location.href;
		var ci = cl.indexOf('#');
		var keyword = ci >= 0 ? cl.substring(ci + 1, cl.length) : "";
		if ( window.location.hash != keyword ) {
			if ( MiniHistory.initialized ) {
				window.location.hash = keyword;
			}
			else {
				MiniLocation.iframenext(keyword);
			}
			MiniHistory.doAction();
		}
	}
	catch (e) { alert("Error\n\n" + e); }
};
MiniHistory.getIFrame = function() {
	var obj = document.getElementById(MiniHistory.IFrameName);
	if ( !!!obj ) {
		obj = document.createElement("iframe");
		obj.name = MiniHistory.IFrameName;
		obj.id = obj.name;
		obj.src = 'blank1.html';
		obj.setAttribute("frameBorder", "0");
		obj.setAttribute("scrolling", "no");
		obj.style.position = "relative";
		obj.style.width = "0px";
		obj.style.height = "0px";
		obj.style.display = "none";
		document.body.appendChild(obj);
		if ( !!obj.contentWindow && obj.contentWindow.name != obj.name ) {
			obj.contentWindow.name = obj.name;
		}
		
		// for IE
		obj.onreadystatechange = function() {
			if (this.readyState == "complete") {
				MiniHistory.checkSearchChange();
				MiniHistory.initialized = true;
			}
		};
		// for Opera
		obj.onload = function() {
			MiniHistory.checkSearchChange();
			MiniHistory.initialized = true;
		};
	}
	return obj;
};
MiniHistory.checkHashChange = function () {
	// oldhashが画面に表示されているものと異なる
	if ( MiniHistory.oldhash != window.location.hash ) {
		MiniHistory.oldhash = window.location.hash;
		MiniHistory.doAction();
	}
	MiniHistory.initialized = true;
};
MiniHistory.init = function() {
	
	// IE/Operaはiframeで処理。
	if ( MiniLocation.Browser.IE || MiniLocation.Browser.Opera ) {
		var fr;
		try {
			fr = MiniHistory.getIFrame();
		}
		catch ( e ) {
			alert("Error\n\n" + e);
		}
	}
	else {
		setInterval(MiniHistory.checkHashChange, 50);
	}
};

MiniLocation.iframenext = function (keyword) {
	var fr = MiniHistory.getIFrame();
	var l = fr.contentWindow.location;
	var i = l.hash.indexOf('#');
	var s = ( i == 0 && l.hash.length > 1 ) ? l.hash.substring(1, l.hash.length) : "";
	if ( s != keyword ) {
		var newpath = !!l.pathname.match('blank1\.html') ? 'blank2.html' : 'blank1.html';
		var newurl = newpath + '#' + keyword;
		if ( MiniHistory.initialized ) {
			fr.src = newurl;
		}
		else {
			fr.contentWindow.location.replace(newurl);
		}
	}
};
MiniLocation.windownext = function (keyword) {
	var h = window.location.href;
	var i = h.indexOf('#');
	var s = ( i >= 0 && ( h.length > i + 1 ) ) ? h.substring(i + 1, h.length) : "";
	if ( s != keyword ) {
		window.location.href = ( i >= 0 ? h.substring(0, i) : h ) + '#' + keyword;
	}
};
MiniLocation.next = function (keyword) {
	// IE/Operaはiframeのソース変更
	if ( MiniLocation.Browser.IE || MiniLocation.Browser.Opera ) {
		MiniLocation.iframenext(keyword);
	}
	// それ以外は、hash変更
	else {
		MiniLocation.windownext(keyword);
	}
};

if ( window.addEventListener ) {
	window.addEventListener("load", MiniHistory.init, false);
}
else if ( window.attachEvent ) {
	window.attachEvent("onload", MiniHistory.init);
}
else {
	window.onload = MiniHistory.init;
}

※ブラウザ判定部分は、prototype.jsから流用。その他はオリジナルコード。
※本ソースコードのオリジナルコードに対する著作権は、tkkochanにあります。
※本ソースコードを流用する場合、@History/@Author欄を残すことで、自由に利用・改造可能です。
※使用に際して費用を請求することは永遠にありません。
※もし不具合等があれば、教えてくれるとハッピーです。