IndexedDB : tutoriel pour la mémoire dans le navigateur

La rapidité joue un rôle crucial dans la navigation sur le World Wide Web. Personne n’a envie d’attendre une éternité pour pouvoir accéder à une page. Pour que la connexion à la page soit aussi rapide que possible, il est utile de disposer d’une partie des informations dans le navigateur de l’utilisateur et de ne pas avoir à les transmettre à nouveau. IndexedDB offre cette possibilité : une mémoire placée directement dans le navigateur de l’utilisateur, à laquelle chaque site Internet peut accéder. Comment cela fonctionne-t-il ?

À quoi sert IndexedDB ?

Le fait que les serveurs enregistrent les données des clients, mais aussi que les clients conservent certaines informations relatives à un site Internet apparaît logique. Cela permet d’accélérer la navigation en évitant de charger à nouveau l’intégralité des informations à chaque consultation. Ce stockage rend également possible l’utilisation d’applications Web hors ligne et les informations saisies par les utilisateurs peuvent très bien être hébergées côté client. Les cookies ont été conçus précisément à cette fin. Ces derniers n’ont toutefois qu’une portée très restreinte en termes de volume de fichiers et d’utilisation, une portée très largement insuffisante pour les applications Web modernes. Par ailleurs, les cookies doivent être envoyés via le réseau à chaque consultation HTTP.

Une première solution s’est présentée avec le stockage Web également appelé fréquemment « DOM Storage » : si cette technologie est fortement basée sur l’idée des cookies, elle fait toutefois passer le volume de quelques kilo-octets à 10 Mo, un volume une fois encore insuffisant. Ces fichiers, que l’on surnomme souvent « supercookies », sont structurés de façon extrêmement simple et ne comporte pas les caractéristiques d’une base de données moderne. En raison de leur taille limitée, les cookies et les supercookies ne constituent toutefois pas une solution optimale et ne permettent pas d’avoir des données structurées et des index, ce qui rend impossible toute recherche.

Une nouvelle orientation nous avait tout d’abord été promise avec le développement de Web SQL, une mémoire côté client basée sur SQL. Mais le World Wide Web Consortium (W3C), qui a pour but de développer des normes Web, a cessé son travail sur cette mémoire au profit d’IndexedDB. Sous l’égide de Mozilla, une norme qui supporte aujourd’hui la plupart des navigateurs modernes a ainsi vu le jour.

Navigateurs supportés par IndexedDB

Chrome Firefox Opera Opera mini Safari IE Edge

Quels sont les avantages des bases IndexedDB ?

Avant toute chose, cette norme est une interface configurée dans le navigateur permettant aux sites Internet d’enregistrer des informations directement dans ce dernier. Elle s’appuie sur JavaScript. Chaque site Internet peut ainsi créer sa propre base de données. Et seul le site Web correspondant peut accéder à la base IndexedDB (abréviation d’Indexed Database API), garantissant ainsi la sécurité des données. Plusieurs « Object Storages » sont disponibles dans les bases de données. Là encore, différents formats peuvent y être déposés : chaînes, chiffres, objets, tableaux et dates.

IndexedDB n’est pas une base de données relationnelle, mais un système de tableaux indexés. Il s’agit en fait d’une base de données NoSQL, comme par exemple MongoDB. Les entrées sont toujours créées par paires, avec une clé et une valeur. La valeur est un objet et la clé la propriété de cet objet. S’y ajoutent des index permettant une recherche rapide.

Dans IndexedDB, les actions sont toujours effectuées sous la forme de transactions. Chaque procédure d’écriture, de lecture ou de modification est intégrée à une transaction afin de garantir l’application dans leur intégralité ou non des modifications à la base de données. L’un des avantages d’IndexedDB est que, dans la plupart des cas, le transfert des données n’a pas à être effectué de façon synchronisée. Les opérations sont effectuées de façon asynchrone, ce qui permet d’empêcher tout blocage du navigateur Web pendant l’opération et de garantir son utilisation par l’utilisateur.

