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

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

ファイルストアからコンテンツを順番にダウンロードする(JavaScript SDK編)

f:id:mbaasdevrel:20200622174334p:plain

ファイルストアを使うと、アプリ内で用いるリソース(画像、動画、音楽など)を保存しておけます。また、ユーザが写真をアップロードしたり、CSVファイルをアプリ内で作成してアップロードしておく場所としても利用できます。

利用用途として、多くの方が写真をアップロードするのに使っていますが、その写真を表示する際に手間取ることも多いようです。特に写真にACLを付けて、特定の人しか表示できないようにしていたり、写真のコンテンツを非同期で取得する際に問題が起きがちです。

今回は写真のコンテンツを非同期で取得し、それを表示するまでのステップを紹介します。

データ構成

よくある形として、写真本体はファイルストアに保存しつつ、その写真に関するメタデータ(コメント、撮影場所など)はデータストアに保存するという形があります。

f:id:mbaasdevrel:20200622174334p:plain

この形式の場合、データストアには写真のファイル名を保存します。ファイル名はクライアント側で自動生成する、ユニークなものにしておくのが良いでしょう(多人数で利用するアプリの場合、スマートフォン側で設定されるファイル名では重複する可能性があるため)。シンプルな方法としては、メタデータ(データストア)側のオブジェクトIDをファイル名に利用する方法もあります。この方法ならば、データストアにファイル名を保存しておく必要はありません(データストアからデータを取得した時点でファイル名が特定されるので)。

HTTPSアクセスの場合

ファイルストアからコンテンツを取得する方法は2つあります。一つはSDK経由での取得、もう一つはHTTPSアクセスです。HTTPSアクセスの場合、画像の表示に際して非同期処理であることを意識せず、Webブラウザやイメージコンポーネントに表示を任せられます。楽である反面、ACLによる制御はできないので、URLが分かると誰でも写真を閲覧できるようになります。

今回の例ではSDK経由での実装例について解説します。

メタデータを取得する

まず最初にメタデータを取得します。今回は無条件に全データを対象としていますが、自分(または特定のユーザ)のデータであったり、位置情報で付近のデータのみを絞り込んでもいいでしょう。

const Photo = ncmb.DataStore('Photo')

写真データを取得する

そして、この取得されたデータに対して紐付いている写真データを取得します。この時、取得方法は三つあります。

  • async/awaitを使った方法
  • Promise.allを使った方法
  • Promiseを使った方法

async/awaitを使った方法の場合、実装はシンプルになります。ただし、処理は直列になります。例えば1写真取得するのに3秒かかったとして、10枚の写真があれば30秒かかります。これはUXとしてよくありません。

f:id:mbaasdevrel:20200609154235p:plain

Promise.allを使うと、ネットワーク処理を並列化できます。この場合、ネットワーク処理によりますが、5つくらいの処理が並列で実行されます。つまり10枚の写真であれば、6秒で完了することになります。async/awaitを単純に使った場合に比べて、5倍くらいの高速化が臨めるでしょう。ただし、Promise.allはすべての処理を一括実行して、返ってくるまで待たなければなりません、一つ一つ個別の処理はできないので注意が必要です。

Promiseを使った方法は最も一般的なNCMBの使い方になります。処理は若干複雑になりますが、並列で処理されるので高速化が期待できるでしょう。この場合、前のネットワーク処理が終わる前に次の処理が呼ばれるため、場合によっては変数が上書きされてしまう可能性があります。その結果、期待した内容でない状態になる場合があります。この点に注意が必要です。

async/awaitを使った場合

async/awaitを使った場合、次のような形になります。

const photos = await Photo.fetchAll()
for (let photo of photos) {
  const data = await ncmb.File.download(photo.get('fileName'), 'blob')
  // 以下、写真の表示処理
  console.log(data)
}

この場合はデータ一件、一件を順番に処理していますので処理が分かりやすい反面、実行が遅いのが難点です。

Promise.allを使った方法

Promise.allを使った場合は次のようになります。

const photos = await Photo.fetchAll()
const data = await Promise.all(
  photos.map(photo => ncmb.File.download(photo.get('fileName'), 'blob'))
)
// 以下、写真の表示処理
for (let index in data) {
  // 対象となる写真のメタデータを探す
  const photo = photos[index]
  const file = data[index]
  console.log(photo.get('fileName'), file)
}

この方法は処理が並列化されるので高速になる一方、ダウンロード対象のファイル名とコンテンツが結びつかないという問題があります。Promise.allで指定した順番と、結果の返ってくる順番は保証されているので photos[index] のような形で取得対象になったデータを取得しています。

Promiseを使った方法

Promiseを使った場合は次のようになります。いわゆる普通の ncmb.File.download を使った方法で、処理は並列化されます。thenの中に処理が移動しますので、forループの後に処理があった場合、表示処理よりも先に呼ばれてしまうのが難点です。

const photos = await Photo.fetchAll()
// 以下、写真の表示処理
for (let photo of photos) {
  // 対象となる写真のメタデータを探す
  ncmb.File.download(photo.get('fileName'), 'blob')
  .then(blob => {
    console.log(photo.get('fileName'), blob.length)
  })
}

なお、上記の処理では let photo とすることで変数のスコープを限定しています。これを使わない場合、変数が上書きされてしまうので注意してください。以下はvarを使った場合です。

const photos = await Photo.order('objectId', false).fetchAll()
// 以下、写真の表示処理
for (var photo of photos) {
  // 対象となる写真のメタデータを探す
  ncmb.File.download(photo.get('fileName'), 'blob')
    .then(blob => {
      console.log(photo.get('fileName'), blob.length)
    });
}

この場合、次のように結果が返ってきました。変数名が上書きされてしまっているのが分かります。

techbookfest6-2.jpeg 3931908
techbookfest6-2.jpeg 2588834
techbookfest6-7.jpeg 2383385
techbookfest6-7.jpeg 2209172

まとめ

処理の書きやすさでいえばasync/awaitを使いたくなりますが、UXを考えるならPromise.allを使いましょう。書き方は多少複雑になりますが、処理は高速になります。Promiseを使う場合には、変数のスコープに気をつけてください。

中津川 篤司

中津川 篤司

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