Passaggio 2: importa un'app web esistente

In questo passaggio scoprirai:

  • Come adattare un'applicazione web esistente per la piattaforma App di Chrome.
  • Come rendere conformi gli script dell'app ai Criteri di sicurezza del contenuto (CSP).
  • Come implementare lo spazio di archiviazione locale utilizzando chrome.storage.local.

Tempo stimato per completare questo passaggio: 20 minuti.
Per visualizzare un'anteprima di ciò che dovrai completare in questo passaggio, vai in fondo a questa pagina ↓.

Importare un'app To Do esistente

Come punto di partenza, importa nel tuo progetto la versione JavaScript di TodoMVC, un'app di benchmark comune.

Abbiamo incluso una versione dell'app TodoMVC nel codice di riferimento zip della cartella todomvc. Copia tutti i file (incluse le cartelle) da todomvc nella cartella del progetto.

Copia cartella todomvc nella cartella codelab

Ti verrà chiesto di sostituire index.html. Vai avanti e accetta.

Sostituisci index.html

A questo punto nella cartella dell'applicazione dovresti avere la seguente struttura di file:

Nuova cartella di progetto

I file evidenziati in blu provengono dalla cartella todomvc.

Ricarica l'app ora (fai clic con il tasto destro del mouse > Ricarica app). Dovresti vedere l'interfaccia utente di base, ma non puoi aggiungere cose da fare.

Rendere gli script conformi ai criteri di sicurezza del contenuto (CSP)

Apri la console DevTools (fai clic con il tasto destro del mouse > Ispeziona elemento, quindi seleziona la scheda Console). Verrà visualizzato un errore relativo al rifiuto di eseguire uno script in linea:

App di liste di cose da fare con errore nel log della console CSP

Correggiamo questo errore rendendo l'app conforme alle norme Content Security Policy. Una delle mancanze di conformità ai CSP più comuni è causata da JavaScript in linea. Esempi di JavaScript incorporato includono gestori di eventi come attributi DOM (ad es. <button onclick=''>) e tag <script> con contenuti all'interno dell'HTML.

La soluzione è semplice: sposta i contenuti in linea in un nuovo file.

1. Nella parte inferiore di index.html, rimuovi il codice JavaScript incorporato e includi invece 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. Crea un file nella cartella js denominato bootstrap.js. Sposta il codice precedentemente incorporato in questo file:

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

Se ricarichi l'app adesso, avrai ancora un'app Da fare non funzionante, ma ti stai avvicinando.

Convertire localStorage in chrome.storage.local

Se apri ora la console DevTools, l'errore precedente dovrebbe essere scomparso. Tuttavia, è stato rilevato un nuovo errore relativo alla mancata disponibilità di window.localStorage:

App To Do con errore nel log della console di localStorage

Le app di Chrome non supportano localStorage perché localStorage è sincrono. L'accesso sincrono alle risorse di blocco (I/O) in un runtime a thread singolo potrebbe causare l'interruzione della risposta dell'app.

Le app di Chrome hanno un'API equivalente che può archiviare gli oggetti in modo asincrono. In questo modo eviterai il costoso processo di serializzazione oggetto->stringa->oggetto.

Per risolvere il messaggio di errore nella nostra app, devi convertire localStorage in chrome.storage.local.

Aggiornamento delle autorizzazioni per le app

Per utilizzare chrome.storage.local, devi richiedere l'autorizzazione storage. In manifest.json, aggiungi "storage" all'array permissions:

"permissions": ["storage"],

Scopri di più su local.storage.set() e local.storage.get()

Per salvare e recuperare le voci di attività, devi conoscere i metodi set() e get() dell'API chrome.storage.

Il metodo set() accetta un oggetto di coppie chiave/valore come primo parametro. Un'eventuale funzione di callback è il secondo parametro. Ad esempio:

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

Il metodo get() accetta un primo parametro facoltativo per le chiavi del datastore che vuoi recuperare. Una singola chiave può essere passata come stringa; più chiavi possono essere organizzate in un array di stringhe o in un oggetto dizionario.

Il secondo parametro, obbligatorio, è una funzione di callback. Nell'oggetto restituito, utilizza le chiavi richieste nel primo parametro per accedere ai valori archiviati. Ad esempio:

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

Se vuoi get() tutto ciò che è attualmente in chrome.storage.local, ometti il primo parametro:

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

A differenza di localStorage, non potrai ispezionare gli elementi archiviati localmente utilizzando il riquadro Risorse DevTools. Tuttavia, puoi interagire con chrome.storage dalla console JavaScript come segue:

Utilizzare la console per eseguire il debug di chrome.storage

Visualizza l'anteprima delle modifiche richieste all'API

