Schritt 2: Vorhandene Web-App importieren

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.

Ordner „todomvc“ in den Codelab-Ordner kopieren

Sie werden aufgefordert, index.html zu ersetzen. Akzeptieren Sie die Anfrage.

index.html ersetzen

Ihr Anwendungsordner sollte jetzt die folgende Dateistruktur haben:

Neuer Projektordner

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:

Todo-App mit CSP-Konsolenprotokollfehler

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:

Todo-App mit localStorage-Konsolenprotokollfehler

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:

chrome.storage mit der Console debuggen

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 Sie chrome.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 Sie chrome.storage.local.get(myStorageName,function(storage){...}) verwenden und dann das zurückgegebene storage-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 sich this auf das this des Store-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:

Todo app with localStorage console log error

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:

Sind Sie bereit, mit dem nächsten Schritt fortzufahren? Gehen Sie zu Schritt 3: Wecker und Benachrichtigungen hinzufügen.