使用 IndexedDB

IndexedDB 是一種在使用者瀏覽器中持久儲存資料的方式。由於它允許您建立具有豐富查詢功能的 Web 應用程式,而無論網路可用性如何,因此您的應用程式可以離線和線上工作。

關於本文件

本教程將引導您使用 IndexedDB 的非同步 API。如果您不熟悉 IndexedDB,應首先閱讀IndexedDB 關鍵特性和基本術語一文。

有關 IndexedDB API 的參考文件,請參閱IndexedDB API一文及其子頁面。本文件記錄了 IndexedDB 使用的物件型別,以及非同步 API 的方法(同步 API 已從規範中移除)。

基本模式

IndexedDB 鼓勵的基本模式如下:

  1. 開啟資料庫。
  2. 在資料庫中建立物件儲存。
  3. 啟動一個事務併發出一個請求來執行一些資料庫操作,例如新增或檢索資料。
  4. 透過監聽正確型別的 DOM 事件來等待操作完成。
  5. 處理結果(可在請求物件上找到)。

掌握了這些大概念之後,我們可以開始更具體的內容。

建立和結構化儲存

開啟資料庫

我們這樣開始整個過程:

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

看到了嗎?開啟資料庫就像任何其他操作一樣——您必須“請求”它。

開啟請求不會立即開啟資料庫或啟動事務。對 open() 函式的呼叫會返回一個 IDBOpenDBRequest 物件,其中包含成功或錯誤值,您將其作為事件處理。IndexedDB 中的大多數其他非同步函式也執行相同的操作——返回一個 IDBRequest 物件,其中包含結果或錯誤。開啟函式的結果是 IDBDatabase 的一個例項。

開啟方法的第二個引數是資料庫的版本。資料庫版本決定了資料庫架構——資料庫中的物件儲存及其結構。如果資料庫尚不存在,它將由 open 操作建立,然後觸發 onupgradeneeded 事件,您可以在此事件的處理程式中建立資料庫架構。如果資料庫確實存在,但您指定了升級後的版本號,則會立即觸發 onupgradeneeded 事件,允許您在其處理程式中提供更新的架構。稍後在下面的建立或更新資料庫版本以及IDBFactory.open參考頁面中會詳細介紹這一點。

警告:版本號是一個 unsigned long long 數字,這意味著它可能是一個非常大的整數。這也意味著您不能使用浮點數,否則它將被轉換為最接近的較小整數,並且事務可能不會啟動,也不會觸發 upgradeneeded 事件。因此,例如,不要使用 2.4 作為版本號:const request = indexedDB.open("MyTestDatabase", 2.4); // 不要這樣做,因為版本將被四捨五入到 2

生成處理程式

您對幾乎所有生成的請求都想做的第一件事是新增成功和錯誤處理程式:

js
request.onerror = (event) => {
  // Do something with request.error!
};
request.onsuccess = (event) => {
  // Do something with request.result!
};

如果請求成功,則觸發 success 事件,並呼叫分配給 onsuccess 的函式。如果請求失敗,則觸發 error 事件,並呼叫分配給 onerror 的函式。

IndexedDB API 旨在最大程度地減少錯誤處理的需求,因此您不太可能看到許多錯誤事件(至少,一旦您習慣了該 API!)。但是,在開啟資料庫的情況下,存在一些常見的條件會生成錯誤事件。最可能的問題是使用者決定不授予您的 Web 應用程式建立資料庫的許可權。IndexedDB 的主要設計目標之一是允許為離線使用儲存大量資料。(要了解每個瀏覽器可以儲存多少資料,請參閱瀏覽器儲存配額和逐出標準頁面上的“可以儲存多少資料?”。)

顯然,瀏覽器不希望允許某些廣告網路或惡意網站汙染您的計算機,因此瀏覽器過去在任何給定的 Web 應用程式首次嘗試開啟 IndexedDB 以進行儲存時都會提示使用者。使用者可以選擇允許或拒絕訪問。此外,瀏覽器隱私模式下的 IndexedDB 儲存僅在記憶體中持續到隱身會話關閉。

