FJCT_ニフクラ mobile backend(mBaaS)お役立ちブログ

スマホアプリ開発にニフクラ mobile backend(mBaaS)。アプリ開発に役立つ情報をおとどけ!

ファイルストアから写真一覧を取得して表示する際のTips

f:id:mbaasdevrel:20201204215542p:plain

ファイルストアにアップロードした写真を画面上に一覧表示したいというニーズはよくあります。今回は表示をなるべく高速化するTipsを紹介します。

結論

最初に画面を作りましょう。非同期処理が終わった後、データを埋め込む場所を探して適用します。

基本的なコード

たとえばJavaScript SDKを使った場合、このようなコードになるでしょう。

const dom = document.querySelector('#list');
const ary = ['a.png', 'b.png', 'c.png'];
ary.forEach(file => {
  ncmb.File.download(file, 'blob')
    .then(res => {
      // 表示処理を実装する
      const image = new Image;
      const reader = new FileReader;
      reader.onload = function(e) {
        image.src = e.target.result;
        dom.appendChild(image);
      }
      reader.readAsDataURL(res);
    });
})

この場合問題になるのは、まず表示場所です。一覧表示で動的に個数が変わる場合、複雑な画面構成が面倒になります。上記例のような配列の場合はまだいいでしょう。もう少し複雑になると大変です。

const dom = document.querySelector('#list');
const ary = [
  {
    id: 1
    file: 'a.png',
  },
  {
    id: 2,
    file: 'b.png'
  },
  {
    id: 3,
    file: 'c.png'
  }
];

ary.forEach(file => {
  ncmb.File.download(file.file, 'blob')
    .then(res => {
      // 表示処理を実装する
      const image = new Image;
      const reader = new FileReader;
      reader.onload = function(e) {
        image.src = e.target.result;
        image.id = file.id;            // ここを追加
        dom.appendChild(image);
      }
      reader.readAsDataURL(res);
    });
})

このコードは場合によって、うまく動きません。非同期処理である ncmb.File.download が完了する前に次のループ(ary.forEach)に入ってしまい、file変数が変わってしまっているからです。ファイルサイズが極端に違うものを連続で取得使用すると、処理が終了する順番が変わってしまいます。idが異なる状態だと、アプリが不具合を起こすことでしょう。

async/awaitを使う

非同期処理で一時停止させられるasync/awaitを使えば解決はします。

ary.forEach(file => {
  const res = await ncmb.File.download(file.file, 'blob')
  // 表示処理を実装する
  const image = new Image;
  const data = await loadPhoto(res);
  image.src = data;
  image.id = file.id;
})

async function loadPhoto(photo) {
  return new Promise(res => {
    const reader = new FileReader();
    reader.onload = function(e) {
      res(e.target.result);
    }
    reader.readAsDataURL(photo);
  })
}

これは恐らくうまく動きます。しかしファイルのダウンロード、ファイルの読み込みが終わるまで一つずつ処理が停止するので遅くなります。 loadPhoto を使わない場合、大きなファイルを読み込むのに数秒待たされるため、ループが先に進んでしまう問題があります。

Promise.allを使う

forループの中でasync/awaitを使うのはあまりお勧めされていません(処理が直列化してしまうため)。そこでPromise.allを使うの手もあります。

const promises = [];
ary.forEach(file => {
  promises.push(ncmb.File.download(file.file, 'blob'))
});
const res = await Promise.all(promises);

この場合、ネットワーク処理は確かに並列化されます。しかし、すべてのネットワーク処理が終わるまでPromise.allは結果を返してくれません。100件の写真を読み込む場合、100件すべて終わるまで結果を得られないので、相当時間がかかります。この方法もお勧めできません。

解決策

そこで解決策です。まずループ処理の最初でHTMLを作ってしまいます。

const dom = document.querySelector('#list');
ary.forEach(file => {
  const list = document.createElement('div');
  list.innerHTML = `<img id="${file.id}" src="" data-file="${file.file}" />`;
  dom.appendChild(li);
});

そしてファイルのダウンロードを実行し、完了したらHTMLを改めて探します。

ncmb.File.download(file.file, 'blob')
  .then(res => {
    // 表示処理を実装する
    const image = new Image;
    const reader = new FileReader;
    reader.onload = function(e) {
      const image = dom.querySelector(`img [data-file="${res.name}"]`);
      image.setAttribute('src', e.target.result);
      image.setAttribute('id', file.id);
    }
    reader.readAsDataURL(res);
  });

後からHTMLを追加する方法の場合、非同期処理前後のデータが変わってしまうことを注意しなければなりません。そうではなく、最初に画面要素を作ってしまえば、非同期処理の終わった後に表示する場所を探せばいいということです。

まとめ

非同期処理をasync/awaitで止めてしまえば処理は簡単になります。しかし処理が止まってしまう分、ネットワーク処理をループ処理した時に時間がかかるようになってしまいます。ループ処理ではthen/catchを使うようにしたり、async/awaitとの処理分けが大事になるでしょう。

中津川 篤司

中津川 篤司

NCMBエヴァンジェリスト。プログラマ、エンジニアとしていくつかの企業で働き、28歳のときに独立。 2004年、まだ情報が少なかったオープンソースソフトの技術ブログ「MOONGIFT」を開設し、毎日情報を発信している。2013年に法人化、ビジネスとエンジニアを結ぶDXエージェンシー「DevRel」活動をスタート。