Passaggio 2: importa un'app web esistente

In questo passaggio, imparerai:

  • Come adattare un'applicazione web esistente alla piattaforma App di Chrome.
  • Come rendere conforme il CSP (Content Security Policy) per gli script delle app.
  • Come implementare lo spazio di archiviazione locale utilizzando chrome.storage.local.

Tempo stimato per completare questo passaggio: 20 minuti.
Per visualizzare l'anteprima di ciò che completerai in questo passaggio, vai alla fine di questa pagina ↓.

Importare un'app Attività esistente

Per iniziare, importa la versione JavaScript Vanilla di TodoMVC, un'app di benchmark comune, nel tuo progetto.

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

Copia la cartella todomvc nella cartella codelab

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

Sostituisci index.html

Ora dovresti avere la seguente struttura di file nella tua cartella dell'applicazione:

Nuova cartella di progetto

I file evidenziati in blu provengono dalla cartella todomvc.

Ricarica l'app adesso (fai clic con il tasto destro del mouse > Ricarica app). Dovresti vedere l'interfaccia utente di base, ma non potrai aggiungere promemoria.

Rendi gli script conformi al 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 incorporato:

Errore di log della console CSP nell'app di promemoria

Risolviamo questo errore rendendo l'app conforme ai Criteri di sicurezza del contenuto. Una delle non conformità ai CSP più comuni è causata da JavaScript incorporato. Esempi di JavaScript incorporato includono gestori di eventi come attributi DOM (ad es. <button onclick=''>) e tag <script> con contenuti all'interno del codice HTML.

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

1. Nella parte inferiore della pagina 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 denominata bootstrap.js. Sposta il codice precedentemente incorporato in questo file:

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

Avrai ancora un'app Attività non funzionante se ricarichi l'app ora ma ti stai avvicinando.

Converti localStorage in chrome.storage.local

Se apri la console DevTools, l'errore precedente dovrebbe essere risolto. Tuttavia, si è verificato un nuovo errore che indica che window.localStorage non è disponibile:

Errore di log della console localStorage nell&#39;app di cose da fare

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 rendere la tua app non risponde.

Le app di Chrome hanno un'API equivalente in grado di archiviare 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"],

Ulteriori informazioni su local.storage.set() e local.storage.get()

Per salvare e recuperare gli elementi di promemoria, devi conoscere i metodi set() e get() dell'API chrome.storage.

Il metodo set() accetta un oggetto di coppie chiave-valore come primo parametro. Una funzione di callback facoltativa è 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 trasmessa come stringa; più chiavi possono essere organizzate in un array di stringhe o in un oggetto del 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 esaminare gli elementi archiviati localmente utilizzando il riquadro Risorse DevTools. Tuttavia, puoi interagire con chrome.storage dalla console JavaScript in questo modo:

Utilizzare la console per eseguire il debug di chrome.storage

Visualizza l'anteprima delle modifiche API richieste

La maggior parte dei passaggi rimanenti per la conversione dell'app Todo consiste in piccole modifiche alle chiamate API. È obbligatorio modificare tutte le posizioni in cui localStorage è attualmente in uso, anche se richiede molto tempo e presenta errori.

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

  • Anziché scrivere a localStorage utilizzando un'assegnazione semplice, devi utilizzare chrome.storage.local.set() con callback facoltativi.

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

    a confronto con

    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 analizzare l'oggetto storage restituito nella funzione di callback.

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

    a confronto con

    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 nei documenti MDN: Function.prototype.bind().

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

    a confronto con

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

Tieni presente queste principali differenze quando tratteremo recupero, salvataggio e rimozione degli elementi di promemoria nelle sezioni seguenti.

Recupera elementi attività

Aggiorniamo l'app Da fare per recuperare le cose da fare:

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

In js/store.js, converti l'uso di localStorage nel metodo del costruttore per utilizzare invece 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 gli impegni dal modello. I risultati restituiti cambiano a seconda che tu stia filtrando i dati per "Tutti", "Attivi" o "Completati".

Converti find() per 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. Come per find(), findAll() ottiene tutti i promemoria dal modello. Converti findAll() per 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));
};

Salva gli elementi dei promemoria

L'attuale metodo save() presenta una sfida. Dipende da due operazioni asincrone (get e set) che operano ogni volta sull'intero spazio di archiviazione JSON monolitico. Eventuali aggiornamenti batch per più voci di attività, ad esempio "contrassegna tutti i promemoria come completati", comporterà un pericolo per i dati noto come Read-After-Write. Questo problema non si verifica se stessimo utilizzando uno spazio di archiviazione dei dati più appropriato, come IndexedDB, ma stiamo cercando di ridurre al minimo lo sforzo di conversione per questo codelab.

Esistono diversi modi per risolvere il problema, quindi utilizzeremo questa opportunità per effettuare un refactoring leggermente di save() impostando un array di ID attività da aggiornare contemporaneamente:

1. Per iniziare, aggrega tutti i contenuti già all'interno di save() con un callback di 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 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));
};

Contrassegna le cose da fare come completate

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

1. In controller.js, aggiorna toggleAll() in modo che chiami toggleComplete() solo una volta con un array di cose da fare, anziché contrassegnare un promemoria come completato uno alla volta. Elimina anche la chiamata a _filter() poiché modificherai le 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 promemoria sia un array di attività. Ciò include lo spostare filter() in modo che sia 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();
};

Rilascia tutti gli elementi di promemoria

Esiste un altro metodo in store.js utilizzando localStorage:

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

Questo metodo non viene chiamato nell'app attuale, quindi, se vuoi un'ulteriore verifica, prova a implementarla autonomamente. Suggerimento: guarda chrome.storage.local.clear().

Avvia l'app Attività completata

Fatto! Passaggio 2. Ricarica la tua app e ora dovresti avere una versione di TodoMVC in pacchetto di Chrome completamente funzionante.

Per maggiori informazioni

Per informazioni più dettagliate su alcune delle API introdotte in questo passaggio, fai riferimento a:

Vuoi continuare con il passaggio successivo? Vai al Passaggio 3: aggiungi sveglie e notifiche »