現在,假設使用者允許您建立資料庫的請求,並且您收到了一個成功事件來觸發成功回撥;接下來是什麼?這裡的請求是透過呼叫 indexedDB.open() 生成的,因此 request.resultIDBDatabase 的一個例項,您絕對希望將其儲存以備後用。您的程式碼可能看起來像這樣:

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

處理錯誤

如上所述,錯誤事件會冒泡。錯誤事件以生成錯誤的請求為目標,然後事件冒泡到事務,最後冒泡到資料庫物件。如果您想避免向每個請求新增錯誤處理程式,您可以改為在資料庫物件上新增一個單一的錯誤處理程式,如下所示:

js
db.onerror = (event) => {
  // Generic error handler for all errors targeted at this database's
  // requests!
  console.error(`Database error: ${event.target.error?.message}`);
};

開啟資料庫時常見的可能錯誤之一是 VER_ERR。它表示儲存在磁碟上的資料庫版本大於您嘗試開啟的版本。這是一個必須始終由錯誤處理程式處理的錯誤情況。

建立或更新資料庫版本

當您建立新資料庫或增加現有資料庫的版本號(透過指定比以前開啟資料庫時更高的版本號)時,將觸發 onupgradeneeded 事件,並且 IDBVersionChangeEvent 物件將傳遞給在 request.result 上設定的任何 onversionchange 事件處理程式(即示例中的 db)。在 upgradeneeded 事件的處理程式中,您應該為該版本的資料庫建立所需的物件儲存:

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

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

在這種情況下,資料庫將已經包含來自資料庫先前版本的物件儲存,因此您不必再次建立這些物件儲存。您只需建立任何新的物件儲存,或刪除先前版本中不再需要的物件儲存。如果您需要更改現有物件儲存(例如,更改 keyPath),則必須刪除舊物件儲存並使用新選項再次建立它。(請注意,這將刪除物件儲存中的資訊!如果您需要儲存該資訊,應在升級資料庫之前將其讀出並儲存到其他位置。)

嘗試建立已存在名稱的物件儲存(或嘗試刪除不存在名稱的物件儲存)將丟擲錯誤。

如果 onupgradeneeded 事件成功退出,則開啟資料庫請求的 onsuccess 處理程式將被觸發。

結構化資料庫

現在來結構化資料庫。IndexedDB 使用物件儲存而不是表,單個數據庫可以包含任意數量的物件儲存。每當值儲存在物件儲存中時,它都與一個鍵相關聯。根據物件儲存是使用鍵路徑還是鍵生成器,鍵的提供方式有幾種不同的方式。

下表顯示了提供鍵的不同方式:

鍵路徑 (keyPath) 鍵生成器 (autoIncrement) 描述
此物件儲存可以容納任何型別的值,甚至包括數字和字串等原始值。每當您想新增新值時,都必須提供一個單獨的鍵引數。
此物件儲存只能容納 JavaScript 物件。物件必須具有與鍵路徑同名的屬性。
此物件儲存可以容納任何型別的值。鍵會自動為您生成,或者如果您想使用特定鍵,也可以提供一個單獨的鍵引數。
此物件儲存只能容納 JavaScript 物件。通常會生成一個鍵,並將生成的鍵的值儲存在具有與鍵路徑同名屬性的物件中。但是,如果此類屬性已經存在,則該屬性的值將用作鍵,而不是生成新鍵。

您還可以在任何物件儲存上建立索引,前提是該物件儲存儲存的是物件,而不是原始值。索引允許您使用儲存物件的屬性值(而不是物件的鍵)來查詢物件儲存中儲存的值。

此外,索引能夠對儲存的資料強制執行簡單的約束。透過在建立索引時設定 unique 標誌,索引可確保不會儲存兩個物件,並且它們具有相同的索引鍵路徑值。因此,例如,如果您有一個物件儲存,其中包含一組人員,並且您想確保沒有兩個人具有相同的電子郵件地址,您可以使用設定了 unique 標誌的索引來強制執行此操作。

