写真ギャラリーのソース解説③ ~Javascriptによる要素の遅延読み込み~

先日公開した写真ギャラリーのソースコードを解説していきます。

今回は、第3回目で要素の遅延読み込みのJavascriptの解説です。
Javascriptは、次回、画像を先頭から順番に読み込むものを解説します。

機能などの紹介は紹介の記事を見て下さい。



遅延読み込みとは?なぜこんなことをするのか?

まずは、遅延読み込み (英語 : Lazy Loading) についてです。

遅延読み込みとは一般には、あるオブジェクト (関数とか、今回の場合は画像) の初期化を必要になった時に実行させる手法のことです。
これを行うことでそのオブジェクトが必要ない場合は初期化の高速化や処理負荷の低減が期待できます。

Webサイトに於いては、画像などの容量の大きなファイルを大量に読み込ませると通信負荷が大きくなるばかりではなくページ全体の読み込み自体も遅くなります。
(HTMLでは上から順番に処理されます)

つまり、今回の写真ギャラリーのように全ての画像要素を最初から読み込んでしまうと表示されなかった分の画像は無駄になります。
これは閲覧者もサーバーもインターネット通信も全てに不利益です。

そこで、はじめは画面に表示されるだけの要素を読み込ませ、スクロールによって新たな要素の表示が必要になった場合に順次要素を追加することで双方にとって有益となります。

但し、画面に表示される要素ぴったりだけしか読み込まないとするとスクロールがもたつくことが考えられるので少し余計に読み込むように設定します。
(どれだけ読み込むかは自分でスクロールさせて調整して下さい)

ソースコード

それでは、ソースコードを見ていきましょう。

<Javascript>

window.onload = function(){
	// 画面に写真を並べる
	// 表示されている画面までにしてそれ以降は、順次スクロールされたら読み込む → 次回の第4回で解説します
	var _addCard, _addImage, _addThumb, _addExif;
	var _windowHeight = document.documentElement.clientHeight;
	var _thumbList    = document.getElementById("thumb_list");
	for(var i = 0; i < gallery_list.length; i++){
		_addCard            = _thumbList.appendChild(document.createElement("div"));
		_addCard.className  = "card";
		_addCard.dataset.i  = i;
		_addImage           = _addCard.appendChild(document.createElement("a"));
		_addImage.href      = gallery_list[i].path;
		_addImage.dataset.i = i;
		_addImage.onclick   = function(){ _imgZoom( this.href, this.dataset.i ); return false; };  // クリックで拡大表示して、リンクは無効にする
		_addThumb           = _addImage.appendChild(document.createElement("img"));
	//	画像を順次読み込ませる必要がない場合は、直接画像ファイル (下の例ではリサイズのPHPを指定) をsrc属性に入れてやります
	//	_addThumb.src       = "./create_thumbnail.php?url=" + gallery_list[i].path + "&width=300&height=300";
		_addThumb.src       = "./loading.jpg";   // 初期に読み込む画像は"NowLoading..."にする
		_addThumb.width     = thumbWidth;    // 300px
		_addThumb.height    = thumbHeight;   // 300px
		_addExif            = _addCard.appendChild(document.createElement("div"));
		_addExif.className  = "exif";
		_addExif.innerHTML  = "f="  + gallery_list[i].focal    + "mm , "  +
		                     ""    + gallery_list[i].aperture + " , "    +   // 絞り値(aperture)はf/**の書式で格納されている
		                     ""    + gallery_list[i].shutter  + "sec , " +
		                     "ISO" + gallery_list[i].ISO      + ""       +
		                     "";
		if(screenWidth < minWidth) _addExif.style.display = "none";   // 画面解像度が低い場合はEXIFを表示しない
		if(_addCard.getBoundingClientRect().top > _windowHeight) break;   // 画面外の要素は描画しない
	}
	// 画像を順番に読み込ませる必要がない場合は以下不要
	// 順番にサムネイルを読み込む → 詳細は次回解説
	_loadImages();
};