La maggior parte dei passaggi rimanenti per la conversione dell'app Todo sono piccole modifiche alle chiamate API. È necessaria la modifica di tutti i luoghi in cui localStorage è attualmente in uso, anche se questa operazione è lunga e soggetta a errori.

Le principali differenze tra localStorage e chrome.storage derivano dalla natura asincrona di chrome.storage:

  • Invece di scrivere in localStorage utilizzando un'assegnazione semplice, devi utilizzare chrome.storage.local.set() con callback facoltativi.

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

    rispetto a

    var storage = {};
    storage[dbName] = { todos: [] };
    chrome.storage.local.set( storage, function() {
      // optional callback
    });
    
  • Anziché accedere direttamente a localStorage[myStorageName], devi utilizzare chrome.storage.local.get(myStorageName,function(storage){...}) e poi analizzare l'oggetto storage restituito nella funzione di callback.

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

    rispetto a

    chrome.storage.local.get(dbName, function(storage) {
      var todos = storage[dbName].todos;
    });
    
  • La funzione .bind(this) viene utilizzata in tutti i callback per garantire che this faccia riferimento al this del prototipo Store. Ulteriori informazioni sulle funzioni associate sono disponibili nella documentazione MDN: Function.prototype.bind().

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

    rispetto a

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

Tieni presenti queste differenze principali mentre nelle sezioni seguenti parleremo di recupero, salvataggio e rimozione degli elementi di promemoria.

Recuperare gli elementi della lista di cose da fare

Aggiorna l'app To Do per recuperare gli elementi della lista di cose da fare:

1. Il metodo costruttore Store si occupa di inizializzare l'app Todo con tutte le voci di attività esistenti del datastore. Il metodo verifica innanzitutto se il datastore esiste. In caso contrario, verrà creato un array vuoto di todos e salvato nel datastore in modo che non si verifichino errori di lettura di runtime.

In js/store.js, converti l'utilizzo di localStorage nel metodo del costruttore in modo da utilizzare 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. Il metodo find() viene utilizzato per leggere le attività dal modello. I risultati restituiti cambiano a seconda che il filtro sia "Tutti", "Attivo" o "Completato".

Per convertire find() in modo da utilizzare chrome.storage.local:

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. Analogamente a find(), findAll() ottiene tutti i promemoria dal modello. Per convertire findAll() in modo da utilizzare 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));
};

Salvare gli elementi della lista di cose da fare

Il metodo save() attuale presenta una sfida. Dipende da due operazioni asincrone (get e set) che operano ogni volta sull'intero spazio di archiviazione JSON monolitico. Eventuali aggiornamenti collettivi su più elementi della lista di cose da fare, ad esempio "Contrassegna tutti i compiti come completati", comporteranno un rischio per i dati noto come Lettura dopo scrittura. Questo problema non si verificherebbe se utilizzassimo un sistema di archiviazione dei dati più appropriato, come IndexedDB, ma stiamo cercando di ridurre al minimo le operazioni di conversione per questo codelab.

Esistono diversi modi per risolvere il problema, quindi utilizzeremo questa opportunità per eseguire una leggera riorganizzazione di save() selezionando un array di ID attività da aggiornare contemporaneamente:

1. Per iniziare, racchiudi tutto ciò che è già all'interno di save() con un 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. Converti tutte le istanze di localStorage con 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. Quindi aggiorna la logica in modo da operare su un array anziché su un singolo elemento:

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

Contrassegnare gli elementi della lista di cose da fare come completati

Ora che l'app funziona su array, devi modificare il modo in cui l'app gestisce un utente facendo clic sul pulsante Cancella completate (#):

1. In controller.js, aggiorna toggleAll() per chiamare toggleComplete() una sola volta con un array di cose da fare invece di contrassegnare un'attività come completata uno alla volta. Elimina anche la chiamata a _filter() poiché modificherai toggleComplete _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. Ora aggiorna toggleComplete() in modo che accetti sia un singolo elemento da fare sia un array di elementi da fare. ad esempio, spostare filter() all'interno di update() anziché all'esterno.

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

Elimina tutti gli elementi della lista di cose da fare

In store.js è presente un altro metodo che utilizza localStorage:

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

Questo metodo non è stato chiamato nell'app corrente quindi, se vuoi una sfida in più, prova a implementarlo autonomamente. Suggerimento: dai un'occhiata a chrome.storage.local.clear().

Avvia l'app To Do completata

Hai completato il passaggio 2. Ricarica l'app e ora dovresti avere una versione pacchettizzata di Chrome completamente funzionante di TodoMVC.

Per ulteriori informazioni

Per informazioni più dettagliate su alcune delle API introdotte in questo passaggio, consulta:

Vuoi andare al passaggio successivo? Vai al passaggio 3: aggiungi sveglie e notifiche »