riina-k.net

音楽・小説・プログラミングを手がけるリイナの拠点。

タグ ‘ jQuery

遅延ロードのライブラリをクロスブラウザ対応

先日の記事「遅延ロードをライブラリ化してみた」がIEで動作しなかった原因が分かったので、一部書き直し。

delayload.js

変更点は2箇所。ひとつは「オブジェクトのプロパティとして class という名前は使用できない」という制約があるため、これを「target」というプロパティ名に変更。
もう一つは、img要素を生成する際、先にwidth, heightを設定してからsrcを設定してしまうと、IEでは先に設定したサイズを無視してオリジナルサイズでwidth, heightを上書きしてしまうため、srcを設定してからwidth, heightを設定という順番に変更。

var DelayLoad = {
	target: "delay",
	error: "error",
	retry: 1,
	init: function(params)
	{
		if ( params && params.hasOwnProperty("target") ) {
			this.target = params.target;
		}
		if ( params && params.hasOwnProperty("retry") ) {
			this.retry = params.retry;
		}
		if ( params && params.hasOwnProperty("error") ) {
			this.error = params.error;
		}
		
		// 対象のjQueryオブジェクトにパラメータを設定
		var jqTarget = jQuery( "*[class^='" + this.target + "']" );
		for ( i=0; i<jqTarget.length; ++i ) {
			jQuery.data( jqTarget.get(i), "retry", this.retry );
			jQuery.data( jqTarget.get(i), "error", this.error );
		}
		jqTarget.each(function(){
			// 引数をパース
			var paramsString = this.className.split(";")[1];
			var arrParams = paramsString.match(/^params=\{(.*?)\}$/)[1].match(/([a-z]+):('(.*?)'|[0-9]+)/g);
			var objParams = {};
			for ( i=0; i<arrParams.length; ++i ) {
				var tmp = arrParams[i].split(":");
				if ( tmp[1].match(/^'(.*?)'$/) ) {
					objParams[ tmp[0] ] = RegExp.$1;
				} else {
					objParams[ tmp[0] ] = tmp[1];
				}
			}
			if ( !objParams.hasOwnProperty("url") ) {
				throw "DelayLoad: url required";
			}
			if ( !objParams.hasOwnProperty("title") ) {
				objParams.title = "";
			}
			if ( !objParams.hasOwnProperty("retry") ) {
				objParams.retry = jQuery.data( this, "retry" );
			}
			objParams.error = jQuery.data( this, "error" );
			
			var jqImg = jQuery("<img />");
			// エラー時のイベントハンドラ
			jqImg.bind("error", function(e){
				// 再試行回数だけリロード
				var retry = jQuery.data( this, "retry" );
				if ( --retry > 0 ) {
					jQuery.data( this, "retry", retry );
					this.src = this.src;
				} else {
					// 試行回数切れ
					var target = jQuery.data( this, "target" );
					var error = jQuery.data( this, "error" );
					target.replaceWith(error);
					// 後始末
					jQuery.removeData( this, "retry" );
					jQuery.removeData( this, "error" );
					jQuery.removeData( this, "target" );
				}
			});
			// ロード時のイベントハンドラ
			jqImg.bind("load", function(e){
				var target = jQuery.data( this, "target" );
				target.replaceWith( jQuery(this) );
				// 後始末
				jQuery.removeData( this, "retry" );
				jQuery.removeData( this, "error" );
				jQuery.removeData( this, "target" );
			});
			// 再試行回数を設定
			jQuery.data( jqImg.get(0), "retry", objParams.retry );
			// エラーHTMLを設定
			jQuery.data( jqImg.get(0), "error", objParams.error );
			// 画像置換対象
			jQuery.data( jqImg.get(0), "target", jQuery(this) );
			// 読み込み
			jqImg.attr("src", objParams.url);
			if ( objParams.hasOwnProperty("width") ) {
				jqImg.attr("width", objParams.width);
			}
			if ( objParams.hasOwnProperty("height") ) {
				jqImg.attr("height", objParams.height);
			}
			if ( objParams.hasOwnProperty("title") ) {
				jqImg.attr("alt", objParams.title);
			}
		});
	}
};