La sécurité joue un rôle crucial dans IndexedDB. Il convient en effet de s’assurer que les sites Internet ne peuvent pas accéder aux bases de données d’autres sites Internet. À cette fin, IndexedDB a établi une Same Origin Policy (« politique de même origine ») : le domaine, le protocole de couche d’application et le port doivent être identiques, sans quoi les données ne seront pas disponibles. Dans ce cadre, il est tout à fait possible que des sous-dossiers d’un domaine puissent accéder à la base IndexedDB d’un autre sous-dossier puisque les deux ont la même origine. En revanche, il est impossible d’y accéder lorsqu’un autre port est utilisé ou lorsque le protocole passe de HTTP à HTTPS ou inversement.

Tutoriel IndexedDB : utilisation de cette technologie

Nous vous expliquons IndexedDB par un exemple. Avant de pouvoir créer une base de données et des Object Stores, il convient toutefois de procéder à une vérification. Même si aujourd’hui, IndexedDB est compatible avec tous les navigateurs modernes, ce n’est pas le cas des navigateurs obsolètes. Par conséquent, vérifiez tout d’abord que votre navigateur supporte IndexedDB. Pour ce faire, vérifiez l’objet Window.

Note

Vous pouvez suivre les exemples de code via la console des outils de développeur dans le navigateur. Ces outils vous permettent également de consulter les bases IndexedDB d’autres sites.

if (!window.IndexedDB) {
	alert("IndexedDB n’est pas supporté !");
}

Si votre navigateur ne supporte pas IndexedDB, une fenêtre de dialogue vous en informant apparaît. Vous pouvez également générer un message d’erreur dans votre fichier journal avec console.error.

À présent, ouvrons une base de données. En principe, un site Internet peut ouvrir plusieurs bases de données, mais la pratique a montré qu’il était préférable de créer une seule IndexedDB par domaine. Cette base de données permettra de travailler avec plusieurs Object Stores. L’ouverture d’une base de données fonctionne via une requête asynchrone.

var request = window.IndexedDB.open("Mabasededonnées", 1);

À l’ouverture, elle indique deux arguments : tout d’abord un nom auto-choisi (sous forme de chaîne) puis un numéro de version (sous forme de nombre entier). On commence logiquement à la version 1. L’objet qui en résulte fournit l’un des trois événements suivants :

  • error : une erreur est survenue lors de la création.
  • upgradeneeded : la version de la base de données a été modifiée. Ceci apparaît donc également lors de la création, puisqu’ici aussi le numéro de version passe de non-existant à 1.
  • success : la base de données a été ouverte avec succès.

Il est à présent possible de créer la base de données à proprement parler ainsi qu’un Object Store.

request.onupgradeneeded = function(event) {
	var db = event.target.result;
	var objectStore = db.createObjectStore("Nutzer", { keyPath: "id", autoIncrement: true });
}

Notre Object Store contient le nom Utilisateur. La clé est id, une numérotation simple qui ne cessera d’augmenter avec l’autoIncrement. Vous pouvez maintenant alimenter la base de données ou l’Object Store avec des données. Pour ce faire, créez tout d’abord un ou plusieurs index. Dans notre exemple, nous souhaitons créer un index pour les noms d’utilisateurs et un index pour les adresses email utilisées.

objectStore.createIndex("Nickname", "Nickname", { unique: false });
objectStore.createIndex("eMail", "eMail", { unique: true });

Vous pouvez ainsi trouver en toute simplicité des ensembles de données avec le pseudonyme utilisé par un utilisateur ou son adresse email. La différence entre les deux index est que le pseudonyme ne doit pas être attribué une seule fois, en revanchechaque adresse email ne peut comporter qu’une seule entrée.

Vous pouvez maintenant créer des entrées. Toutes les opérations réalisées avec la base de données doivent être intégrées à une transaction. Il en existe trois types :

  • readonly : permet de lire les données d’un Object Store. Plusieurs transactions de ce type peuvent se dérouler en parallèle, même si elles se rapportent au même domaine.
  • readwrite : permet de lire et de créer des entrées. Ces transactions peuvent uniquement se dérouler en parallèle lorsqu’elles se rapportent à différents domaines.
  • versionchange : procède à des modifications sur l’Object Store ou les index, mais crée et modifie également les entrées. Ce mode ne peut pas être créé manuellement et est automatiquement déclenché avec l’événement « upgradeneeded ».

Pour créer une nouvelle entrée, on utilise donc « readwrite »

const dbconnect = window.IndexedDB.open('Mabasededonnées', 1);

