Étape 2: Importez une application Web existante

Au cours de cette étape, vous allez apprendre à:

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

Temps estimé pour effectuer cette étape: 20 minutes
Pour afficher un aperçu de cette étape, accédez au bas de la page ↓.

Importer une application Liste de tâches existante

Pour commencer, importez la version JavaScript vanilla de TodoMVC, un benchmark courant dans votre projet.

Nous avons inclus une version de l'application TodoMVC dans le code de référence ZIP du 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 êtes invité à remplacer index.html. Allez-y et acceptez.

Remplacer "index.html"

La structure de fichiers suivante doit s'afficher dans le dossier de votre application:

Nouveau dossier de projet

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

Actualisez votre application maintenant (effectuez un clic droit > Actualiser l'application). Vous devriez voir l'interface utilisateur de base, et ajouter des tâches.

Rendre les scripts conformes à Content Security Policy (CSP)

Ouvrez la console DevTools (effectuez un clic droit > Inspecter l'élément, puis sélectionnez l'onglet Console). Toi un message d'erreur vous indiquant de refuser d'exécuter un script intégré s'affichera:

Erreur de journal de l'application de liste de tâches avec la console CSP

Corrigeons cette erreur en rendant l'application conforme à Content Security Policy. L'une des plus Les cas de non-conformité courants des CSP sont dus à du code JavaScript intégré. Exemples de code JavaScript intégré : les gestionnaires d'événements en tant qu'attributs DOM (par exemple, <button onclick=''>) et les balises <script> avec du contenu. dans le code HTML.

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

1. Vers le bas du fichier index.html, supprimez le code JavaScript intégré et ajoutez-le à 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éplacer le code précédemment intégré dans ce fichier:

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

Si vous actualisez l'application maintenant, votre application Todo ne fonctionnera toujours pas, mais vous vous en rapprochez.

Convertir localStorage en chrome.storage.local

Si vous ouvrez la console des outils de développement maintenant, l'erreur précédente devrait avoir disparu. Une nouvelle erreur s'est produite. Toutefois, window.localStorage n'est pas disponible:

Application de liste de tâches avec erreur de journal de la console localStorage

Les applications Chrome ne sont pas compatibles avec localStorage, car localStorage est synchrone. Accès synchrone au blocage des ressources (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 peut stocker des objets de manière asynchrone. Cela permet d'éviter processus de sérialisation objet->chaîne->objets 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 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 chrome.storage.

La méthode set() accepte un objet de paires clé-valeur comme premier paramètre. Une option de rappel est le deuxième paramètre. 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 du datastore que vous souhaitez retreive. Une seule clé peut être transmise sous forme de chaîne. plusieurs clés peuvent être organisées en un tableau ou un objet de dictionnaire.

Le deuxième paramètre, qui est obligatoire, est une fonction de rappel. Dans l'objet renvoyé, utilisez la fonction 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 utiliser get() pour tout ce qui se trouve actuellement dans chrome.storage.local, omettez la première :

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

Contrairement à localStorage, vous ne pourrez pas inspecter les éléments stockés localement à l'aide des outils de développement Panneau des ressources. En revanche, vous pouvez interagir avec chrome.storage depuis la console JavaScript, comme suit : Par conséquent:

Déboguer chrome.storage à l&#39;aide de la console

Prévisualiser les modifications requises pour l'API

La plupart des étapes restantes dans la conversion de l'application Todo sont de petites modifications apportées aux appels d'API. Modification... tous les emplacements où localStorage est actuellement utilisé, bien qu'il soit chronophage et source d'erreurs, est obligatoire.

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 les storage 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 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 ces différences clés à l'esprit, car nous abordons la récupération, l'enregistrement et la suppression des tâches dans le dans les sections suivantes.

Récupérer les tâches

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

1. La méthode de constructeur Store se charge d'initialiser l'application Todo avec toutes les des tâches à effectuer dans le datastore. La méthode vérifie d'abord si le datastore existe. Si ce n’est pas le cas, Créez un tableau vide de todos et enregistrez-le dans le datastore afin d'éviter toute erreur de lecture au moment de l'exécution.

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

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 des tâches à effectuer à partir du modèle. Les résultats renvoyés changent en fonction selon le filtre "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() récupère toutes les tâches du modèle. Convertir 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 éléments des tâches

La méthode save() actuelle présente un défi. Cela 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 sur plusieurs une tâche, comme « marquer toutes les tâches comme terminées », entraînera 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 minimiser l'effort de conversion pour cet atelier de programmation.

Il existe plusieurs façons de résoudre le problème. Nous allons donc profiter de cette opportunité pour refactoriser légèrement save() : en prenant un tableau des ID de tâches à mettre à jour en une seule fois:

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

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. Modifiez ensuite la logique pour qu'elle s'exécute sur un tableau au lieu d'un seul élément:

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 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 au lieu de marquer une tâche comme terminée une par une. Supprimer également l'appel à _filter() puisque vous allez ajuster la _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. Modifiez maintenant toggleComplete() pour accepter à la fois une seule tâche ou un tableau de tâches. Cela inclut déplacement de filter() à l'intérieur de update(), plutôt qu'à 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 tâche

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'appli actuelle. Si vous voulez un défi supplémentaire, essayez pour l'implémenter vous-même. Indice: Consultez chrome.storage.local.clear().

Lancer l'application Todo terminée

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

Pour en savoir plus

Pour en savoir plus sur certaines des API présentées lors de cette étape, consultez les pages suivantes:

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