漸進式web應用開發-- 使用後臺同步保證離線功能(六)

龍恩0707發表於2019-07-29

閱讀目錄

一:什麼是後臺同步保證離線功能呢?

在我們做移動端開發也好,還是做PC端應用也好,我們經常會碰到填寫表單這樣的功能,如果我們的表單填寫完成以後,我們點選提交,但是這個時候我突然進入了電梯,或者我們在高鐵上做這麼一個操作,突然斷網了,或者說我們的網路不好的情況下,那麼一般的情況下會一直請求,當我們的請求超時的時候就會請求失敗,或者說請求異常,最後就會提示我們網路異常這些資訊,那麼這樣對於使用者體驗來說並不是很好,那麼現在我們來理解下什麼是後臺同步保證離線功能呢?後臺同步離線功能就是說當我們的網路不好的時候,我們點選提交按鈕時,我們會保證該應用一定是成功的,不會提示網路異常這些資訊,當網路連線失敗的時候,我們會在前端頁面顯示一個提示,比如說,正在請求中,請稍微.... 這樣的一個提示,當我們的網路恢復正常了,我們會重新去請求下該介面,那麼我們這個應用就提示操作成功狀態了。

後臺同步:它使得我們能夠確保使用者採取的任何操作都能完成,不管使用者的連結狀態如何,甚至當使用者點選提交後,直接關閉我們這個應用,不再回來,並且關閉瀏覽器,後臺同步操作也能夠完成。

後臺同步的優點:

1. 對於使用者而言,能夠信任我們的漸進式web應用能一直工作,這也意味者我們與傳統的web開發是有區別的,我們可以實現原生應用類似的效果。

2. 對於企業來講,讓使用者在連結失敗的時候,也能夠訂火車票,訂閱新聞或傳送訊息,對於這樣的使用者體驗也會更好。

二:後臺同步是如何實現的呢?

後臺同步原理的實質是:它是將操作從頁面上下文中剝離開來,並且在後臺執行。

通過將這些操作放到後臺,它就不會受到單個網頁的影響,即使網頁被關閉,使用者連線會斷開,甚至伺服器有時候會出現故障,但是隻要我們電腦上安裝了瀏覽器,後臺同步的操作就不會消失,直到它成功完成為止。

1. 註冊一個同步事件

使用後臺同步很簡單,我們首先要註冊一個同步事件,如下程式碼:

navigator.serviceWorker.ready.then(function(registration) {
  registration.sync.register('send-messages');
});

如上程式碼可以在頁面上執行,它獲取了當前啟用的service Worker 的 registration 物件,並註冊了一個叫 send-messages 的sync事件。

現在,我們就可以將一個監聽該同步事件的事件監聽器新增到 service worker 中,該事件包含的邏輯將會在service worker 中執行,而不是在頁面上執行的。

如下程式碼:

self.addEventListener("sync", function(event) {
  if (event.tag === "send-messages") {
    event.waitUntil(function(){
      var sent = sendMessages();
      if (sent) {
        return Promise.resolve();
      } else {
        return Promise reject();
      }
    })
  }
});

2. 理解 SyncManager

我們上面已經註冊了一個sync事件,並且在service worker中監聽了該sync事件。
那麼所有與sync事件的互動都是通過 SyncManager 來完成的。SyncManager是 service worker 的一個介面。它可以讓我們註冊sync事件,並且我們可以獲取已經註冊的sync事件列表。

訪問 SyncManager

我們可以通過已經啟用的service worker的registration物件來訪問 SyncManager, 在service worker裡面,我們可以通過呼叫navigator.serviceWorker.ready 來訪問當前啟用的 service worker 的 registration物件,該方法會返回一個Promise物件,當成功時候我們可以拿到service worker的registration物件。

如下程式碼:

navigator.serviceWorker.ready.then(function(registration){});

如上程式碼,我們已經獲得到了 registration 物件後,不管我們是在service worker上還是在頁面上,和SyncManager互動現在都是一樣的。

3. 註冊事件

想要註冊 sync事件,我們可以在 SyncManager中呼叫 register, 傳入一個我們想要註冊的 sync事件名稱。