window.onscroll = function(){
	var _addCard, _addImage, _addThumb, _addExif;
	var _cellPhoneOffset = screenWidth < minWidth ? thumbHeight : 100;
	var _windowHeight = document.documentElement.clientHeight;
	var _thumbList    = document.getElementById("thumb_list");
	for(var i = parseInt(_thumbList.lastChild.dataset.i) + 1; i < gallery_list.length; i++){
		_addCard            = _thumbList.appendChild(document.createElement("div"));
		_addCard.className  = "card";
		_addCard.dataset.i  = i;
		_addImage           = _addCard.appendChild(document.createElement("a"));
		_addImage.href      = gallery_list[i].path;
		_addImage.dataset.i = i;
		_addImage.onclick   = function(){ _imgZoom( this.href, this.dataset.i ); return false; };  // クリックで拡大表示して、リンクは無効にする
		_addThumb           = _addImage.appendChild(document.createElement("img"));
		_addThumb.src       = "./loading.jpg";   // 初期に読み込む画像は"NowLoading..."にする
		_addThumb.width     = thumbWidth;    // 300px
		_addThumb.height    = thumbHeight;   // 300px
		_addExif            = _addCard.appendChild(document.createElement("div"));
		_addExif.className  = "exif";
		_addExif.innerHTML  = "f="  + gallery_list[i].focal    + "mm , "  +
		                      ""    + gallery_list[i].aperture + " , "    +   // 絞り値(aperture)はf/**の書式で格納されている
		                      ""    + gallery_list[i].shutter  + "sec , " +
		                      "ISO" + gallery_list[i].ISO      + ""       +
		                      "";
		if(_addCard.getBoundingClientRect().top > _windowHeight + _cellPhoneOffset){
			_thumbList.removeChild(_addCard);
			break;   // 画面外の要素は描画しない
		}
		if(screenWidth < minWidth) _addExif.style.display = "none";
	}
	// 順番にサムネイルを読み込む
	_loadImages();
};

<CSS>

body{
	background-color    : black;
}
img{
	margin              : 0px;
}
h1{
	font-family         : 'Shadows Into Light', cursive;
	font-size           : 3rem; // 44px;
	color               : #696969;
	margin-bottom       : 1rem;
}
a{
	font-family         : 'Noto Serif', serif;
	font-size           : 1.5rem;
	color               : #696969;
	text-decoration     : none;
}
a:hover{
	color               : #640000;
}
a.sort{
	margin-left         : 5px;
	margin-right        : 5px;
}
span{
	font-family         : 'Noto Serif', serif;
	font-size           : 1.5rem;
	color               : #696969;
}
div#menu{
	margin-top          : 0.5rem;
	margin-bottom       : 1.5rem;
}
div.card{
	margin              : 5px;
	display             : inline-block;
	position            : relative;
	border              : 1px solid #262626;
}
div.exif{
	width               : 100%;
	height              : 1rem;
	position            : absolute;
	bottom              : 0px;
	left                : 0px;
	font-size           : 0.7rem;
	line-height         : 1rem;
	text-align          : center;
	color               : #696969;
	background-color    : rgba(0, 0, 0, 0.5);
}
div.detailexif{
	width               : 100%;
	height              : 2rem;
	position            : absolute;
	left                : 0px;
	font-size           : 1rem;
	line-height         : 2rem;
	text-align          : center;
	color               : white;
	background-color    : rgba(0, 0, 0, 0.5);
}
div.caches{
	position            : absolute;
	top                 : 8rem;
	right               : 10px;
	font-size           : 10px;
	color               : #696969;
}
div.swipe{
	height              : 100px;
	width               : 100px;
	position            : absolute;
	visibility          : hidden;
	background-color    : white;
	background          : radial-gradient(50px,rgba(255,255,255,0.6), rgba(255,255,255,0));
	font-weight         : 400;
	font-size           : 44px;
	color               : black;
	color               : rgba(0,0,0,0.5);
	display             : table-cell;
	text-align          : center;
	vertical-align      : middle;
	line-height         : 100px;
}
div.moveimage{
	position            : absolute;
	width               : 200px;
	top                 : 0px;
	z-index             : 5;
}
div.originalimage{
	visibility          : hidden;
	position            : absolute;
	width               : 50px;
	height              : 50px;
	font-family         : 'Font Awesome 5 Free';
	font-weight         : 400;
	display             : table-cell;
	text-align          : center;
	vertical-align      : middle;
	line-height         : 50px;
	font-size           : 50px;
	color               : black;
	color               : rgba(0,0,0,0.5);
	opacity             : 0.4;
}
div.swipe:hover{
	cursor              : pointer;
}
div.moveimage{
	position            : absolute;
	width               : 200px;
	top                 : 0px;
	z-index             : 5;
}
div.originalimage{
	visibility          : hidden;
	position            : absolute;
	width               : 50px;
	height              : 50px;
	font-weight         : 400;
	font-family         : 'Font Awesome 5 Free';
	display             : table-cell;
	text-align          : center;
	vertical-align      : middle;
	line-height         : 50px;
	font-size           : 50px;
	color               : white;
	opacity             : 0.4;
}
div.originalimage:hover{
	cursor              : pointer;
}
div.loadingimage{
	position            : absolute;
	width               : 7rem;
	height              : 7rem;
	font-size           : 1rem;
	line-height         : 3rem;
	text-align          : center;
	color               : white;
	opacity             : 0.65;
}

