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.
Vous êtes invité à remplacer index.html. Allez-y et acceptez.
La structure de fichiers suivante doit s'afficher dans le dossier de votre application:
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:
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:
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:
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 utiliserchrome.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 utiliserchrome.storage.local.get(myStorageName,function(storage){...})
, puis analyser lesstorage
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 quethis
fait référence authis
duStore
. 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:
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:
- Content Security Policy ↑
- Déclarer des autorisations ↑
- chrome.storage ↑
- chrome.storage.local.get() ↑
- chrome.storage.local.set() ↑
- chrome.storage.local.remove() ↑
- chrome.storage.local.clear() ↑
Prêt à passer à l'étape suivante ? Passez à l'étape 3 : Ajoutez des alarmes et des notifications »