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 & 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 zrequest.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ąckeyPath
oraz innych opcji. Ten proces należy wykonać wewnątrz zdarzeniaonupgradeneeded
. -
keyPath: 'id'
UstawkeyPath
na właściwośćid
, która jednoznacznie identyfikuje każdy rekord jako klucz główny. Dzięki temuid
może być używane automatycznie podczas dodawania, wyszukiwania lub aktualizowania danych. -
createIndex
Aby umożliwić wyszukiwanie, użyj metodycreateIndex
, aby utworzyć indeks na podstawie właściwościname
. Ustawiającunique: false
, pozwalasz na istnienie wielu rekordów o tej samej wartościname
.
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 (obiektIDBDatabase
), 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 przypadkureadwrite
). - 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ącIDBKeyRange.only
, możesz wybrać tylko te rekordy, których klucz dokładnie odpowiadaoldName
. Jest to przydatne, gdy chcesz bezpośrednio uzyskać dostęp do konkretnej wartości. -
cursor.update()
Po zaktualizowaniucursor.value
, wywołanieupdate()
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ąccursor.delete()
, rekord na bieżącej pozycji kursora zostaje usunięty. Ponieważ wynik zwracany jest asynchronicznie, proces można sprawdzić wonsuccess
.
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
czyaddUserAsync
wPromise
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.