JavaScript i IndexedDB

JavaScript i IndexedDB

W tym artykule wyjaśnimy JavaScript i IndexedDB.

Ten samouczek zawiera szczegółowe wyjaśnienia dotyczące JavaScript i IndexedDB, wraz z praktycznymi przykładami kodu na każdym etapie, aby pomóc pogłębić zrozumienie zagadnienia.

YouTube Video

javascript-indexed-db.html
  1<!DOCTYPE html>
  2<html lang="en">
  3<head>
  4  <meta charset="UTF-8">
  5  <title>JavaScript &amp; HTML</title>
  6  <style>
  7    * {
  8        box-sizing: border-box;
  9    }
 10
 11    body {
 12        margin: 0;
 13        padding: 1em;
 14        padding-bottom: 10em;
 15        font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
 16        background-color: #f7f9fc;
 17        color: #333;
 18        line-height: 1.6;
 19    }
 20
 21    .container {
 22        max-width: 800px;
 23        margin: 0 auto;
 24        padding: 1em;
 25        background-color: #ffffff;
 26        border: 1px solid #ccc;
 27        border-radius: 10px;
 28        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
 29    }
 30
 31    .container-flex {
 32        display: flex;
 33        flex-wrap: wrap;
 34        gap: 2em;
 35        max-width: 1000px;
 36        margin: 0 auto;
 37        padding: 1em;
 38        background-color: #ffffff;
 39        border: 1px solid #ccc;
 40        border-radius: 10px;
 41        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
 42    }
 43
 44    .left-column, .right-column {
 45        flex: 1 1 200px;
 46        min-width: 200px;
 47    }
 48
 49    h1, h2 {
 50        font-size: 1.2rem;
 51        color: #007bff;
 52        margin-top: 0.5em;
 53        margin-bottom: 0.5em;
 54        border-left: 5px solid #007bff;
 55        padding-left: 0.6em;
 56        background-color: #e9f2ff;
 57    }
 58
 59    button {
 60        display: block;
 61        margin: 1em auto;
 62        padding: 0.75em 1.5em;
 63        font-size: 1rem;
 64        background-color: #007bff;
 65        color: white;
 66        border: none;
 67        border-radius: 6px;
 68        cursor: pointer;
 69        transition: background-color 0.3s ease;
 70    }
 71
 72    button:hover {
 73        background-color: #0056b3;
 74    }
 75
 76    #output {
 77        margin-top: 1em;
 78        background-color: #1e1e1e;
 79        color: #0f0;
 80        padding: 1em;
 81        border-radius: 8px;
 82        min-height: 200px;
 83        font-family: Consolas, monospace;
 84        font-size: 0.95rem;
 85        overflow-y: auto;
 86        white-space: pre-wrap;
 87    }
 88
 89    .highlight {
 90        outline: 3px solid #ffc107; /* yellow border */
 91        background-color: #fff8e1;  /* soft yellow background */
 92        transition: background-color 0.3s ease, outline 0.3s ease;
 93    }
 94
 95    .active {
 96        background-color: #28a745; /* green background */
 97        color: #fff;
 98        box-shadow: 0 0 10px rgba(40, 167, 69, 0.5);
 99        transition: background-color 0.3s ease, box-shadow 0.3s ease;
100    }
101  </style>
102</head>
103<body>
104    <div class="container">
105        <h1>JavaScript Console</h1>
106        <button id="executeBtn">Execute</button>
107        <div id="output"></div>
108    </div>
109
110    <script>
111        // Override console.log to display messages in the #output element
112        (function () {
113            // Override console.log
114            const originalLog = console.log;
115            console.log = function (...args) {
116                originalLog.apply(console, args);
117                const message = document.createElement('div');
118                message.textContent = args
119                    .map(arg => (typeof arg === "object" && arg !== null ? JSON.stringify(arg) : String(arg)))
120                    .join(" ");
121                output.appendChild(message);
122            };
123
124            // Override console.error
125            const originalError = console.error;
126            console.error = function (...args) {
127                originalError.apply(console, args);
128                const message = document.createElement('div');
129                message.textContent = args
130                    .map(arg => (typeof arg === "object" && arg !== null ? JSON.stringify(arg) : String(arg)))
131                    .join(" ");
132                message.style.color = 'red'; // Color error messages red
133                output.appendChild(message);
134            };
135        })();
136
137        document.getElementById('executeBtn').addEventListener('click', () => {
138            // Prevent multiple loads
139            if (document.getElementById('externalScript')) return;
140
141            const script = document.createElement('script');
142            script.src = 'javascript-indexed-db.js';
143            script.id = 'externalScript';
144            //script.onload = () => console.log('javascript-indexed-db.js loaded and executed.');
145            //script.onerror = () => console.log('Failed to load javascript-indexed-db.js.');
146            document.body.appendChild(script);
147        });
148    </script>
149</body>
150</html>

