IndexedDB詳解

flydean發表於2021-01-11

簡介

IndexedDB是一種在瀏覽器端儲存資料的方式。既然稱之為DB,是因為它豐富了客戶端的查詢方式,並且因為是本地儲存,可以有效的減少網路對頁面資料的影響。

有了IndexedDB,瀏覽器可以儲存更多的資料,從而豐富了瀏覽器端的應用型別。

IndexedDB簡介

IndexedDB和傳統的關係型資料不同的是,它是一個key-value型的資料庫。

value可以是複雜的結構體物件,key可以是物件的某些屬性值也可以是其他的物件(包括二進位制物件)。你可以使用物件中的任何屬性做為index,以加快查詢。

IndexedDB是自帶transaction的,所有的資料庫操作都會繫結到特定的事務上,並且這些事務是自動提交了,IndexedDB並不支援手動提交事務。

IndexedDB API大部分都是非同步的,在使用非同步方法的時候,API不會立馬返回要查詢的資料,而是返回一個callback。

非同步API的本質是向資料庫傳送一個操作請求,當操作完成的時候,會收到一個DOM event,通過該event,我們會知道操作是否成功,並且獲得操作的結果。

IndexedDB是一種 NoSQL 資料庫,和關係型資料庫不同的是,IndexedDB是物件導向的,它儲存的是Javascript物件。

IndexedDB還有一個很重要的特點是其同源策略,每個源都會關聯到不同的資料庫集合,不同源是不允許訪問其他源的資料庫,從而保證了IndexedDB的安全性。

IndexedDB的使用

這一節,我們將會以具體的例子來講解如何使用IndexedDB。

IndexedDB的瀏覽器支援

不同的瀏覽器對於IndexedDB有不同的實現,正常來說,我們可以使用window.indexedDB來獲取到瀏覽器的indexedDB物件。但是對於某些瀏覽器來說,還沒有使用標準的window.indexedDB,而是用帶字首的實現。

所以我們在使用過程中通常需要進行判斷和轉換:

// In the following line, you should include the prefixes of implementations you want to test.
window.indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB;
// DON'T use "var indexedDB = ..." if you're not in a function.
// Moreover, you may need references to some window.IDB* objects:
window.IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction || window.msIDBTransaction || {READ_WRITE: "readwrite"}; // This line should only be needed if it is needed to support the object's constants for older browsers
window.IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange || window.msIDBKeyRange;
// (Mozilla has never prefixed these objects, so we don't need window.mozIDB*)

上面我們從window獲取了indexedDB,IDBTransaction和IDBKeyRange三個物件。

其中indexedDB表示的是資料庫的連線。IDBTransaction表示的是transaction,而IDBKeyRange則是用從資料庫的某個特定key range中取出資料。

但是,通常來說帶字首的實現一般都是不穩定的,所以我們通常不建議在正式環境中使用,所以如果不支援標準表示式的話,需要直接報錯:

if (!window.indexedDB) {
    console.log("Your browser doesn't support a stable version of IndexedDB. Such and such feature will not be available.");
}

建立IndexedDB

要使用IndexedDB,我們首先需要open it:

// Let us open our database
var request = window.indexedDB.open("MyTestDatabase", 3);

open方法返回一個IDBOpenDBRequest物件,同時這是一個非同步操作,open操作並不會立馬開啟資料庫或者開啟事務,我們可以通過監聽request的事件來進行相應的處理。

open方法傳入兩個引數,第一個引數是資料庫的名字,第二個引數是資料庫的版本號。

當你建立一個新的資料庫或者升級一個現有的資料庫版本的時候,將會觸發一個onupgradeneeded事件,並在事件中傳入IDBVersionChangeEvent,我們可以通過event.target.result來獲取到IDBDatabase物件,然後通過這個物件來進行資料庫的版本升級操作。如下所示:

// This event is only implemented in recent browsers   
request.onupgradeneeded = function(event) { 
  // Save the IDBDatabase interface 
  var db = event.target.result;

  // Create an objectStore for this database
  var objectStore = db.createObjectStore("name", { keyPath: "myKey" });
};

注意,這裡的版本號是一個整數。如果你傳入一個float,那麼將會對該float進行取整操作。

