In diesem Schritt lernen Sie:
- Vorhandene Webanwendung für die Chrome Apps-Plattform anpassen
- Die Content Security Policy (CSP) für Ihre App-Skripts konform machen
- Implementieren des lokalen Speichers mithilfe von chrome.storage.local
Geschätzte Zeit für diesen Schritt: 20 Minuten
Wenn Sie eine Vorschau der Schritte in diesem Schritt sehen möchten, springen Sie zum Ende der Seite ↓.
Vorhandene Todo-App importieren
Importieren Sie als Erstes die einfache JavaScript-Version von TodoMVC, einer gängigen Benchmark-Anwendung, in Ihr Projekt.
Wir haben eine Version der TodoMVC-App in der Referenzcode-ZIP-Datei im Ordner todomvc gespeichert. Kopiere alle Dateien (einschließlich Ordner) von todomvc in deinen Projektordner.
Sie werden aufgefordert, index.html zu ersetzen. Akzeptieren Sie sie.
Der Anwendungsordner sollte jetzt die folgende Dateistruktur haben:
Die blau hervorgehobenen Dateien stammen aus dem Ordner todomvc.
Laden Sie die App jetzt neu (klicken Sie mit der rechten Maustaste > App aktualisieren). Sie sollten jetzt die Standard-UI sehen, aber keine Aufgaben hinzufügen.
Content Security Policy (CSP) für Skripts konform machen
Öffnen Sie die Entwicklertools-Konsole (klicken Sie mit der rechten Maustaste > Element untersuchen und wählen Sie dann den Tab Console aus). Sie erhalten eine Fehlermeldung, dass die Ausführung eines Inline-Skripts verweigert wird:
Lassen Sie uns diesen Fehler beheben, indem wir die App mit der Content Security Policy konform machen. Eine der häufigsten Verstöße gegen CSPs wird durch Inline-JavaScript verursacht. Beispiele für Inline-JavaScript sind Event-Handler als DOM-Attribute (z.B. <button onclick=''>
) und <script>
-Tags mit Inhalt im HTML-Code.
Die Lösung ist einfach: Verschieben Sie den Inline-Inhalt in eine neue Datei.
1. Entfernen Sie unten in der Datei index.html das Inline-JavaScript und fügen Sie stattdessen js/bootstrap.js ein:
<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. Erstellen Sie im Ordner js eine Datei namens bootstrap.js. Verschieben Sie den vorherigen Inline-Code in diese Datei:
// Bootstrap app data
window.app = {};
Du hast immer noch eine nicht funktionierende Todo-App, wenn du die App jetzt aktualisierst, aber der Sache näher kommst.
„localStorage“ in „chrome.storage.local“ konvertieren
Wenn du die Entwicklertools-Konsole jetzt öffnest, sollte der vorherige Fehler nicht mehr vorhanden sein. Es gibt jedoch einen neuen Fehler, dass window.localStorage
nicht verfügbar ist:
localStorage
wird von Chrome-Apps nicht unterstützt, da localStorage
synchron ist. Synchroner Zugriff auf blockierende Ressourcen (E/A) in einer Single-Threaded-Laufzeit kann dazu führen, dass Ihre Anwendung nicht mehr reagiert.
Chrome-Apps haben eine entsprechende API, die Objekte asynchron speichern kann. Dies trägt dazu bei, den manchmal kostspieligen Prozess zur Objekt->Zeichenfolge->Objekt-Serialisierung zu vermeiden.
Um die Fehlermeldung in der App zu beheben, müssen Sie localStorage
in chrome.storage.local konvertieren.
App-Berechtigungen aktualisieren
Wenn Sie chrome.storage.local
verwenden möchten, müssen Sie die Berechtigung storage
anfordern. Fügen Sie in manifest.json "storage"
zum Array permissions
hinzu:
"permissions": ["storage"],
Weitere Informationen zu „local.storage.set()“ und „local.storage.get()“
Wenn Sie To-do-Elemente speichern und abrufen möchten, müssen Sie die Methoden set()
und get()
der chrome.storage
API kennen.
Für die Methode set() wird ein Objekt mit Schlüssel/Wert-Paaren als ersten Parameter akzeptiert. Der zweite Parameter ist eine optionale Callback-Funktion. Beispiel:
chrome.storage.local.set({secretMessage:'Psst!',timeSet:Date.now()}, function() {
console.log("Secret message saved");
});
Die Methode get() akzeptiert einen optionalen ersten Parameter für die Datenspeicherschlüssel, die Sie abrufen möchten. Ein einzelner Schlüssel kann als String übergeben werden. Mehrere Schlüssel können in einem Array von Strings oder in einem Wörterbuchobjekt angeordnet werden.
Der zweite erforderliche Parameter ist eine Callback-Funktion. Verwenden Sie im zurückgegebenen Objekt die im ersten Parameter angeforderten Schlüssel, um auf die gespeicherten Werte zuzugreifen. Beispiel:
chrome.storage.local.get(['secretMessage','timeSet'], function(data) {
console.log("The secret message:", data.secretMessage, "saved at:", data.timeSet);
});
Wenn Sie alle Elemente in chrome.storage.local
mit get()
versehen möchten, lassen Sie den ersten Parameter weg:
chrome.storage.local.get(function(data) {
console.log(data);
});
Im Gegensatz zu localStorage
können Sie lokal gespeicherte Elemente nicht über den Bereich „Ressourcen“ der Entwicklertools prüfen. Sie können über die JavaScript-Konsole jedoch so mit chrome.storage
interagieren:
Vorschau erforderlicher API-Änderungen anzeigen
Bei den meisten verbleibenden Schritten zur Konvertierung der Todo-App handelt es sich um kleine Änderungen an den API-Aufrufen. Alle Orte, an denen localStorage
derzeit verwendet wird, müssen geändert werden, obwohl dies zeitaufwendig und fehleranfällig ist.
Die wichtigsten Unterschiede zwischen localStorage
und chrome.storage
sind auf die asynchrone Natur von chrome.storage
zurückzuführen:
Anstatt mit einer einfachen Zuweisung an
localStorage
zu schreiben, müssen Siechrome.storage.local.set()
mit optionalen Callbacks verwenden.var data = { todos: [] }; localStorage[dbName] = JSON.stringify(data);
im Vergleich mit
var storage = {}; storage[dbName] = { todos: [] }; chrome.storage.local.set( storage, function() { // optional callback });
Anstatt direkt auf
localStorage[myStorageName]
zuzugreifen, müssen Siechrome.storage.local.get(myStorageName,function(storage){...})
verwenden und dann das zurückgegebenestorage
-Objekt in der Callback-Funktion parsen.var todos = JSON.parse(localStorage[dbName]).todos;
im Vergleich mit
chrome.storage.local.get(dbName, function(storage) { var todos = storage[dbName].todos; });
Die Funktion
.bind(this)
wird für alle Callbacks verwendet, damitthis
auf denthis
desStore
-Prototyps verweist. Weitere Informationen zu gebundenen Funktionen finden Sie in der MDN-Dokumentation unter Function.prototype.bind().function Store() { this.scope = 'inside Store'; chrome.storage.local.set( {}, function() { console.log(this.scope); // outputs: 'undefined' }); } new Store();
im Vergleich mit
function Store() { this.scope = 'inside Store'; chrome.storage.local.set( {}, function() { console.log(this.scope); // outputs: 'inside Store' }.bind(this)); } new Store();
Behalten Sie diese wichtigen Unterschiede im Hinterkopf, wenn wir in den folgenden Abschnitten das Abrufen, Speichern und Entfernen von To-Do-Elementen beschreiben.
Aufgaben abrufen
Aktualisieren wir die To-Do-App, um To-Do-Elemente abzurufen:
1. Die Konstruktormethode Store
sorgt für die Initialisierung der Todo-Anwendung mit allen vorhandenen To-Do-Elementen aus dem Datenspeicher. Die Methode prüft zuerst, ob der Datenspeicher vorhanden ist. Ist dies nicht der Fall, wird ein leeres Array von todos
erstellt und im Datenspeicher gespeichert, damit keine Laufzeitlesefehler auftreten.
Konvertieren Sie in js/store.js die Verwendung von localStorage
in der Konstruktormethode so, dass stattdessen chrome.storage.local
verwendet wird:
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. Die Methode find()
wird beim Lesen von Aufgaben aus dem Modell verwendet. Die zurückgegebenen Ergebnisse ändern sich je nachdem, ob Sie nach „Alle“, „Aktiv“ oder „Abgeschlossen“ filtern.
Konvertiere find()
so, dass chrome.storage.local
verwendet wird:
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. Wie find()
erhält findAll()
alle Aufgaben des Modells. Konvertieren Sie findAll()
, um chrome.storage.local
zu verwenden:
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));
};
Aufgaben speichern
Die aktuelle Methode save()
stellt eine Herausforderung dar. Dies hängt von zwei asynchronen Vorgängen (get und set) ab, die jedes Mal den gesamten monolithischen JSON-Speicher verarbeiten. Alle Batch-Aktualisierungen für mehr als ein To-do-Element, z. B. „Alle Aufgaben als erledigt markieren“, führen zu einer Datengefahr, die als Read-After-Write bezeichnet wird. Dieses Problem würde nicht auftreten, wenn wir eine geeignetere Datenspeicherung wie IndexedDB verwenden würden, aber wir versuchen, den Konvertierungsaufwand für dieses Codelab zu minimieren.
Es gibt mehrere Möglichkeiten, das Problem zu beheben. Wir nutzen diese Gelegenheit, um save()
geringfügig zu refaktorieren, indem wir ein Array von Aufgaben-IDs verwenden, die alle gleichzeitig aktualisiert werden sollen:
1. Umschließen Sie alle Elemente, die bereits save()
enthalten, mit einem 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. Konvertieren Sie alle localStorage
-Instanzen mit 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. Aktualisieren Sie dann die Logik so, dass sie mit einem Array statt mit einem einzelnen Element verarbeitet wird:
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));
};
Aufgaben als erledigt markieren
Da die Anwendung nun mit Arrays arbeitet, müssen Sie ändern, wie die Anwendung mit einem Nutzer umgehen soll, indem Sie auf die Schaltfläche Abgeschlossene löschen (#) klicken:
1. Aktualisieren Sie in controller.js toggleAll()
, um toggleComplete()
nur einmal mit einem Array von Aufgaben aufzurufen, anstatt eine Aufgabe einzeln als erledigt zu markieren. Löschen Sie auch den Aufruf von _filter()
, da Sie toggleComplete
_filter()
anpassen.
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. Aktualisiere toggleComplete()
jetzt so, dass sowohl eine einzelne Aufgabe als auch ein Array mit Aufgaben akzeptiert wird. Dazu gehört auch, dass filter()
innerhalb von update()
und nicht nach außen verschoben wird.
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();
};
Alle To-Do-Elemente ablegen
Es gibt eine weitere Methode in store.js mit localStorage
:
Store.prototype.drop = function (callback) {
localStorage[this._dbName] = JSON.stringify({todos: []});
callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
};
Diese Methode wird in der aktuellen App nicht aufgerufen. Wenn Sie eine zusätzliche Herausforderung wünschen, können Sie versuchen, sie selbst zu implementieren. Tipp: Wirf einen Blick auf chrome.storage.local.clear()
.
Fertige Todo-App starten
Sie haben Schritt 2 abgeschlossen! Wenn Sie Ihre App neu laden, sollten Sie eine voll funktionsfähige gepackte Chrome-Version von TodoMVC haben.
Weitere Informationen
Detailliertere Informationen zu einigen der in diesem Schritt vorgestellten APIs finden Sie unter:
- Content Security Policy ↑
- Erklärung von Berechtigungen ↑
- chrome.storage ↑
- chrome.storage.local.get() ↑
- chrome.storage.local.set() ↑
- chrome.storage.local.remove() ↑
- chrome.storage.local.clear() ↑
Sind Sie bereit, mit dem nächsten Schritt fortzufahren? Gehe zu Schritt 3: Wecker und Benachrichtigungen hinzufügen »