比如我們想註冊一個在service worker中叫 send-message的事件,我們可以使用如下程式碼:

self.registration.sync.register("send-message");

如果我們想在service-worker中想做一樣的事情的話,我們可以使用如下程式碼:

navigator.serviceWorker.ready.then(function(registration) {
  registration.sync.register("send-message");
});

4. 理解Sync事件原理

SyncManager 維護了一個Sync事件標籤列表,SyncManager只知道哪些事件被註冊了,何時被呼叫,以及如何傳送sync事件。

當我們下面任何一個事件發生的時候,SyncManager會給列表中的每一個註冊事件名傳送一個sync事件。

1) sync事件註冊後會立即傳送。
2)當使用者離線變成線上的時候也會傳送。
3)如果還有未完成的事件時,每隔幾分鐘會傳送。

在service worker中,我們傳送sync事件,那麼該事件就可以被監聽到,並且我們可以使用promise進行響應,如果我們的這個promise完成了,那麼對應的sync註冊會從 SyncManager中刪除,如果promise拒絕了,那麼我們的sync註冊的事件就會保留在SyncManager中,並且每隔幾分鐘會在下一個同步機會進行重試。

5. 理解 sync事件中的事件名稱

sync事件中的事件名稱是唯一的。如果在SyncManager中使用一個已經被註冊的事件名稱繼續來註冊的話,那麼SyncManager 會忽略它,比如說:我們正在構建一個郵件服務,每當使用者傳送訊息的時候,我們可以把訊息儲存到 indexedDB的發件箱中,並且註冊一個send-email-message這樣的後臺同步事件,那麼我們的service worker可以包含一個事件監聽器進行監聽,它會遍歷indexedDB發件箱中的每一條訊息,嘗試傳送他們,並且當傳送成功後,將會從 indexedDB佇列中刪除它,如果我們當中有某條訊息並沒有傳送成功的話,那麼該sync事件就會被拒絕,SyncManager將會在稍後再次傳送該事件,但是該事件是我們上次事件中傳送失敗的那個事件。使用這種設定,我們永遠不需要檢查發件箱中是否存在訊息,只要有未傳送的電子郵件,sync事件就會保持註冊,並且嘗試清空我們發的發件箱。

5. 理解獲取已經註冊的sync事件列表

我們使用SyncManager的getTags()方法,就可以得到完整的已註冊同步事件列表。該getTags()方法也會返回一個Promise物件,該promise物件完成後,會獲得一個sync註冊事件名稱的陣列。

在service-worker中,我們可以註冊一個叫 hello-world的sync事件,然後將當前註冊的完整事件列表列印在控制檯中中;如下程式碼:

self.registration.sync.register("hello-world").then(function() {
  return self.registration.sync.getTags();
}).then(function(tags) {
  console.log(tags);
});

在我們的service worker中,我們首先通過使用 ready 獲取 registration物件,也可以獲取一樣的結果,如下程式碼所示:

navigator.serviceWorker.ready.then(function(registration){
  registration.sync.register('hello-world').then(function() {
    return registration.sync.getTags();
  }).then(function(tags) {
    console.log(tags);
  });
});

6. 最後一次發生sync事件

在有些情況下,SyncManager可以會判斷出嘗試傳送的sync事件已經多次失敗,當發生這種情況的時候,SyncManager將會傳送最後一次事件,給我們最後一次響應的機會,我們可以通過sync事件的lastChance屬性來判斷什麼時候會發生這種情況,如下程式碼:

self.addEventListener("sync", event => {
  if (event.tag === "hello-world") {
    event.waitUntil(
      // 呼叫 addReservation方法
      addReservation().then(function(){
        return Promise.resolve();
      }).catch(function(error) {
        if (event.lastChance) {
          return removeReservation();
        } else {
          return Promise.reject();
        }
      })
    )
  }
})

三:如何給sync事件傳遞資料?

在頁面介面互動中,我們可能需要傳遞一些引數進去,比如說,發生一個訊息的介面中,可能我們需要把訊息文字傳送過去,一個為帖子點讚的介面,我們需要把帖子id的引數傳遞過去。但是當我們註冊sync事件時,我們目前來看,我們之前只能傳遞事件名稱,但是我們如何把一些對應的引數也傳遞給sync中的事件當中呢?