這聽起來可能令人困惑,但這個簡單的示例應該能說明這些概念。首先,我們將定義一些客戶資料以在示例中使用:

js
// 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" },
];

當然,您不會將某人的社會安全號碼用作客戶表的主鍵,因為不是每個人都有社會安全號碼,並且您會儲存他們的出生日期而不是年齡,但為了方便起見,讓我們忽略這些不幸的選擇,繼續前進。

現在讓我們看看如何建立 IndexedDB 來儲存我們的資料:

js
const dbName = "the_name";

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

request.onerror = (event) => {
  // Handle errors.
};
request.onupgradeneeded = (event) => {
  const 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.
  const 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 = (event) => {
    // Store values in the newly created objectStore.
    const customerObjectStore = db
      .transaction("customers", "readwrite")
      .objectStore("customers");
    customerData.forEach((customer) => {
      customerObjectStore.add(customer);
    });
  };
};

如前所述,onupgradeneeded 是唯一可以更改資料庫結構的地方。在此處,您可以建立和刪除物件儲存,以及構建和刪除索引。

物件儲存是透過對 createObjectStore() 的一次呼叫建立的。該方法接受儲存的名稱和一個引數物件。儘管引數物件是可選的,但它非常重要,因為它允許您定義重要的可選屬性並細化要建立的物件儲存型別。在我們的例子中,我們請求了一個名為“customers”的物件儲存,並定義了一個 keyPath,它是使儲存中的單個物件唯一的屬性。在此示例中,該屬性是“ssn”,因為社會安全號碼保證是唯一的。“ssn”必須存在於儲存在 objectStore 中的每個物件上。

我們還請求了一個名為“name”的索引,該索引檢視儲存物件的 name 屬性。與 createObjectStore() 一樣,createIndex() 接受一個可選的 options 物件,該物件細化要建立的索引型別。新增沒有 name 屬性的物件仍然會成功,但這些物件不會出現在“name”索引中。

現在我們可以直接從物件儲存中透過 ssn 檢索儲存的客戶物件,或者透過使用索引透過其名稱檢索。要了解如何完成此操作,請參閱使用索引部分。

使用鍵生成器

在建立物件儲存時設定 autoIncrement 標誌將為該物件儲存啟用鍵生成器。預設情況下,此標誌未設定。

使用鍵生成器,當您將值新增到物件儲存時,鍵將自動生成。鍵生成器的當前編號在首次建立該鍵生成器的物件儲存時始終設定為 1。基本上,新自動生成的鍵會根據前一個鍵增加 1。鍵生成器的當前編號永遠不會減少,除非由於資料庫操作被還原而導致,例如,資料庫事務被中止。因此,從物件儲存中刪除記錄甚至清除所有記錄都不會影響物件儲存的鍵生成器。

我們可以建立另一個帶有鍵生成器的物件儲存,如下所示:

js
// Open the indexedDB.
const request = indexedDB.open(dbName, 3);

request.onupgradeneeded = (event) => {
  const db = event.target.result;

  // Create another object store called "names" with the autoIncrement flag set as true.
  const objStore = db.createObjectStore("names", { autoIncrement: true });

  // Because the "names" object store has the key generator, the key for the name value is generated automatically.
  // The added records would be like:
  // key : 1 => value : "Bill"
  // key : 2 => value : "Donna"
  customerData.forEach((customer) => {
    objStore.add(customer.name);
  });
};

有關鍵生成器的更多詳細資訊,請參閱規範中的鍵生成器

新增、檢索和刪除資料

在對新資料庫執行任何操作之前,您需要啟動一個事務。事務來自資料庫物件,您必須指定事務將跨越哪些物件儲存。一旦進入事務,您就可以訪問儲存資料的物件儲存併發出請求。接下來,您需要決定是更改資料庫還是僅從中讀取。事務有三種可用模式:readonlyreadwriteversionchange

要更改資料庫的“架構”或結構(這涉及建立或刪除物件儲存或索引),事務必須處於 versionchange 模式。此事務透過呼叫指定了 versionIDBFactory.open 方法開啟。

