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 al criterio di sicurezza del contenuto (CSP) degli script per le 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 di promemoria esistente

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

Abbiamo incluso una versione dell'app TodoMVC nel codice postale di riferimento della 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. Procedi 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'UI di base, ma non in grado di aggiungere cose da fare.

Rendi conforme il criterio di sicurezza del contenuto (CSP) degli script

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

Errore di log dell'app di cose da fare con la console CSP

Risolviamo questo errore rendendo l'app conforme alle Norme sulla sicurezza del contenuto. Uno dei principali la mancata conformità di CSP è causata da JavaScript incorporato. Esempi di codice 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 incorporati in un nuovo file.

1. Nella parte inferiore del file index.html, rimuovi il codice JavaScript incorporato e includilo 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. Nella cartella js, crea un file denominato bootstrap.js. Sposta il codice incorporato in precedenza 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.

Converti localStorage in chrome.storage.local

Se apri ora la console DevTools, l'errore precedente dovrebbe essere scomparso. Si è verificato un nuovo errore, tuttavia, circa window.localStorage non è disponibile:

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

Le app di Chrome non supportano localStorage perché localStorage è sincrono. Accesso sincrono alle risorse di blocco (I/O) in un runtime a thread singolo potrebbe impedire all'app di rispondere.

Le app di Chrome hanno un'API equivalente in grado di archiviare gli oggetti in modo asincrono. In questo modo eviterai a volte è 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 usare chrome.storage.local, devi richiedere l'autorizzazione storage. Nella 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 attività da svolgere, devi conoscere i metodi set() e get() del API chrome.storage.

Il metodo set() accetta un oggetto di coppie chiave-valore come primo parametro. Un'intestazione 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 inutile. Una singola chiave può essere passata come stringa; più chiavi possono essere disposte in un array o un oggetto dizionario.

Il secondo parametro, obbligatorio, è una funzione di callback. Nell'oggetto restituito, utilizza 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 :

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

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

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. Modifica tutte le posizioni in cui viene attualmente utilizzato localStorage, sebbene sia dispendioso in termini di tempo e soggetto a errori, è obbligatorio.

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

  • Invece di scrivere a localStorage con un compito semplice, devi usare 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){...}), quindi analizza il file restituito storage 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 su tutti i callback per garantire che this faccia riferimento al this del Store prototipo. 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 chiave quando tratteremo il recupero, il salvataggio e la rimozione di elementi da fare nella le sezioni seguenti.

Recupera elementi da fare

Aggiorna l'app Da fare per recuperare le voci dell'elenco di cose da fare:

1. Il metodo costruttore Store si occupa di inizializzare l'app Todo con tutte le risorse esistenti le cose da fare dal datastore. Il metodo verifica innanzitutto se il datastore esiste. In caso contrario, crea un array vuoto di todos e salvalo nel datastore in modo che non ci siano errori di lettura del runtime.

In js/store.js, converti l'utilizzo di localStorage nel metodo costruttore in modo da usare 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 durante la lettura delle cose da fare dal modello. I risultati restituiti cambiano in base e scegliere se applicare il filtro in base a "Tutte", "Attive" o "Completate".

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. Analogamente a 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 voci di cose da fare

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 su più di un ad esempio "contrassegna tutti i promemoria come completati", comporterà un rischio per i dati noto come Lettura dopo scrittura. Questo problema non si verifica se utilizzavamo uno spazio di archiviazione dati più appropriato come IndexedDB, ma stiamo cercando di ridurre al minimo il lavoro di conversione per questo codelab.

Esistono diversi modi per risolvere il problema, quindi sfrutteremo questa opportunità per eseguire un leggero refactoring di save() entro di aggiornare contemporaneamente un array di ID attività:

1. Per iniziare, aggrega tutto ciò che è già all'interno di save() con un 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. 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 elementi da fare come completati

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

1. In controller.js, aggiorna toggleAll() per chiamare toggleComplete() solo una volta con un array delle cose da fare invece di contrassegnare un'attività come completata uno alla volta. Elimina anche la chiamata a _filter() dato che modificherai _filter() di toggleComplete.

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() per accettare una singola attività o un array di attività. Sono inclusi spostamento di 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();
};

Rilascia tutte le voci di attività

Esiste un altro metodo in store.js 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 implementare questa funzionalità in autonomia. Suggerimento: dai un'occhiata a chrome.storage.local.clear().

Lancia l'app delle cose da fare completata

Hai completato il passaggio 2. Ricarica la tua app. A questo punto dovresti avere una versione del pacchetto di Chrome completamente funzionante di TodoMVC.

Per maggiori informazioni

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

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