Stap 2: Importeer een bestaande webapp

In deze stap leer je:

  • Hoe u een bestaande webapplicatie aanpast voor het Chrome Apps-platform.
  • Hoe u uw app-scripts compatibel kunt maken met Content Security Policy (CSP).
  • Hoe u lokale opslag implementeert met behulp van chrome.storage.local .

Geschatte tijd om deze stap te voltooien: 20 minuten.
Om een ​​voorbeeld te zien van wat u in deze stap gaat voltooien, springt u naar de onderkant van deze pagina ↓ .

Importeer een bestaande Todo-app

Importeer als startpunt de standaard JavaScript-versie van TodoMVC , een veelgebruikte benchmark-app, in uw project.

We hebben een versie van de TodoMVC-app opgenomen in de referentiecode-zip in de todomvc- map. Kopieer alle bestanden (inclusief mappen) van todomvc naar uw projectmap.

Kopieer de todomvc-map naar de codelab-map

U wordt gevraagd index.html te vervangen. Ga je gang en accepteer.

Vervang index.html

U zou nu de volgende bestandsstructuur in uw applicatiemap moeten hebben:

Nieuwe projectmap

De blauw gemarkeerde bestanden komen uit de map todomvc .

Laad uw app nu opnieuw ( klik met de rechtermuisknop > App opnieuw laden ). U zou de basisgebruikersinterface moeten zien, maar u kunt geen taken toevoegen.

Maak scripts Content Security Policy (CSP)-compatibel

Open de DevTools-console ( klik met de rechtermuisknop op > Element inspecteren en selecteer vervolgens het tabblad Console ). U zult een foutmelding zien over het weigeren om een ​​inline script uit te voeren:

Todo-app met logfout in de CSP-console

Laten we deze fout oplossen door het Content Security Policy van de app compatibel te maken. Een van de meest voorkomende niet-nalevingen van CSP wordt veroorzaakt door inline JavaScript. Voorbeelden van inline JavaScript zijn gebeurtenishandlers als DOM-attributen (bijvoorbeeld <button onclick=''> ) en <script> -tags met inhoud in de HTML.

De oplossing is eenvoudig: verplaats de inline-inhoud naar een nieuw bestand.

1. Verwijder onderaan index.html het inline JavaScript en neem in plaats daarvan js/bootstrap.js op:

<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. Maak een bestand in de js- map met de naam bootstrap.js . Verplaats de eerder inline code naar dit bestand:

// Bootstrap app data
window.app = {};

Je hebt nog steeds een niet-werkende Todo-app als je de app nu opnieuw laadt, maar je komt dichterbij.

Converteer localStorage naar chrome.storage.local

Als u nu de DevTools-console opent, zou de vorige fout verdwenen moeten zijn. Er is echter een nieuwe fout dat window.localStorage niet beschikbaar is:

Todo-app met logfout van de localStorage-console

Chrome-apps ondersteunen geen localStorage omdat localStorage synchroon is. Synchrone toegang tot blokkerende bronnen (I/O) in een runtime met één thread kan ervoor zorgen dat uw app niet meer reageert.

Chrome-apps hebben een gelijkwaardige API die objecten asynchroon kan opslaan. Dit helpt het soms kostbare object->string->object-serialisatieproces te vermijden.

Om de foutmelding in onze app op te lossen, moet u localStorage converteren naar chrome.storage.local .

Update app-machtigingen

Om chrome.storage.local te kunnen gebruiken, moet u storage aanvragen. Voeg in manifest.json "storage" toe aan de permissions array:

"permissions": ["storage"],

Meer informatie over local.storage.set() en local.storage.get()

Als u taken wilt opslaan en ophalen, moet u op de hoogte zijn van de methoden set() en get() van de chrome.storage API.

De set()- methode accepteert een object met sleutel-waardeparen als eerste parameter. Een optionele callback-functie is de tweede parameter. Bijvoorbeeld:

chrome.storage.local.set({secretMessage:'Psst!',timeSet:Date.now()}, function() {
  console.log("Secret message saved");
});

De methode get() accepteert een optionele eerste parameter voor de gegevensopslagsleutels die u wilt ophalen. Een enkele sleutel kan als een string worden doorgegeven; meerdere sleutels kunnen worden gerangschikt in een reeks strings of een woordenboekobject.

De tweede parameter, die vereist is, is een callback-functie. Gebruik in het geretourneerde object de sleutels die zijn aangevraagd in de eerste parameter om toegang te krijgen tot de opgeslagen waarden. Bijvoorbeeld:

chrome.storage.local.get(['secretMessage','timeSet'], function(data) {
  console.log("The secret message:", data.secretMessage, "saved at:", data.timeSet);
});

Als je alles wilt get() wat zich momenteel in chrome.storage.local bevindt, laat dan de eerste parameter weg:

chrome.storage.local.get(function(data) {
  console.log(data);
});

In tegenstelling tot localStorage kunt u lokaal opgeslagen items niet inspecteren met behulp van het DevTools Resources-paneel. U kunt echter als volgt communiceren met chrome.storage vanuit de JavaScript-console:

Gebruik de console om fouten in chrome.storage op te sporen

Bekijk een voorbeeld van de vereiste API-wijzigingen

De meeste resterende stappen bij het converteren van de Todo-app zijn kleine wijzigingen in de API-aanroepen. Het wijzigen van alle plaatsen waar localStorage momenteel wordt gebruikt, is weliswaar tijdrovend en foutgevoelig, maar is wel vereist.

