In diesem Schritt erfahren Sie:
- Vorhandene Webanwendung für die Chrome-Apps-Plattform anpassen
- Hier erfahren Sie, wie Sie Ihre App-Skripts mit der Content Security Policy (CSP) kompatibel machen.
- Lokalen Speicher mithilfe von chrome.storage.local implementieren
Geschätzte Dauer für diesen Schritt: 20 Minuten.
Um eine Vorschau zu sehen, was Sie in diesem Schritt tun werden, springen Sie zum Ende dieser Seite ↓.
Vorhandene To-do-App importieren
Importieren Sie zuerst die Vanilla-JavaScript-Version von TodoMVC, einer gängigen Benchmark. in Ihr Projekt integrieren.
Wir haben eine Version der TodoMVC-App in die Referenzcode-Zip in der Datei todomvc aufgenommen. Ordner. Kopieren Sie alle Dateien (einschließlich Ordner) von todomvc in Ihren Projektordner.
Sie werden aufgefordert, index.html zu ersetzen. Bitte akzeptieren Sie sie.
Ihr Anwendungsordner sollte nun 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. Sie sollten die einfache Benutzeroberfläche sehen, To-do-Listen hinzufügen.
Scripts mit der Content Security Policy (CSP) kompatibel machen
Öffnen Sie die Entwicklertools-Konsole (Rechtsklick > Element untersuchen und dann den Tab Konsole). Ich wird ein Fehler angezeigt, weil Sie die Ausführung eines Inline-Skripts ablehnen:
Diesen Fehler beheben wir, indem wir die Content Security Policy der App entsprechend anpassen. Eine der besten
Häufige CSP-Verstöße werden durch Inline-JavaScript verursacht. Beispiele für Inline-JavaScript-Code:
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 schließen Sie stattdessen 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. Erstellen Sie im Ordner js eine Datei namens bootstrap.js. Vorherigen Inline-Code verschieben in dieser Datei:
// Bootstrap app data
window.app = {};
Wenn du die App jetzt aktualisierst, bist du immer noch nicht funktionsfähig, aber du kommst der App immer näher.
„localStorage“ in „chrome.storage.local“ konvertieren
Wenn du die Entwicklertools-Konsole jetzt öffnest, sollte der vorherige Fehler nicht mehr angezeigt werden. Es gibt einen neuen Fehler.
Allerdings ist window.localStorage
nicht verfügbar:
Chrome-Apps unterstützen localStorage
nicht, da localStorage
synchron ist. Synchroner Zugriff
zu blockierenden Ressourcen (E/A) in einer Single-Threaded-Laufzeit kann dazu führen, dass Ihre App nicht mehr reagiert.
Chrome-Apps verfügen über eine entsprechende API, die Objekte asynchron speichern kann. So lässt sich vermeiden, manchmal kostspieliger Objekt->String->Objektserialisierungsprozess.
Um die Fehlermeldung in unserer App zu beheben, musst du localStorage
konvertieren in
chrome.storage.local
App-Berechtigungen aktualisieren
Zur Verwendung von „chrome.storage.local
“ musst du die Berechtigung „storage
“ anfordern. In
manifest.json fügen Sie "storage"
zum Array permissions
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()
des
chrome.storage
API verwenden.
Für die set()-Methode wird ein Objekt mit Schlüssel/Wert-Paaren als ersten Parameter akzeptiert. Eine optionale Callback-Funktion der zweite Parameter. 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 verwenden möchten. retreive. Ein einzelner Schlüssel kann als String übergeben werden. können mehrere Schlüssel in einem Array oder ein Wörterbuchobjekt.
Der zweite erforderliche Parameter ist eine Callback-Funktion. Verwenden Sie im zurückgegebenen Objekt die Methode Schlüssel, die im ersten Parameter angefordert wurden, 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, die sich derzeit in chrome.storage.local
befinden, mit get()
versehen werden sollen, lassen Sie die erste
Parameter:
chrome.storage.local.get(function(data) {
console.log(data);
});
Im Gegensatz zu localStorage
kannst du keine lokal gespeicherten Elemente mit den Entwicklertools prüfen
Ressourcenbereich. Du kannst jedoch über die JavaScript-Konsole mit chrome.storage
interagieren:
Also:
Erforderliche API-Änderungen als Vorschau ansehen
Die meisten verbleibenden Schritte zur Konvertierung der Todo-Anwendung sind kleine Änderungen an den API-Aufrufen. Wird geändert
an allen Orten, an denen localStorage
gerade verwendet wird, auch wenn sie zeitaufwändig und fehleranfällig ist,
ist erforderlich.
Die Hauptunterschiede zwischen localStorage
und chrome.storage
sind auf den asynchronen Charakter von
chrome.storage
:
Anstatt mit einer einfachen Aufgabe in
localStorage
zu schreiben, müssen Siechrome.storage.local.set()
mit optionalen Callbacks.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 Sie diechrome.storage.local.get(myStorageName,function(storage){...})
und parsen Sie dann das zurückgegebenestorage
-Objekt in der Callback-Funktion.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 bei allen Callbacks verwendet, um sicherzustellen, dassthis
auf denthis
desStore
-Prototyp. 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();
Behalten Sie diese wichtigen Unterschiede im Hinterkopf, wenn wir uns mit dem Abrufen, Speichern und Entfernen von To-do-Elementen im folgenden Abschnitten.
Aufgaben abrufen
So aktualisieren Sie die To-Do-App, um To-do-Elemente abzurufen:
1. Die Konstruktormethode Store
übernimmt die Initialisierung der Todo-App mit allen vorhandenen
To-do-Elemente aus dem Datenspeicher. Die Methode prüft zunächst, ob der Datenspeicher vorhanden ist. Ist dies nicht der Fall,
ein leeres Array von todos
erstellen und im Datenspeicher speichern, damit keine Laufzeitlesefehler auftreten.
Konvertieren Sie in js/store.js die Verwendung von localStorage
in der Konstruktormethode in:
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 beim Lesen von Aufgaben aus dem Modell verwendet. Die zurückgegebenen Ergebnisse ändern sich
ob Sie nach „Alle“, „Aktiv“ oder „Abgeschlossen“ filtern.
Konvertieren Sie find()
, um chrome.storage.local
zu verwenden:
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 find()
erhält findAll()
alle Aufgaben vom Modell. findAll()
konvertieren, um zu verwenden
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));
};
Aufgaben speichern
Die aktuelle Methode save()
stellt eine Herausforderung dar. Sie hängt von zwei asynchronen Vorgängen ab (get und set)
die den gesamten monolithischen JSON-Speicher nutzen. Alle Batch-Updates auf mehr als einem
zu erledigen, wie „Alle Aufgaben als erledigt markieren“, führt zu einer Datengefährdung,
Read-After-Write: Dieses Problem würde nicht auftreten, wenn wir eine geeignetere Datenspeicherung verwenden würden.
wie IndexedDB, aber wir versuchen, den Konvertierungsaufwand für dieses Codelab zu minimieren.
Es gibt mehrere Möglichkeiten, das Problem zu beheben. Wir werden diese Gelegenheit nutzen, um save()
geringfügig zu refaktorieren, indem wir
mit einer Reihe von To-Do-IDs, die alle gleichzeitig aktualisiert werden sollen:
1. Fasse zuerst alle Elemente, die sich bereits in save()
befinden, in chrome.storage.local.get()
ein.
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 ein Array anstelle eines einzelnen Elements verwendet 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 nun mit Arrays arbeitet, müssen Sie ändern, wie die App mit einem Nutzer durch Klicken auf Schaltfläche Erledigte löschen (#):
1. Aktualisieren Sie toggleAll()
in controller.js so, dass toggleComplete()
nur einmal mit einem Array aufgerufen wird.
anstatt einzelne Aufgaben als erledigt zu markieren. Auch _filter()
-Anruf löschen
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. Aktualisieren Sie toggleComplete()
jetzt so, dass sowohl eine einzelne Aufgabe als auch ein Array von Aufgaben akzeptiert wird. Dazu gehören
filter()
wird so verschoben, dass es sich innerhalb von update()
befindet, nicht nach außen.
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 eine weitere Methode, die localStorage
verwendet:
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 Aufgabe wünschen, versuchen Sie,
selbst implementieren. Tipp: Werfen Sie einen Blick auf chrome.storage.local.clear()
.
Fertige To-do-App starten
Sie haben Schritt 2 abgeschlossen. Aktualisieren Sie Ihre App. Sie sollten nun eine voll funktionsfähige Chrome-Version haben. von TodoMVC.
Weitere Informationen
Ausführlichere Informationen zu einigen der in diesem Schritt vorgestellten APIs finden Sie hier:
- 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 für den nächsten Schritt? Gehen Sie zu Schritt 3: Wecker und Benachrichtigungen hinzufügen.