In questo passaggio, scoprirai:
- Come adattare un'applicazione web esistente per la piattaforma App di Chrome.
- Come rendere conformi al criterio di sicurezza del contenuto (CSP) degli script per le app.
- Come implementare lo spazio di archiviazione locale utilizzando chrome.storage.local.
Tempo stimato per completare questo passaggio: 20 minuti.
Per visualizzare l'anteprima di ciò che completerai in questo passaggio, vai alla fine di questa pagina ↓.
Importare un'app di promemoria esistente
Come punto di partenza, importa la versione vanilla JavaScript di TodoMVC, un benchmark comune. nel tuo progetto.
Abbiamo incluso una versione dell'app TodoMVC nel codice postale di riferimento della todomvc . Copia tutti i file (incluse le cartelle) da todomvc nella cartella del progetto.
Ti verrà chiesto di sostituire index.html. Procedi 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'UI di base, ma non in grado di aggiungere cose da fare.
Rendi conforme il criterio di sicurezza del contenuto (CSP) degli script
Apri la console DevTools (fai clic con il tasto destro del mouse > Ispeziona elemento, quindi seleziona la scheda Console). Tu verrà visualizzato un errore relativo al rifiuto di eseguire uno script in linea:
Risolviamo questo errore rendendo l'app conforme alle Norme sulla sicurezza del contenuto. Uno dei principali
la mancata conformità di CSP è causata da JavaScript incorporato. Esempi di codice 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 incorporati in un nuovo file.
1. Nella parte inferiore del file index.html, rimuovi il codice JavaScript incorporato e includilo 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. Nella cartella js, crea un file denominato bootstrap.js. Sposta il codice incorporato in precedenza 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.
Converti localStorage in chrome.storage.local
Se apri ora la console DevTools, l'errore precedente dovrebbe essere scomparso. Si è verificato un nuovo errore,
tuttavia, circa window.localStorage
non è disponibile:
Le app di Chrome non supportano localStorage
perché localStorage
è sincrono. Accesso sincrono
alle risorse di blocco (I/O) in un runtime a thread singolo potrebbe impedire all'app di rispondere.
Le app di Chrome hanno un'API equivalente in grado di archiviare gli oggetti in modo asincrono. In questo modo eviterai a volte è 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 usare chrome.storage.local
, devi richiedere l'autorizzazione storage
. Nella
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 attività da svolgere, devi conoscere i metodi set()
e get()
del
API chrome.storage
.
Il metodo set() accetta un oggetto di coppie chiave-valore come primo parametro. Un'intestazione facoltativa è 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 inutile. Una singola chiave può essere passata come stringa; più chiavi possono essere disposte in un array o un oggetto dizionario.
Il secondo parametro, obbligatorio, è una funzione di callback. Nell'oggetto restituito, utilizza 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
:
chrome.storage.local.get(function(data) {
console.log(data);
});
A differenza di localStorage
, non potrai ispezionare gli elementi archiviati localmente utilizzando DevTools
Riquadro Risorse. Tuttavia, puoi interagire con chrome.storage
dalla console JavaScript come
quindi:
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. Modifica
tutte le posizioni in cui viene attualmente utilizzato localStorage
, sebbene sia dispendioso in termini di tempo e soggetto a errori,
è obbligatorio.
Le principali differenze tra localStorage
e chrome.storage
derivano dalla natura asincrona di
chrome.storage
:
Invece di scrivere a
localStorage
con un compito semplice, devi usarechrome.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){...})
, quindi analizza il file restituitostorage
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 su tutti i callback per garantire chethis
faccia riferimento althis
delStore
prototipo. 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 chiave quando tratteremo il recupero, il salvataggio e la rimozione di elementi da fare nella le sezioni seguenti.
Recupera elementi da fare
Aggiorna l'app Da fare per recuperare le voci dell'elenco di cose da fare:
1. Il metodo costruttore Store
si occupa di inizializzare l'app Todo con tutte le risorse esistenti
le cose da fare dal datastore. Il metodo verifica innanzitutto se il datastore esiste. In caso contrario,
crea un array vuoto di todos
e salvalo nel datastore in modo che non ci siano errori di lettura del runtime.
In js/store.js, converti l'utilizzo di localStorage
nel metodo costruttore in modo da usare invece
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 durante la lettura delle cose da fare dal modello. I risultati restituiti cambiano in base
e scegliere se applicare il filtro in base a "Tutte", "Attive" o "Completate".
Converti find()
per 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. Converti findAll()
per 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));
};
Salva voci di cose da fare
L'attuale metodo save()
presenta una sfida. Dipende da due operazioni asincrone (get e set)
che operano ogni volta sull'intero spazio di archiviazione JSON monolitico. Eventuali aggiornamenti batch su più di un
ad esempio "contrassegna tutti i promemoria come completati", comporterà un rischio per i dati noto come
Lettura dopo scrittura. Questo problema non si verifica se utilizzavamo uno spazio di archiviazione dati più appropriato
come IndexedDB, ma stiamo cercando di ridurre al minimo il lavoro di conversione per questo codelab.
Esistono diversi modi per risolvere il problema, quindi sfrutteremo questa opportunità per eseguire un leggero refactoring di save()
entro
di aggiornare contemporaneamente un array di ID attività:
1. Per iniziare, aggrega tutto ciò che è già all'interno di save()
con un chrome.storage.local.get()
callback:
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 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));
};
Contrassegna elementi da fare come completati
Ora che l'app funziona su array, devi modificare il modo in cui l'app gestisce un utente che fa clic sul Pulsante Cancella completate (#):
1. In controller.js, aggiorna toggleAll()
per chiamare toggleComplete()
solo una volta con un array
delle cose da fare invece di contrassegnare un'attività come completata uno alla volta. Elimina anche la chiamata a _filter()
dato che modificherai _filter()
di 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. Ora aggiorna toggleComplete()
per accettare una singola attività o un array di attività. Sono inclusi
spostamento di 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();
};
Rilascia tutte le voci di attività
Esiste un altro metodo in store.js 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 implementare questa funzionalità in autonomia. Suggerimento: dai un'occhiata a chrome.storage.local.clear()
.
Lancia l'app delle cose da fare completata
Hai completato il passaggio 2. Ricarica la tua app. A questo punto dovresti avere una versione del pacchetto di Chrome completamente funzionante di TodoMVC.
Per maggiori informazioni
Per informazioni più dettagliate su alcune delle API introdotte in questo passaggio, fai riferimento a:
- 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 »