JavaScript i IndexedDB

IndexedDB to asynchroniczna baza danych typu klucz-wartość wbudowana w przeglądarki. Oferuje funkcje podobne do relacyjnych baz danych i pozwala na przechowywanie oraz wyszukiwanie dużych ilości danych strukturalnych po stronie klienta. Jest szczególnie przydatny w aplikacjach działających offline oraz PWA (Progressive Web Apps).

Cechy IndexedDB

  • Działa w sposób asynchroniczny i sterowany zdarzeniami.
  • Obiekty JavaScript mogą być przechowywane w magazynach obiektów.
  • Możliwe jest wyszukiwanie za pomocą zapytań lub indeksów.
  • Posiada dużą pojemność (setki MB lub więcej), umożliwiając przechowywanie znacznie większej ilości danych niż ciasteczka czy localStorage.

Otwieranie i tworzenie bazy danych

Aby korzystać z IndexedDB, najpierw musisz otworzyć bazę danych. Jeśli nie istnieje, zostanie utworzona automatycznie.

1const request = indexedDB.open('MyDatabase', 1); // Specify DB name and version
  • Metoda open otwiera bazę danych asynchronicznie i wywołuje następujące trzy zdarzenia.

onsuccess

1// Fired when database is successfully opened
2request.onsuccess = (event) => {
3    const db = event.target.result; // Database instance
4    console.log('Database opened successfully:', db.name);
5};
  • Zdarzenie onsuccess jest wywoływane, gdy baza danych zostanie pomyślnie otwarta. Kolejne operacje należy wykonywać, korzystając z request.result, które jest dostępne na tym etapie.

onerror

1// Fired when database fails to open
2request.onerror = (event) => {
3    console.error('Failed to open database:', event.target.error);
4};
  • Zdarzenie onerror jest wywoływane, gdy nie uda się otworzyć bazy danych. W tym miejscu należy przeprowadzić logowanie i obsługę błędów.

onupgradeneeded

 1// Fired when database is newly created or upgraded
 2request.onupgradeneeded = (event) => {
 3    const db = event.target.result;
 4    console.log('Database upgrade needed (or newly created):', db.name);
 5
 6    // Example: Create an object store (like a table) if it doesn’t exist
 7    if (!db.objectStoreNames.contains('users')) {
 8        db.createObjectStore('users', { keyPath: 'id' });
 9        console.log('Object store "users" created');
10    }
11};
  • onupgradeneeded jest wywoływane, gdy baza danych jest tworzona po raz pierwszy lub gdy określona wersja jest wyższa niż aktualna. Tworzenie tabel (magazynów obiektów) i definiowanie schematu powinno się odbywać w tym momencie.

Tworzenie magazynu obiektów

Najpierw utwórz 'magazyn obiektów' (odpowiednik tabeli) w metodzie onupgradeneeded.

1request.onupgradeneeded = function (event) {
2    const db = event.target.result;
3    console.log("onupgradeneeded triggered. Database version:", db.version);
4    if (!db.objectStoreNames.contains('users')) {
5        console.log("Creating object store: users");
6        const store = db.createObjectStore('users', { keyPath: 'id' });
7        store.createIndex('name', 'name', { unique: false });
8    }
9};

