画像の一覧を XML ファイルに書いておき、プログラムで読み込んで順に並べる、という作り方があります。画像を差し替えるときに XML だけを書き換えればよいので便利ですが、よく起きるのが「XML に書いた順番に並ばず、表示するたびに順番が変わる」という問題です。原因は、画像の読み込みが非同期で、終わる順番が決まっていないことにあります。ここでは、Flash の ActionScript 3 で起きていた例を入り口に、今の JavaScript での解決策を説明します。
ActionScript 3 で起きていた問題
Flash でギャラリーを作るときは、XML から画像の URL を取り出し、画像ごとに Loader で読み込む書き方がよく使われました。読み込みが終わったときの処理 (Event.COMPLETE) で画面に追加すると、次のようになります。
// 読み込みの終わった順に並んでしまう例
var images:XMLList = xml.image;
var count:int = 0;
for (var i:int = 0; i < images.length(); i++) {
var loader:Loader = new Loader();
loader.contentLoaderInfo.addEventListener(Event.COMPLETE, onComplete);
loader.load(new URLRequest(String(images[i].@src)));
}
function onComplete(event:Event):void {
var content:DisplayObject = LoaderInfo(event.target).content;
content.x = count * 110; // 終わった順に、左から置いていく
addChild(content);
count++;
}
読み込みは XML の順に始まりますが、終わる順番は、画像の大きさやネットワークの状態で変わります。小さい画像が先に終われば、その画像が左に来ます。手元では毎回同じ順に並んでいても、公開すると順番が入れ替わる、ということも起こります。
当時の解決策は、読み込みを始める前に、Loader を XML の順番どおりに画面へ並べておくことでした。Loader そのものが表示用の部品なので、中身の読み込みが後から終わっても、位置は変わりません。
for (var i:int = 0; i < images.length(); i++) {
var loader:Loader = new Loader();
loader.x = i * 110; // XML の順番で位置を決める
addChild(loader); // 読み込みの前に、枠として並べておく
loader.load(new URLRequest(String(images[i].@src)));
}
なお、Flash Player は 2020 年末でサポートが終わり、今のブラウザーでは動きません。同じことは JavaScript で作ります。
JavaScript で XML を読む
次のような XML を用意したとします。
<?xml version="1.0" encoding="UTF-8"?>
<gallery>
<image src="images/01.jpg" alt="春の公園"/>
<image src="images/02.jpg" alt="夏の海"/>
<image src="images/03.jpg" alt="秋の山"/>
</gallery>
fetch() で文字列として読み込み、DOMParser で XML の文書に変換します。XML の形式が正しくないと、例外にはならず、parsererror という要素を含む文書が返るので、それを確かめます。
// XML を読み込んで、画像の情報の配列を返す
async function loadGalleryXml(url) {
const response = await fetch(url);
if (!response.ok) {
throw new Error("XML を読み込めません: " + response.status);
}
const text = await response.text();
const doc = new DOMParser().parseFromString(text, "application/xml");
if (doc.querySelector("parsererror")) {
throw new Error("XML の形式が正しくありません");
}
return [...doc.querySelectorAll("image")].map((node) => ({
src: node.getAttribute("src"),
alt: node.getAttribute("alt") ?? "",
}));
}
方法 1: 先に枠を順番どおりに作る
以下の例は、type="module" のスクリプトとして読み込み、関数の外で await を使っています。
いちばん簡単なのは、ActionScript 3 のときと同じく、読み込みを待たずに img 要素を XML の順番どおりにページへ追加してしまう方法です。要素の並びは追加した順に決まるので、画像の読み込みがどの順に終わっても、表示の順番は変わりません。
const gallery = document.querySelector("#gallery");
const items = await loadGalleryXml("gallery.xml");
for (const item of items) {
const img = document.createElement("img");
img.src = item.src;
img.alt = item.alt;
img.loading = "lazy"; // 画面に近づいてから読み込む
gallery.append(img);
}
画像の幅と高さがわかっているなら、width と height も指定しておくと、読み込みの途中でレイアウトがずれません。値は src や alt のプロパティに入れ、XML の文字を innerHTML で HTML に埋め込むことはしません。XML の中に思わぬ文字が入っていても、HTML として解釈されずに済みます。
方法 2: すべてそろってから並べる (Promise.all)
すべての画像の読み込みが終わってから、一度に表示したい場合もあります。画像の読み込みを Promise にして、Promise.all() で待ちます。Promise.all() の結果は、終わった順ではなく、渡した配列の順に並ぶので、順番が保たれます。
// 画像 1 枚の読み込みを Promise にする
function loadImage(src, alt) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = () => reject(new Error("画像を読み込めません: " + src));
img.alt = alt;
img.src = src;
});
}
const gallery = document.querySelector("#gallery");
const items = await loadGalleryXml("gallery.xml");
const results = await Promise.allSettled(
items.map((item) => loadImage(item.src, item.alt))
);
for (const result of results) {
if (result.status === "fulfilled") {
gallery.append(result.value); // XML の順番のまま追加される
}
}
ここでは Promise.all() の代わりに Promise.allSettled() を使っています。Promise.all() は 1 枚でも失敗すると全体が失敗になりますが、Promise.allSettled() なら、読み込めた画像だけを順番どおりに並べられます。ただし、この方法ではいちばん遅い画像を待つことになるので、枚数が多いときは方法 1 のほうが早く表示を始められます。
非同期処理の待ち方そのものについては、「処理の終わりを待ってから次へ ── コールバックから async/await まで」で詳しく説明しています。
まとめ
- 非同期の読み込みは、始めた順に終わるとは限らない。終わった順に並べると順番が崩れる
- 先に枠 (ActionScript 3 の
Loader、JavaScript のimg要素) を順番どおりに並べておけば、読み込みの順番に関係なく並ぶ - すべてそろってから並べるなら、
Promise.all()やPromise.allSettled()を使う。結果は渡した順に並ぶ - XML は
fetch()とDOMParserで読み、parsererrorで形式の誤りを確かめる