In deze stap leer je:
- Hoe u een bestaande webapplicatie aanpast voor het Chrome Apps-platform.
- Hoe u uw app-scripts compatibel kunt maken met Content Security Policy (CSP).
- Hoe u lokale opslag implementeert met behulp van chrome.storage.local .
Geschatte tijd om deze stap te voltooien: 20 minuten.
Om een voorbeeld te zien van wat u in deze stap gaat voltooien, springt u naar de onderkant van deze pagina ↓ .
Importeer een bestaande Todo-app
Importeer als startpunt de standaard JavaScript-versie van TodoMVC , een veelgebruikte benchmark-app, in uw project.
We hebben een versie van de TodoMVC-app opgenomen in de referentiecode-zip in de todomvc -map. Kopieer alle bestanden (inclusief mappen) van todomvc naar uw projectmap.
U wordt gevraagd index.html te vervangen. Ga je gang en accepteer.
U zou nu de volgende bestandsstructuur in uw applicatiemap moeten hebben:
De blauw gemarkeerde bestanden komen uit de map todomvc .
Laad uw app nu opnieuw ( klik met de rechtermuisknop > App opnieuw laden ). U zou de basisgebruikersinterface moeten zien, maar u kunt geen taken toevoegen.
Maak scripts Content Security Policy (CSP)-compatibel
Open de DevTools-console ( klik met de rechtermuisknop op > Element inspecteren en selecteer vervolgens het tabblad Console ). U zult een foutmelding zien over het weigeren om een inline script uit te voeren:
Laten we deze fout oplossen door het Content Security Policy van de app compatibel te maken. Een van de meest voorkomende niet-nalevingen van CSP wordt veroorzaakt door inline JavaScript. Voorbeelden van inline JavaScript zijn gebeurtenishandlers als DOM-attributen (bijvoorbeeld <button onclick=''>
) en <script>
-tags met inhoud in de HTML.
De oplossing is eenvoudig: verplaats de inline-inhoud naar een nieuw bestand.
1. Verwijder onderaan index.html het inline JavaScript en neem in plaats daarvan js/bootstrap.js op:
<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. Maak een bestand in de js -map met de naam bootstrap.js . Verplaats de eerder inline code naar dit bestand:
// Bootstrap app data
window.app = {};
Je hebt nog steeds een niet-werkende Todo-app als je de app nu opnieuw laadt, maar je komt dichterbij.
Converteer localStorage naar chrome.storage.local
Als u nu de DevTools-console opent, zou de vorige fout verdwenen moeten zijn. Er is echter een nieuwe fout dat window.localStorage
niet beschikbaar is:
Chrome-apps ondersteunen geen localStorage
omdat localStorage
synchroon is. Synchrone toegang tot blokkerende bronnen (I/O) in een runtime met één thread kan ervoor zorgen dat uw app niet meer reageert.
Chrome-apps hebben een gelijkwaardige API die objecten asynchroon kan opslaan. Dit helpt het soms kostbare object->string->object-serialisatieproces te vermijden.
Om de foutmelding in onze app op te lossen, moet je localStorage
converteren naar chrome.storage.local .
Update app-machtigingen
Om chrome.storage.local
te kunnen gebruiken, moet u storage
aanvragen. Voeg in manifest.json "storage"
toe aan de permissions
-array:
"permissions": ["storage"],
Meer informatie over local.storage.set() en local.storage.get()
Als u taken wilt opslaan en ophalen, moet u op de hoogte zijn van de methoden set()
en get()
van de chrome.storage
API.
De set()- methode accepteert een object met sleutel-waardeparen als eerste parameter. Een optionele callback-functie is de tweede parameter. Bijvoorbeeld:
chrome.storage.local.set({secretMessage:'Psst!',timeSet:Date.now()}, function() {
console.log("Secret message saved");
});
De methode get() accepteert een optionele eerste parameter voor de gegevensopslagsleutels die u wilt ophalen. Een enkele sleutel kan als een string worden doorgegeven; meerdere sleutels kunnen worden gerangschikt in een reeks strings of een woordenboekobject.
De tweede parameter, die vereist is, is een callback-functie. Gebruik in het geretourneerde object de sleutels die zijn aangevraagd in de eerste parameter om toegang te krijgen tot de opgeslagen waarden. Bijvoorbeeld:
chrome.storage.local.get(['secretMessage','timeSet'], function(data) {
console.log("The secret message:", data.secretMessage, "saved at:", data.timeSet);
});
Als je alles wilt get()
wat zich momenteel in chrome.storage.local
bevindt, laat dan de eerste parameter weg:
chrome.storage.local.get(function(data) {
console.log(data);
});
In tegenstelling tot localStorage
kunt u lokaal opgeslagen items niet inspecteren met behulp van het DevTools Resources-paneel. U kunt echter als volgt communiceren met chrome.storage
vanuit de JavaScript-console:
Bekijk een voorbeeld van de vereiste API-wijzigingen
De meeste resterende stappen bij het converteren van de Todo-app zijn kleine wijzigingen in de API-aanroepen. Het wijzigen van alle plaatsen waar localStorage
momenteel wordt gebruikt, is weliswaar tijdrovend en foutgevoelig, maar is wel vereist.
De belangrijkste verschillen tussen localStorage
en chrome.storage
komen voort uit het asynchrone karakter van chrome.storage
:
In plaats van naar
localStorage
te schrijven met behulp van een eenvoudige toewijzing, moet uchrome.storage.local.set()
gebruiken met optionele callbacks.var data = { todos: [] }; localStorage[dbName] = JSON.stringify(data);
tegen
var storage = {}; storage[dbName] = { todos: [] }; chrome.storage.local.set( storage, function() { // optional callback });
In plaats van rechtstreeks toegang te krijgen tot
localStorage[myStorageName]
, moet uchrome.storage.local.get(myStorageName,function(storage){...})
gebruiken en vervolgens het geretourneerdestorage
parseren in de callback-functie.var todos = JSON.parse(localStorage[dbName]).todos;
tegen
chrome.storage.local.get(dbName, function(storage) { var todos = storage[dbName].todos; });
De functie
.bind(this)
wordt bij alle callbacks gebruikt om ervoor te zorgen datthis
verwijst naar dethis
van hetStore
prototype. (Meer informatie over gebonden functies vindt u in de MDN-documentatie: Function.prototype.bind() .)function Store() { this.scope = 'inside Store'; chrome.storage.local.set( {}, function() { console.log(this.scope); // outputs: 'undefined' }); } new Store();
tegen
function Store() { this.scope = 'inside Store'; chrome.storage.local.set( {}, function() { console.log(this.scope); // outputs: 'inside Store' }.bind(this)); } new Store();
Houd deze belangrijke verschillen in gedachten terwijl we in de volgende secties het ophalen, opslaan en verwijderen van taken bespreken.
Haal taken op
Laten we de Todo-app updaten om taken op te halen:
1. De Store
constructormethode zorgt voor het initialiseren van de Todo-app met alle bestaande taken uit de datastore. De methode controleert eerst of de datastore bestaat. Als dit niet het geval is, wordt er een lege reeks todos
gemaakt en opgeslagen in de datastore, zodat er geen runtime-leesfouten optreden.
Converteer in js/store.js het gebruik van localStorage
in de constructormethode om in plaats daarvan chrome.storage.local
te gebruiken:
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. De methode find()
wordt gebruikt bij het lezen van taken uit het model. De geretourneerde resultaten veranderen afhankelijk van of u filtert op 'Alle', 'Actief' of 'Voltooid'.
Converteer find()
om chrome.storage.local
te gebruiken:
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. Net als find()
haalt findAll()
alle taken uit het model. Converteer findAll()
om chrome.storage.local
te gebruiken:
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));
};
Bewaar takenitems
De huidige save()
methode vormt een uitdaging. Het hangt af van twee asynchrone bewerkingen (get en set) die elke keer op de hele monolithische JSON-opslag werken. Alle batchupdates voor meer dan één taakitem, zoals "markeer alle taken als voltooid", zal resulteren in een gegevensrisico dat bekend staat als Read-After-Write . Dit probleem zou niet optreden als we een geschiktere gegevensopslag zouden gebruiken, zoals IndexedDB, maar we proberen de conversie-inspanningen voor dit codelab te minimaliseren.
Er zijn verschillende manieren om dit probleem op te lossen, dus we zullen van deze gelegenheid gebruik maken om save()
enigszins te refactoriseren door een reeks taken-ID's te nemen die in één keer moeten worden bijgewerkt:
1. Om te beginnen wikkelt u alles dat zich al in save()
bevindt, in met een 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. Converteer alle localStorage
instanties met 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. Werk vervolgens de logica bij zodat deze op een array werkt in plaats van op een enkel item:
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));
};
Markeer taken als voltooid
Nu de app op arrays werkt, moet u de manier wijzigen waarop de app omgaat met een gebruiker die op de knop Wissen voltooid (#) klikt:
1. Update toggleAll()
in controller.js zodat toggleComplete()
slechts één keer wordt aangeroepen met een reeks taken, in plaats van een taak één voor één als voltooid te markeren. Verwijder ook de aanroep van _filter()
omdat u de toggleComplete
_filter()
gaat aanpassen.
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. Update nu toggleComplete()
om zowel een enkele taak als een reeks taken te accepteren. Dit omvat het verplaatsen van filter()
naar binnen de update()
, in plaats van daarbuiten.
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();
};
Laat alle taken vallen
Er is nog een methode in store.js die localStorage
gebruikt:
Store.prototype.drop = function (callback) {
localStorage[this._dbName] = JSON.stringify({todos: []});
callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
};
Deze methode wordt niet aangeroepen in de huidige app, dus als je een extra uitdaging wilt, probeer deze dan zelf te implementeren. Tip: kijk eens naar chrome.storage.local.clear()
.
Start uw voltooide Todo-app
Je bent klaar Stap 2! Laad uw app opnieuw en u zou nu een volledig werkende, in Chrome verpakte versie van TodoMVC moeten hebben.
Voor meer informatie
Voor meer gedetailleerde informatie over enkele van de API's die in deze stap zijn geïntroduceerd, raadpleegt u:
- Inhoudsbeveiligingsbeleid ↑
- Verklaar machtigingen ↑
- chrome.opslag ↑
- chrome.storage.local.get() ↑
- chrome.storage.local.set() ↑
- chrome.storage.local.remove() ↑
- chrome.storage.local.clear() ↑
Klaar om door te gaan naar de volgende stap? Ga naar Stap 3 - Alarmen en meldingen toevoegen »