1. 在indexedDB中維護操作佇列

要想把一些引數傳遞過去,我們可以把這些引數先儲存到我們的indexedDB中,然後,我們在service worker中的sync事件程式碼我們可以迭代該物件儲存,並且在每個條目上執行所需的操作,一旦操作成功了,我們就可以把該實體從物件儲存中刪除掉。

現在我們來做個demo,我們現在需要把每一條訊息可以新增到 message-queue物件儲存中,然後我們註冊一個 send-message 後臺同步事件來處理,該事件會遍歷 message-queue物件中的所有訊息,依次將他們傳送到網路中,如果當所有訊息都傳送成功的話,我們會依次將訊息佇列中的資料刪除,因此物件儲存就為空了。但是如果有任何一條訊息沒有傳送成功的話,就會向sync事件返回一個拒絕的promsie,SyncManager在稍後一段時間內會再次執行該sync事件。

如果我們之前使用如下程式碼,來請求一個介面,如下程式碼所示:

var sendMessage = function(subject, message) {
  fetch('/new-message', {
    method: 'post',
    body: JSON.stringify({
      subject: subject,
      msg: message
    })
  })
};

現在我們使用service worker,需要把程式碼改成如下所示:

var triggerMessageQueueUpdate = function() {
  navigator.serviceWorker.ready.then(function(registration) {
    registration.sync.register("message-queue-sync");
  });
};

var sendMessage = function(subject, message) {
  addToObjectStore("message-queue", {
    subject: subject,
    msg: message
  });
  triggerMessageQueueUpdate();
};

然後我們需要在service worker中監聽sync事件程式碼如下:

self.addEventListener("sync", function(event) {
  if (event.target === 'message-queue-sync') {
    event.waitUntil(function() {
      return getAllMessages().then(function(messages) {
        return Promise.all(
          messages.map(function(message) {
            return fetch('/new-message', {
              method: 'post',
              body: JSON.stringify({
                subject: subject,
                msg: message
              })
            }).then(function(){
              return deleteMessageFromQueue(message); 
            })
          })
        )
      })
    })
  }
});

如上改寫後的程式碼,首先我們會呼叫 addToObjectStore 這個方法來把訊息儲存到我們的key為 'message-queue' 當中,然後呼叫 triggerMessageQueueUpdate 這個方法,使用sync註冊message-queue-sync這個事件,並且我們使用sync監聽了該事件名稱,然後我們使用了 getAllMessages 方法獲取indexedDB的訊息佇列中的所有訊息,並且最終返回了一個promise給sync事件,在該程式碼中,我們使用了Promise.all方法,在該方法內部,只有我們的訊息傳送成功後,我們才會使用 deleteMessageFromQueue方法來刪除該訊息,在我們的訊息陣列中,我們使用了map()方法遍歷為每條訊息傳送一個promise物件.

2. 在indexedDB中維護請求佇列

有時候在我們的專案中,我們需要實現本地儲存架構來對物件狀態進行跟蹤,如果頁面上有多個ajax請求的話,我們可以使用service worker 在indexedDB中來維護請求佇列,我們可以將網路上的每個請求儲存到indexedDB中,然後該方法會註冊一個sync事件,該事件會遍歷物件儲存中所有請求,並依次執行。

比如專案中有如下程式碼:

var sendMessage = function(subject, message) {
  fetch('/new-message', {
    method: 'post',
    body: JSON.stringify({
      subject: subject,
      msg: message
    })
  })
};

var getRequest = function(id) {
  fetch('/like-post?id='+id);
};

如上兩個請求,我們使用service worker 換成如下程式碼:

var triggerRequestQueueSync = function() {
  navigator.serviceWorker.ready.then(function(registration){
    registration.sync.register("request-queue");
  });
};

var sendMessage = function(subject, message) {
  addToObjectStore("request-queue", {
    url: '/new-message',
    method: 'post',
    body: JSON.stringify({
      subject: subject,
      msg: message
    })
  });
  triggerRequestQueueSync();
};