dbconnect.onupgradeneeded = ev => {
  console.log('Upgrade DB');
  const db = ev.target.result;
  const store = db.createObjectStore('utilisateur', { keyPath: 'id', autoIncrement: true });
  store.createIndex('Nickname', 'Nickname', { unique: false });
  store.createIndex('eMail', 'eMail', { unique: true });
}

dbconnect.onsuccess = ev => {
  console.log('mise à jour DB avec succès');
  const db = ev.target.result;
  const transaction = db.transaction('Utilisateur', 'readwrite');
  const store = transaction.objectStore('Utilisateur');
  const data = [
    {Nickname: 'Raptor123', eMail: 'raptor@example.com'},
    {Nickname: 'Dino2', eMail: 'dino@example.com'}
  ];
  data.forEach(el => store.add(el));

  transaction.onerror = ev => {
    console.error('Une erreur est survenue!', ev.target.error.message);
  };

  transaction.oncomplete = ev => {
    console.log('Les données ont été ajoutées avec succès !');
    const store = db.transaction('Utilisateur', 'readonly').objectStore('Utilisateur');
    //const query = store.get(1); // Requête indivuelle
    const query = store.openCursor()

    query.onerror = ev => {
      console.error('Echec de la requête !', ev.target.error.message);
    };

    /*
    // Traitement de la requête individuelle
    query.onsuccess = ev => {
      if (query.result) {
        console.log('Ensemble de données , query.result.Nickname, query.result.eMail);
      } else {
        console.warn(Aucune entrée disponible !');
      }
    };
    */

    query.onsuccess = ev => {
      const cursor = ev.target.result;
      if (cursor) {
        console.log(cursor.key, cursor.value.Nickname, cursor.value.eMail);
        cursor.continue();
      } else {
        console.log('Plus d’entrées disponibles');
      }
    };
  };
};

Cette fonction permet d’ajouter des informations à votre Object Store. Vous pouvez également afficher des messages via la console en fonction du succès de la transaction. En règle générale, on souhaite également lire les données que l’on a placées dans une base IndexedDB. Pour ce faire, on utilise get.

var transaction = db.transaction(["Utilisateur"]);
var objectStore = transaction.objectStore("Utilisateur");
var request = objectStore.get(1);

request.onerror = function(event) {
  console.log("Echec de la requête !");
}

request.onsuccess = function(event) {
  if (request.result) {
    console.log(request.result.Nickname);
    console.log(request.result.eMail);
  } else {
    console.log("Aucune entrée disponible");
  }
};

Ce code vous permet de rechercher l’entrée sous la clé 1, c’est-à-dire avec la valeur id 1. Si la transaction est un échec, un message d’erreur est généré. En revanche, lorsque la transaction est un succès, vous découvrez le contenu des deux entrées Nickname et Email. Si aucune entrée ne peut être trouvée sous le numéro, vous en êtes également informé.

Un curseur vous aide lorsque vous recherchez plusieurs entrées en même temps. Cette fonction appelle une entrée après l’autre. Dans ce cadre, vous pouvez prendre en considération toutes les entrées de la base de données ou sélectionner uniquement un domaine clé déterminé.

var objectStore = db.transaction("Utilisateur").objectStore("Utilisateur");
objectStore.openCursor().onsuccess = function(event) {
  var cursor = event.target.result;
  if (cursor) {
    console.log(cursor.key);
    console.log(cursor.value.Nickname);
    console.log(cursor.value.eMail);
    cursor.continue();
  } else {
    console.log("Plus d’entrées disponibles !");
  }
};

Nous avons généré deux index au préalable pour pouvoir consulter ces informations. Cette consultation s’effectue aussi via get.

var index = objectStore.index("Nickname");

index.get("Raptor123").onsuccess = function(event) {
  console.log(event.target.result.eMail);
};

Enfin, si vous souhaitez supprimer une entrée d’une base de données, procédez de la même façon que pour ajouter un ensemble de données avec une transaction readwrite.

var request = db.transaction(["Utilisateur"], "readwrite")
  .objectStore("Utilisateur")
  .delete(1);

request.onsuccess = function(event) {
  console.log("Entrée surpprimée avec succès !");
};
En résumé

Cet article vous a accompagné dans vos premiers pas avec IndexedDB. Vous trouverez de plus amples informations auprès de Mozilla ou Google. Toutefois, Google utilise une bibliothèque spéciale dans le code type, ce qui explique pourquoi le code peut différer en partie de celui de Mozilla.