In diesem Schritt erfahren Sie:
- Vorhandene Webanwendung für die Chrome-Apps-Plattform anpassen
- So sorgen Sie dafür, dass Ihre App-Scripts der Content Security Policy (CSP) entsprechen.
- So implementieren Sie lokalen Speicher mit chrome.storage.local.
Geschätzte Dauer für diesen Schritt: 20 Minuten.
Eine Vorschau dessen, was Sie in diesem Schritt tun, finden Sie unten auf dieser Seite ↓.
Vorhandene To-do-App importieren
Importieren Sie zuerst die Vanilla-JavaScript-Version von TodoMVC, einer gängigen Benchmarkanwendung, in Ihr Projekt.
Wir haben eine Version der TodoMVC-App im Zip-Archiv mit dem Referenzcode im Ordner todomvc abgelegt. Kopieren Sie alle Dateien (einschließlich Ordner) aus todomvc in Ihren Projektordner.
Sie werden aufgefordert, index.html zu ersetzen. Akzeptieren Sie die Anfrage.
Ihr Anwendungsordner sollte jetzt die folgende Dateistruktur haben:
Die blau hervorgehobenen Dateien stammen aus dem Ordner todomvc.
Laden Sie die App jetzt neu. Klicken Sie dazu mit der rechten Maustaste und wählen Sie „App neu laden“ aus. Die grundlegende Benutzeroberfläche sollte angezeigt werden, Sie können aber keine Aufgaben hinzufügen.
Scripts CSP-konform machen
Öffnen Sie die Entwicklertools-Konsole (mit der rechten Maustaste klicken > Element untersuchen und dann den Tab Konsole auswählen). Sie erhalten eine Fehlermeldung, wenn Sie die Ausführung eines Inline-Skripts ablehnen:
Beheben Sie diesen Fehler, indem Sie die App so anpassen, dass sie der Content Security Policy entspricht. Eine der häufigsten Nichteinhaltungen von CSP-Anforderungen wird durch Inline-JavaScript verursacht. Beispiele für Inline-JavaScript sind Ereignishandler als DOM-Attribute (z.B. <button onclick=''>
) und <script>
-Tags mit Inhalt in der HTML-Datei.
Die Lösung ist einfach: Verschieben Sie den Inline-Inhalt in eine neue Datei.
1. Entfernen Sie unten in 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 mit dem Namen bootstrap.js. Verschieben Sie den zuvor Inline-Code enthaltenden Code in diese Datei:
// Bootstrap app data
window.app = {};
Wenn Sie die App jetzt neu laden, funktioniert sie immer noch nicht. Sie sind aber auf dem richtigen Weg.
localStorage in chrome.storage.local konvertieren
Wenn Sie jetzt die Entwicklertools-Konsole öffnen, sollte der vorherige Fehler nicht mehr angezeigt werden. Es gibt jedoch einen neuen Fehler, dass window.localStorage
nicht verfügbar ist:
Chrome-Apps unterstützen localStorage
nicht, da localStorage
synchron ist. Der synchrone 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, mit der Objekte asynchron gespeichert werden können. So lässt sich der manchmal kostspielige Serialization-Prozess von Objekten in Strings und zurück in Objekte vermeiden.
Um die Fehlermeldung in unserer App zu beheben, müssen Sie localStorage
in chrome.storage.local konvertieren.
App-Berechtigungen aktualisieren
Zur Verwendung von „chrome.storage.local
“ musst du die Berechtigung „storage
“ anfordern. Fügen Sie in manifest.json dem Array permissions
das Element "storage"
hinzu:
"permissions": ["storage"],
Informationen zu local.storage.set() und local.storage.get()
Wenn Sie Aufgaben speichern und abrufen möchten, müssen Sie die Methoden set()
und get()
der chrome.storage
API kennen.
Die set()-Methode akzeptiert ein Objekt mit Schlüssel/Wert-Paaren als ersten Parameter. Der zweite Parameter ist eine optionale Rückruffunktion. 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 Datastore-Schlüssel, die Sie abrufen möchten. Ein einzelner Schlüssel kann als String übergeben werden. Mehrere Schlüssel können in einem String-Array oder einem Dictionary-Objekt angeordnet werden.
Der zweite Parameter, der erforderlich ist, 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 alles get()
, was sich derzeit in chrome.storage.local
befindet, löschen 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“ in den DevTools prüfen. Du kannst jedoch über die JavaScript-Konsole so mit chrome.storage
interagieren:
Erforderliche API-Änderungen als Vorschau ansehen
Die meisten verbleibenden Schritte zur Konvertierung der Todo-Anwendung sind kleine Änderungen an den API-Aufrufen. Es ist zwar zeitaufwendig und fehleranfällig, aber erforderlich, alle Stellen zu ändern, an denen localStorage
derzeit verwendet wird.
Die wichtigsten Unterschiede zwischen localStorage
und chrome.storage
ergeben sich aus der asynchronen Natur von chrome.storage
:
Anstatt mit einer einfachen Zuweisung in
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 in allen Callbacks verwendet, damit sichthis
auf dasthis
desStore
-Prototyps bezieht. Weitere Informationen zu gebundenen Funktionen finden Sie in der MDN-Dokumentation: 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();
Beachten Sie diese wichtigen Unterschiede, wenn wir in den folgenden Abschnitten das Abrufen, Speichern und Entfernen von Aufgaben behandeln.
Aufgaben abrufen
Aktualisieren wir die To-do-App, um Aufgaben abzurufen:
1. Die Konstruktormethode Store
initialisiert die Todo-App mit allen vorhandenen Aufgaben aus dem Datenspeicher. Die Methode prüft zuerst, ob der Datenspeicher vorhanden ist. Andernfalls wird ein leeres Array von todos
erstellt und im Datenspeicher gespeichert, damit keine Laufzeitlesefehler auftreten.
Ersetzen Sie in js/store.js die Verwendung von localStorage
in der Konstruktormethode durch 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. Die Methode find()
wird verwendet, um Aufgaben aus dem Modell zu lesen. Die zurückgegebenen Ergebnisse ändern sich je nachdem, ob Sie nach „Alle“, „Aktiv“ oder „Abgeschlossen“ filtern.
find()
in chrome.storage.local
konvertieren:
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. Ähnlich wie bei find()
ruft findAll()
alle Aufgaben aus dem Modell ab. findAll()
in chrome.storage.local
konvertieren:
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 save()
-Methode stellt eine Herausforderung dar. Es hängt von zwei asynchronen Vorgängen (get und set) ab, die jedes Mal auf den gesamten monolithischen JSON-Speicher angewendet werden. Batch-Aktualisierungen für mehrere To-do-Elemente, z. B. „Alle To-do-Elemente als erledigt markieren“, führen zu einem Datenrisiko, das als Lesen nach dem Schreiben bezeichnet wird. Dieses Problem würde nicht auftreten, wenn wir eine geeignetere Datenspeicherung wie IndexedDB verwenden würden. Wir versuchen jedoch, den Aufwand für die Umstellung für dieses Codelab zu minimieren.
Es gibt mehrere Möglichkeiten, das Problem zu beheben. Wir nutzen diese Gelegenheit, um save()
leicht zu überarbeiten. Dazu verwenden wir ein Array von To-do-IDs, die alle gleichzeitig aktualisiert werden sollen:
1. Um zu beginnen, umschließen Sie alles, was sich bereits in save()
befindet, 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. Alle Instanzen von localStorage
in chrome.storage.local
konvertieren:
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 auf einem Array statt auf einem einzelnen Element ausgeführt 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 App jetzt mit Arrays arbeitet, müssen Sie ändern, wie die App damit umgeht, wenn ein Nutzer auf die Schaltfläche Abgeschlossene Aufgaben löschen (#) klickt:
1. Aktualisieren Sie toggleAll()
in controller.js so, dass toggleComplete()
nur einmal mit einem Array von Aufgaben aufgerufen wird, anstatt jede Aufgabe einzeln als erledigt zu markieren. Löschen Sie auch den Aufruf von _filter()
, da Sie die toggleComplete
anpassen werden._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. Aktualisieren Sie toggleComplete()
jetzt so, dass sowohl eine einzelne Aufgabe als auch ein Array von Aufgaben akzeptiert wird. Dazu gehört auch, filter()
innerhalb von update()
zu platzieren.
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 Aufgaben ablegen
In store.js gibt es noch eine weitere Methode, in der localStorage
verwendet wird:
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 sich einer zusätzlichen Herausforderung stellen möchten, können Sie sie selbst implementieren. Tipp: Werfen Sie einen Blick auf chrome.storage.local.clear()
.
Fertige To-do-App starten
Schritt 2 ist abgeschlossen. Laden Sie die App neu. Sie sollten jetzt eine vollständig funktionierende, in Chrome verpackte Version von TodoMVC haben.
Weitere Informationen
Weitere Informationen zu einigen der in diesem Schritt vorgestellten APIs finden Sie unter:
- Content Security Policy ↑
- Berechtigungen deklarieren ↑
- 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? Gehen Sie zu Schritt 3: Wecker und Benachrichtigungen hinzufügen.