JavaScript et IndexedDB

JavaScript et IndexedDB

Dans cet article, nous allons expliquer JavaScript et IndexedDB.

Ce tutoriel propose une explication pas à pas de JavaScript et IndexedDB, avec du code d’exemple pratique à chaque étape afin d’approfondir votre compréhension.

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 et IndexedDB

IndexedDB est une base de données clé-valeur asynchrone intégrée aux navigateurs. Elle offre des fonctionnalités similaires à celles des bases de données relationnelles et vous permet de stocker et rechercher de grandes quantités de données structurées côté client. Elle est particulièrement utile pour les applications compatibles hors ligne et les PWAs (Progressive Web Apps).

Caractéristiques de IndexedDB

  • Fonctionne de manière asynchrone et pilotée par les événements.
  • Les objets JavaScript peuvent être stockés dans des object stores.
  • La recherche par requêtes ou par index est possible.
  • Elle dispose d'une grande capacité de stockage (plusieurs centaines de Mo ou plus), permettant de stocker beaucoup plus de données que les cookies ou localStorage.

Ouverture et création d'une base de données

Pour utiliser IndexedDB, il faut d'abord ouvrir une base de données. Si elle n'existe pas, elle est créée automatiquement.

1const request = indexedDB.open('MyDatabase', 1); // Specify DB name and version
  • La méthode open ouvre la base de données de façon asynchrone et déclenche les trois événements suivants.

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};
  • L’événement onsuccess se déclenche lorsque la base de données s’ouvre avec succès. Les opérations suivantes doivent être exécutées en utilisant request.result, qui devient disponible à ce moment-là.

onerror

1// Fired when database fails to open
2request.onerror = (event) => {
3    console.error('Failed to open database:', event.target.error);
4};
  • L’événement onerror se déclenche lorsque l’ouverture de la base de données échoue. C'est ici que doivent être effectués la gestion et le journalisation des erreurs.

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 est déclenché lors de la création de la base de données ou si la version spécifiée est supérieure à la version actuelle. La création des tables (object stores) et la définition du schéma doivent être effectuées à ce moment-là.

Création d’un object store

Commencez par créer un 'object store' (équivalent d’une table) dans 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};

Ici, les paramètres suivants sont définis :.

  • createObjectStore createObjectStore est une méthode permettant de créer un nouvel object store dans la base de données. Vous pouvez stocker des enregistrements dans un object store et définir la gestion des données via keyPath et d’autres options. Ce processus doit être réalisé dans l’événement onupgradeneeded.

  • keyPath: 'id' Réglez keyPath sur la propriété id, qui identifie chaque enregistrement de façon unique comme clé primaire. Cela permet d’utiliser automatiquement id lors de l’ajout, de la recherche ou de la mise à jour des données.

  • createIndex Utilisez la méthode createIndex pour créer un index basé sur la propriété name afin d’effectuer des recherches. En définissant unique: false, plusieurs enregistrements avec le même name sont autorisés.

Gestion des connexions réussies à la base de données

Attribuez les traitements à exécuter en cas de connexion réussie à l'événement onsuccess. Dans ce processus, vous obtenez l’instance de la base de données et préparez la lecture et l’écriture des données.

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};
  • Ce processus s’exécute lorsque la connexion à la base de données IndexedDB aboutit.

  • event.target.result contient l’instance ouverte de la base de données (objet IDBDatabase), utilisée pour démarrer des transactions et accéder aux object stores.

  • Les opérations réelles de lecture et d’écriture (ajout, récupération, modification et suppression de données) sont effectuées à l’aide de l’objet db.

À ce stade, la base de données est prête, vous pouvez donc lancer des transactions en toute sécurité.

Ajout de données

Voici comment ajouter de nouvelles données à 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};
  • Créez une transaction avec db.transaction() en spécifiant l’object store concerné et le mode (ici, readwrite).
  • Ajoutez les nouvelles données avec la méthode store.add().
  • Les transactions sont validées automatiquement, mais pour regrouper plusieurs opérations vous pouvez les gérer via les événements de fin de transaction.

Récupération de données (recherche par clé primaire)

Voici comment récupérer des données spécifiques avec la clé primaire.

 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};
  • Créez une transaction en lecture seule avec db.transaction().
  • Utilisez store.get(id) pour récupérer les données correspondant à la clé primaire spécifiée.
  • onsuccess est appelé lorsque la récupération est réussie, et le résultat est affiché s’il existe. S’il n’y a pas de résultat, cela signifie qu’il n’existe pas de donnée correspondante.

Recherche via un index

Si vous souhaitez rechercher par d’autres propriétés que la clé primaire, utilisez l’index créé au préalable.

 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};
  • Accédez à l’index name créé précédemment avec store.index('name').
  • index.get(value) permet de récupérer le premier enregistrement correspondant à la valeur. Si plusieurs enregistrements ont la même valeur, vous pouvez tous les récupérer avec index.getAll(value).

Mise à jour de données

Pour mettre à jour des données existantes (écrasement), utilisez la méthode put().

Si un enregistrement avec la même clé primaire existe, il est mis à jour ; sinon, un nouvel enregistrement est ajouté.

 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() est une méthode pratique qui prend en charge à la fois l’ajout et la mise à jour.
  • Si des données ayant la même clé primaire existent déjà, elles seront écrasées.
  • Si vous souhaitez vérifier l’existence avant de mettre à jour, vous pouvez utiliser get() pour confirmer au préalable.

Suppression de données

Pour supprimer des données correspondant à une clé primaire donnée, utilisez la méthode 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};
  • Utilisez store.delete(id) pour supprimer des données avec la clé primaire correspondante.
  • Notez que même si la donnée n’existe pas, aucune erreur ne sera générée et l’opération sera considérée comme réussie.
  • La gestion des erreurs rendra votre code plus robuste.

Récupération de toutes les données

getAll()

Pour récupérer tous les enregistrements de l’object store, utilisez la méthode 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() récupère tous les enregistrements de l’object store spécifié sous forme de tableau.
  • Même lors de la récupération d’un grand volume de données d’un coup, le traitement reste efficace.
  • Les résultats sont stockés sous forme de tableau dans request.result.
  • Ajoutez toujours une gestion des erreurs afin de pouvoir traiter les échecs.

openCursor()

openCursor() est une méthode permettant de parcourir séquentiellement les enregistrements d’un object store ou d’un index. Elle est utile si vous souhaitez traiter les données une à une plutôt que d’en récupérer une grande quantité d’un coup.

 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};
  • En utilisant openCursor(), vous démarrez un curseur et récupérez les enregistrements de l’object store un par un.
  • Utilisez cursor.value pour obtenir l’objet de données de l’enregistrement courant.
  • Passez à l’enregistrement suivant en utilisant cursor.continue().
  • Lorsque cursor === null, tous les enregistrements ont été parcourus.

Exemple d'un processus de mise à jour utilisant openCursor()

Par exemple, le processus de modification du nom d'un utilisateur dont le name est Alice en Alicia serait le suivant :.

 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) En utilisant IDBKeyRange.only, vous pouvez cibler uniquement les enregistrements dont la clé correspond exactement à oldName. Ceci est utile lorsque vous souhaitez accéder directement à une valeur spécifique.

  • cursor.update() Après la mise à jour de cursor.value, l'appel à update() remplacera l'enregistrement correspondant.

  • Gestion de plusieurs correspondances En appelant cursor.continue(), vous pouvez déplacer le curseur vers le prochain enregistrement correspondant. Cela vous permet de traiter plusieurs enregistrements correspondant à la même clé ou condition, de manière séquentielle.

  • Gestion des erreurs En affichant des journaux dans onerror lorsqu'un processus échoue, il devient plus facile d'enquêter sur les causes et de résoudre les problèmes pendant l'opération.

Exemple d'un processus de suppression utilisant openCursor()

Par exemple, le processus de suppression de tous les utilisateurs dont le name est Bob ressemblerait à ceci :.

 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() Avec cursor.delete(), l'enregistrement à la position actuelle du curseur est supprimé. Comme le résultat est retourné de façon asynchrone, vous pouvez vérifier le processus dans onsuccess.

Remarques sur les transactions et le traitement asynchrone

IndexedDB est asynchrone et basé sur les événements. Toutes les opérations doivent être gérées avec les événements onsuccess ou onerror. Lorsque vous souhaitez regrouper plusieurs processus, il est pratique de les encapsuler dans une 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}
  • Encapsuler des fonctions comme openDatabase ou addUserAsync dans une Promise permet de gérer les processus asynchrones de façon intuitive avec async/await.
  • Cela permet d'éviter l'enfer des callbacks et rend le code plus lisible.

Résumé

IndexedDB est une fonctionnalité très puissante lorsque vous souhaitez effectuer une gestion avancée des données côté navigateur. Au début, le traitement asynchrone basé sur les événements peut prêter à confusion, mais une fois que vous avez compris la structure, vous pouvez effectuer des opérations de données à grande échelle côté client.

En particulier, garder à l'esprit les points suivants vous aidera à l'utiliser plus facilement :.

  • Effectuez la configuration initiale avec onupgradeneeded.
  • Faites attention au mode lecture/écriture des transactions.
  • Les index permettent une recherche efficace.
  • Les données peuvent être stockées sous forme d'objets, ce qui les rend hautement compatibles avec JSON.

En maîtrisant IndexedDB, la gestion des données pour les PWA et les applications hors-ligne devient beaucoup plus facile.

Vous pouvez suivre l'article ci-dessus avec Visual Studio Code sur notre chaîne YouTube. Veuillez également consulter la chaîne YouTube.

YouTube Video