漸進式web應用開發---使用indexedDB實現ajax本地資料儲存(四)

龍恩0707發表於2019-07-21

在前幾篇文章中,我們使用service worker一步步優化了我們的頁面,現在我們學習使用我們之前的indexedDB, 來快取我們的ajax請求,第一次訪問頁面的時候,我們請求ajax,當我們繼續重新整理頁面的時候,我們從快取裡面去讀取該json資料,想要了解indexedDB,請看這篇文章
我們下面的demo專案是建立在我們第三篇文章的基礎之上再進行的,想了解之前的文章,請點選這裡.

我們還是按照我們之前的思路來做,首先我們先看看我們整個專案的架構如下:

|----- 專案
|  |--- public
|  | |--- js               # 存放所有的js
|  | | |--- main.js        # js入口檔案
|  | | |--- store.js
|  | | |--- myAccount.js
|  | |--- style            # 存放所有的css
|  | | |--- main.styl      # css 入口檔案
|  | |--- json             # 存放本地模擬資料的json檔案
|  | | |--- index.json 
|  | |--- index.html       # index.html 頁面
|  | |--- images
|  |--- package.json
|  |--- webpack.config.js
|  |--- node_modules
|  |--- sw.js

public/js/index.json(假如後端介面返回的資料是如下資料) 程式碼如下:

{
  "code": 0,
  "data": [
    { "name": "kongzhi111", "age": 28},
    { "name": "kongzhi222", "age": 29},
    { "name": "kongzhi333", "age": 30}
  ]
}

在我們的 public/js 下新建一個 store.js 檔案,該js檔案的作用是使用indexedDB來快取我們的ajax請求資料的。並且我們需要把該 store.js 檔案存放在我們的 sw.js 中的 CACHE_URLS 中,比如如下所示:

var CACHE_URLS = [
  "/public/index.html",      // html檔案
  "/main.css",               // css 樣式表
  "/public/images/xxx.jpg",  // 圖片
  "/main.js",                 // js 檔案 
  "/public/js/store.js"
];

然後我們開始編寫我們的 store.js 程式碼,/public/js/store.js 程式碼如下:

import axios from 'axios';

var openDataBase = function() {
  if (!window.indexedDB) {
    return false;
  }
  // 開啟或建立 store-data 資料庫
  var result = window.indexedDB.open('store-data', 2);

  // 監聽error函式觸發
  result.onerror = function(event) {
    console.log("DataBase error:", event.target.error);
  }
  // 監聽當前版本號被升級的時候觸發該函式
  result.onupgradeneeded = function(event) {
    var db = event.target.result;
    /*
     是否包含該物件倉庫名(或叫表名)。如果不包含就建立一個。
     該物件中的 keyPath屬性id為主鍵
    */
    if (!db.objectStoreNames.contains('store')) {
      db.createObjectStore("store", { keyPath: "id", autoIncrement: true });
    }
  }
  return result;
};
/*
 @param {storeName} 倉庫名或表名
 @param {successCallback} 需要執行的回撥函式
 @param {transactionMode} 事務模式 readOnly 只讀,readwrite 可讀可寫
*/
var openObjectStore = function(storeName, successCallback, transactionMode) {
  var db = openDataBase();
  if (!db) {
    return false;
  }
  db.onsuccess = function(event) {
    var targetValue = event.target.result;
    /* 
     1. 使用 targetValue.transaction(storeName, transactionMode) 來建立事務
     2. 建立事務之後,我們使用 targetValue.transaction(storeName, transactionMode).objectStore(storeName)
     這個方法,拿到 IDBObjectStore物件。
    */
    var objectStore = targetValue.transaction(storeName, transactionMode).objectStore(storeName);
    successCallback(objectStore);
  };
  return true;
};

var getStore = function (successCallback) {
  var datas = [];
  var db = openObjectStore("store", function(objectStore) {
    // 使用流標 objectStore.openCursor()
    objectStore.openCursor().onsuccess = function(event) {
      var cursor = event.target.result;
      // 如果有流標的話,就把資料放入陣列datas裡面去,依次迴圈存入進去
      if (cursor) {
        datas.push(cursor.value);
        cursor.continue();
      } else {
        // 否則的話,如果datas有資料的話,就支援呼叫回撥函式
        if (datas.length > 0) {
          successCallback(datas);
        } else {
          // 如果datas資料為空,傳送一個json請求
          axios.get("http://localhost:8081/public/json/index.json").then(datas => {
            var list = datas.data.data;
            // 開啟資料倉儲或表名,執行對應的事務操作
            openObjectStore("store", function(datasStore) {
              for (let i = 0; i < list.length; i++) {
                datasStore.add(list[i]);
              }
              successCallback(datas);
            }, "readwrite");
          });
        }
      }
    }
  });
  if (!db) {
    axios.get("http://localhost:8081/public/json/index.json", successCallback);
  }
};

