/

深入了解 IndexedDB

深入了解 IndexedDB

IndexedDB 是瀏覽器多年來引入的一種存儲功能之一。這是一個關於 IndexedDB 的介紹,這是一種由所有現代瀏覽器支持的 Web 數據庫。

介紹 IndexedDB

IndexedDB 是多年來引入瀏覽器的數據存儲能力之一。
它是一個鍵值存儲(NoSQL 數據庫),被認為是“在瀏覽器中存儲數據的終極解決方案”。

它是一個異步 API,這意味著執行昂貴的操作不會阻塞 UI 线程,為用戶提供了流暢的體驗。它可以存儲無限量的數據,但一旦超過一定閾值,用戶將被提示提高站點限制。

它在所有現代瀏覽器上都受支持。

它支持事務、版本控制並提供良好的性能。

在瀏覽器內部,我們也可以使用:

  • Cookies:可以存儲大量字符串
  • Web Storage(或 DOM Storage),這是一個常用來識別 localStorage 和 sessionStorage 的術語,它們都是鍵值存儲。sessionStorage 不會保留數據,當會話結束時,數據就被清除了,而 localStorage 會在不同的會話之間保留數據。

本地/會話存儲的缺點是被限制在一個小(且不一致)的大小,不同瀏覽器的實現為每個站點提供2MB到10MB的空間。

過去,我們還有 Web SQL,一個對 SQLite 的封裝。但現在 Web SQL 已經被棄用,在一些現代瀏覽器上不受支持,它從未被公認為標準,因此不應使用。然而,根據 Can I Use 的數據,83% 的用戶設備上有此技術。

雖然您可以在一個站點中創建多個數據庫,但通常您只創建一個數據庫,並在該數據庫內創建多個對象存儲。

數據庫對於域是私有的,因此其他網站無法訪問另一個網站的 IndexedDB 存儲。

每個存儲通常包含一組“物品”,這些物品可以是:

  • 字符串
  • 數字
  • 對象
  • 數組
  • 日期

例如,您可能有一個存儲包含帖子,另一個存儲包含評論。

存儲包含多個項目,每個項目都具有唯一的鍵,鍵表示可以通過該方式標識對象的方式。

您可以使用事務來更改這些存儲,通過執行添加、編輯和刪除操作,並迭代它們包含的項目。

自從 ES6 中引入了 Promises 以及隨後將 API 移至使用 Promises,IndexedDB API 看起來有點“過時”。

雖然這沒有什麼問題,但在我即將解釋的所有示例中,我將使用 Jake Archibald 的 IndexedDB Promised Library,這是一個位於 IndexedDB API 之上的微小層,使其更容易使用。

Google 開發者網站上關於 IndexedDB 的所有示例也都使用了這個庫。

創建 IndexedDB 數據庫

最簡單的方法是使用 unpkg,將以下代碼添加到頁面標頭:

1
2
3
<script type="module">
import { openDB, deleteDB } from 'https://unpkg.com/idb?module'
</script>

在使用 IndexedDB API 之前,請始終確保檢查瀏覽器是否支持。儘管它在各種瀏覽器上都得到了廣泛支持,但您永遠不知道用戶在使用哪個瀏覽器:

1
2
3
4
5
6
7
8
9
10
(() => {
'use strict'

if (!('indexedDB' in window)) {
console.warn('IndexedDB not supported')
return
}

//...IndexedDB code
})()

如何創建數據庫

使用openDB():

1
2
3
4
5
6
7
8
9
10
11
12
13
(async () => {
//...

const dbName = 'mydbname'
const storeName = 'store1'
const version = 1 //版本從1開始

const db = await openDB(dbName, version, {
upgrade(db, oldVersion, newVersion, transaction) {
const store = db.createObjectStore(storeName)
}
})
})()

前兩個參數是數據庫名稱和版本。第三個參數(可選)是一個對象,包含一個只有在版本號高於當前安裝的數據庫版本時才調用的函數。在函數體中,您可以升級數據庫的結構(存儲和索引)。

將數據添加到數據庫中

在創建存儲時添加數據,進行初始化

您可以使用對象存儲的 put 方法,但首先我們需要一個對它的引用,可以通過創建它時的 db.createObjectStore() 獲得。

當使用 put 時,值是第一個參數,鍵是第二個參數。這是因為如果在創建對象存儲時指定了 keyPath,則您不需要在每個 put() 請求中輸入鍵名,只需編寫值。

這會在我們創建 store0 時立即填充它:

1
2
3
4
5
6
7
8
9
10
11
12
13
(async () => {
//...
const dbName = 'mydbname'
const storeName = 'store0'
const version = 1

const db = await openDB(dbName, version,{
upgrade(db, oldVersion, newVersion, transaction) {
const store = db.createObjectStore(storeName)
store.put('Hello world!', 'Hello')
}
})
})()

在存儲創建後添加數據,使用事務

要稍後添加項目,您需要創建一個讀/寫的事務,可以確保數據庫完整性(如果一個操作失敗,事務中的所有操作都將回滾,狀態回到已知狀態)。

