
IndexedDB to jedno z rozwiązań do przechowywania danych, które trafiły do przeglądarek na przestrzeni lat.
Jest to baza klucz/wartość (baza noSQL) uznawana za docelowe rozwiązanie do przechowywania danych w przeglądarce.
API jest asynchroniczne, czyli wykonanie kodu nie zablokuje reszty programu, z wyraźną korzyścią dla użytkownika. IndexedDB pomieści dowolną ilość danych, choć po przekroczeniu pewnego progu przeglądarka poprosi użytkownika o zgodę na zapisanie większej porcji.
Są też inne metody: ciasteczka i Web Storage ( localStorage oraz sessionStorage ). Wadą local/session storage jest mały limit, przeglądarki dają od 2MB do 10MB na witrynę.
Technicznie możesz utworzyć kilka baz na witrynę, ale zwykle tworzy się jedną, a wewnątrz niej kilka object store’ów.
Baza jest prywatna dla domeny, więc żadna inna witryna nie sięgnie do cudzej bazy IndexedDB.
Każdy object store zwykle zawiera zestaw rzeczy, których można użyć:
- objects
- arrays
- strings
- numbers
- dates
Możesz na przykład mieć jeden store z wpisami, a drugi z komentarzami.
Store zawiera pewną liczbę obiektów, z których każdy ma unikalny klucz, to po nim obiekt jest identyfikowany.
Obiekty modyfikujesz w ramach transakcji: dodajesz, zmieniasz, usuwasz i iterujesz po zawartości store’a.
Od czasu pojawienia się Promises w ES6 i przechodzenia kolejnych API na obietnice, IndexedDB wygląda trochę staroświecko.
Nie ma w tym nic złego, ale we wszystkich przykładach użyję biblioteki IndexedDB Promised Jake’a Archibalda, czyli cienkiej warstwy nad API IndexedDB, która ułatwia pracę.
Ta sama biblioteka pojawia się we wszystkich przykładach dotyczących IndexedDB na Google Developers
Tworzenie bazy IndexedDB
Najprościej użyć unpkg i dodać skrypt w nagłówku strony:
<script type="module">
import { openDB, deleteDB } from 'https://unpkg.com/idb?module'
</script>Zanim sięgniesz po API IndexedDB, zawsze sprawdź wsparcie przeglądarki, nawet jeśli jest szerokie, nigdy nie wiesz, z czego korzysta użytkownik:
(async () => {
'use strict'
if (!('indexedDB' in window)) {
console.warn('IndexedDB not supported')
return
}
})()Jak utworzyć bazę IndexedDB
Używamy openDB():
(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)
}
})
})()Dwa pierwsze parametry to nazwa bazy i jej wersja. Trzeci, opcjonalny, to obiekt z funkcją wywoływaną tylko wtedy, gdy numer wersji jest wyższy niż wersja bazy zainstalowanej u użytkownika. W ciele tej funkcji aktualizujesz strukturę bazy (object store’y i indeksy).
Dodawanie danych do store’a
Dodawanie danych przy tworzeniu store’a, czyli inicjalizacja
Do dodawania danych służy metoda put object store’a, ale najpierw trzeba go zainicjalizować przez db.createObjectStore() w momencie tworzenia.
W wywołaniu put pierwszym argumentem jest wartość, drugim klucz. Dzieje się tak dlatego, że jeśli przy tworzeniu object store’a podasz keyPath, nie musisz przekazywać nazwy klucza w każdym put(), wystarczy sama wartość.
Poniższy kod wypełnia store0 zaraz po jego utworzeniu:
(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')
}
})
})()Dodawanie danych do istniejącego object store’a przy użyciu transakcji
Żeby dorzucić elementy później, trzeba otworzyć transakcję do odczytu i zapisu, która pilnuje spójności bazy (jeśli jedna operacja się nie powiedzie, wszystkie operacje transakcji są wycofywane i stan wraca do punktu wyjścia).
Wykorzystaj referencję do obiektu dbPromise, którą zwróciło wywołanie openDB, i uruchom:
(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
})()Pobieranie danych z object store’a
Pobranie jednego elementu z object store’a: get()
const key = 'Hello again'
const item = await db.transaction(storeName).objectStore(storeName).get(key)Pobranie wszystkich elementów z object store’a: getAll()
Pobranie wszystkich zapisanych kluczy
const items = await db.transaction(storeName).objectStore(storeName).getAllKeys()Pobranie wszystkich zapisanych wartości
const items = await db.transaction(storeName).objectStore(storeName).getAll()Usuwanie danych z IndexedDB
Usuwanie bazy, object store’a i danych
Usunięcie całej bazy IndexedDB
const dbName="mydbname"
await deleteDB(dbName)Usuwanie danych z object store’a
Korzystamy z transakcji:
(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)
}
})
const tx = await db.transaction(storeName, 'readwrite')
const store = await tx.objectStore(storeName)
const key = 'Hello again'
await store.delete(key)
await tx.done
})()Migracja z wcześniejszej wersji bazy
Trzeci (opcjonalny) parametr funkcji openDB() to obiekt, który może zawierać funkcję aktualizującą, wywoływaną tylko wtedy, gdy numer wersji jest wyższy niż wersja bazy zainstalowanej u użytkownika. W ciele tej funkcji aktualizujesz strukturę bazy (object store’y i indeksy):
const name="mydbname"
const version = 1
openDB(name, version, {
upgrade(db, oldVersion, newVersion, transaction) {
console.log(oldVersion)
}
})W tym callbacku możesz sprawdzić, z której wersji użytkownik się aktualizuje, i wykonać odpowiednie operacje.
Migrację z poprzedniej wersji bazy zapiszesz w takiej składni
(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:
// store wprowadzony w wersji 1
db.createObjectStore('store1')
case 1:
// usuń stary store w wersji 2, utwórz nowy
db.createObjectStore('store2', { keyPath: 'name' })
}
db.createObjectStore(storeName)
}
})
})()Unikalny klucz
createObjectStore() przyjmuje, jak widać w przypadku 1, drugi parametr wskazujący klucz indeksu bazy. To bardzo wygodne przy przechowywaniu obiektów: wywołania put() nie potrzebują wtedy drugiego parametru, wystarczy sama wartość (obiekt), a klucz zostanie odwzorowany na właściwość obiektu o tej nazwie.
Indeks pozwala później odczytać wartość po tym konkretnym kluczu, dlatego musi być unikalny (każdy element ma mieć inny klucz).
Klucz można ustawić na automatyczną inkrementację, więc nie musisz pilnować go po stronie klienta:
db.createObjectStore('notes', { autoIncrement: true })Sięgaj po automatyczną inkrementację, jeśli twoje wartości nie zawierają jeszcze unikalnego klucza.
Sprawdzenie, czy object store istnieje
Istnienie object store’a sprawdzisz metodą objectStoreNames():
const storeName="store1"
if (!db.objectStoreNames.contains(storeName)) {
db.createObjectStore(storeName)
}Usuwanie z IndexedDB
Usuwanie bazy, object store’a i danych
Usunięcie bazy
await deleteDB('mydb')Usunięcie object store’a
Object store można usunąć wyłącznie w callbacku otwarcia bazy, a ten callback zostanie wywołany tylko wtedy, gdy podasz wersję wyższą od aktualnie zainstalowanej:
const db = await openDB('dogsdb', 2, {
upgrade(db, oldVersion, newVersion, transaction) {
switch (oldVersion) {
case 0:
// store wprowadzony w wersji 1
db.createObjectStore('store1')
case 1:
// usuń stary store w wersji 2, utwórz nowy
db.deleteObjectStore('store1')
db.createObjectStore('store2')
}
}
})Usuwanie danych z object store’a przy użyciu transakcji
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