解説

大きく2つの処理でできていますが、1つ目と2つ目の中身はほとんど同じです。
関数化しても良いのですが、分かりやすくする為にほぼ同じ処理を2度書いています。
(必要に応じて関数化しても良いです)

初期表示

HTMLやCSSが読み込み終わったら初期表示をさせていきます。

はじめの段階では画像等は一切読み込まれていないので、表示領域に追加していきます。
表示領域は予め "thumb_list" というidを持つDIV要素としてHTMLき記述しておきます。

window.onload = function(){
	// 画面に写真を並べる
	// 表示されている画面までにしてそれ以降は、順次スクロールされたら読み込む
	var _addCard, _addImage, _addThumb, _addExif;
	var _windowHeight = document.documentElement.clientHeight;
	var _thumbList    = document.getElementById("thumb_list");
	for(var i = 0; i < gallery_list.length; i++){
		_addCard            = _thumbList.appendChild(document.createElement("div"));
		_addCard.className  = "card";
		_addCard.dataset.i  = i;
		_addImage           = _addCard.appendChild(document.createElement("a"));
		_addImage.href      = gallery_list[i].path;
		_addImage.dataset.i = i;
		_addImage.onclick   = function(){ _imgZoom( this.href, this.dataset.i ); return false; };  // クリックで拡大表示して、リンクは無効にする
		_addThumb           = _addImage.appendChild(document.createElement("img"));
	//	画像を順次読み込ませる必要がない場合は、直接画像ファイル (下の例ではリサイズのPHPを指定) をsrc属性に入れてやります
	//	_addThumb.src       = "./create_thumbnail.php?url=" + gallery_list[i].path + "&width=300&height=300";
		_addThumb.src       = "./loading.jpg";   // 初期に読み込む画像は"NowLoading..."にする
		_addThumb.width     = thumbWidth;    // 300px
		_addThumb.height    = thumbHeight;   // 300px
		_addExif            = _addCard.appendChild(document.createElement("div"));
		_addExif.className  = "exif";
		_addExif.innerHTML  = "f="  + gallery_list[i].focal    + "mm , "  +
		                     ""    + gallery_list[i].aperture + " , "    +   // 絞り値(aperture)はf/**の書式で格納されている
		                     ""    + gallery_list[i].shutter  + "sec , " +
		                     "ISO" + gallery_list[i].ISO      + ""       +
		                     "";
		if(screenWidth < minWidth) _addExif.style.display = "none";   // 画面解像度が低い場合はEXIFを表示しない
		if(_addCard.getBoundingClientRect().top > _windowHeight) break;   // 画面外の要素は描画しない
	}
	// 画像を順番に読み込ませる必要がない場合は以下不要
	// 順番にサムネイルを読み込む → 詳細は次回の第4回で解説
	_loadImages();
};