為此,使用調用 openDB 時獲得的 dbPromise 對象的引用,並運行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(async () => {
//...
const dbName = 'mydbname'
const storeName = 'store0'
const version = 1

const db = await openDB(/*...*/)

const tx = db.transaction(storeName, 'readwrite')
const store = await tx.objectStore(storeName)

const val = 'hey!'
const key = 'Hello again'
const value = await store.put(val, key)
await tx.done
})()

從存儲中獲取數據

從存儲中獲取一個項目:get()

1
2
const key = 'Hello again'
const item = await db.transaction(storeName).objectStore(storeName).get(key)

從存儲中獲取所有項目:getAll()

獲取存儲中的所有鍵

1
const items = await db.transaction(storeName).objectStore(storeName).getAllKeys()

獲取存儲中的所有值

1
const items = await db.transaction(storeName).objectStore(storeName).getAll()

從 IndexedDB 中刪除數據

刪除數據庫、對象存儲和數據

刪除整個 IndexedDB 數據庫

1
2
const dbName = 'mydbname'
await deleteDB(dbName)

刪除對象存儲中的數據

使用事務:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(async () => {
//...

const dbName = 'mydbname'
const storeName = 'store1'
const version = 1

const db = await openDB(dbName, version, {
upgrade(db, oldVersion, newVersion, transaction) {
const store = db.createObjectStore(storeName)
}
})

const tx = await db.transaction(storeName, 'readwrite')
const store = await tx.objectStore(storeName)

const key = 'Hello again'
await store.delete(key)
await tx.done
})()

從之前的數據庫版本遷移

openDB() 函數的第三個(可選)參數是一個對象,可以包含一個 upgrade 函數,該函數只在版本號高於當前安裝的數據庫版本時才調用。在該函數體內,您可以根據需要升級數據庫的結構(存儲和索引):

1
2
3
4
5
6
7
const name = 'mydbname'
const version = 1
openDB(name, version, {
upgrade(db, oldVersion, newVersion, transaction) {
console.log(oldVersion)
}
})

在此回調函數中,您可以檢查用戶正在進行更新的版本,並相應地執行一些操作。

您可以使用以下語法從過去的數據庫版本進行遷移:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(async () => {
//...
const dbName = 'mydbname'
const storeName = 'store0'
const version = 1

const db = await openDB(dbName, version, {
upgrade(db, oldVersion, newVersion, transaction) {
switch (oldVersion) {
case 0: // 如果之前沒有創建 db
// 在版本 1 中引入的存儲
db.createObjectStore('store1')
case 1:
// 在版本 2 中的新存儲
db.createObjectStore('store2', { keyPath: 'name' })
}
db.createObjectStore(storeName)
}
})
})()

唯一鍵

可如您在案例1中所見,createObjectStore() 接受第二個參數,指示數據庫的索引鍵。這在存儲對象時非常有用:put() 調用不需要第二個參數,只需將值(對象)傳遞給它,並且鍵將映射到具有該名稱的對象屬性。

索引提供了一種通過特定鍵檢索後續值的方法,它必須是唯一的(每個項目必須具有不同的鍵)。

如果您的值中沒有唯一鍵(例如,如果您收集沒有相關聯名稱的電子郵件地址),則可以將鍵設置為自動增量,這樣您就不需要在客戶端代碼中跟踪它:

1
db.createObjectStore('notes', { autoIncrement: true })

檢查存儲是否存在

您可以通過調用 objectStoreNames() 方法來檢查是否已經存在一個對象存儲:

1
2
3
4
5
const storeName = 'store1'

if (!db.objectStoreNames.contains(storeName)) {
db.createObjectStore(storeName)
}

從 IndexedDB 中刪除

刪除數據庫、對象存儲和數據

刪除數據庫

1
await deleteDB('mydb')

刪除對象存儲

只能在打開數據庫時的回調函數中刪除對象存儲,並且只有在指定的版本高於當前安裝的版本時才調用該回調函數:

1
2
3
4
5
6
7
8
9
10
11
12
13
const db = await openDB('dogsdb', 2, {
upgrade(db, oldVersion, newVersion, transaction) {
switch (oldVersion) {
case 0: // 如果之前沒有創建過 db
// 在版本 1 中引入的存儲
db.createObjectStore('store1')
case 1:
// 刪除版本 2 中的舊存儲,創建一個新的存儲
db.deleteObjectStore('store1')
db.createObjectStore('store2')
}
}
})

要刪除對象存儲中的數據,請使用事務

1
2
3
4
5
6
7
const key = 232 // 隨機鍵

const db = await openDB(/*...*/)
const tx = await db.transaction('store', 'readwrite')
const store = await tx.objectStore('store')
await store.delete(key)
await tx.complete

還有更多!

這只是基礎部分。我沒有談到遊標和更高級的功能。IndexedDB 還有更多內容,但我希望這能夠讓你有一個良好的開始。