Blog ENI : Toute la veille numérique !
🐠 -25€ dès 75€ 
+ 7 jours d'accès à la Bibliothèque Numérique ENI. Cliquez ici
Accès illimité 24h/24 à tous nos livres & vidéos ! 
Découvrez la Bibliothèque Numérique ENI. Cliquez ici
  1. Livres et vidéos
  2. Progressive Web App
  3. IndexedDB
Extrait - Progressive Web App Utilisez les standards du web pour développer vos applications mobiles
Extraits du livre
Progressive Web App Utilisez les standards du web pour développer vos applications mobiles Revenir à la page d'achat du livre

IndexedDB

Introduction

Nous avons l’habitude de manipuler des bases de données côté serveur afin de persister des données. Mais que se passe-t-il si nous n’avons pas de connexion ? Le navigateur n’arrive pas à communiquer avec le serveur, donc impossible de persister des données dans la base principale. Il serait intéressant de pouvoir persister des données côté client dans certains cas. Nous allons présenter dans ce chapitre les différentes solutions qui s’offrent à nous.

API localStorage et sessionStorage

La première que nous pourrions envisager pour sauvegarder des données dans le navigateur est l’API localStorage. Par sa simplicité, cette API est une excellente solution pour des cas d’utilisation très basiques comme la sauvegarde des préférences de l’utilisateur. Pour pouvoir l’utiliser, nous allons manipuler directement l’objet localStorage défini sur l’objet windows de notre navigateur.

Contrairement à l’API sessionStorage, qui se base sur la même interface d’objet, localStorage permet de sauvegarder des données afin de les rendre persistantes d’une session à une autre. Ainsi, si nous sauvegardons certaines données dans un onglet de notre navigateur, ces mêmes données sont disponibles dans les autres onglets que nous ouvrons.

Toutes les actions que nous pouvons exécuter sont des tâches synchrones. Voici l’ensemble des méthodes disponibles :

  • setItem() : permet d’ajouter une nouvelle donnée dans le localStorage. Attention, nous ne pouvons pas sauvegarder d’objet. Pour cela, il faut au préalable le convertir en JSON avec la méthode JSON.stringify. Pour les autres types de valeurs, elles sont converties en chaîne de caractères.