W tym miejscu stosowane są następujące ustawienia:.

  • createObjectStore createObjectStore to metoda tworzenia nowego magazynu obiektów w bazie danych. Możesz przechowywać rekordy w magazynie i określić sposób zarządzania danymi, używając keyPath oraz innych opcji. Ten proces należy wykonać wewnątrz zdarzenia onupgradeneeded.

  • keyPath: 'id' Ustaw keyPath na właściwość id, która jednoznacznie identyfikuje każdy rekord jako klucz główny. Dzięki temu id może być używane automatycznie podczas dodawania, wyszukiwania lub aktualizowania danych.

  • createIndex Aby umożliwić wyszukiwanie, użyj metody createIndex, aby utworzyć indeks na podstawie właściwości name. Ustawiając unique: false, pozwalasz na istnienie wielu rekordów o tej samej wartości name.

Obsługa pomyślnych połączeń z bazą danych

Przypisz procesy do wykonania w przypadku pomyślnego połączenia z bazą danych do zdarzenia onsuccess. W tym procesie uzyskujesz instancję bazy danych i przygotowujesz się do odczytu oraz zapisu danych.

1request.onsuccess = function (event) {
2    const db = event.target.result;
3    console.log('Database opened successfully');
4    // Use db for reading and writing in subsequent operations
5};
  • Ten proces jest wykonywany, gdy połączenie z bazą danych IndexedDB zakończy się powodzeniem.

  • event.target.result zawiera otwartą instancję bazy danych (obiekt IDBDatabase), który służy do rozpoczynania transakcji i dostępu do magazynów obiektów.

  • Rzeczywiste operacje odczytu i zapisu, takie jak dodawanie, pobieranie, aktualizowanie czy usuwanie danych, są wykonywane przy użyciu obiektu db.

Na tym etapie baza danych jest gotowa, możesz zatem bezpiecznie rozpocząć transakcje.

Dodawanie danych

Oto jak dodać nowe dane do IndexedDB.

 1function addUser(db, user) {
 2    const transaction = db.transaction('users', 'readwrite');
 3    const store = transaction.objectStore('users');
 4    const request = store.add(user);
 5
 6    request.onsuccess = () => {
 7        console.log('User added:', user);
 8    };
 9
10    request.onerror = () => {
11        console.error('Add failed:', request.error);
12    };
13}
14
15// Example: Add a user
16request.onsuccess = function (event) {
17    const db = event.target.result;
18    addUser(db, { id: 1, name: 'Alice' });
19    addUser(db, { id: 2, name: 'Bob' });
20    addUser(db, { id: 3, name: 'John' });
21};
  • Utwórz transakcję za pomocą db.transaction() i określ magazyn obiektów oraz tryb pracy (w tym przypadku readwrite).
  • Dodaj nowe dane za pomocą metody store.add().
  • Transakcje są zatwierdzane automatycznie, ale jeśli chcesz zgrupować kilka operacji, możesz nimi zarządzać za pomocą zdarzeń końca transakcji.

Pobieranie danych (wyszukiwanie po kluczu głównym)

Oto jak pobrać konkretne dane za pomocą klucza głównego.

 1function getUserById(db, id) {
 2    const transaction = db.transaction('users', 'readonly');
 3    const store = transaction.objectStore('users');
 4    const request = store.get(id);
 5
 6    request.onsuccess = () => {
 7        if (request.result) {
 8            console.log('User found:', request.result);
 9        } else {
10            console.log('User not found');
11        }
12    };
13
14    request.onerror = () => {
15        console.error('Error retrieving user:', request.error);
16    };
17}
18
19// Example: Get a user by id
20request.onsuccess = function (event) {
21    const db = event.target.result;
22    getUserById(db, 1);
23};
  • Utwórz transakcję tylko do odczytu za pomocą db.transaction().
  • Użyj store.get(id), aby pobrać dane odpowiadające określonemu kluczowi głównemu.
  • onsuccess jest wywoływane po pomyślnym pobraniu, a wynik zostaje wyświetlony, jeśli jest dostępny. Jeśli nie ma wyniku, traktuje się to jako 'brak odpowiadających danych.'.