要讀取現有物件儲存的記錄,事務可以是 readonlyreadwrite 模式。要更改現有物件儲存,事務必須處於 readwrite 模式。您可以使用 IDBDatabase.transaction 開啟此類事務。該方法接受兩個引數:storeNames(範圍,定義為您要訪問的物件儲存陣列)和事務的 modereadonlyreadwrite)。該方法返回一個包含 IDBIndex.objectStore 方法的事務物件,您可以使用該方法訪問您的物件儲存。預設情況下,未指定模式時,事務以 readonly 模式開啟。

注意:自 Firefox 40 起,IndexedDB 事務放寬了永續性保證以提高效能(參見Firefox bug 1112702)。以前,在 readwrite 事務中,只有當所有資料都保證已重新整理到磁碟時,才會觸發 complete 事件。在 Firefox 40+ 中,complete 事件在作業系統被告知寫入資料之後但在資料實際重新整理到磁碟之前觸發。因此,complete 事件可能比以前更快地傳遞,但是,如果作業系統崩潰或在資料重新整理到磁碟之前系統斷電,則整個事務可能會丟失。由於此類災難性事件很少發生,因此大多數消費者不應再為此擔心。如果您出於某種原因必須確保永續性(例如,您正在儲存以後無法重新計算的關鍵資料),您可以透過使用實驗性(非標準)readwriteflush 模式建立事務來強制事務在傳遞 complete 事件之前重新整理到磁碟(參見IDBDatabase.transaction)。

您可以透過在事務中使用正確的範圍和模式來加速資料訪問。這裡有一些提示:

  • 定義範圍時,只指定您需要的物件儲存。這樣,您可以併發執行多個具有不重疊範圍的事務。
  • 僅在必要時指定 readwrite 事務模式。您可以併發執行多個具有重疊範圍的 readonly 事務,但一個物件儲存只能有一個 readwrite 事務。要了解更多資訊,請參閱IndexedDB 關鍵特性和基本術語文章中事務的定義。

向資料庫新增資料

如果您剛剛建立了一個數據庫,那麼您可能想向其中寫入資料。下面是它的樣子:

js
const transaction = db.transaction(["customers"], "readwrite");
// Note: Older experimental implementations use the deprecated constant IDBTransaction.READ_WRITE instead of "readwrite".
// In case you want to support such an implementation, you can write:
// const transaction = db.transaction(["customers"], IDBTransaction.READ_WRITE);

transaction() 函式接受兩個引數(儘管其中一個是可選的)並返回一個事務物件。第一個引數是事務將跨越的物件儲存列表。如果您希望事務跨越所有物件儲存,則可以傳遞一個空陣列,但不要這樣做,因為規範規定空陣列應生成 InvalidAccessError。如果您沒有為第二個引數指定任何內容,則會獲得一個只讀事務。由於您想在這裡寫入資料,因此您需要傳遞 "readwrite" 標誌。

現在您已經有了一個事務,您需要了解它的生命週期。事務與事件迴圈緊密相關。如果您建立了一個事務並在不使用它的情況下返回到事件迴圈,那麼事務將變為非活動狀態。保持事務活動的唯一方法是向它發出請求。當請求完成時,您將獲得一個 DOM 事件,並且,假設請求成功,您將在該回調期間有另一個機會來擴充套件事務。如果您在不擴充套件事務的情況下返回到事件迴圈,那麼它將變為非活動狀態,依此類推。只要有待處理的請求,事務就保持活動狀態。事務生命週期實際上非常簡單,但可能需要一些時間來適應。更多示例也會有所幫助。如果您開始看到 TRANSACTION_INACTIVE_ERR 錯誤程式碼,那麼您就出了問題。