localStorage.setItem('item', JSON.stringigy({ ...

API IndexedDB

Dans une réelle application, en plus de sauvegarder des ressources statiques en cache, nous avons généralement besoin de sauvegarder des données pour les réutiliser par la suite. Dans le monde du développement côté serveur, le meilleur endroit pour sauvegarder des données est une base de données. Pouvons-nous le faire également côté navigateur ? C’est à ce moment-là que l’API IndexedDB entre en jeu.

IndexedDB est une base de données NoSQL que nous pouvons utiliser côté navigateur pour sauvegarder des données et ensuite faire des requêtes afin de les récupérer. Dans cette base de données, nous pouvons sauvegarder tous les objets qui peuvent être clonés : string, object, number, array.

Nous pouvons citer deux exemples de cas d’utilisation d’IndexedDB dans une application :

  • Nous souhaitons que notre application fonctionne hors-ligne, nous allons donc sauvegarder les données récupérées depuis une API dans IndexedDB afin de pouvoir les réutiliser si l’utilisateur n’a pas de connexion.

  • Nous souhaitons proposer à l’utilisateur de commencer à se servir l’application même s’il ne possède aucun compte. Nous allons donc sauvegarder dans IndexedDB toutes les interactions...

Manipuler des objectStore

Les traitements proposés par IndexedDB sont des opérations asynchrones afin d’éviter de bloquer le reste de l’application, à cause du fonctionnement mono-threadé du langage JavaScript. Nous allons à présent détailler cette API.

Avec IndexedDB, nous pouvons manipuler plusieurs bases de données pour une même application, même si en règle générale nous allons nous limiter à une seule.

Pour indiquer la base de données que nous souhaitons utiliser, nous allons faire appel à la méthode open. Cette méthode accepte deux paramètres :

  • name : le nom de la base de données.

  • version : la version de la base de données.

window.indexedDB.open(name, version) 

Dans l’exemple ci-après, nous initialisons une variable db permettant de manipuler la base de données nommée article.

const db = window.indexedDB.open('article', 1); 

Une fois la base de données créée, nous pouvons créer des objectStore, qui correspondent à des tables dans le monde des bases de données relationnelles. Ces objectStore nous permettront de regrouper les données que nous souhaitons sauvegarder.

Pour cela, nous utilisons la méthode createObjectStore. Cette méthode accepte deux paramètres :...

Manipuler des données

Afin de faciliter l’écriture de requêtes pour interagir avec IndexedDB, nous allons utiliser une librairie utilitaire permettant de réduire la complexité de cette API. Notre choix se porte sur IndexedDB Promise, développée par Jake Archibald. Avec cette librairie, nous manipulerons des Promises dans les exemples que nous allons présenter par la suite.

L’exemple suivant montre la syntaxe pour ouvrir une base de données via cette API. La librairie est définie dans la variable idb. Pour ouvrir une base de données, nous utilisons la méthode openDB au lieu de la méthode open de l’API officielle. Autre différence : la valeur retournée par cette fonction est un objet Promise que nous manipulerons tout au long de la présentation de cette API.

const dbPromise = idb.openDB(‘pwa', 2); 

Une fois l’objectStore créé, il est à présent temps de manipuler des données, en les indexant et en les récupérant depuis IndexedDB. IndexedDB propose une API permettant de manipuler l’objectStore. Nous y trouvons les méthodes nécessaires pour faire le traitement basique sur une donnée (CRUD pour Create, Read, Update et Delete) :

someObjectStore.getAll() ; 
someObjectStore.add(data, optionalKey); 
someObjectStore.get(primaryKey); 
someObjectStore.put(data, optionalKey); 
someObjectStore.delete(primaryKey); 

Ces API nécessitent l’utilisation de transactions afin de nous assurer que la requête d’écriture que nous souhaitons exécuter s’est terminée...

Cursor

La dernière solution pour récupérer des enregistrements indexés dans IndexedDB est d’utiliser des cursors. Un cursor permet de récupérer au fur et à mesure des enregistrements seulement si nous en avons besoin.

Pour cela, nous appellons d’abord la méthode openCursor. Dans la méthode qui sera appelée lorsque la Promise sera résolue, le paramètre défini correspondra au cursor qui vient d’être créé.

dbPromise.then(function(db) { 
  const tx = db.transaction('store', 'readonly'); 
  const store = tx.objectStore('store'); 
  return store.openCursor() ; 
}).then(cursor => { 
  ...  
})  

Sur cet objet cursor, nous pouvons récupérer la donnée de l’enregistrement courant via la propriété value.

dbPromise.then(function(db) { 
  const tx = db.transaction('store', 'readonly'); 
  const store = tx.objectStore('store'); 
  return store.openCursor() ; 
}).then(cursor => { 
  console.log(cursor.value) ; 
}) ; 

Pour pouvoir récupérer l’enregistrement suivant de manière récursive, nous appelons la méthode...

Index

L’API utilise des index nous permettant de faire des recherches performantes sur nos données. Par défaut, nous pouvons faire des recherches seulement via la clé primaire générée lors de l’indexation du document. Mais en fonction de nos besoins, nous devons parfois faire des recherches à partir d’autres champs des objets : recherche par e-mail, par nom… Pour cela, nous devons mettre en place ces index.

Créer des index permet de lancer de nouvelles recherches, mais fait également grossir la base de données. Il faut donc les utiliser avec précaution.

Pour créer un nouvel index, nous utilisons la méthode createIndex disponible sur un objet objectStore.

createIndex(nomIndex, nomCle, options) ; 

Cette méthode accepte trois paramètres :

  • nomIndex : le nom que nous souhaitons donner à l’index

  • nomCle : le nom de la propriété qui doit être utilisée pour créer l’index.

  • options : les options permettant de configurer l’index. Par exemple, l’option unique:true empêche la duplication de la clé.

Dans l’exemple ci-après, nous créons un index que nous appelons articleNameIndex sur la propriété articleName des objets. Nous souhaitons de plus que cette propriété soit unique sur l’ensemble du jeu de données....

Stratégie de stockage

Le fait d’utiliser IndexedDB dans nos applications ne veut pas dire que nous pouvons stocker autant de données que nous le souhaitons. En effet, le navigateur propose deux stratégies de stockage que nous allons présenter dans cette section : la stratégie best-effort et la stratégie permanent.

La stratégie best-effort est celle utilisée par défaut. Elle est transparente pour l’utilisateur, car elle ne nécessite aucune validation de celui-ci. Mais cette stratégie est limitée en espace de stockage. Et cette limite est différente en fonction du navigateur utilisé. En effet, les navigateurs allouent tout d’abord un pourcentage de l’espace disque disponible. Sous Firefox, ce pourcentage est de 50 %, et sous Chrome, il est de 60 %. Quant à Internet Explorer et Safari, la limite est plutôt de 250 Mo pour le premier et de 1 Go pour le second.

Ensuite, le navigateur alloue un pourcentage par origine (origin). Sous Chrome, ce pourcentage est de 20 %.

Si la limite par origine est dépassée, le navigateur émet une exception QuotaExceededError que nous pouvons utiliser pour éventuellement afficher un message d’erreur à l’utilisateur.

Si la limite en fonction de l’espace libre disponible sur le disque dur est dépassée, le navigateur...

Compatibilité navigateurs

Le tableau suivant récapitule le support d’IndexedDB par l’ensemble des navigateurs du marché.

images/08El01.png

Nous allons présenter une solution permettant de garder une application fonctionnelle pour les navigateurs ne supportant pas IndexedDB. La première étape consiste à extraire le code d’accès à la base de données dans un objet spécifique chargé uniquement de faire appel à IndexedDB.

class DataIndexedDBAccess { 
  get(key){ ... } 
  remove(key){ ... } 
  add(){ ... } 
  update(key){ ... } 
} 

Ensuite, pour les navigateurs ne supportant pas IndexedDB, nous fournissons une deuxième implémentation utilisant par exemple LocalStorage. Nous nommons cette seconde implémentation DataLocalStorageAccess.

Pour finir, au lancement de l’application, en fonction du support ou pas d’IndexedDB, il s’agit de choisir l’implémentation à utiliser et de l’instancier. Le résultat de cette installation peut être sauvegardé dans l’objet windows par exemple.

window.dataAccess = 'indexedDB' in window 
? new DataIndexedDBAccess() 
: new DataLocalStorageAccess() 

Bien évidemment, nous n’avons pas à implémenter ce pattern dans...

Application fil rouge

Nous allons mettre en place le stockage des repositories dans une base de données IndexedDB qui sera utilisée si et seulement si l’utilisateur est hors connexion. Pour cela, nous allons utiliser localForage.

Il faut tout d’abord importer cette librairie dans le projet. Dans de vrais projets, cet import se fait via une dépendance NPM. Pour notre application, et pour faire simple, nous allons l’importer depuis un CDN. Ci-après, nous importons la librairie dans le fichier index.html de l’application.

<script  
src="https://cdnjs.cloudflare.com/ajax/libs/localforage 
/1.7.3/localforage.min.js"></script> 

Comme nous souhaitons utiliser cette librairie lorsque l’utilisateur est hors connexion, il ne faut pas oublier de définir ce fichier comme étant une ressource devant être mise en cache par le Service Worker.

const files = [ 
  "/", 
  "/script.js",, 
"https://cdn.jsdelivr.net/npm/bulma@0.8.0/css/bulma.css", 
 
"https://bulma.io/images/placeholders/1280x960.png", 
 
"https://bulma.io/images/placeholders/96x96.png ", 
 
"https://cdnjs.cloudflare.com/ajax/libs/localforage/1.7.3/ 
localforage.min.js" 
]; 

L’avantage de l’utilisation d’une librairie comme localForage est d’avoir...