Help us understand the problem. What is going on with this article?

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

More than 3 years have passed since last update.

はじめに

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/

horo
Chrome作ってる中の人の一人です。 Qiita上の私の記事・コメントは個人的な意見に基づくものであり、所属する組織、団体とは一切関係ありません。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした