riina-k.net

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

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

開発中の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を返すほどサーバに負荷をかけてる事実をなんとかするべきじゃ?

  1. コメント 0

  1. トラックバック 0

return top