DelayLoad デモ

現時点では、以下のブラウザで動作を確認。

  • [Windows XP] IE 8.0
  • [Windows XP] Firefox 13.0.1
  • [Windows XP] Opera 11.64
  • [Windows 7] Opera 23.0

クロスブラウザ対応がとりあえずできたということで、今回をもって「DelayLoad ver 1.0」としますか。
前回書いたものはβ版ってことで。

デモファイルは http://riina-k.net/delayload/1.0/ 以下にあるので、欲しい人はソース漁ってください 要望があればzipにまとめてDLできるようにしますかねー。

遅延ロードをライブラリ化してみた

先ほどのエントリー「画像ロード時のエラー制御で試行錯誤」で組んだjavascript、エラー制御というより「読み込み中」的な意味で使い回しができそうなので汎用性を持たせてライブラリ化。

delayload.js

DelayLoad = {
	class: "delay",
	error: "error",
	retry: 1,
	init: function(params)
	{
		if ( params && params.hasOwnProperty("class") ) {
			this.class = params.class;
		}
		if ( params && params.hasOwnProperty("retry") ) {
			this.retry = params.retry;
		}
		if ( params && params.hasOwnProperty("error") ) {
			this.error = params.error;
		}
		
		// 対象のjQueryオブジェクトにパラメータを設定
		var jqTarget = jQuery( "*[class^='" + this.class + "']" );
		for ( i=0; i<jqTarget.length; ++i ) {
			jQuery.data( jqTarget.get(i), "retry", this.retry );
			jQuery.data( jqTarget.get(i), "error", this.error );
		}
		jqTarget.each(function(){
			// 引数をパース
			var paramsString = this.className.split(";")[1];
			var arrParams = paramsString.match(/^params=\{(.*?)\}$/)[1].match(/([a-z]+):('(.*?)'|[0-9]+)/g);
			var objParams = {};
			for ( i=0; i<arrParams.length; ++i ) {
				var tmp = arrParams[i].split(":");
				if ( tmp[1].match(/^'(.*?)'$/) ) {
					objParams[ tmp[0] ] = RegExp.$1;
				} else {
					objParams[ tmp[0] ] = tmp[1];
				}
			}
			if ( !objParams.hasOwnProperty("url") ) {
				throw "DelayLoad: url required";
			}
			if ( !objParams.hasOwnProperty("title") ) {
				objParams.title = "";
			}
			if ( !objParams.hasOwnProperty("retry") ) {
				objParams.retry = jQuery.data( this, "retry" );
			}
			objParams.error = jQuery.data( this, "error" );
			
			var jqImg = jQuery("<img />");
			// エラー時のイベントハンドラ
			jqImg.bind("error", function(e){
				// 再試行回数だけリロード
				var retry = jQuery.data( this, "retry" );
				if ( --retry > 0 ) {
					jQuery.data( this, "retry", retry );
					this.src = this.src;
				} else {
					// 試行回数切れ
					var target = jQuery.data( this, "target" );
					var error = jQuery.data( this, "error" );
					target.replaceWith(error);
					// 後始末
					jQuery.removeData( this, "retry" );
					jQuery.removeData( this, "error" );
					jQuery.removeData( this, "target" );
				}
			});
			// ロード時のイベントハンドラ
			jqImg.bind("load", function(e){
				var target = jQuery.data( this, "target" );
				target.replaceWith( jQuery(this) );
				// 後始末
				jQuery.removeData( this, "retry" );
				jQuery.removeData( this, "error" );
				jQuery.removeData( this, "target" );
			});
			// 再試行回数を設定
			jQuery.data( jqImg.get(0), "retry", objParams.retry );
			// エラーHTMLを設定
			jQuery.data( jqImg.get(0), "error", objParams.error );
			// 画像置換対象
			jQuery.data( jqImg.get(0), "target", jQuery(this) );
			// 読み込み
			if ( objParams.hasOwnProperty("width") ) {
				jqImg.attr("width", objParams.width);
			}
			if ( objParams.hasOwnProperty("height") ) {
				jqImg.attr("height", objParams.height);
			}
			if ( objParams.hasOwnProperty("title") ) {
				jqImg.attr("alt", objParams.title);
			}
			jqImg.attr("src", objParams.url);
		});
	}
};