ページの読み込みが完了した時に window.onload でイベント処理を開始します。
インラインフレーム (GoogleMAPの埋込など) があるとそのページが読み込まれるまでイベントが発生しないので注意して下さい。

まずは必要な変数を宣言していきます。
Javascriptでは宣言しなくても使えますが、宣言しない変数はグローバル変数として扱われるので特別な理由がない限り必ず使う関数内での宣言をおススメします。
色々なコードを組み合わせる場合に変数の干渉で思わぬ不具合も回避できます。
(自分でコーディングしても全ての変数名を覚えていられるわけではないですし)

ここで注意が必要な値が1つあります。
それがブラウザの表示領域の高さを取得する "document.documentElement.clientHeight" です。

これ自体は特に悪いことはないのですが、HTML側にある宣言をしないとうまく動作しません。
何を宣言しないといけないかというと <!doctype html> です。
<html>タグよりも前に記述するドキュメントタイプの宣言です。

ドキュメントタイプを宣言しないと、ブラウザは互換モード (Quirks) というCSSが普及していなかった時代のレイアウトを保つための表示になってしまいます。
するとレイアウトの解釈が現代の標準とは異なり、寸法が思った通りになりません。

詳しくは以下のページもご確認下さい。
これまで余り意識してなかったので、大変助けられました。ありがとうございます。
・HTMLクイックリファレンス - <!DOCTYPE html>
・アンギス - 事件です!jQuery(window).height()が動かねぇ ~原因究明と解決まで~

さて、ようやく中身ですね!
といっても表示領域に要素を追加していくだけです。

要素の構造は以下のようになっています。
(画像と_addCardの隙間は実際にはありません、作画上の都合です)

まずは、_addCardという変数にサムネイルやEXIF情報などをくっつけていきます。

予め表示領域として用意した "<div id="thumb_list"></div>" 要素の参照を _thumbList という変数に代入します。
次に _thumbList に対してappendChildメソッドで要素を追加します。
追加する要素は空のDIV要素とするので "document.createElement("div")" を使います。

この時、appendChildは追加した要素への参照を返り値とするので、そのまま _addCard という変数に代入してしまいます。
これらの処理は以下と等価ですが、より簡単な記述の方が良いでしょう。

var _addCard = document.createElement("div");
_thumbList.appendChild(_addCard);

_addCardは要素への参照となっているので、.classNameでスタイルシートを適応します。
必要に応じて個別にスタイルを指定しても良いです。

遅延読み込みさせるためにどこまで読み込んだかを簡単に呼び出す為、datasetで番号を振っておきます。
"_thumbList.children.length" としても番号を取得できますが、後から何か機能を追加するときにIDがあった方が楽かなと思ったのでdatasetを使っています。
(それと古いIEだと誤動作するとかしないとか)
(補足)
 HTMLのタグには "data-***" という任意の名前の属性を付与できます。
 Javascriptからは "要素参照.dataset.***" で参照できます。

_addCardの設定が終わったら、画像へのリンクを作成していきます。
A要素を追加してdatasetするところまでは同様にできます。

ここで、通常のA要素はクリックするとhref属性でしていしたページに移動します。
しかし今回はクリックしたらまずは拡大表示するようにしたいので、onclick属性に関数を指定して通常のアンカーイベントを無効化してしまいます。

_addImage.onclick = function(){ _imgZoom( this.href, this.dataset.i ); return false; };

関数の中身は_imagZoomという関数を呼び出すだけです。
返り値にFALSEを返すことでページ移動のイベントを取り消すことができます。

※this.hrefはthis.dataset.iがあれば無くても良いですが、コードを読みやすくする為に入れてあります。詳細は_imagZoom関数の解説とします。

