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

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

mBaaSでasync/awaitを使って非同期処理を分かりやすく書こう(JavaScript/Monaca編)

f:id:mbaasdevrel:20171212211252p:plain

WebブラウザのJavaScriptはスレッドが一つしかありません。そのため、処理完了までに時間がかかる動作を行うと、他の動作ができない、いわゆるフリーズした状態になってしまいます。それはとてもUXが悪いです。

その状態を回避するために、非同期処理が生まれています。ネットワーク処理や、ファイル処理など様々な場面でJavaScriptは非同期処理を用いています。そして、mBaaSはあらゆる処理において非同期処理が行われています。非同期処理をマスターすることがmBaaSを使いこなすのに直結しているといっても過言ではありません。

この記事では非同期処理の一般的な手法であるPromise形式から、最近の書き方であるasync/awaitへの切り替え方を紹介します。

PromiseによるmBaaS操作

NCMBのドキュメントではすべての操作をPromise形式で記述しています。例えばデータストア (JavaScript) : 基本的な使い方 | ニフクラ mobile backendでは、データの保存法について次のように紹介しています。

var GameScore = ncmb.DataStore("GameScore");
var gameScore = new GameScore();

gameScore.set("score", 1337)
  .set("playerName", "Taro")
  .set("cheatMode", false)
  .save()
  .then(function(gameScore){
    // 保存後の処理
  })
  .catch(function(err){
    // エラー処理
  });

async/awaitへの書き直し

上記のデータストア保存処理をasync/awaitに書き直すと、次のようになります。

try {
  var GameScore = ncmb.DataStore("GameScore");
  var gameScore = new GameScore();
  await gameScore.set("score", 1337)
    .set("playerName", "Taro")
    .set("cheatMode", false)
    .save();
} catch (err) {
  // エラー処理
}

gameScoreが保存された後のobjectIdはそのまま登録されていますので、返値を処理する必要はありません。エラー時は例外処理が発生するので、try/catchを使って補足しなければなりません。

検索時

データストアを検索して、その値からさらに何かデータを検索するといった処理はよくあるかと思います。Promiseで書くと次のようになります。Taroというユーザのスコアを取得し、それぞれのスコアに対して他に何人のユーザがいるのかを出すといった具合です。非同期処理の中で行う繰り返し処理は、ネストがどんどん深くなるのが難点です。

// プレイヤーがTaroのスコアを降順で取得
var GameScore = ncmb.DataStore("GameScore");
GameScore.equalTo("playerName", "Taro")
  .order("score",true)
  .fetchAll()
  .then(function(results){
    console.log("Successfully retrieved " + results.length + " scores.");
    for (var i = 0; i < results.length; i++) {
      var object = results[i];
      GameScore
        .equalTo("score", object.get("score"))
        .count()
        .fetchAll()
        .then(function(scores) {
          // 同じスコアのユーザが何人いるか分かる
          console.log(scores.count)
        });
    }
  })
  .catch(function(err){
    console.log(err);
  });

同様の処理をasync/awaitで書いてみます。インデントがぐっと少なくなって、可読性がよくなっているのが分かるでしょうか。

try {
  var GameScore = ncmb.DataStore("GameScore");
  var results = await GameScore.equalTo("playerName", "Taro")
    .order("score",true)
    .fetchAll();
  console.log("Successfully retrieved " + results.length + " scores.");
  for (var i = 0; i < results.length; i++) {
    var object = results[i];
    var scores = await await GameScore
      .equalTo("score", object.get("score"))
      .count()
      .fetchAll();
    // 同じスコアのユーザが何人いるか分かる
    console.log(scores.count)
  }
} catch (err) {
  console.log(err);
}

async/awaitの注意点

async/awaitを使う場合の注意点としては、必ず関数で囲まれている必要があり、かつasyncと書かれていなければいけません。基本は次のような形です。

async function() {
  // この中ではawaitが使えます
}

JavaScriptの場合 document.addEventListener を使うと思いますので、その中で使う場合には次のようになります。どちらでもawaitが使えます。