使い方はこんな感じ。

<script type="text/javascript" src="js/jquery.js"></script>
<script type="text/javascript" src="js/delayload.js"></script>
<script type="text/javascript">
	jQuery(function(){
		DelayLoad.init({
			error: "<strong style='color:red;'>読み込みに失敗しました</strong>"
		});
	});
</script>
<body>
	<strong>画像直リン</strong>
	<img class="delay;params={url:'images/ongakuji.jpg',width:480,height:270,title:'音楽寺'}" src="images/loading.gif" width="16" height="16" />
	<strong>意図的に2秒待たせるPHP</strong>
	<img class="delay;params={url:'wait2seconds.php',width:480,height:270,title:'音楽寺(2秒遅れ)'}" src="images/loading.gif" width="16" height="16" />
	<strong>存在しないリンクを3回再試行</strong>
	<img class="delay;params={url:'no_longer_exists',width:480,height:270,title:'存在しないURL',retry:3}" src="images/loading.gif" width="16" height="16" />
</body>

実行結果はこんな感じ。DelayLoad デモ

軽く解説。
あらかじめjQueryがロードされていることが条件。
DOMのロードが終わった状態で、DelayLoad.init()関数を実行する。
このとき引数として置換対象クラスclass、エラー時に表示されるHTMLerror、再試行回数retryをパラメータとするオブジェクトを渡すことができる。
値を省略した場合、対象クラスは「delay」、エラーメッセージは「error」、再試行回数は1回。

DelayLoad.init({class:"mydelay",retry:3});

HTMLでは、置換パラメータをクラス指定の中に記述する。
最終的にはクラス指定した要素が画像で置き換わるので、クラス指定をするのはimgタグである必要はない。
パラメータの指定方法は 「init関数で指定したクラス名;params={……}」、前記のとおり、クラス名のデフォルトは「delay」。
paramsにはいくつかのパラメータを書くことができる。
url : 必須、読み込む画像のURL。
width : 画像を表示する横幅、省略した場合はimgタグでwidthを書かなかったときと同じ動作。
height : 画像を表示する縦幅、省略した場合はimgタグでheightを書かなかったときと同じ動作。
title : 画像のaltに設定される画像のタイトル。省略した場合はaltが出力されない。
retry : エラー時の再試行回数。init関数での指定よりも優先される。省略した場合はinit関数の設定またはデフォルト値。

<img class="delay;params={url:'images/ongakuji.jpg',width:480,height:270,title:'音楽寺'}" src="material/loading.gif" width="16" height="16" />

現時点ではIEには対応してません。
要望があればバージョンアップしたいなー。

画像ロード時のエラー制御で試行錯誤

開発中のCDデータベースを試しにこのサーバにアップロードしたところ、
開発環境であるローカルホストでは発生しなかった「画像へのリクエストで500エラー」という状況が発生。

実はCDのジャケット画像をサーバ内部の見えないところでキャッシュしておいて、パラメータで出力する画像を選択、キャッシュが存在すればそのまま出力、存在しなければ404を返すというPHPを組んでいた。

404エラーと500エラーの双方に対応するため、まずAjaxで500エラーを捕捉するよう、次のように書いてみた。

PHP

まずはAjaxでジャケット画像の縦横サイズを取得できるよう、独自ヘッダでサイズを出力。