window.getStore = getStore;

如上程式碼,有三個函式,分別為 openDataBase、openObjectStore、及 getStore, 那麼第一個函式 openDataBase() 會開啟一個新的資料庫請求,該函式程式碼內部,首先會判斷瀏覽器是否支援 window.indexedDB ,如果不支援的話,直接返回,然後接著我們建立了一個 store-data 資料庫,並且監聽了 onerror, onupgradeneeded 事件,最後我們建立了一個 store 倉庫名或叫表名。並且以id作為主鍵,並且設定了 autoIncrement 為true,自動增長。然後我們返回了該 result。因為我們的 onsuccess事件並沒有在該方法中監聽。在第二個函式 openObjectStore 中,我們會呼叫 建立資料庫的這個函式,並且去監聽 onsuccess這個事件。
openObjectStore() 該函式會在物件上開啟一個事務,並且在其執行函式,該方法中的第一個引數為 倉庫名稱,第二個引數為開啟倉庫後成功的回撥函式,第三個引數是可選引數,它的含義是事務的型別,有 readonly(只讀) 或 readwrite(可讀可寫),如程式碼:

db.onsuccess = function(event) {
  var targetValue = event.target.result;
  /* 
   1. 使用 targetValue.transaction(storeName, transactionMode) 來建立事務
   2. 建立事務之後,我們使用 targetValue.transaction(storeName, transactionMode).objectStore(storeName)
   這個方法,拿到 IDBObjectStore物件。
  */
  var objectStore = targetValue.transaction(storeName, transactionMode).objectStore(storeName);
  successCallback(objectStore);
};

首先我們使用該程式碼:使用 targetValue.transaction(storeName, transactionMode) 來建立事務,然後建立事務完成後,然後我們使用

targetValue.transaction(storeName, transactionMode).objectStore(storeName);
這個方法,拿到 IDBObjectStore物件。然後把該物件 傳入 successCallback 函式內部,在該回撥函式中,我們可以使用 objectStore 來增加資料。

getStore(): 該函式接收一個successCallback引數,指回撥函式,在程式碼內部,我們首先會建立一個事務,如下程式碼:

var db = openObjectStore("store", function(objectStore) {
  
}

然後我們就會建立流標,並且對所有資料進行迭代,且監聽onsuccess函式,如下程式碼:

var db = openObjectStore("store", function(objectStore) {
  // 使用流標 objectStore.openCursor()
  objectStore.openCursor().onsuccess = function(event) {
    var cursor = event.target.result;
    // 如果有流標的話,就把資料放入陣列datas裡面去,依次迴圈存入進去
    if (cursor) {
      datas.push(cursor.value);
      cursor.continue();
    } else {
      // 否則的話,如果datas有資料的話,就支援呼叫回撥函式
      if (datas.length > 0) {
        successCallback(datas);
      } else {
        // 如果datas資料為空,傳送一個json請求
        axios.get("http://localhost:8081/public/json/index.json").then(datas => {
          var list = datas.data.data;
          // 開啟資料倉儲或表名,執行對應的事務操作
          openObjectStore("store", function(datasStore) {
            for (let i = 0; i < list.length; i++) {
              datasStore.add(list[i]);
            }
            successCallback(datas);
          }, "readwrite");
        });
      }
    }
  }
}

如上程式碼,如果有流標的話,在流標每次前進到一個新的記錄時都會被呼叫,甚至在流標通過最後一條記錄之後也會被呼叫,它是通過 continue 來對內部進行迴圈呼叫,當到最後一條記錄的時候,它後面就沒有資料了,因此就會進入 else 語句內部。因此首先會判斷datas 是否有資料,如果有資料的話,就會呼叫 successCallback(datas); 這句程式碼,把資料datas作為引數傳回給 successCallback 回撥函式,否則的話,如果datas為空的話,我們就會去請求我們本地的json請求,傳送ajax請求,然後把請求的資料,存入到 store倉庫名中,依次迴圈完成後,我們再呼叫 successCallback方法,把資料datas作為引數傳遞出去。當然在我們的 第三個函式 getStore函式中,如果不支援window.indexedDB的話,那麼該瀏覽器的話,我們直接去請求ajax, 如getStore最後一句程式碼:

if (!db) {
  axios.get("http://localhost:8081/public/json/index.json", successCallback);
}

最後再我們的store.js 中,我們會使用 window.getStore = getStore; 讓其成為全域性的。然後在我們的 /public/js/myAccount.js 程式碼如下,就可以呼叫我們的 store.js 中的 getStore方法了,如下程式碼所示:

import $ from 'jquery';

$(function() {
  // 請求資料並且渲染資料
  requestAndRenderFunc();


  // 向伺服器請求資料,並且渲染頁面
  function requestAndRenderFunc () {
    getStore(renderHTMLFunc);
  };

  function renderHTMLFunc(datas) {
    console.log(datas);
  }
});

如上是所有優化後的程式碼,使用indexedDB來儲存我們的資料。因此我們來測試下頁面,我們首先清空下我們瀏覽器的快取資料,然後我們第一次訪問下我們的頁面,我們可以看到我們的網路上,顯示如下請求:

然後我們在控制檯中會看到返回的資料,如下圖所示:

如上我們第一次請求的時候,我們可以看到會請求ajax,然後會返回內容,現在我們繼續重新整理我們的頁面,可以看到如下請求,如下所示:

然後再看我們的控制檯列印如下所示:

我們可以看到我們ajax請求並沒有發請求,但是我們依然可以拿到資料,這是為什麼呢?這是因為我們使用 indexedDB快取ajax資料到本地,因此當我們第二次以後請求的時候,我們拿的都是 indexedDB裡面的資料,我們並沒有發ajax請求,所以使用該訪問,哪怕以後訪問我們的頁面,即使沒有網路的情況下,我們依然可以拿到資料,並且更快載入我們的頁面。我們再來看下我們的 indexedDB儲存的資料如下所示:

如上程式碼我們已經實現了使用indexedDB對資料快取了,並且使用 indexedDB快取裡面的資料了,但是現在有一個新的問題,並且使用者點選一個查詢按鈕,但是查詢按鈕的條件發生改變了,因此ajax請求返回的資料也是根據頁面中查詢的條件來返回的,因此這個時候我們就不能一直使用 indexedDB中的資料了,我們需要重新請求頁面的資料,因此我們需要在我們的 store.js 新增如下程式碼了:

var addToObjectStore = function(storeName, object) {
  openObjectStore(storeName, function(store) {
    store.add(object);
  }, "readwrite");
};

var updateInObjectStore = function(storeName, id, object) {
  openObjectStore(storeName, function(objectStore) {
    objectStore.openCursor().onsuccess = function(event) {
      var cursor = event.target.result;
      if (!cursor) {
        return;
      }
      if (cursor.value.id === id) {
        objectStore.put(object);
        return;
      }
      cursor.continue();
    }
  }, "readwrite");
}

window.addToObjectStore = addToObjectStore;
window.updateInObjectStore = updateInObjectStore;

如上 addToObjectStore 函式接收物件儲存的名稱以及要放進儲存的新物件作為引數,該函式我們可以使用如下方式來進行呼叫:

addToObjectStore("store", { id: 1 });

第二個函式 updateInObjectStore 接收物件儲存的名稱,找到與給定的id引數匹配的id物件,並且用它來更新物件,這是通過在物件儲存上開啟 readwrite事務,並且使用流標進行迭代來完成的。在流標到達最後一條記錄或匹配成功之前,函式會一直迭代。如果找到匹配項,就會通過 objectStore.put(object); 來進行更新,此時函式就會通過return返回回來,因為一旦找到匹配,就不需要繼續迭代下一條記錄了。該函式可以如下呼叫:

updateInObjectStore("store", 1, {"id": 1, name: 'kongzhi', age: 30 });

因此為了演示下,我們需要把我們的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>

如上程式碼,我們新增了 id = "submit" div元素,和 id = "update" 的元素,然後需要在我們的myAccount.js 程式碼新增如下:

function updateDisplay(d) {
  console.log(d);
};
function renderHTMLFunc(datas) {
  console.log(datas);
}
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);
  });
});

如上程式碼,當我們點選 id 為 submit的元素的時候,我們會呼叫 addStore 函式,在該函式內部會根據 id, name, age 引數來新增一條資料,然後會呼叫 addToObjectStore 函式,先把資料新增到本地儲存裡面去,然後在渲染頁面 呼叫 renderHTMLFunc 函式,最後使用 $.getJSON 請求一條資料,然後把最新的資料渲染到頁面上去。

同樣的道理,update資料的時候,我們會發ajax請求,無論伺服器端是否返回了新的資料,我們都會呼叫 updateInObjectStore 這個函式來更新我們本地的資料。這樣就實現了,如果是狀態傳送改變的話,那麼本地indexedDB儲存的資料庫也會重新得到更新。

我們可以在我們的專案點選下就可以看到效果了。我們點選新增一條資料後,在我們的 indexedDB中看到資訊如下:

如上我們這邊沒有把 data裡面的資料抽離出來,直接把一整個資料直接新增進去了,反正就是這個意思,新增的時候,重新能更新我們的indexedDB裡面的資料,同理我們update修改資料的時候,我們也一樣可以修改我們的某一條資料的。

github原始碼檢視demo

相關文章