Wykorzystanie wyszukiwania po indeksie

Jeśli chcesz przeszukiwać po właściwościach innych niż klucz główny, użyj wcześniej utworzonego indeksu.

 1function getUserByName(db, name) {
 2    const transaction = db.transaction('users', 'readonly');
 3    const store = transaction.objectStore('users');
 4    const index = store.index('name');
 5    const request = index.get(name);
 6
 7    request.onsuccess = () => {
 8        if (request.result) {
 9            console.log('User by name:', request.result);
10        } else {
11            console.log('No user found with that name');
12        }
13    };
14
15    request.onerror = () => {
16        console.error('Error retrieving user by name:', request.error);
17    };
18}
19
20// Example: Get a user by id
21request.onsuccess = function (event) {
22    const db = event.target.result;
23    getUserByName(db, 'Alice');
24};
  • Uzyskaj dostęp do utworzonego indeksu name za pomocą store.index('name').
  • index.get(value) pobiera pierwszy rekord z pasującą wartością. Jeśli istnieje wiele rekordów z tą samą wartością, możesz pobrać je wszystkie za pomocą index.getAll(value).

Aktualizowanie danych

Aby zaktualizować istniejące dane (nadpisać), użyj metody put().

Jeśli istnieje rekord o tym samym kluczu głównym, zostanie zaktualizowany; w przeciwnym razie zostanie dodany nowy rekord.

 1function updateUser(db, updatedUser) {
 2    const transaction = db.transaction('users', 'readwrite');
 3    const store = transaction.objectStore('users');
 4    const request = store.put(updatedUser);
 5
 6    request.onsuccess = () => {
 7        console.log('User updated:', updatedUser);
 8    };
 9
10    request.onerror = () => {
11        console.error('Update failed:', request.error);
12    };
13}
14
15// Example: Update user
16request.onsuccess = async (event) => {
17    const db = event.target.result;
18
19    // Test : update existing user
20    updateUser(db, { id: 3, name: 'John Updated' });
21
22    // Test : insert new user
23    updateUser(db, { id: 4, name: 'Charlie' });
24};
  • put() to wygodna metoda służąca zarówno do aktualizacji, jak i dodawania danych.
  • Jeśli dane z tym samym kluczem głównym już istnieją, zostaną nadpisane.
  • Jeśli chcesz sprawdzić istnienie danych przed aktualizacją, możesz wcześniej wykorzystać get().

Usuwanie danych

Aby usunąć dane odpowiadające określonemu kluczowi głównemu, użyj metody delete().

 1function deleteUser(db, id) {
 2    const transaction = db.transaction('users', 'readwrite');
 3    const store = transaction.objectStore('users');
 4    const request = store.delete(id);
 5
 6    request.onsuccess = () => {
 7        console.log(`User with id=${id} deleted successfully`);
 8    };
 9
10    request.onerror = () => {
11        console.error(`Failed to delete user with id=${id}:`, request.error);
12    };
13}
14
15// Example: Delete a user by id
16request.onsuccess = function (event) {
17    const db = event.target.result;
18    deleteUser(db, 4);
19};
  • Użyj store.delete(id), aby usunąć dane o pasującym kluczu głównym.
  • Pamiętaj, że nawet jeśli dane nie istnieją, nie wystąpi błąd i operacja zostanie uznana za udaną.
  • Wdrożenie obsługi błędów sprawi, że kod będzie bardziej niezawodny.

Pobieranie wszystkich danych

getAll()

Aby pobrać wszystkie rekordy z magazynu obiektów, użyj metody getAll().

 1function getAllUsers(db) {
 2    const transaction = db.transaction('users', 'readonly');
 3    const store = transaction.objectStore('users');
 4    const request = store.getAll();
 5
 6    request.onsuccess = () => {
 7        console.log('All users:', request.result);
 8    };
 9
10    request.onerror = () => {
11        console.error('Failed to retrieve users:', request.error);
12    };
13}
14
15// Example: Get all users
16request.onsuccess = function (event) {
17    const db = event.target.result;
18    getAllUsers(db);
19};
  • getAll() pobiera wszystkie rekordy z określonego magazynu obiektów jako tablicę.
  • Nawet przy pobieraniu dużych ilości danych za jednym razem przetwarzanie jest wydajne.
  • Wyniki są przechowywane jako tablica w request.result.
  • Zawsze dodawaj obsługę błędów, aby móc radzić sobie z niepowodzeniami.