De belangrijkste verschillen tussen localStorage en chrome.storage komen voort uit het asynchrone karakter van chrome.storage :

  • In plaats van naar localStorage te schrijven met behulp van een eenvoudige toewijzing, moet u chrome.storage.local.set() gebruiken met optionele callbacks.

    var data = { todos: [] };
    localStorage[dbName] = JSON.stringify(data);
    

    versus

    var storage = {};
    storage[dbName] = { todos: [] };
    chrome.storage.local.set( storage, function() {
      // optional callback
    });
    
  • In plaats van rechtstreeks toegang te krijgen tot localStorage[myStorageName] , moet u chrome.storage.local.get(myStorageName,function(storage){...}) gebruiken en vervolgens het geretourneerde storage parseren in de callback-functie.

    var todos = JSON.parse(localStorage[dbName]).todos;
    

    versus

    chrome.storage.local.get(dbName, function(storage) {
      var todos = storage[dbName].todos;
    });
    
  • De functie .bind(this) wordt bij alle callbacks gebruikt om ervoor te zorgen this verwijst naar de this van het Store prototype. (Meer informatie over gebonden functies vindt u in de MDN-documentatie: Function.prototype.bind() .)

    function Store() {
      this.scope = 'inside Store';
      chrome.storage.local.set( {}, function() {
        console.log(this.scope); // outputs: 'undefined'
      });
    }
    new Store();
    

    versus

    function Store() {
      this.scope = 'inside Store';
      chrome.storage.local.set( {}, function() {
        console.log(this.scope); // outputs: 'inside Store'
      }.bind(this));
    }
    new Store();
    

Houd deze belangrijke verschillen in gedachten terwijl we in de volgende secties het ophalen, opslaan en verwijderen van taken bespreken.

Haal taken op

Laten we de Todo-app updaten om taken op te halen:

1. De Store constructormethode zorgt voor het initialiseren van de Todo-app met alle bestaande taken uit de datastore. De methode controleert eerst of de datastore bestaat. Als dit niet het geval is, wordt er een lege reeks todos gemaakt en opgeslagen in de datastore, zodat er geen runtime-leesfouten optreden.

Converteer in js/store.js het gebruik van localStorage in de constructormethode om in plaats daarvan chrome.storage.local te gebruiken:

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. De methode find() wordt gebruikt bij het lezen van taken uit het model. De geretourneerde resultaten veranderen afhankelijk van of u filtert op 'Alle', 'Actief' of 'Voltooid'.

Converteer find() om chrome.storage.local te gebruiken:

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. Net als find() haalt findAll() alle taken uit het model. Converteer findAll() om chrome.storage.local te gebruiken:

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));
};

Bewaar takenitems

De huidige save() methode vormt een uitdaging. Het hangt af van twee asynchrone bewerkingen (get en set) die elke keer op de hele monolithische JSON-opslag werken. Elke batchupdate voor meer dan één taakitem, zoals "markeer alle taken als voltooid", zal resulteren in een gegevensrisico dat bekend staat als Read-After-Write . Dit probleem zou niet optreden als we een geschiktere gegevensopslag zouden gebruiken, zoals IndexedDB, maar we proberen de conversie-inspanningen voor dit codelab te minimaliseren.

Er zijn verschillende manieren om dit probleem op te lossen, dus we zullen van deze gelegenheid gebruik maken om save() enigszins te refactoriseren door een reeks taken-ID's te nemen die in één keer moeten worden bijgewerkt:

1. Om te beginnen wikkelt u alles dat zich al in save() bevindt, in met een callback chrome.storage.local.get() :

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. Converteer alle localStorage -instanties met 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. Werk vervolgens de logica bij zodat deze op een array werkt in plaats van op één item:

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));
};

Markeer taken als voltooid

Nu de app op arrays werkt, moet u de manier wijzigen waarop de app omgaat met een gebruiker die op de knop Wissen voltooid (#) klikt:

1. Update in controller.js toggleAll() zodat toggleComplete() slechts één keer wordt aangeroepen met een reeks taken, in plaats van een taak één voor één als voltooid te markeren. Verwijder ook de aanroep van _filter() omdat u de toggleComplete _filter() gaat aanpassen.

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. Update nu toggleComplete() om zowel een enkele taak als een reeks taken te accepteren. Dit omvat het verplaatsen filter() naar binnen de update() , in plaats van daarbuiten.

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();
};

Laat alle taken vallen

Er is nog een methode in store.js die localStorage gebruikt:

Store.prototype.drop = function (callback) {
  localStorage[this._dbName] = JSON.stringify({todos: []});
  callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
};

Deze methode wordt niet aangeroepen in de huidige app, dus als je een extra uitdaging wilt, probeer deze dan zelf te implementeren. Tip: kijk eens naar chrome.storage.local.clear() .

Start uw voltooide Todo-app

Je bent klaar Stap 2! Laad uw app opnieuw en u zou nu een volledig werkende Chrome-verpakte versie van TodoMVC moeten hebben.

Voor meer informatie

Voor meer gedetailleerde informatie over enkele van de API's die in deze stap zijn geïntroduceerd, raadpleegt u:

Klaar om door te gaan naar de volgende stap? Ga naar Stap 3 - Alarmen en meldingen toevoegen »