// アロー関数の場合
document.addEventListener('DOMContentLoaded', async (e) => {

});
// または
document.addEventListener('DOMContentLoaded', async function(e) {
});

JavaScriptでよく使うjQueryであっても同じです。

// アロー関数の場合
$(async () => {

});

// または
$(async function() {

});

さらにイベント処理(クリックなど)でも同じように書けます。

$('#hoge').on('click', async (e) => {

});
// または
$('#hoge').on('click', async function(e) {

});

既存の処理をasync/await化する

async/awaitは新しい書き方なので、これまでのJavaScriptライブラリと使い方がマッチしない場合もあります。よくあるのは、次のように引数を渡すケースです。

Hoge.exec(successHandler, errorHandler, options);

これをそのまま使うと、処理が長くなりがちです。オプションが一番最後だと、何の設定だったのか追うのも大変になります。

Hoge.exec(function(result) {
  // 成功時の処理
  // :
}, function(err) {
  // エラー時の処理
  // :
}, {
  // オプション
  a: 'b',
  c: 'd'
})

そこで、次のような関数を用意します。関数名は適当です。この関数は、旧来の処理をPromise化しています。Promiseは2つの引数があり、最初が処理成功時、後ろが失敗時に呼び出されるものになります。

function Fuga(options) {
  return new Promise((res, rej) => {
    Hoge.exec(res, rej, options);
  }
}

これを通常の処理で呼び出すには、次のようになります。

try {
  var result = await Fuga(options);
  // 成功時の処理
} catch (err) {
  // エラー時の処理
}

ファイルの読み取りをasync/await化する

次によくある例として、FileReaderを使ったファイル読み込みについて紹介します。これはユーザが指定した画像サイズを小さくしたり、Canvasに描画したりするのによく使われます。JavaScript でのローカル ファイルの読み込み - HTML5 Rocksのコードをサンプルにします。

function handleFileSelect(evt) {
  var files = evt.target.files; // FileList object

  // Loop through the FileList and render image files as thumbnails.
  for (var i = 0, f; f = files[i]; i++) {

    // Only process image files.
    if (!f.type.match('image.*')) {
      continue;
    }

    var reader = new FileReader();

    // Closure to capture the file information.
    reader.onload = (function(theFile) {
      return function(e) {
        // Render thumbnail.
        var span = document.createElement('span');
        span.innerHTML = ['<img class="thumb" src="', e.target.result,
                          '" title="', escape(theFile.name), '"/>'].join('');
        document.getElementById('list').insertBefore(span, null);
      };
    })(f);

    // Read in the image file as a data URL.
    reader.readAsDataURL(f);
  }
}

これをasync/awaitを使ってなるべくインデントが少ない状態にしてみます。元のコードが4つ分インデントしていたのに対して、async/awaitで書き直すと2つまで減っています。関数に分割することで、全体の見通しもよくなります。

async function handleFileSelect(evt) {
  var files = evt.target.files; // FileList object

  // Loop through the FileList and render image files as thumbnails.
  for (var i = 0, f; f = files[i]; i++) {
    await fileExecute(f);
  }
}

async function fileExecute(f) {
  return new Promise(async res => {
    // Only process image files.
    if (!f.type.match('image.*')) {
      return res();
    }
    var e = await fileRead(f)
    var span = document.createElement('span');
    span.innerHTML = ['<img class="thumb" src="', e.target.result,
                      '" title="', escape(theFile.name), '"/>'].join('');
    document.getElementById('list').insertBefore(span, null);
    res();
  });
}

async function fileRead(f) {
  return new Promise(res => {
    var reader = new FileReader();
    reader.onload = res;
    reader.readAsDataURL(f);
  });
}

まとめ

ネットワークの非同期処理はPromiseで処理を行いますが、繰り返し使っていると徐々にネストが深くなって、コードの見通しが悪くなっていきます。async/awaitを使って、分かりやすい記述を心がけると、後々のメンテナンスも効率的になるでしょう。

ぜひasync/awaitでの記述にトライしてください。

中津川 篤司

中津川 篤司

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