var getRequest = function(id) {
  addToObjectStore('request-queue', {
    url: '/like-post?id=' + id,
    method: 'get'
  });
  triggerRequestQueueSync();
};

如上程式碼,我們將所有的網路請求替換成如上的程式碼,將代表請求物件儲存到 request-queue的物件儲存中,這個儲存中每個物件代表一個網路請求,接下來我們需要新增一個sync事件監聽器到service worker中,它負責遍歷 request-queue的所有請求,依次會發起一個網路請求,傳送成功後,依次從物件儲存中刪除。如下程式碼所示:

self.addEventListener("sync", function(event) {
  if (event.tag === "request-queue") {
    event.waitUntil(function(){
      return getAllObjectsFrom("request-queue").then(function(requests) {
        return Promise.all(
          requests.map(function(req) {
            return fetch(req.url, {
              method: req.method,
              body: req.body
            }).then(function() {
              return deleteRequestFromQueue(req); // 返回一個promise
            })
          })
        )
      });
    })
  }
});

如上程式碼,如果一個請求傳送成功了,就會從indexedDB中佇列中刪除掉,失敗的請求會保留到佇列中,並且返回被拒絕的promise,那麼失敗的promise會在請求佇列的下一次sync事件中再次迭代。

3. 使用一種更簡單的方式傳遞資料給事件名稱

當我們需要傳遞一個簡單的資料給sync函式時候,我們就不需要使用上面的indexedDB來儲存資料,然後再service worker中依次遍歷拿到該物件了,我們可以使用一種更簡單的方式來解決如上的問題。我們之前的程式碼是這樣的:

var likePost = function(postId) {
  fetch("/like-post?id="+postId);
};

我們可以在service worker中使用如下程式碼來進行改造,如下程式碼所示:

var likePost = function(postId) {
  navigator.serviceWorker.ready.then(function(registration){
    registration.sync.register("like-post-"+postId);
  });
};

我們使用sync事件來監聽上面的函式,程式碼如下:

self.addEventListener("sync", function(event) {
  if (event.tag.startsWith("like-post-")) {  
    event.waitUntil(function(){
      var postId = event.tag.slice(10);
      return fetch('/like-post?id='+postId);
    })
  }
});

四:在我們的專案中新增後臺同步功能

在我們專案中新增後臺同步功能之前,我們還是來看下我們專案中的整個目錄架構如下所示:

|----- service-worker-demo6
|  |--- node_modules        # 專案依賴的包
|  |--- public              # 存放靜態資原始檔
|  | |--- js
|  | | |--- main.js         # js 的入口檔案
|  | | |--- store.js        # indexedDB儲存
|  | | |--- myAccount.js    
|  | |--- styles
|  | |--- images
|  | |--- index.html        # html 檔案
|  |--- package.json
|  |--- webpack.config.js
|  |--- sw.js

該篇文章是在上篇文章基礎之上繼續擴充套件的,如果想要看上篇文章,請點選這裡

我們首先來看下我們 public/index.html 程式碼如下:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>service worker 實列</title>
</head>
<body>
  <div id="app">222226666</div>
  <img src="/public/images/xxx.jpg" />
  <div style="cursor: pointer;color:red;font-size:18px;padding:5px;border:1px solid #333;" id="submit">點選我新增</div>

  <div style="cursor: pointer;color:red;font-size:18px;padding:5px;border:1px solid #333;" id="update">點選我修改</div>

</body>
</html>

public/js/myAccout.js 程式碼如下(該程式碼的作用最主要是做頁面業務邏輯程式碼)

import $ from 'jquery';

$(function() {
  function renderHTMLFunc(obj) {
    console.log(obj);
  }
  function updateDisplay(d) {
    console.log(d);
  };
  var addStore = function(id, name, age) {
    var obj = {
      id: id,
      name: name,
      age: age
    };
    addToObjectStore("store", obj);
    renderHTMLFunc(obj);
    $.getJSON("http://localhost:8081/public/json/index.json", obj, function(data) {
      updateDisplay(data);
    });
  };
  $("#submit").click(function(e) {
    addStore(3, 'longen1', '111');
  });
  $("#update").click(function(e) {
    $.getJSON("http://localhost:8081/public/json/index.json", {id: 1}, function(data) {
      updateInObjectStore("store", 1, data);
      updateDisplay(data);
    });
  });
});