_addImageも設定できたので、サムネイル画像とEXIF情報を追加します。

サムネイル画像は後から順番に読み込ませるので、読み込み中に表示させる画像を指定します。
順番に読み込ませる必要がない場合はサムネイル画像を指定しましょう。

EXIF情報は必要な情報だけ表示させます。自分で必要な情報を選んで下さい。
また、スマホ表示の時はEXIFがあるととても見辛いので非表示にします。

最後に画面外の要素が描画されたらループを抜けて初期表示を終了します。
ループを抜けるには "break" 命令で抜けることができます。

残りはスクロールしたタイミングで読み込むか判断します。

スクロールした時の追加読込

続いてスクロールした時の動作です。
要素の追加は基本的に同じ方法なので、解説は割愛します。

window.onscroll = function(){
	var _addCard, _addImage, _addThumb, _addExif;
	var _cellPhoneOffset = screenWidth < minWidth ? thumbHeight : 100;
	var _windowHeight = document.documentElement.clientHeight;
	var _thumbList    = document.getElementById("thumb_list");
	for(var i = parseInt(_thumbList.lastChild.dataset.i) + 1; i < gallery_list.length; i++){
		_addCard            = _thumbList.appendChild(document.createElement("div"));
		_addCard.className  = "card";
		_addCard.dataset.i  = i;
		_addImage           = _addCard.appendChild(document.createElement("a"));
		_addImage.href      = gallery_list[i].path;
		_addImage.dataset.i = i;
		_addImage.onclick   = function(){ _imgZoom( this.href, this.dataset.i ); return false; };  // クリックで拡大表示して、リンクは無効にする
		_addThumb           = _addImage.appendChild(document.createElement("img"));
		_addThumb.src       = "./loading.jpg";   // 初期に読み込む画像は"NowLoading..."にする
		_addThumb.width     = thumbWidth;    // 300px
		_addThumb.height    = thumbHeight;   // 300px
		_addExif            = _addCard.appendChild(document.createElement("div"));
		_addExif.className  = "exif";
		_addExif.innerHTML  = "f="  + gallery_list[i].focal    + "mm , "  +
		                      ""    + gallery_list[i].aperture + " , "    +   // 絞り値(aperture)はf/**の書式で格納されている
		                      ""    + gallery_list[i].shutter  + "sec , " +
		                      "ISO" + gallery_list[i].ISO      + ""       +
		                      "";
		if(_addCard.getBoundingClientRect().top > _windowHeight + _cellPhoneOffset){
			_thumbList.removeChild(_addCard);
			break;   // 画面外の要素は描画しない
		}
		if(screenWidth < minWidth) _addExif.style.display = "none";
	}
	// 順番にサムネイルを読み込む
	_loadImages();
};

初期表示との差異は、読み込む要素を既に表示されている要素の続きからとすることです。
ループの初期値を最後の要素の次から始めるようにします。

※datasetで取得した値は文字列なのでparseIntで数値に変換します。
※parseIntは処理が遅いので繰り返し処理では使わない方が無難ですが、pxなどの単位も削除してくれるので場合によっては非常に強力です。

要素は初期化と同じように追加していきますが、画面外の要素を描画したら終了させます。
画面外の要素を検出したら画面外になった要素を削除してループを抜けます。
最後の要素を削除しないとスクロールイベントの度に少しずつ要素が追加されてどんどん増えていってしまいますので、削除してしまいます。

但し、少し余計に読み込んでおいた方がスクロールがスムーズになるので、_cellPhoneOffsetという変数にどれだけ余計に読み込むかをしていします。
上のコードだと、PC表示の時は100px、スマホの時は1行分としています。
値を変えているのはスマホだと100pxではスクロールがギクシャクしたので1行分余計に読み込むことにしています。

まとめ

以上、遅延読み込みの解説でした。

説明不足等あると思うので、ご質問等あればコメントやお問合せ下さい。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)