openCursor()

openCursor() to metoda pozwalająca na sekwencyjne przeglądanie rekordów w magazynie obiektów lub indeksie. Jest przydatna, gdy chcesz przetwarzać dane pojedynczo, zamiast pobierać je wszystkie naraz.

 1function getAllUsersWithCursor(db) {
 2    const transaction = db.transaction('users', 'readonly');
 3    const store = transaction.objectStore('users');
 4    const request = store.openCursor();
 5
 6    request.onsuccess = () => {
 7        const cursor = request.result;
 8        if (cursor) {
 9            console.log('User:', cursor.value); // Process the current record
10            cursor.continue(); // Move to the next record
11        } else {
12            console.log('All users have been processed.');
13        }
14    };
15
16    request.onerror = () => {
17        console.error('Failed to open cursor:', request.error);
18    };
19}
20
21// Example: Get all users
22request.onsuccess = function (event) {
23    const db = event.target.result;
24    getAllUsersWithCursor(db);
25};
  • Używając openCursor(), uruchamiasz kursor i pobierasz rekordy z magazynu obiektów jeden po drugim.
  • Użyj cursor.value, aby pobrać obiekt danych bieżącego rekordu.
  • Przesuń kursor do kolejnego rekordu za pomocą cursor.continue().
  • Gdy cursor === null, wszystkie rekordy zostały przejrzane.

Przykład procesu aktualizacji z użyciem openCursor()

Na przykład proces zmiany nazwy użytkownika, którego name to Alice, na Alicia wyglądałby następująco:.

 1function updateUserName(db, oldName, newName) {
 2    const transaction = db.transaction('users', 'readwrite');
 3    const store = transaction.objectStore('users');
 4    const index = store.index('name'); // Use the 'name' index
 5    const request = index.openCursor(IDBKeyRange.only(oldName));
 6
 7    request.onsuccess = () => {
 8        const cursor = request.result;
 9        if (cursor) {
10            const user = cursor.value;
11            user.name = newName; // Update the name
12            const updateRequest = cursor.update(user);
13
14            updateRequest.onsuccess = () => {
15                console.log('Updated user:', user);
16            };
17
18            updateRequest.onerror = () => {
19                console.error('Failed to update user:', updateRequest.error);
20            };
21
22            cursor.continue();
23        } else {
24            console.log('All matching users have been updated.');
25        }
26    };
27
28    request.onerror = () => {
29        console.error('Cursor error:', request.error);
30    };
31}
32
33// Example: Update user name
34request.onsuccess = function (event) {
35    const db = event.target.result;
36    updateUserName(db, 'Alice', 'Alicia');
37};
  • IDBKeyRange.only(oldName) Używając IDBKeyRange.only, możesz wybrać tylko te rekordy, których klucz dokładnie odpowiada oldName. Jest to przydatne, gdy chcesz bezpośrednio uzyskać dostęp do konkretnej wartości.

  • cursor.update() Po zaktualizowaniu cursor.value, wywołanie update() nadpisze odpowiedni rekord.

  • Obsługa wielu pasujących rekordów Wywołując cursor.continue(), możesz przesunąć kursor do kolejnego pasującego rekordu. Pozwala to przetwarzać kolejno wiele rekordów spełniających ten sam klucz lub warunek.

  • Obsługa błędów Wypisując logi w onerror w przypadku niepowodzenia procesu, łatwiej jest zidentyfikować przyczyny i rozwiązywać problemy podczas działania.

Przykład procesu usuwania z użyciem openCursor()