有了request,我們可以通過監聽onerror或者onsuccess事件來進行相應的處理。

var db;
var request = indexedDB.open("MyTestDatabase");
request.onerror = function(event) {
  console.log("Why didn't you allow my web app to use IndexedDB?!");
};
request.onsuccess = function(event) {
  db = event.target.result;
};

拿到db物件之後,我們可以設定全域性的異常處理:

db.onerror = function(event) {
  // Generic error handler for all errors targeted at this database's
  // requests!
  console.error("Database error: " + event.target.errorCode);
};

IndexedDB中的table叫做object stores,和關係型資料庫中的table一樣,object stores中的每一個物件都和一個key相關聯,和key相關的有兩個概念 key path 和 key generator.

如果儲存的是javascript Object物件,那麼可以指定該物件中的某一個屬性作為key path,那麼這個屬性將會被作為key。

如果沒有指定key path,那麼儲存的Object可以是任何物件,甚至是基礎型別比如數字和String。

而key generator就是key的生成器。

假如我們想要儲存這樣的資料:

// This is what our customer data looks like.
const customerData = [
  { ssn: "444-44-4444", name: "Bill", age: 35, email: "bill@company.com" },
  { ssn: "555-55-5555", name: "Donna", age: 32, email: "donna@home.org" }
];

看一下對應的資料庫操作是怎麼樣的:

const dbName = "the_name";

var request = indexedDB.open(dbName, 2);

request.onerror = function(event) {
  // Handle errors.
};
request.onupgradeneeded = function(event) {
  var db = event.target.result;

  // Create an objectStore to hold information about our customers. We're
  // going to use "ssn" as our key path because it's guaranteed to be
  // unique - or at least that's what I was told during the kickoff meeting.
  var objectStore = db.createObjectStore("customers", { keyPath: "ssn" });

  // Create an index to search customers by name. We may have duplicates
  // so we can't use a unique index.
  objectStore.createIndex("name", "name", { unique: false });

  // Create an index to search customers by email. We want to ensure that
  // no two customers have the same email, so use a unique index.
  objectStore.createIndex("email", "email", { unique: true });

  // Use transaction oncomplete to make sure the objectStore creation is 
  // finished before adding data into it.
  objectStore.transaction.oncomplete = function(event) {
    // Store values in the newly created objectStore.
    var customerObjectStore = db.transaction("customers", "readwrite").objectStore("customers");
    customerData.forEach(function(customer) {
      customerObjectStore.add(customer);
    });
  };
};

我們需要在onupgradeneeded事件中處理所有的schema相關的操作。

首先使用db.createObjectStore建立了一個customers的ObjectStore,並且使用了物件的keypath作為key。

除了key之外,我們建立了兩個index,以提高查詢速度。

最後我們監聽transaction.oncomplete事件,並在裡面加入儲存object的操作。

上面的程式碼中,我們使用了keyPath作為key。

下面是一個使用key Generator的例子:

 var objStore = db.createObjectStore("names", { autoIncrement : true });

indexdb中的CURD

indexedDB的所有操作都需要在事務中,我們看一個開啟事務的操作:

var transaction = db.transaction(["customers"], "readwrite");

上面的例子中使用readwrite來操作customers ObjectStore。

transaction接收兩個引數,第一個引數是一個陣列,陣列中是這個trans中將會處理的ObjectStores,第二個引數是處理的模式。

有了transaction之後,我們可以監聽事務的complete和error操作,然後就可以進行add操作了:

// Do something when all the data is added to the database.
transaction.oncomplete = function(event) {
  console.log("All done!");
};

transaction.onerror = function(event) {
  // Don't forget to handle errors!
};

var objectStore = transaction.objectStore("customers");
customerData.forEach(function(customer) {
  var request = objectStore.add(customer);
  request.onsuccess = function(event) {
    // event.target.result === customer.ssn;
  };
});

上面的例子中,我們使用了add方法,add的前提是資料庫中並不存在相同key的物件。除了add方法之外,我們還可以使用put方法,put方法主要用來進行更新操作。

再看一個刪除的操作:

var request = db.transaction(["customers"], "readwrite")
                .objectStore("customers")
                .delete("444-44-4444");
request.onsuccess = function(event) {
  // It's gone!
};

