ServiceWorkerのBackground Syncでオンライン復帰時にデータ送信

  • 25
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

はじめに

2016年3月にリリースされたChrome 49からBackground Syncという新しい機能を使えるようになった。この機能を使うと、オフライン時に送信しようとしたデータを、その後ブラウザが閉じられたとしても、オンラインに復帰した際に自動的に送信することができる。そこで、LocChat.comというチャットサイトでこの機能を試してみた。
Background Sync demo
左のAndroidの画面で、機内モードに設定後、送信ボタンを押し、ブラウザを終了後、機内モードをオフにすると、右のPCのブラウザにチャットが届いている。

Background Syncの使い方

ServiceWorkerの登録

まず、Background SyncはServiceWorkerを使うので、下記のコードでServiceWorkerを登録する。

navigator.serviceWorker.register('./sw.js', {scope: './'})

送信処理

ユーザーが送信ボタンを押した際には下記のようにして、ブラウザがBackground Syncに対応している場合はthis._sendMassageWithSync(data)を実行し、それに失敗したり、ブラウザが対応していない場合は、this._sendMessage(data)を実行している。

if (navigator.serviceWorker && window.SyncManager) {
  this._sendMassageWithSync(data)
    .catch((function() {
      this._sendMessage(data);
    }).bind(this));
} else {
  this._sendMessage(data);
}

Syncの登録

this._sendMassageWithSync(data)では下記のようにIndexedDBの中にdataを保存してから、reg.sync.register()を呼び出しSyncを登録している。引数として渡すタグ名に使っているresultは、IndexedDBで自動採番されたidが渡ってくる。

  _sendMassageWithSync: function(data) {
    var INDEXED_DB_NAME = 'send_queue';
    var INDEXED_DB_VERSION = 1;
    var STORE_NAME = 'messages';
    function openDataBase() {
      return new Promise(function(resolve, reject) {
          var req = indexedDB.open(INDEXED_DB_NAME, INDEXED_DB_VERSION);
          req.onupgradeneeded = function (event) {
            req.result.createObjectStore(
                STORE_NAME, { keyPath: 'id' , autoIncrement: true });
          };
          req.onsuccess = function (event) { resolve(req.result); }
          req.onerror = reject;
        });
    }
    function addMessageToDataBase(data) {
      return openDataBase().then(function(db) {
          return new Promise(function(resolve, reject) {
            var transaction = db.transaction(STORE_NAME, 'readwrite');
            var store = transaction.objectStore(STORE_NAME);
            var req = store.add(data);
            req.onsuccess = function() { resolve(req.result); }
            req.onerror = reject;
          });
        });
    }
    return addMessageToDataBase(data).then(function(result) {
        return navigator.serviceWorker.ready.then(function(reg) {
            return reg.sync.register('send-msg:' + result);
          });
      });
  },

this._sendMessage(data)では単純にXMLHttpRequestで送信しているだけなので省略。

ServiceWorkerのSyncEventハンドラ

オンライン時に呼ばれるSyncEventハンドラではSyncのタグ名から先程のidを取り出し、sendAndDeleteMessage(id)を実行して返ってくるPromiseをevt.waitUntil()に渡している。こうすることで、sendAndDeleteMessageが失敗した場合、つまりネットワークが再度切断された場合に、再度オンライン復帰時にSyncEventハンドラが呼ばれるようになる。

self.addEventListener('sync', function(evt) {
  if (evt.tag.startsWith('send-msg:')) {
    var id = parseInt(evt.tag.substr(9))
    if (isNaN(id))
      return;
    evt.waitUntil(sendAndDeleteMessage(id));
  }
});

sendAndDeleteMessage()ではgetMessage(id)でIndexedDBからデータを取ってきて、sendRequestで実際に送信し、deleteMessage()でIndexedDBからデータを削除している。

function sendAndDeleteMessage(id) {
  return getMessage(id).then(sendRequest).then(function() {
    return deleteMessage(id);
  });
}

IndexedDBからデータの取得

getMessage(id)ではopenDataBase()で取得したIDBDatabaseからIDBObjectStoreを取得し、store.get(id)でデータを取得している。

function getMessage(id) {
  return openDataBase().then(function(db) {
      return new Promise(function(resolve, reject) {
        var transaction = db.transaction(STORE_NAME, 'readonly');
        var store = transaction.objectStore(STORE_NAME);
        var req = store.get(id);
        req.onsuccess = function() { resolve(req.result); }
        req.onerror = reject;
      });
    });
}

データの送信

sendRequestではFetch APIを使ってデータを実際に送信している。

function sendRequest(request) {
  if (!request)
    return Promise.resolve();
  var formData = new FormData();
  formData.append('pid', request.pid);
  formData.append('content', request.content);
  return fetch(new Request(
      '/m',
      {method: 'POST', body: formData, credentials: 'include'}));
}

IndexedDBからデータの削除

deleteMessage()では、不要になったデータをIndexedDBから削除している。

function deleteMessage(id) {
  return openDataBase().then(function(db) {
      return new Promise(function(resolve, reject) {
        var transaction = db.transaction(STORE_NAME, 'readwrite');
        var store = transaction.objectStore(STORE_NAME);
        var req = store.delete(id);
        req.onsuccess = resolve;
        req.onerror = reject;
      });
    });
}

参考

https://developers.google.com/web/updates/2015/12/background-sync
https://wicg.github.io/BackgroundSync/spec/