Na przykład, proces usuwania wszystkich użytkowników, których name to Bob, wyglądałby następująco:.

 1function deleteUsersByName(db, targetName) {
 2    const transaction = db.transaction('users', 'readwrite');
 3    const store = transaction.objectStore('users');
 4    const index = store.index('name');
 5    const request = index.openCursor(IDBKeyRange.only(targetName));
 6
 7    request.onsuccess = () => {
 8        const cursor = request.result;
 9        if (cursor) {
10            const deleteRequest = cursor.delete();
11
12            deleteRequest.onsuccess = () => {
13                console.log('Deleted user:', cursor.value);
14            };
15
16            deleteRequest.onerror = () => {
17                console.error('Failed to delete user:', deleteRequest.error);
18            };
19
20            cursor.continue();
21        } else {
22            console.log('All matching users have been deleted.');
23        }
24    };
25
26    request.onerror = () => {
27        console.error('Cursor error:', request.error);
28    };
29}
30
31// Example: Delete user by name
32request.onsuccess = function (event) {
33    const db = event.target.result;
34    deleteUsersByName(db, 'Bob');
35};
  • cursor.delete() Używając cursor.delete(), rekord na bieżącej pozycji kursora zostaje usunięty. Ponieważ wynik zwracany jest asynchronicznie, proces można sprawdzić w onsuccess.

Uwagi dotyczące transakcji i przetwarzania asynchronicznego

IndexedDB działa asynchronicznie i jest oparta na zdarzeniach. Wszystkie operacje muszą być obsługiwane za pomocą zdarzeń onsuccess lub onerror. Gdy chcesz połączyć kilka procesów razem, wygodnie jest opakować je w Promise.

 1function openDatabase() {
 2    return new Promise((resolve, reject) => {
 3        const request = indexedDB.open('MyDatabase', 1);
 4
 5        request.onupgradeneeded = function (event) {
 6            const db = event.target.result;
 7
 8            // Initialization process (creating object stores and indexes, etc.)
 9            const store = db.createObjectStore('users', { keyPath: 'id' });
10            store.createIndex('name', 'name', { unique: false });
11        };
12
13        request.onsuccess = function (event) {
14            const db = event.target.result;
15            resolve(db);
16        };
17
18        request.onerror = function () {
19            reject(request.error);
20        };
21    });
22}
23
24function addUserAsync(db, user) {
25    return new Promise((resolve, reject) => {
26        const transaction = db.transaction('users', 'readwrite');
27        const store = transaction.objectStore('users');
28        const request = store.add(user);
29
30        request.onsuccess = () => resolve();
31        request.onerror = () => reject(request.error);
32    });
33}
34
35async function main() {
36    try {
37        const db = await openDatabase();
38        await addUserAsync(db, { id: 1, name: 'Alice' });
39        console.log('User added successfully');
40    } catch (error) {
41        console.error('Error:', error);
42    }
43}
  • Opakowanie funkcji takich jak openDatabase czy addUserAsync w Promise pozwala intuicyjnie obsługiwać procesy asynchroniczne za pomocą async/await.
  • Pozwala to uniknąć „callback hell” i czyni kod bardziej czytelnym.

Podsumowanie

IndexedDB to bardzo potężne narzędzie, gdy chcesz wykonywać zaawansowane zarządzanie danymi po stronie przeglądarki. Początkowo asynchroniczne przetwarzanie oparte na zdarzeniach może być mylące, ale gdy zrozumiesz strukturę, możesz wykonywać pełne operacje na danych po stronie klienta.

W szczególności pamiętanie o poniższych kwestiach pomoże korzystać z niej bardziej płynnie:.

  • Wykonaj początkową konfigurację za pomocą onupgradeneeded.
  • Zwracaj uwagę na tryb odczytu/zapisu transakcji.
  • Indeksy umożliwiają wydajne wyszukiwanie.
  • Dane można przechowywać jako obiekty, co zapewnia wysoką zgodność z JSON.

Opanowanie IndexedDB sprawia, że zarządzanie danymi w PWA i aplikacjach offline staje się znacznie łatwiejsze.

Możesz śledzić ten artykuł, korzystając z Visual Studio Code na naszym kanale YouTube. Proszę również sprawdzić nasz kanał YouTube.

YouTube Video