如上myAccout.js程式碼,當我點選 id 為 "submit" 的div元素的時候(我們可以假設這是一個form表單提交,這邊為了演示這個作用懶得使用form表單來演示),當我點選該div元素的時候,我們的addStore函式會被呼叫,這個函式內部會呼叫 addToObjectStore()這個方法,這個函式會新增一個store物件的儲存,它會把該物件新增到IndexedDB的store物件中,新增完成以後,我們會呼叫renderHTMLFunc() 這個方法來渲染我們的html頁面,並且之後我們會發起一個ajax請求。如果我們的網路一直是可以用的話,那麼我們就不需要做任何處理操作,但是如果我們的網路連線失敗的情況下,我們呼叫了 addStore 方法,那麼我們新的資料會被新增到indexedDB中,並且會呼叫renderHTMLFunc方法來渲染我們的頁面,但是後面的ajax請求就會呼叫失敗。頁面雖然更新了,indexedDB資料也儲存到本地了,但是我們的伺服器完全不知情,因此在這種情況下,我們需要使用service worker中的sync事件來解決這個問題。

我們要完成如下步驟:

1)在addStore函式新增程式碼,檢查瀏覽器是否支援後臺同步。如果支援,則註冊一個 sync-store 同步事件,否則的話,便使用長規的ajax呼叫。

2)在store.js 中,新增到indexedDB程式碼,需要把狀態改為 sending(傳送中),在傳送請求到伺服器之前,這就是使用者看到的狀態,操作成功後,伺服器會返回新的狀態。

3)我們會向service worker新增一個事件監聽器,用來監聽sync事件,如果我們檢測到sync的事件名稱是 sync-store ,事件監聽器就會遍歷每一個處於 sending 狀態的預訂,並且嘗試傳送給伺服器。成功新增到伺服器之後,indexedDB中的狀態就會被修改成為新的狀態,如果任何伺服器請求失敗的話,那麼整個sync事件就會被拒絕,瀏覽器就會嘗試在隨後再執行這個事件了。

因此我們現在的第一步是在 addStore函式中,新增瀏覽器是否支援同步功能,如果支援的話,就會註冊一個sync事件。如下程式碼(在myAccount.js 程式碼修改):

var addStore = function(id, name, age) {
  var obj = {
    id: id,
    name: name,
    age: age
  };
  addToObjectStore("store", obj);
  renderHTMLFunc(obj);
  // 先判斷瀏覽器支付支援sync事件
  if ("serviceWorker" in navigator && "SyncManager" in window) {
    navigator.serviceWorker.ready.then(function(registration) {
      registration.sync.register("sync-store")
    });
  } else {
    $.getJSON("http://localhost:8081/public/json/index.json", obj, function(data) {
      updateDisplay(data);
    });
  }
};

因此我們的 public/js/myAccount.js 所有的程式碼如下:

import $ from 'jquery';

$(function() {
  function renderHTMLFunc(obj) {
    console.log(obj);
  }
  function updateDisplay(d) {
    console.log(d);
  };
  var addStore = function(id, name, age) {
    var obj = {
      id: id,
      name: name,
      age: age
    };
    addToObjectStore("store", obj);
    renderHTMLFunc(obj);
    // 先判斷瀏覽器支付支援sync事件
    if ("serviceWorker" in navigator && "SyncManager" in window) {
      navigator.serviceWorker.ready.then(function(registration) {
        registration.sync.register("sync-store").then(function() {
          console.log("後臺同步已觸發");
        }).catch(function(err){
          console.log('後臺同步觸發失敗', err);
        })
      });
    } else {
      $.getJSON("http://localhost:8081/public/json/index.json", obj, function(data) {
        updateDisplay(data);
      });
    }
  };
  $("#submit").click(function(e) {
    addStore(3, 'longen1', '111');
  });
  $("#update").click(function(e) {
    $.getJSON("http://localhost:8081/public/json/index.json", {id: 1}, function(data) {
      updateInObjectStore("store", 1, data);
      updateDisplay(data);
    });
  });
});