header( 'Content-type: ' . $this->mime );
header( 'Content-Length: ' . $this->bytes );
// 独自ヘッダ
header( 'X-Width: ' . $this->width );
header( 'X-Height: ' . $this->height );

HTML

ジャケット画像が正常に読み込まれるまではローディング中を示す例のくるくる回るやつを表示しておいて、alt属性に読み込みたいURLを書いておく。
※ {{ …… }} はSmartyのタグ、$pkgはパッケージ情報のインスタンス、$imgは画像情報のインスタンス。

<tr id="{{$pkg->htmlid|escape}}">
	<td class="jacket">
		<img src="material/loading.gif" width="16" height="16" alt="{{$img->pathWeb|escape}}" title="{{$title_artist}}" />
	</td>
</tr>

javascript

コールバック関数内で、altに記述されたURLを読み込み、ステータスが200になったらsrcにジャケット画像のURLを設定。

function loadJacketImage(jqImage, count)
{
	$.ajax(
		{
			jq: jqImage,	// 対象となるimgタグのjQueryオブジェクト
			count: count,	// 再試行回数
			url: jqImage.attr("alt"),
			success: function(data, status, xhr){
				var width = xhr.getResponseHeader("X-Width");
				var height = xhr.getResponseHeader("X-Height");
				this.jq.attr("src", this.url).attr("alt", "").attr("width", width).attr("height", height);
			},
			error: function(xhr, status, error){
				if ( xhr.status == 404 ) {
					this.jq.replaceWith("no image");
				} else {
					if ( this.count > 0 ) {
						// 再試行
						loadJacketImage(this.jq, this.count-1);
					} else {
						this.jq.replaceWith("error");
					}
				}
			}
		}
	);
}

$(function(){
	$(".jacket img").each(function(){
		loadJacketImage( $(this), 3 );
	});
});

しかしこの方法には大きな罠があり、「Ajaxで取得できた画像のURLをsrcに設定する」タイミングでもう一度リクエストが飛んでしまい、そのときに500エラーとなってしまうという状況が時々発生してしまう。
キャッシュしてくれないんかい……

そこでイベントハンドラを利用した以下のような方法を試してみた。

HTML

<tr id="{{$pkg->htmlid|escape}}">
	<td class="jacket">
		<img src="{{$img->pathWeb|escape}}" width="{{$img->width|escape}}" height="{{$img->height|escape}}" alt="{{$title_artist}}" />
	</td>
</tr>

javascript

img要素のイベントハンドラで、エラー時に単純にリロードさせるためにsrcを上書き。

$(function(){
	$(".jacket img").bind("error", function(e){
		// リロード
		this.src = this.src;
	});
});

今度はイベントハンドラで404と500の区別が付かない。
そもそもPHPのほうで404が返ってくるようなURLは最初から書かないように変更し、500エラーが返ってきてもリロードが働いて正常に表示されるようになった。
ただ、PHPが何らかの影響でエラーを吐き続けた場合、リロードが無限ループしてしまうので、Ajaxの時と同様に再試行回数を設定できるようにしたい。

あとやっぱり例のくるくる欲しいよね。
ってことでいじくり回して、最終的にこんな感じ。

HTML

ジャケット画像が存在しない場合はあらかじめ「no image」を表示、存在する場合は例のくるくるに対してalt指定でジャケット画像のURL、横幅、高さをカンマ区切りで設定。

<tr id="{{$pkg->htmlid|escape}}">
	<td class="jacket">{{strip}}
		{{if $img->exists }}
			<img src="material/loading.gif" width="16" height="16" alt="{{$img->pathWeb|escape}},{{$img->width|escape}},{{$img->height|escape}}" title="{{$title_artist}}" />
		{{else}}
			no image
		{{/if}}
	{{/strip}}</td>
</tr>

javascript

成功時に例のくるくるをジャケット画像で置き換え、エラー時には指定回数だけリロードを試み、一定回数(このサンプルの場合は3回)を超えると例のくるくるを「error」という文字列で置き換え。
コールバック関数内で置換対象のくるくると試行回数を見失ってしまうと意味が無いので、jQueryの内部APIで要素に関連付けておく。コールバック関数に引数を渡す方法がめっちゃめんどくさいの

$(function(){
	$(".jacket img").each(function(){
		var tmp = this.alt.split(",");
		var jqImg = $("<img />");
		
		// エラー時のイベントハンドラ
		jqImg.bind("error", function(e){
			// 再試行回数だけリロード
			var tryCount = jQuery.data( this, "tryCount" );
			if ( --tryCount > 0 ) {
				jQuery.data( this, "tryCount", tryCount );
				this.src = this.src;
			} else {
				// 試行回数切れ
				var id = jQuery.data( this, "target" );
				$("#" + id + " img").replaceWith("error");
			}
		});
		// ロード時のイベントハンドラ
		jqImg.bind("load", function(e){
			var id = jQuery.data( this, "target" );
			$("#" + id + " img").replaceWith( $(this) );
			// 後始末
			jQuery.removeData( this, "tryCount" );
			jQuery.removeData( this, "target" );
		});
		
		jQuery.data( jqImg.get(0), "tryCount", 3 );								// 再試行回数を設定
		jQuery.data( jqImg.get(0), "target", $(this).parents("tr").get(0).id );	// 画像置換対象のID
		jqImg.attr("src", tmp[0]).attr("width", tmp[1]).attr("height", tmp[2]).attr("alt", $(this).attr("title"));
	});
});

ここまでやってようやく気が済んだのでした。
そもそもPHPが500を返すほどサーバに負荷をかけてる事実をなんとかするべきじゃ?

テーマ「FLAT」のHTML5化

導入しているWordPressテーマ「FLAT」をHTML5に対応させてみる。

……しかし、WordPress本体が直接HTMLを吐き出している箇所が多くあるため、完全なHTML5化は本体を改造しない限り不可能。
そんなわけで、「div id=”header”」を「header」に書き換えるなど、基本的なタグの置き換えに留まる。

と、ここで問題が。
このテーマは常に右下に「ページ最上部に戻るためのボタン」が表示されているが、
リンク先が「#header」だったため、headerタグに置き換えてしまうとリンクが機能しなくなる。
そこで、無条件でスクロールするようjavascriptを書き換えてみた。

scroll.js

	jQuery('a[href*=#header],a[href*=#respond]').click(function() {
		if (location.pathname.replace(/^\//,'') == this.pathname.replace(/^\//,'') && location.hostname == this.hostname) {
			var $target = jQuery(this.hash);
			$target = $target.length && $target || jQuery('[name=' + this.hash.slice(1) +']');
			if ($target.length) {
				var targetOffset = $target.offset().top;
				jQuery('html,body').animate({ scrollTop: targetOffset }, 1200, 'quart');
				return false;
			}
		}
	});

これを以下のように書き換えて、リンク先が「#header」の場合は無条件でページの先頭までスクロールするように。

	jQuery('a[href*=#header]').click(function() {
		if (location.pathname.replace(/^\//,'') == this.pathname.replace(/^\//,'') && location.hostname == this.hostname) {
			jQuery('html,body').animate({ scrollTop: 0 }, 1200, 'quart');
			return false;
		}
	});

	jQuery('a[href*=#respond]').click(function() {
		if (location.pathname.replace(/^\//,'') == this.pathname.replace(/^\//,'') && location.hostname == this.hostname) {
			var $target = jQuery(this.hash);
			$target = $target.length && $target || jQuery('[name=' + this.hash.slice(1) +']');
			if ($target.length) {
				var targetOffset = $target.offset().top;
				jQuery('html,body').animate({ scrollTop: targetOffset }, 1200, 'quart');
				return false;
			}
		}
	});

return top