In questo passaggio scoprirai:
- Come adattare un'applicazione web esistente per la piattaforma App di Chrome.
- Come rendere conformi gli script dell'app ai Criteri di sicurezza del contenuto (CSP).
- Come implementare lo spazio di archiviazione locale utilizzando chrome.storage.local.
Tempo stimato per completare questo passaggio: 20 minuti.
Per visualizzare un'anteprima di ciò che dovrai completare in questo passaggio, vai in fondo a questa pagina ↓.
Importare un'app To Do esistente
Come punto di partenza, importa nel tuo progetto la versione JavaScript di TodoMVC, un'app di benchmark comune.
Abbiamo incluso una versione dell'app TodoMVC nel codice di riferimento zip della cartella todomvc. Copia tutti i file (incluse le cartelle) da todomvc nella cartella del progetto.
Ti verrà chiesto di sostituire index.html. Vai avanti e accetta.
A questo punto nella cartella dell'applicazione dovresti avere la seguente struttura di file:
I file evidenziati in blu provengono dalla cartella todomvc.
Ricarica l'app ora (fai clic con il tasto destro del mouse > Ricarica app). Dovresti vedere l'interfaccia utente di base, ma non puoi aggiungere cose da fare.
Rendere gli script conformi ai criteri di sicurezza del contenuto (CSP)
Apri la console DevTools (fai clic con il tasto destro del mouse > Ispeziona elemento, quindi seleziona la scheda Console). Verrà visualizzato un errore relativo al rifiuto di eseguire uno script in linea:
Correggiamo questo errore rendendo l'app conforme alle norme Content Security Policy. Una delle mancanze di conformità ai CSP più comuni è causata da JavaScript in linea. Esempi di JavaScript incorporato includono
gestori di eventi come attributi DOM (ad es. <button onclick=''>
) e tag <script>
con contenuti
all'interno dell'HTML.
La soluzione è semplice: sposta i contenuti in linea in un nuovo file.
1. Nella parte inferiore di index.html, rimuovi il codice JavaScript incorporato e includi invece 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. Crea un file nella cartella js denominato bootstrap.js. Sposta il codice precedentemente incorporato in questo file:
// Bootstrap app data
window.app = {};
Se ricarichi l'app adesso, avrai ancora un'app Da fare non funzionante, ma ti stai avvicinando.
Convertire localStorage in chrome.storage.local
Se apri ora la console DevTools, l'errore precedente dovrebbe essere scomparso. Tuttavia, è stato rilevato un nuovo errore relativo alla mancata disponibilità di window.localStorage
:
Le app di Chrome non supportano localStorage
perché localStorage
è sincrono. L'accesso sincrono alle risorse di blocco (I/O) in un runtime a thread singolo potrebbe causare l'interruzione della risposta dell'app.
Le app di Chrome hanno un'API equivalente che può archiviare gli oggetti in modo asincrono. In questo modo eviterai il costoso processo di serializzazione oggetto->stringa->oggetto.
Per risolvere il messaggio di errore nella nostra app, devi convertire localStorage
in
chrome.storage.local.
Aggiornamento delle autorizzazioni per le app
Per utilizzare chrome.storage.local
, devi richiedere l'autorizzazione storage
. In
manifest.json, aggiungi "storage"
all'array permissions
:
"permissions": ["storage"],
Scopri di più su local.storage.set() e local.storage.get()
Per salvare e recuperare le voci di attività, devi conoscere i metodi set()
e get()
dell'API
chrome.storage
.
Il metodo set() accetta un oggetto di coppie chiave/valore come primo parametro. Un'eventuale funzione di callback è il secondo parametro. Ad esempio:
chrome.storage.local.set({secretMessage:'Psst!',timeSet:Date.now()}, function() {
console.log("Secret message saved");
});
Il metodo get() accetta un primo parametro facoltativo per le chiavi del datastore che vuoi recuperare. Una singola chiave può essere passata come stringa; più chiavi possono essere organizzate in un array di stringhe o in un oggetto dizionario.
Il secondo parametro, obbligatorio, è una funzione di callback. Nell'oggetto restituito, utilizza le chiavi richieste nel primo parametro per accedere ai valori archiviati. Ad esempio:
chrome.storage.local.get(['secretMessage','timeSet'], function(data) {
console.log("The secret message:", data.secretMessage, "saved at:", data.timeSet);
});
Se vuoi get()
tutto ciò che è attualmente in chrome.storage.local
, ometti il primo parametro:
chrome.storage.local.get(function(data) {
console.log(data);
});
A differenza di localStorage
, non potrai ispezionare gli elementi archiviati localmente utilizzando il riquadro Risorse DevTools. Tuttavia, puoi interagire con chrome.storage
dalla console JavaScript come segue:
Visualizza l'anteprima delle modifiche richieste all'API
La maggior parte dei passaggi rimanenti per la conversione dell'app Todo sono piccole modifiche alle chiamate API. È necessaria la modifica di tutti i luoghi in cui localStorage
è attualmente in uso, anche se questa operazione è lunga e soggetta a errori.
Le principali differenze tra localStorage
e chrome.storage
derivano dalla natura asincrona di
chrome.storage
:
Invece di scrivere in
localStorage
utilizzando un'assegnazione semplice, devi utilizzarechrome.storage.local.set()
con callback facoltativi.var data = { todos: [] }; localStorage[dbName] = JSON.stringify(data);
rispetto a
var storage = {}; storage[dbName] = { todos: [] }; chrome.storage.local.set( storage, function() { // optional callback });
Anziché accedere direttamente a
localStorage[myStorageName]
, devi utilizzarechrome.storage.local.get(myStorageName,function(storage){...})
e poi analizzare l'oggettostorage
restituito nella funzione di callback.var todos = JSON.parse(localStorage[dbName]).todos;
rispetto a
chrome.storage.local.get(dbName, function(storage) { var todos = storage[dbName].todos; });
La funzione
.bind(this)
viene utilizzata in tutti i callback per garantire chethis
faccia riferimento althis
del prototipoStore
. Ulteriori informazioni sulle funzioni associate sono disponibili nella documentazione MDN: Function.prototype.bind().function Store() { this.scope = 'inside Store'; chrome.storage.local.set( {}, function() { console.log(this.scope); // outputs: 'undefined' }); } new Store();
rispetto a
function Store() { this.scope = 'inside Store'; chrome.storage.local.set( {}, function() { console.log(this.scope); // outputs: 'inside Store' }.bind(this)); } new Store();
Tieni presenti queste differenze principali mentre nelle sezioni seguenti parleremo di recupero, salvataggio e rimozione degli elementi di promemoria.
Recuperare gli elementi della lista di cose da fare
Aggiorna l'app To Do per recuperare gli elementi della lista di cose da fare:
1. Il metodo costruttore Store
si occupa di inizializzare l'app Todo con tutte le voci di attività esistenti del datastore. Il metodo verifica innanzitutto se il datastore esiste. In caso contrario, verrà creato un array vuoto di todos
e salvato nel datastore in modo che non si verifichino errori di lettura di runtime.
In js/store.js, converti l'utilizzo di localStorage
nel metodo del costruttore in modo da utilizzare
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. Il metodo find()
viene utilizzato per leggere le attività dal modello. I risultati restituiti cambiano a seconda che il filtro sia "Tutti", "Attivo" o "Completato".
Per convertire find()
in modo da utilizzare 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. Analogamente a find()
, findAll()
ottiene tutti i promemoria dal modello. Per convertire findAll()
in modo da utilizzare
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));
};
Salvare gli elementi della lista di cose da fare
Il metodo save()
attuale presenta una sfida. Dipende da due operazioni asincrone (get e set) che operano ogni volta sull'intero spazio di archiviazione JSON monolitico. Eventuali aggiornamenti collettivi su più elementi della lista di cose da fare, ad esempio "Contrassegna tutti i compiti come completati", comporteranno un rischio per i dati noto come Lettura dopo scrittura. Questo problema non si verificherebbe se utilizzassimo un sistema di archiviazione dei dati più appropriato, come IndexedDB, ma stiamo cercando di ridurre al minimo le operazioni di conversione per questo codelab.
Esistono diversi modi per risolvere il problema, quindi utilizzeremo questa opportunità per eseguire una leggera riorganizzazione di save()
selezionando un array di ID attività da aggiornare contemporaneamente:
1. Per iniziare, racchiudi tutto ciò che è già all'interno di save()
con un callback 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. Converti tutte le istanze di localStorage
con 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. Quindi aggiorna la logica in modo da operare su un array anziché su un singolo elemento:
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));
};
Contrassegnare gli elementi della lista di cose da fare come completati
Ora che l'app funziona su array, devi modificare il modo in cui l'app gestisce un utente facendo clic sul pulsante Cancella completate (#):
1. In controller.js, aggiorna toggleAll()
per chiamare toggleComplete()
una sola volta con un array
di cose da fare invece di contrassegnare un'attività come completata uno alla volta. Elimina anche la chiamata a _filter()
poiché modificherai toggleComplete
_filter()
.
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. Ora aggiorna toggleComplete()
in modo che accetti sia un singolo elemento da fare sia un array di elementi da fare. ad esempio, spostare filter()
all'interno di update()
anziché all'esterno.
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();
};
Elimina tutti gli elementi della lista di cose da fare
In store.js è presente un altro metodo che utilizza localStorage
:
Store.prototype.drop = function (callback) {
localStorage[this._dbName] = JSON.stringify({todos: []});
callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
};
Questo metodo non è stato chiamato nell'app corrente quindi, se vuoi una sfida in più, prova a implementarlo autonomamente. Suggerimento: dai un'occhiata a chrome.storage.local.clear()
.
Avvia l'app To Do completata
Hai completato il passaggio 2. Ricarica l'app e ora dovresti avere una versione pacchettizzata di Chrome completamente funzionante di TodoMVC.
Per ulteriori informazioni
Per informazioni più dettagliate su alcune delle API introdotte in questo passaggio, consulta:
- Criterio di sicurezza del contenuto ↑
- Dichiarare le autorizzazioni ↑
- chrome.storage ↑
- chrome.storage.local.get() ↑
- chrome.storage.local.set() ↑
- chrome.storage.local.remove() ↑
- chrome.storage.local.clear() ↑
Vuoi andare al passaggio successivo? Vai al passaggio 3: aggiungi sveglie e notifiche »