2)其次我們需要在 public/js/store.js 中openDataBase方法中的 result.onupgradeneeded 函式程式碼改成如下(當然要觸發該函式,我們需要升級我們的版本號,即把 var DB_VERSION = 2; 把之前的 DB_VERSION 值為1 改成2):

var DB_VERSION = 2;
var DB_NAME = 'store-data2';

// 監聽當前版本號被升級的時候觸發該函式
result.onupgradeneeded = function(event) {
  var db = event.target.result;
  var upgradeTransaction = event.target.transaction;
  var reservationsStore;
  /*
   是否包含該物件倉庫名(或叫表名)。如果不包含就建立一個。
   該物件中的 keyPath屬性id為主鍵
  */
  if (!db.objectStoreNames.contains('store')) {
    reservationsStore = db.createObjectStore("store", { keyPath: "id", autoIncrement: true });
  } else {
    reservationsStore = upgradeTransaction.objectStore("store");
  }
  if (!reservationsStore.indexNames.contains("idx_status")) {
    reservationsStore.createIndex("idx_status", "status", {unique: false});
  }
}

如上程式碼,我們在建立 store物件之前,我們會先判斷該物件是否存在,如果不存在的話,我們會建立該物件,否則的話,我們就通過呼叫 event.target.transaction.objectStore("store")將獲得更新事件中的事務,並且從事務中獲取store物件儲存的引用。

最後我們確認我們的store物件儲存是否已經有 idx_status 這個,如果不存在的話,如果不存在的話,我們就建立該索引。

3)現在我們需要修改我們的 public/js/store.js 中的getStore函式了,我們在該函式內部使用這個新的索引,就可以獲取到該某個請求中的某個狀態了,因此我們要對 我們的 getStore函式進行修改,讓其支援接收兩個可選的引數,索引名稱,以及傳遞給該索引的值。如下程式碼的修改:

var getStore = function (indexName, indexValue) {

  return new Promise(function(resolve, reject) {
    openDataBase().then(function(db) {
      var objectStore = openObjectStore(db, 'store');
      var datas = [];
      var cursor;
      if (indexName && indexValue) {
        cursor = objectStore.index(indexName).openCursor(indexValue);
      } else {
        cursor = objectStore.openCursor();
      }

      cursor.onsuccess = function(event) {
        var cursor = event.target.result;
        if (cursor) {
          datas.push(cursor.value);
          cursor.continue();
        } else {
          if (datas.length > 0) {
            resolve(datas);
          } else {
            getDataFromServer().then(function(d) {
              openDataBase().then(function(db) {
                var objectStore = openObjectStore(db, "store", "readwrite");
                for (let i = 0; i < datas.length; i++) {
                  objectStore.add(datas[i]);
                }
                resolve(datas);
              });
            });
          }
        }
      }
    }).catch(function() {
      getDataFromServer().then(function(datas) {
        resolve(datas);
      });
    });
  });
};

如上程式碼,我們對getStore函式接受了兩個可選的新引數(indexName和indexValue)。其次,如果我們的函式接收了這些引數的話,就使用引數在特定的索引(indexName)上開啟流標,然後開啟特定值(indexValue)的流標,會把結果限定在指定的範圍內。如果沒有傳遞這些引數的話,它會向以前那樣執行。

做出這兩個地方的修改,我們的函式可以返回所有的結果,也可以返回結果中的一個子集,如下程式碼所示:

getStore().then(function(reservations){
  // reservations 包含了所有的資料
});

getStore("idx_status", "Sending").then(function() {
  // reservations 變數僅僅包含了狀態為 "Sending" 的資料
});

4)現在我們需要在我們的 sw.js 中新增後臺同步的事件監聽器到 service worker中了。

首先我們在我們的sw.js中引入 store.js ,程式碼如下所示:

importScripts("/public/js/store.js");

然後在我們sw.js的底部,新增如下程式碼:

var createStoreUrl = function(storeDetails) {
  var storeUrl = new URL("http://localhost:8081/public/json/index.json");
  Object.keys(storeDetails).forEach(function(key) {
    storeUrl.searchParams.append(key, storeDetails[key]);
  });
  return storeUrl;
};

var syncStores = function() {
  return getStore("idx_status", "Sending").then(function(reservations) {
    return Promise.all(
      reservations.map(function(reservation){
        var reservationUrl = createStoreUrl(reservation);
        return fetch(reservationUrl);
      })
    )
  });
};

self.addEventListener("sync", function(event) {
  if (event.tag === "sync-store") {
    event.waitUntil(syncStores());
  }
});

如上程式碼,我們使用 self.addEventListener 為sync事件新增一個新的事件監聽器,這個事件監聽器會響應事件名稱為 "sync-store" 的事件,然後使用 waitUntil方法等待 syncStores()函式返回的promise,會根據該promise的完成或拒絕來判斷sync事件是完成還是拒絕,如果是完成的話,那麼 sync-store的sync事件就會從SyncManager中刪除,如果promise是拒絕的話,那麼我們的SyncManager會保持 sync事件的註冊,並且在隨後會再次觸發該事件。

syncStores() 會遍歷IndexedDB中每一個標記為"Sending" 狀態的資料,嘗試會再次傳送到伺服器,並且返回一個promise,只有當promise傳送成功了的話,那麼就會完成狀態。

然後在我們的getStore函式中,可以獲取所有處於 Sending 狀態的資料,該函式也返回了一個promise物件,該promise會決定整個syncStores函式的結果。要實現這點,我們使用了Promise.all()傳入了一個promise陣列,我們拿到該資料物件陣列後,會通過Array.map()方法將陣列的元素轉化為 promise,我們使用 map對每個元素進行迭代,建立一個fetch請求傳送到伺服器來建立這個請求,fetch也會返回一個promise。

createStoreUrl 函式使用URL介面建立了一個新的URL物件,這個物件表示的是fetch請求介面,使用這種方式更優雅的建立帶有查詢字串的URL,比如如下程式碼會列印帶引數的url。

console.log(createStoreUrl({'name': 'kongzhi', 'age': 30}));

那麼列印的 結構就是:http://localhost:8081/public/json/index.json?name=kongzhi&age=30; 這樣的了。

完成上面的程式碼後,我們來開啟我們的應用 http://localhost:8081/ 重新整理下,然後我們把我們的網路斷開,然後再點選 "點選我新增"這個文字,就會在控制檯上列印如下資訊了;如下圖所示:

然後我們開啟我們的網路,沒過一會兒,就可以看到我們的請求會自動請求一次,如下圖所示:

請求成功後,我們就可以把頁面的訊息 "請求載入中, 請稍後..." 這幾個字 可以改成 "請求成功了..." 這樣的提示了。

如上程式碼後,當我們的網路恢復完成後,我們會重新發ajax請求,請求完成後,可能會有新的請求狀態資料,因此我們現在最後一步是需要更新我們的indexedDB資料庫了,以便顯示最新的訊息給我們的使用者,並且我們要更新我們的資料狀態,等我們下一次 sync-store 事件註冊的時候,不會重新傳送。因此我們需要改變syncStores函式程式碼:

在更新之前,我們之前的程式碼是如下這樣的:

var syncStores = function() {
  return getStore("idx_status", "Sending").then(function(reservations) {
    console.log(reservations);
    return Promise.all(
      reservations.map(function(reservation){
        var reservationUrl = createStoreUrl(reservation);
        return fetch(reservationUrl);
      })
    )
  });
};

更新之後的程式碼如下所示:

var syncStores = function() {
  return getStore("idx_status", "Sending").then(function(reservations) {
    console.log(reservations);
    return Promise.all(
      reservations.map(function(reservation){
        var reservationUrl = createStoreUrl(reservation);
        return fetch(reservationUrl).then(function(response) {
          return response.json();
        }).then(function(newResponse) {
          return updateInObjectStore("store", 1, newResponse);
        })
      })
    )
  });
};

github原始碼檢視

相關文章