Schritt 2: Vorhandene Web-App importieren

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.

Ordner „todomvc“ in den Codelab-Ordner kopieren

Sie werden aufgefordert, index.html zu ersetzen. Akzeptieren Sie sie.

„index.html“ ersetzen

Der 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 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:

Fehler beim Protokollieren der Aufgaben-App mit der CSP-Konsole

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:

To-do-App mit Protokollfehler in der localStorage-Konsole

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:

Mit der Console Fehler in chrome.storage beheben

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 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 für alle Callbacks verwendet, damit this auf den this des Store-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:

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 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:

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