Étape 2: Importez une application Web existante

Au cours de cette étape, vous allez découvrir:

  • Comment adapter une application Web existante à la plate-forme d'applications Chrome
  • Assurer la conformité de vos scripts d'application avec la CSP (Content Security Policy)
  • Mettre en œuvre le stockage local à l'aide de chrome.storage.local

Temps estimé pour terminer cette étape: 20 minutes.
Pour prévisualiser ce que vous allez réaliser au cours de cette étape, accédez au bas de cette page ↓.

Importer une application Todo existante

Pour commencer, importez la version JavaScript vanilla de TodoMVC, une application de benchmark courante, dans votre projet.

Nous avons inclus une version de l'application TodoMVC dans le code ZIP du code de référence du dossier todomvc. Copiez tous les fichiers (y compris les dossiers) de todomvc dans le dossier de votre projet.

Copier le dossier todomvc dans le dossier de l'atelier de programmation

Vous serez invité à remplacer index.html. Vous pouvez l'accepter.

Remplacer index.html

Le dossier de votre application doit maintenant comporter la structure de fichiers suivante:

Dossier du nouveau projet

Les fichiers surlignés en bleu proviennent du dossier todomvc.

Actualisez maintenant votre application (effectuez un clic droit > Actualiser l'application). L'interface utilisateur de base devrait s'afficher, mais vous ne pourrez pas ajouter de tâches.

Rendre les scripts conformes à la Content Security Policy (CSP)

Ouvrez la console DevTools (effectuez un clic droit > Inspecter l'élément, puis sélectionnez l'onglet Console). Un message d'erreur indiquant le refus d'exécuter un script intégré s'affiche:

Erreur dans le journal de la console CSP dans l'application Liste de tâches

Corrigeons cette erreur en rendant l'application conforme à la Content Security Policy. L'une des non-conformités les plus courantes avec les CSP est causée par le code JavaScript intégré. Parmi les exemples de code JavaScript intégré, citons les gestionnaires d'événements en tant qu'attributs DOM (par exemple, <button onclick=''>) et les balises <script> dont le contenu se trouve dans le code HTML.

La solution est simple: déplacez le contenu intégré vers un nouveau fichier.

1. Vers le bas du fichier index.html, supprimez le code JavaScript intégré et incluez à la place js/bootstrap.js:

<script src="bower_components/director/build/director.js"></script>
<script>
  // Bootstrap app data
  window.app = {};
</script>
<script src="js/bootstrap.js"></script>
<script src="js/helpers.js"></script>
<script src="js/store.js"></script>

2. Dans le dossier js, créez un fichier nommé bootstrap.js. Déplacez le code précédemment intégré pour qu'il se trouve dans ce fichier:

// Bootstrap app data
window.app = {};

Si vous actualisez la page maintenant, mais que vous vous en approchez, l'application Todo ne fonctionne toujours pas.

Convertir localStorage en chrome.storage.local

Si vous ouvrez la console des outils de développement, l'erreur précédente ne devrait plus s'afficher. Toutefois, une nouvelle erreur indique que window.localStorage n'est pas disponible:

Erreur de journal de la console localStorage dans l&#39;application Liste de tâches

Les applications Chrome ne sont pas compatibles avec localStorage, car localStorage est synchrone. L'accès synchrone aux ressources bloquantes (E/S) dans un environnement d'exécution monothread peut empêcher votre application de répondre.

Les applications Chrome disposent d'une API équivalente qui permet de stocker des objets de manière asynchrone. Cela permettra d'éviter le processus de sérialisation objet->chaîne->objet, parfois coûteux.

Pour résoudre le message d'erreur dans notre application, vous devez convertir localStorage en chrome.storage.local.

Mettre à jour les autorisations des applications

Pour utiliser chrome.storage.local, vous devez demander l'autorisation storage. Dans le fichier manifest.json, ajoutez "storage" au tableau permissions:

"permissions": ["storage"],

En savoir plus sur local.storage.set() et local.storage.get()

Pour enregistrer et récupérer des tâches, vous devez connaître les méthodes set() et get() de l'API chrome.storage.

La méthode set() accepte un objet de paires clé/valeur comme premier paramètre. Le deuxième paramètre est une fonction de rappel facultative. Exemple :

chrome.storage.local.set({secretMessage:'Psst!',timeSet:Date.now()}, function() {
  console.log("Secret message saved");
});

La méthode get() accepte un premier paramètre facultatif pour les clés de datastore que vous souhaitez récupérer. Une seule clé peut être transmise sous forme de chaîne. Plusieurs clés peuvent être organisées dans un tableau de chaînes ou un objet de dictionnaire.

Le deuxième paramètre, qui est obligatoire, est une fonction de rappel. Dans l'objet renvoyé, utilisez les clés demandées dans le premier paramètre pour accéder aux valeurs stockées. Exemple :

chrome.storage.local.get(['secretMessage','timeSet'], function(data) {
  console.log("The secret message:", data.secretMessage, "saved at:", data.timeSet);
});

Si vous souhaitez appliquer (get()) tout ce qui se trouve actuellement dans chrome.storage.local, omettez le premier paramètre:

chrome.storage.local.get(function(data) {
  console.log(data);
});

Contrairement à localStorage, vous ne pouvez pas inspecter des éléments stockés localement à l'aide du panneau des ressources des outils de développement. Vous pouvez toutefois interagir avec chrome.storage à partir de la console JavaScript comme suit:

Utiliser la console pour déboguer chrome.storage

Prévisualiser les modifications requises de l'API

La plupart des étapes restantes dans la conversion de l'application Todo sont de légères modifications apportées aux appels d'API. Il est nécessaire de modifier tous les emplacements où localStorage est actuellement utilisé, bien que cela soit chronophage et source d'erreurs.

Les principales différences entre localStorage et chrome.storage proviennent de la nature asynchrone de chrome.storage:

  • Au lieu d'écrire dans localStorage à l'aide d'une attribution simple, vous devez utiliser chrome.storage.local.set() avec des rappels facultatifs.

    var data = { todos: [] };
    localStorage[dbName] = JSON.stringify(data);
    

    contre

    var storage = {};
    storage[dbName] = { todos: [] };
    chrome.storage.local.set( storage, function() {
      // optional callback
    });
    
  • Au lieu d'accéder directement à localStorage[myStorageName], vous devez utiliser chrome.storage.local.get(myStorageName,function(storage){...}), puis analyser l'objet storage renvoyé dans la fonction de rappel.

    var todos = JSON.parse(localStorage[dbName]).todos;
    

    contre

    chrome.storage.local.get(dbName, function(storage) {
      var todos = storage[dbName].todos;
    });
    
  • La fonction .bind(this) est utilisée sur tous les rappels pour s'assurer que this fait référence au this du prototype Store. (Pour en savoir plus sur les fonctions liées, consultez la documentation MDN : Function.prototype.bind().)

    function Store() {
      this.scope = 'inside Store';
      chrome.storage.local.set( {}, function() {
        console.log(this.scope); // outputs: 'undefined'
      });
    }
    new Store();
    

    contre

    function Store() {
      this.scope = 'inside Store';
      chrome.storage.local.set( {}, function() {
        console.log(this.scope); // outputs: 'inside Store'
      }.bind(this));
    }
    new Store();
    

Gardez à l'esprit ces principales différences lorsque nous aborderons la récupération, l'enregistrement et la suppression des tâches dans les sections suivantes.

Récupérer les tâches

Mettons à jour l'application Liste de tâches afin de récupérer les tâches:

1. La méthode constructeur Store se charge d'initialiser l'application de liste de tâches avec tous les éléments de liste de tâches existants du datastore. La méthode vérifie d'abord si le datastore existe. Si ce n'est pas le cas, il créera un tableau vide de todos et l'enregistrera dans le datastore afin d'éviter les erreurs de lecture lors de l'exécution.

Dans js/store.js, convertissez l'utilisation de localStorage dans la méthode constructeur pour utiliser chrome.storage.local à la place:

function Store(name, callback) {
  var data;
  var dbName;

  callback = callback || function () {};

  dbName = this._dbName = name;

  if (!localStorage[dbName]) {
    data = {
      todos: []
    };
    localStorage[dbName] = JSON.stringify(data);
  }
  callback.call(this, JSON.parse(localStorage[dbName]));

  chrome.storage.local.get(dbName, function(storage) {
    if ( dbName in storage ) {
      callback.call(this, storage[dbName].todos);
    } else {
      storage = {};
      storage[dbName] = { todos: [] };
      chrome.storage.local.set( storage, function() {
        callback.call(this, storage[dbName].todos);
      }.bind(this));
    }
  }.bind(this));
}

2. La méthode find() est utilisée lors de la lecture de tâches à partir du modèle. Les résultats renvoyés changent selon que vous filtrez les données sur "Tous", "Actifs" ou "Terminés".

Convertissez find() pour utiliser chrome.storage.local:

Store.prototype.find = function (query, callback) {
  if (!callback) {
    return;
  }

  var todos = JSON.parse(localStorage[this._dbName]).todos;

  callback.call(this, todos.filter(function (todo) {
  chrome.storage.local.get(this._dbName, function(storage) {
    var todos = storage[this._dbName].todos.filter(function (todo) {
      for (var q in query) {
         return query[q] === todo[q];
      }
      });
    callback.call(this, todos);
  }.bind(this));
  }));
};

3. Comme find(), findAll() obtient toutes les tâches du modèle. Convertissez findAll() pour utiliser chrome.storage.local:

Store.prototype.findAll = function (callback) {
  callback = callback || function () {};
  callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
  chrome.storage.local.get(this._dbName, function(storage) {
    var todos = storage[this._dbName] && storage[this._dbName].todos || [];
    callback.call(this, todos);
  }.bind(this));
};

Enregistrer les tâches

La méthode save() actuelle présente un défi. Elle dépend de deux opérations asynchrones (get et set) qui opèrent à chaque fois sur l'ensemble du stockage JSON monolithique. Toute mise à jour groupée de plusieurs tâches à effectuer (par exemple, "Marquer toutes les tâches comme terminées") entraîne un risque lié aux données appelé lecture après écriture. Ce problème ne se produirait pas si nous utilisions un stockage de données plus approprié, comme IndexedDB, mais nous essayons de réduire l'effort de conversion pour cet atelier de programmation.

Il existe plusieurs façons de résoudre ce problème. Nous allons donc profiter de cette opportunité pour refactoriser légèrement save() en utilisant un tableau d'ID de tâches à mettre à jour simultanément:

1. Pour commencer, encapsulez tout ce qui se trouve déjà dans save() avec un rappel chrome.storage.local.get():

Store.prototype.save = function (id, updateData, callback) {
  chrome.storage.local.get(this._dbName, function(storage) {
    var data = JSON.parse(localStorage[this._dbName]);
    // ...
    if (typeof id !== 'object') {
      // ...
    }else {
      // ...
    }
  }.bind(this));
};

2. Convertissez toutes les instances localStorage avec chrome.storage.local:

Store.prototype.save = function (id, updateData, callback) {
  chrome.storage.local.get(this._dbName, function(storage) {
    var data = JSON.parse(localStorage[this._dbName]);
    var data = storage[this._dbName];
    var todos = data.todos;

    callback = callback || function () {};

    // If an ID was actually given, find the item and update each property
    if ( typeof id !== 'object' ) {
      // ...

      localStorage[this._dbName] = JSON.stringify(data);
      callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
      chrome.storage.local.set(storage, function() {
        chrome.storage.local.get(this._dbName, function(storage) {
          callback.call(this, storage[this._dbName].todos);
        }.bind(this));
      }.bind(this));
    } else {
      callback = updateData;

      updateData = id;

      // Generate an ID
      updateData.id = new Date().getTime();

      localStorage[this._dbName] = JSON.stringify(data);
      callback.call(this, [updateData]);
      chrome.storage.local.set(storage, function() {
        callback.call(this, [updateData]);
      }.bind(this));
    }
  }.bind(this));
};

3. Ensuite, mettez à jour la logique pour opérer sur un tableau au lieu d'un seul article:

Store.prototype.save = function (id, updateData, callback) {
  chrome.storage.local.get(this._dbName, function(storage) {
    var data = storage[this._dbName];
    var todos = data.todos;

    callback = callback || function () {};

    // If an ID was actually given, find the item and update each property
    if ( typeof id !== 'object' || Array.isArray(id) ) {
      var ids = [].concat( id );
      ids.forEach(function(id) {
        for (var i = 0; i < todos.length; i++) {
          if (todos[i].id == id) {
            for (var x in updateData) {
              todos[i][x] = updateData[x];
            }
          }
        }
      });

      chrome.storage.local.set(storage, function() {
        chrome.storage.local.get(this._dbName, function(storage) {
          callback.call(this, storage[this._dbName].todos);
        }.bind(this));
      }.bind(this));
    } else {
      callback = updateData;

      updateData = id;

      // Generate an ID
      updateData.id = new Date().getTime();

      todos.push(updateData);
      chrome.storage.local.set(storage, function() {
        callback.call(this, [updateData]);
      }.bind(this));
    }
  }.bind(this));
};

Marquer les tâches comme terminées

Maintenant que l'application fonctionne sur des tableaux, vous devez modifier la façon dont elle gère un utilisateur qui clique sur le bouton Effacer les tâches terminées (#):

1. Dans controller.js, mettez à jour toggleAll() pour n'appeler toggleComplete() qu'une seule fois avec un tableau de tâches au lieu de marquer une tâche comme terminée une par une. Supprimez également l'appel de _filter() puisque vous allez ajuster le _filter() de toggleComplete.

Controller.prototype.toggleAll = function (e) {
  var completed = e.target.checked ? 1 : 0;
  var query = 0;
  if (completed === 0) {
    query = 1;
  }
  this.model.read({ completed: query }, function (data) {
    var ids = [];
    data.forEach(function (item) {
      this.toggleComplete(item.id, e.target, true);
      ids.push(item.id);
    }.bind(this));
    this.toggleComplete(ids, e.target, false);
  }.bind(this));

  this._filter();
};

2. Mettez à jour toggleComplete() pour accepter à la fois une tâche unique ou un tableau de tâches. Cela inclut le déplacement de filter() pour qu'il se trouve à l'intérieur de update(), et non à l'extérieur.

Controller.prototype.toggleComplete = function (ids, checkbox, silent) {
  var completed = checkbox.checked ? 1 : 0;
  this.model.update(ids, { completed: completed }, function () {
    if ( ids.constructor != Array ) {
      ids = [ ids ];
    }
    ids.forEach( function(id) {
      var listItem = $$('[data-id="' + id + '"]');
      
      if (!listItem) {
        return;
      }
      
      listItem.className = completed ? 'completed' : '';
      
      // In case it was toggled from an event and not by clicking the checkbox
      listItem.querySelector('input').checked = completed;
    });

    if (!silent) {
      this._filter();
    }

  }.bind(this));
};

Count todo items

After switching to async storage, there is a minor bug that shows up when getting the number of todos. You'll need to wrap the count operation in a callback function:

1. In model.js, update getCount() to accept a callback:

  Model.prototype.getCount = function (callback) {
  var todos = {
    active: 0,
    completed: 0,
    total: 0
  };
  this.storage.findAll(function (data) {
    data.each(function (todo) {
      if (todo.completed === 1) {
        todos.completed++;
      } else {
        todos.active++;
      }
      todos.total++;
    });
    if (callback) callback(todos);
  });
  return todos;
};

2. Back in controller.js, update _updateCount() to use the async getCount() you edited in the previous step:

Controller.prototype._updateCount = function () {
  var todos = this.model.getCount();
  this.model.getCount(function(todos) {
    this.$todoItemCounter.innerHTML = this.view.itemCounter(todos.active);

    this.$clearCompleted.innerHTML = this.view.clearCompletedButton(todos.completed);
    this.$clearCompleted.style.display = todos.completed > 0 ? 'block' : 'none';

    this.$toggleAll.checked = todos.completed === todos.total;

    this._toggleFrame(todos);
  }.bind(this));

};

You are almost there! If you reload the app now, you will be able to insert new todos without any console errors.

Remove todos items

Now that the app can save todo items, you're close to being done! You still get errors when you attempt to remove todo items:

Todo app with localStorage console log error

1. In store.js, convert all the localStorage instances to use chrome.storage.local:

a) To start off, wrap everything already inside remove() with a get() callback:

Store.prototype.remove = function (id, callback) {
  chrome.storage.local.get(this._dbName, function(storage) {
    var data = JSON.parse(localStorage[this._dbName]);
    var todos = data.todos;

    for (var i = 0; i < todos.length; i++) {
      if (todos[i].id == id) {
        todos.splice(i, 1);
        break;
      }
    }

    localStorage[this._dbName] = JSON.stringify(data);
    callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
  }.bind(this));
};

b) Then convert the contents within the get() callback:

Store.prototype.remove = function (id, callback) {
  chrome.storage.local.get(this._dbName, function(storage) {
    var data = JSON.parse(localStorage[this._dbName]);
    var data = storage[this._dbName];
    var todos = data.todos;

    for (var i = 0; i < todos.length; i++) {
      if (todos[i].id == id) {
        todos.splice(i, 1);
        break;
      }
    }

    localStorage[this._dbName] = JSON.stringify(data);
    callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
    chrome.storage.local.set(storage, function() {
      callback.call(this, todos);
    }.bind(this));
  }.bind(this));
};

2. The same Read-After-Write data hazard issue previously present in the save() method is also present when removing items so you will need to update a few more places to allow for batch operations on a list of todo IDs.

a) Still in store.js, update remove():

Store.prototype.remove = function (id, callback) {
  chrome.storage.local.get(this._dbName, function(storage) {
    var data = storage[this._dbName];
    var todos = data.todos;

    var ids = [].concat(id);
    ids.forEach( function(id) {
      for (var i = 0; i < todos.length; i++) {
        if (todos[i].id == id) {
          todos.splice(i, 1);
          break;
        }
      }
    });

    chrome.storage.local.set(storage, function() {
      callback.call(this, todos);
    }.bind(this));
  }.bind(this));
};