現在我們的資料庫已經有了資料,我們看下怎麼進行查詢:

var transaction = db.transaction(["customers"]);
var objectStore = transaction.objectStore("customers");
var request = objectStore.get("444-44-4444");
request.onerror = function(event) {
  // Handle errors!
};
request.onsuccess = function(event) {
  // Do something with the request.result!
  console.log("Name for SSN 444-44-4444 is " + request.result.name);

這裡,我們直接使用了db.transaction,預設情況下是readonly模式的。

下面是一個更新的例子:

var objectStore = db.transaction(["customers"], "readwrite").objectStore("customers");
var request = objectStore.get("444-44-4444");
request.onerror = function(event) {
  // Handle errors!
};
request.onsuccess = function(event) {
  // Get the old value that we want to update
  var data = event.target.result;
  
  // update the value(s) in the object that you want to change
  data.age = 42;

  // Put this updated object back into the database.
  var requestUpdate = objectStore.put(data);
   requestUpdate.onerror = function(event) {
     // Do something with the error
   };
   requestUpdate.onsuccess = function(event) {
     // Success - the data is updated!
   };
};

更新我們使用的是put方法。

使用遊標cursor

indexedDB支援遊標操作,我們可以使用cursor來遍歷objectStore的資料:

var objectStore = db.transaction("customers").objectStore("customers");

objectStore.openCursor().onsuccess = function(event) {
  var cursor = event.target.result;
  if (cursor) {
    console.log("Name for SSN " + cursor.key + " is " + cursor.value.name);
    cursor.continue();
  }
  else {
    console.log("No more entries!");
  }
};

openCursor可以接受多個引數,第一個引數可以接受key的查詢範圍,第二個引數用來指定遍歷的方向。如果兩個引數都為空的話,預設是所有的資料的以升序的順序遍歷。

如果想遍歷下一個遊標,則可以呼叫cursor.continue。

我們看一下兩個引數的遊標使用:

// Only match "Donna"
var singleKeyRange = IDBKeyRange.only("Donna");

// Match anything past "Bill", including "Bill"
var lowerBoundKeyRange = IDBKeyRange.lowerBound("Bill");

// Match anything past "Bill", but don't include "Bill"
var lowerBoundOpenKeyRange = IDBKeyRange.lowerBound("Bill", true);

// Match anything up to, but not including, "Donna"
var upperBoundOpenKeyRange = IDBKeyRange.upperBound("Donna", true);

// Match anything between "Bill" and "Donna", but not including "Donna"
var boundKeyRange = IDBKeyRange.bound("Bill", "Donna", false, true);

// To use one of the key ranges, pass it in as the first argument of openCursor()/openKeyCursor()
index.openCursor(boundKeyRange, "prev").onsuccess = function(event) {
  var cursor = event.target.result;
  if (cursor) {
    // Do something with the matches.
    cursor.continue();
  }
};

除了openCursor,我們還可以通過使用openKeyCursor來遍歷KeyCursor:

// Using a normal cursor to grab whole customer record objects
index.openCursor().onsuccess = function(event) {
  var cursor = event.target.result;
  if (cursor) {
    // cursor.key is a name, like "Bill", and cursor.value is the whole object.
    console.log("Name: " + cursor.key + ", SSN: " + cursor.value.ssn + ", email: " + cursor.value.email);
    cursor.continue();
  }
};

// Using a key cursor to grab customer record object keys
index.openKeyCursor().onsuccess = function(event) {
  var cursor = event.target.result;
  if (cursor) {
    // cursor.key is a name, like "Bill", and cursor.value is the SSN.
    // No way to directly get the rest of the stored object.
    console.log("Name: " + cursor.key + ", SSN: " + cursor.primaryKey);
    cursor.continue();
  }
};

除此之外,我們還可以直接通過index來進行查詢:

var index = objectStore.index("name");

index.get("Donna").onsuccess = function(event) {
  console.log("Donna's SSN is " + event.target.result.ssn);
};

要使用index的前提就是需要在request.onupgradeneeded中建立index。

本文作者:flydean程式那些事

本文連結:http://www.flydean.com/indexeddb-kickoff/

本文來源:flydean的部落格

歡迎關注我的公眾號:「程式那些事」最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!

相關文章