事務可以接收三種不同型別的 DOM 事件:errorabortcomplete。我們已經討論了 error 事件的冒泡方式,因此事務會接收來自它生成的任何請求的錯誤事件。這裡更微妙的一點是,錯誤的預設行為是中止發生錯誤的事務。除非您透過首先呼叫錯誤事件上的 stopPropagation() 然後執行其他操作來處理錯誤,否則整個事務將被回滾。這種設計迫使您考慮並處理錯誤,但如果細粒度錯誤處理過於繁瑣,您始終可以向資料庫新增一個包羅永珍的錯誤處理程式。如果您不處理錯誤事件或在事務上呼叫 abort(),那麼事務將被回滾,並且會在事務上觸發 abort 事件。否則,在所有待處理請求完成後,您將獲得一個 complete 事件。如果您正在執行大量資料庫操作,那麼跟蹤事務而不是單個請求肯定有助於您的理智。

現在您已經有了一個事務,您需要從中獲取物件儲存。事務只允許您擁有在建立事務時指定的物件儲存。然後您可以新增所需的所有資料。

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

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

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

從呼叫 add() 生成的請求的 result 是新增的值的鍵。因此,在這種情況下,它應該等於新增的物件的 ssn 屬性,因為物件儲存使用 ssn 屬性作為鍵路徑。請注意,add() 函式要求資料庫中不能已經存在具有相同鍵的物件。如果您嘗試修改現有條目,或者您不關心是否存在,則可以使用 put() 函式,如下面的更新資料庫中的條目部分所示。

從資料庫中刪除資料

刪除資料非常相似:

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

從資料庫中獲取資料

現在資料庫中包含了一些資訊,您可以通過幾種方式檢索它。首先是簡單的 get()。您需要提供鍵來檢索值,如下所示:

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

對於“簡單”檢索來說,這程式碼量可不少。假設您在資料庫級別處理錯誤,下面是如何縮短它的一些方法:

js
db
  .transaction("customers")
  .objectStore("customers")
  .get("444-44-4444").onsuccess = (event) => {
  console.log(`Name for SSN 444-44-4444 is ${event.target.result.name}`);
};

看到它是如何工作的了嗎?由於只有一個物件儲存,您可以避免在事務中傳遞所需物件儲存的列表,而只需將名稱作為字串傳遞。此外,您只是從資料庫中讀取資料,因此不需要 "readwrite" 事務。呼叫 transaction() 時不指定模式會為您提供 "readonly" 事務。這裡的另一個細微之處是您實際上沒有將請求物件儲存到變數中。由於 DOM 事件將請求作為其目標,因此您可以使用事件獲取 result 屬性。

更新資料庫中的條目

現在我們已經檢索到了一些資料,更新它並將其插入回 IndexedDB 非常簡單。讓我們對前面的示例進行一些更新:

js
const objectStore = db
  .transaction(["customers"], "readwrite")
  .objectStore("customers");
const request = objectStore.get("444-44-4444");
request.onerror = (event) => {
  // Handle errors!
};
request.onsuccess = (event) => {
  // Get the old value that we want to update
  const 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.
  const requestUpdate = objectStore.put(data);
  requestUpdate.onerror = (event) => {
    // Do something with the error
  };
  requestUpdate.onsuccess = (event) => {
    // Success - the data is updated!
  };
};

因此,在這裡我們建立了一個 objectStore 並從中請求一個客戶記錄,該記錄由其 ssn 值(444-44-4444)標識。然後,我們將該請求的結果放入一個變數(data),更新此物件的 age 屬性,然後建立第二個請求(requestUpdate)將客戶記錄放回 objectStore,覆蓋之前的值。

注意:在這種情況下,我們必須指定 readwrite 事務,因為我們想要寫入資料庫,而不僅僅是從中讀取。

使用遊標

使用 get() 需要您知道要檢索哪個鍵。如果您想遍歷物件儲存中的所有值,那麼您可以使用遊標。下面是它的樣子:

js
const objectStore = db.transaction("customers").objectStore("customers");