b) In controller.js, change removeCompletedItems() to make it call removeItem() on all IDs at once:

Controller.prototype.removeCompletedItems = function () {
  this.model.read({ completed: 1 }, function (data) {
    var ids = [];
    data.forEach(function (item) {
      this.removeItem(item.id);
      ids.push(item.id);
    }.bind(this));
    this.removeItem(ids);
  }.bind(this));

  this._filter();
};

c) Finally, still in controller.js, change the removeItem() to support removing multiple items from the DOM at once, and move the _filter() call to be inside the callback:

Controller.prototype.removeItem = function (id) {
  this.model.remove(id, function () {
    var ids = [].concat(id);
    ids.forEach( function(id) {
      this.$todoList.removeChild($$('[data-id="' + id + '"]'));
    }.bind(this));
    this._filter();
  }.bind(this));
  this._filter();
};

Déposer tous les éléments de la liste des tâches

Il existe une autre méthode dans store.js utilisant localStorage:

Store.prototype.drop = function (callback) {
  localStorage[this._dbName] = JSON.stringify({todos: []});
  callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
};

Cette méthode n'est pas appelée dans l'application actuelle. Par conséquent, si vous souhaitez relever un défi supplémentaire, essayez de l'implémenter par vous-même. Conseil: Examinez chrome.storage.local.clear().

Lancer votre application Todo terminée

Vous avez terminé l'étape 2 ! Actualisez votre application. Vous devriez maintenant disposer d'une version Chrome empaquetée entièrement fonctionnelle de TodoMVC.

Pour en savoir plus

Pour plus d'informations sur certaines des API présentées à cette étape, reportez-vous à:

Prêt à passer à l'étape suivante ? Passez à l'Étape 3 : Ajoutez des alarmes et des notifications »