objectStore.openCursor().onsuccess = (event) => {
  const 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() 函式接受多個引數。首先,您可以使用我們稍後將介紹的鍵範圍物件來限制檢索到的專案範圍。其次,您可以指定要迭代的方向。在上面的示例中,我們以升序迭代所有物件。遊標的成功回撥有點特殊。遊標物件本身就是請求的 result(上面我們使用的是簡寫,所以是 event.target.result)。然後,實際的鍵和值可以在遊標物件的 keyvalue 屬性上找到。如果您想繼續,則必須在遊標上呼叫 continue()。當您到達資料末尾時(或者如果沒有與您的 openCursor() 請求匹配的條目),您仍然會收到成功回撥,但 result 屬性是 undefined

遊標的一個常見模式是檢索物件儲存中的所有物件並將它們新增到陣列中,如下所示:

js
const customers = [];

objectStore.openCursor().onsuccess = (event) => {
  const cursor = event.target.result;
  if (cursor) {
    customers.push(cursor.value);
    cursor.continue();
  } else {
    console.log(`Got all customers: ${customers}`);
  }
};

注意:或者,您可以使用 getAll() 來處理這種情況(以及 getAllKeys())。以下程式碼與上面執行的操作完全相同:

js
objectStore.getAll().onsuccess = (event) => {
  console.log(`Got all customers: ${event.target.result}`);
};

檢視遊標的 value 屬性會產生效能成本,因為物件是惰性建立的。例如,當您使用 getAll() 時,瀏覽器必須一次性建立所有物件。如果您只對檢視每個鍵感興趣,例如,使用遊標比使用 getAll() 效率更高。但是,如果您嘗試獲取物件儲存中所有物件的陣列,請使用 getAll()

使用索引

使用 SSN 作為鍵儲存客戶資料是合乎邏輯的,因為 SSN 唯一標識個人。(這是否是出於隱私的好主意是另一個問題,超出本文範圍。)但是,如果您需要按名稱查詢客戶,則需要遍歷資料庫中的每個 SSN,直到找到正確的 SSN。這種搜尋方式會非常慢,因此您可以改為使用索引。

js
// First, make sure you created index in request.onupgradeneeded:
// objectStore.createIndex("name", "name");
// Otherwise you will get DOMException.

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

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

“name”索引不是唯一的,因此可能存在多個 name 設定為 "Donna" 的條目。在這種情況下,您總是會得到鍵值最低的那個。

如果您需要訪問具有給定 name 的所有條目,則可以使用遊標。您可以在索引上開啟兩種不同型別的遊標。普通遊標將索引屬性對映到物件儲存中的物件。鍵遊標將索引屬性對映到用於將物件儲存在物件儲存中的鍵。差異在此處說明:

js
// Using a normal cursor to grab whole customer record objects
index.openCursor().onsuccess = (event) => {
  const 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 = (event) => {
  const cursor = event.target.result;
  if (cursor) {
    // cursor.key is a name, like "Bill", and cursor.primaryKey is the SSN.
    // No way to directly get the rest of the stored object.
    console.log(`Name: ${cursor.key}, SSN: ${cursor.primaryKey}`);
    cursor.continue();
  }
};

索引也可以在多個屬性上建立,從而允許使用值的組合來查詢記錄,例如透過姓名和電子郵件查詢一個人。要建立複合索引,請在呼叫 createIndex 時將屬性名稱陣列作為鍵路徑傳遞。然後,您可以透過以相同順序傳遞值陣列來查詢索引。

首先,確保您在 request.onupgradeneeded 中建立了索引:

js
const index = objectStore.createIndex("name_email", ["name", "email"]);

然後稍後您可以像這樣查詢索引:

js
const index = objectStore.index("name_email");

index.get(["Donna", "donna@home.org"]).onsuccess = (event) => {
  console.log(event.target.result);
  // {ssn: '555-55-5555', name: 'Donna', age: 32, email: 'donna@home.org'}
};

指定遊標的範圍和方向

如果您想限制遊標中看到的值的範圍,可以使用 IDBKeyRange 物件並將其作為第一個引數傳遞給 openCursor()openKeyCursor()。您可以建立一個僅允許單個鍵的鍵範圍,或者一個具有下限或上限的鍵範圍,或者一個同時具有下限和上限的鍵範圍。邊界可以是“閉合的”(即,鍵範圍包含給定的值)或“開放的”(即,鍵範圍不包含給定的值)。下面是它的工作原理:

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

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

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

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

// Match anything between "Bill" and "Donna", but not including "Donna"
const 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).onsuccess = (event) => {
  const cursor = event.target.result;
  if (cursor) {
    // Do something with the matches.
    cursor.continue();
  }
};

有時您可能希望按降序而不是升序迭代(所有遊標的預設方向)。切換方向透過將 prev 作為第二個引數傳遞給 openCursor() 函式來完成:

js
objectStore.openCursor(boundKeyRange, "prev").onsuccess = (event) => {
  const cursor = event.target.result;
  if (cursor) {
    // Do something with the entries.
    cursor.continue();
  }
};

如果您只想指定方向更改而不限制顯示的結果,則可以僅將 null 作為第一個引數傳遞:

js
objectStore.openCursor(null, "prev").onsuccess = (event) => {
  const cursor = event.target.result;
  if (cursor) {
    // Do something with the entries.
    cursor.continue();
  }
};

由於“name”索引不是唯一的,因此可能存在多個 name 相同的條目。請注意,這種情況在物件儲存中不會發生,因為鍵必須始終是唯一的。如果您希望在索引遊標迭代期間過濾掉重複項,可以將 nextunique(如果您向後遍歷,則為 prevunique)作為方向引數傳遞。當使用 nextuniqueprevunique 時,鍵值最低的條目始終是返回的條目。

js
index.openKeyCursor(null, "nextunique").onsuccess = (event) => {
  const cursor = event.target.result;
  if (cursor) {
    // Do something with the entries.
    cursor.continue();
  }
};

請參閱“IDBCursor 常量”以獲取有效的方向引數。

當 Web 應用程式在另一個標籤頁中開啟時版本更改

當您的 Web 應用程式以需要資料庫版本更改的方式進行更改時,您需要考慮如果使用者在一個標籤頁中開啟舊版本的應用程式,然後在另一個標籤頁中載入新版本的應用程式會發生什麼。當您使用比資料庫實際版本更高的版本呼叫 open() 時,所有其他開啟的資料庫都必須明確確認請求,然後您才能開始更改資料庫(在它們關閉或重新載入之前會觸發 onblocked 事件)。下面是它的工作原理:

js
const openReq = mozIndexedDB.open("MyTestDatabase", 2);

openReq.onblocked = (event) => {
  // If some other tab is loaded with the database, then it needs to be closed
  // before we can proceed.
  console.log("Please close all other tabs with this site open!");
};

openReq.onupgradeneeded = (event) => {
  // All other databases have been closed. Set everything up.
  db.createObjectStore(/* … */);
  useDatabase(db);
};

openReq.onsuccess = (event) => {
  const db = event.target.result;
  useDatabase(db);
};

function useDatabase(db) {
  // Make sure to add a handler to be notified if another page requests a version
  // change. We must close the database. This allows the other page to upgrade the database.
  // If you don't do this then the upgrade won't happen until the user closes the tab.
  db.onversionchange = (event) => {
    db.close();
    console.log(
      "A new version of this page is ready. Please reload or close this tab!",
    );
  };

  // Do stuff with the database.
}

您還應該監聽 VersionError 錯誤,以處理已開啟的應用程式可能啟動導致新嘗試開啟資料庫但使用過時版本的情況。

安全

IndexedDB 使用同源原則,這意味著它將儲存繫結到建立它的站點源(通常是站點域或子域),因此其他源無法訪問它。

如果瀏覽器設定為從不接受第三方 Cookie,則第三方視窗內容(例如,<iframe> 內容)無法訪問 IndexedDB(請參閱Firefox bug 1147821)。

關於瀏覽器關閉的警告

當瀏覽器關閉(因為使用者選擇了退出選項)、包含資料庫的磁碟意外移除或資料庫儲存許可權丟失時,會發生以下情況:

  1. 每個受影響的資料庫(或者在瀏覽器關閉的情況下,所有開啟的資料庫)上的每個事務都將以 AbortError 終止。效果與在每個事務上呼叫 IDBTransaction.abort() 相同。
  2. 一旦所有事務完成,資料庫連線將關閉。
  3. 最後,表示資料庫連線的 IDBDatabase 物件會收到一個 close 事件。您可以使用 IDBDatabase.onclose 事件處理程式來監聽這些事件,以便您知道資料庫何時意外關閉。

上述行為是新的,僅在以下瀏覽器版本中可用:Firefox 50、Google Chrome 31(大約)。

在這些瀏覽器版本之前,事務會靜默中止,並且不會觸發 close 事件,因此無法檢測到意外的資料庫關閉。

由於使用者可以隨時退出瀏覽器,這意味著您不能依賴任何特定的事務完成,而且在舊版瀏覽器中,您甚至不會被告知它們何時未完成。這種行為有幾個含義。

首先,您應該注意始終在每個事務結束時使資料庫保持一致狀態。例如,假設您正在使用 IndexedDB 儲存允許使用者編輯的專案列表。您在編輯後透過清除物件儲存然後寫入新列表來儲存列表。如果您在一個事務中清除物件儲存並在另一個事務中寫入新列表,則存在瀏覽器在清除後但在寫入前關閉的風險,從而導致資料庫為空。為了避免這種情況,您應該將清除和寫入合併到一個事務中。

其次,您不應將資料庫事務與解除安裝事件繫結。如果解除安裝事件是由瀏覽器關閉觸發的,則在解除安裝事件處理程式中建立的任何事務都將永遠不會完成。一種跨瀏覽器會話維護某些資訊的直觀方法是在瀏覽器(或特定頁面)開啟時從資料庫讀取它,在使用者與瀏覽器互動時更新它,然後在瀏覽器(或頁面)關閉時將其儲存到資料庫。但是,這不起作用。資料庫事務將在解除安裝事件處理程式中建立,但由於它們是非同步的,因此它們將在執行之前中止。

實際上,無法保證 IndexedDB 事務會完成,即使在正常瀏覽器關閉時也是如此。請參閱Firefox bug 870645。作為此正常關閉通知的解決方法,您可以跟蹤您的事務並新增一個 beforeunload 事件,以在解除安裝時有任何事務尚未完成時警告使用者。

至少隨著中止通知和 IDBDatabase.onclose 的新增,您可以知道何時發生了這種情況。

完整的 IndexedDB 示例

我們有一個使用 IndexedDB API 的完整示例。該示例使用 IndexedDB 儲存和檢索出版物。

另見

如果您想了解更多資訊,請進一步閱讀。

參考

教程和指南

  • localForage:一個 Polyfill,為客戶端資料儲存提供簡單的名稱:值語法,它在後臺使用 IndexedDB,但如果瀏覽器不支援 IndexedDB,則回退到 Web SQL(已棄用),然後是 localStorage。
  • Dexie.js:一個 IndexedDB 包裝器,透過簡潔、簡單的語法實現更快的程式碼開發。
  • JsStore:一個簡單而高階的 IndexedDB 包裝器,具有類似 SQL 的語法。
  • MiniMongo:一個客戶端記憶體 MongoDB,由 localStorage 支援,透過 http 與伺服器同步。MiniMongo 被 MeteorJS 使用。
  • PouchDB:使用 IndexedDB 在瀏覽器中實現的 CouchDB 客戶端。
  • IDB:一個小型庫,主要映象 IndexedDB API,但具有小的可用性改進。
  • idb-keyval:一個超簡單、小巧(~600B)的基於 Promise 的鍵值儲存,使用 IndexedDB 實現。
  • $mol_db:小型(~1.3kB)TypeScript 外觀,具有基於 Promise 的 API 和自動遷移。
  • RxDB:一個可以在 IndexedDB 之上使用的 NoSQL 客戶端資料庫。支援索引、壓縮和複製。還為 IndexedDB 添加了跨標籤頁功能和可觀測性。