Paso 2: Importa una app web existente

En este paso, aprenderás lo siguiente:

  • Cómo adaptar una aplicación web existente para la plataforma de Apps de Chrome.
  • Cómo hacer que las secuencias de comandos de tu app cumplan con la Política de Seguridad del Contenido (CSP)
  • Cómo implementar el almacenamiento local con chrome.storage.local

Tiempo estimado para completar este paso: 20 minutos
Para obtener una vista previa de lo que completarás en este paso, ve a la parte inferior de esta página ↓.

Importa una app de tareas pendientes existente

Como punto de partida, importa la versión básica de JavaScript de TodoMVC, una comparativa común en tu proyecto.

Incluimos una versión de la app TodoMVC en el código postal de referencia de todomvc. carpeta. Copia todos los archivos (incluidas las carpetas) de todomvc a la carpeta de tu proyecto.

Copia la carpeta todomvc en la carpeta del codelab

Se te pedirá que reemplaces index.html. Continúa y acepta.

Reemplaza index.html

Ahora deberías tener la siguiente estructura de archivos en la carpeta de tu aplicación:

Nueva carpeta del proyecto

Los archivos destacados en azul pertenecen a la carpeta todomvc.

Vuelve a cargar tu aplicación ahora (haz clic con el botón derecho > Volver a cargar la aplicación). Deberías ver la IU básica, pero no agregar todos.

Hacer que las secuencias de comandos cumplan con la Política de Seguridad del Contenido (CSP)

Abra la consola de Herramientas para desarrolladores (haga clic con el botón derecho > Inspeccionar elemento y, luego, seleccione la pestaña Consola). Tú verás un error que indica que no deseas ejecutar una secuencia de comandos intercalada:

Error de registro de la app de tareas pendientes con la consola de CSP

Para corregir este error, asegúrate de que la app cumpla con la Política de Seguridad del Contenido. Uno de los los incumplimientos comunes de la CSP son causados por JavaScript intercalado. Algunos ejemplos de JavaScript intercalado son controladores de eventos como atributos del DOM (p.ej., <button onclick=''>) y etiquetas <script> con contenido dentro del código HTML.

La solución es simple: mueve el contenido intercalado a un archivo nuevo.

1. Cerca de la parte inferior de index.html, quita el código JavaScript intercalado y, en su lugar, inclúyelo 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 archivo en la carpeta js llamado bootstrap.js. Mueve el código insertado anteriormente en este archivo:

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

Si vuelves a cargar la app ahora, pero te estás acercando, seguirás teniendo una app de tareas pendientes que no funciona.

Convierte localStorage en chrome.storage.local

Si abres la consola de Herramientas para desarrolladores ahora, el error anterior debería desaparecer. Hay un error nuevo, Sin embargo, sobre el hecho de que window.localStorage no está disponible:

Error de registro de la app de tareas pendientes con la consola localStorage

Las Apps de Chrome no admiten localStorage, ya que localStorage es síncrono. Acceso síncrono bloquear recursos (E/S) en un tiempo de ejecución de un solo subproceso podría hacer que tu app no responda.

Las Apps de Chrome tienen una API equivalente que puede almacenar objetos de forma asíncrona. Esto ayudará a evitar el proceso de serialización de objeto->string->objeto a veces es costoso.

Para abordar el mensaje de error en nuestra app, debes convertir localStorage a chrome.storage.local;

Actualiza los permisos de la app

Para usar chrome.storage.local, debes solicitar el permiso storage. En En manifest.json, agrega "storage" al array permissions:

"permissions": ["storage"],

Más información sobre local.storage.set() y local.storage.get()

Para guardar y recuperar elementos de tareas pendientes, debes conocer los métodos set() y get() de la API de chrome.storage.

El método set() acepta un objeto de pares clave-valor como su primer parámetro. Un la función de devolución de llamada es el segundo parámetro. Por ejemplo:

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

El método get() acepta un primer parámetro opcional para las claves del almacén de datos que deseas. para recuperarte. Se puede pasar una clave única como una cadena. varias claves pueden organizarse en un array de cadenas o un objeto de diccionario.

El segundo parámetro, que es obligatorio, es una función de devolución de llamada. En el objeto que se muestra, usa el solicitadas en el primer parámetro para acceder a los valores almacenados. Por ejemplo:

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

Si deseas get() todo lo que está actualmente en chrome.storage.local, omite la primera. parámetro:

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

A diferencia de localStorage, no podrás inspeccionar los elementos almacenados de forma local con las Herramientas para desarrolladores Panel de recursos. Sin embargo, puedes interactuar con chrome.storage desde la Consola de JavaScript de la siguiente manera: entonces:

Usa la consola para depurar chrome.storage

Vista previa de los cambios necesarios en la API

La mayoría de los pasos restantes para convertir la app de tareas pendientes son pequeños cambios en las llamadas a la API. Cambiando Todos los lugares en los que se usa localStorage actualmente. Aunque requiere mucho tiempo y es propenso a errores, es obligatorio.

Las diferencias clave entre localStorage y chrome.storage provienen de la naturaleza asíncrona de chrome.storage

  • En lugar de escribirle a localStorage con una tarea simple, debes usar chrome.storage.local.set() con devoluciones de llamada opcionales.

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

    versus

    var storage = {};
    storage[dbName] = { todos: [] };
    chrome.storage.local.set( storage, function() {
      // optional callback
    });
    
  • En lugar de acceder a localStorage[myStorageName] directamente, debes usar chrome.storage.local.get(myStorageName,function(storage){...}) y, luego, analiza los datos que se muestran storage en la función de devolución de llamada.

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

    versus

    chrome.storage.local.get(dbName, function(storage) {
      var todos = storage[dbName].todos;
    });
    
  • La función .bind(this) se usa en todas las devoluciones de llamada para garantizar que this haga referencia al this de la Prototipo Store. (Puedes encontrar más información sobre las funciones vinculadas en los documentos de MDN: 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();
    

Ten en cuenta estas diferencias clave cuando cubrimos la recuperación, el guardado y la eliminación de elementos de tareas pendientes en la secciones a continuación.

Recupera elementos pendientes

Actualicemos la app de tareas pendientes para recuperar los elementos de tareas pendientes:

1. El método constructor Store se encarga de inicializar la app de tareas pendientes con todos los elementos tareas pendientes del almacén de datos. El método primero verifica si el almacén de datos existe. Si no es así, Crea un array vacío de todos y guárdalo en el almacén de datos para que no haya errores de lectura en el entorno de ejecución.

En js/store.js, convierte el uso de localStorage en el método del constructor para usar en su lugar. 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. El método find() se usa cuando se leen todos los del modelo. Los resultados que se muestran cambian según según si filtras por “Todos”, “Activo” o “Completado”.

Convierte find() para usar 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. Al igual que find(), findAll() tiene todas las tareas del modelo. Convierte findAll() para usarlo 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));
};

Guardar todos los elementos

El método save() actual presenta un desafío. Depende de dos operaciones asíncronas (get y set) que operan en todo el almacenamiento JSON monolítico todo el tiempo. Cualquier actualización por lotes en más de un elemento de tareas pendientes, como «marcar todas las tareas como completadas», darán lugar a un peligro de datos conocido como Read-After-Write. Este problema no ocurriría si usáramos un almacenamiento de datos más apropiado como IndexedDB, pero intentamos minimizar el esfuerzo de conversión para este codelab.

Existen varias formas de solucionar el problema, por lo que usaremos esta oportunidad para refactorizar ligeramente save(). Toma un array de IDs de tareas pendientes para que se actualicen de una sola vez:

1. Para comenzar, une todo lo que ya está dentro de save() con una chrome.storage.local.get(). devolución de llamada:

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. Convierte todas las instancias de 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. Luego, actualiza la lógica para que opere en un array en lugar de en un solo 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));
};

Marcar las tareas pendientes como completadas

Ahora que la app funciona con arrays, debes cambiar la forma en que la app controla cuando un usuario hace clic en Botón Borrar completados (#):

1. En controller.js, actualiza toggleAll() para llamar a toggleComplete() solo una vez con un array. de tareas pendientes en lugar de marcar una tarea pendiente como completada una por una. Borrar también la llamada a _filter() ya que ajustarás 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. Ahora, actualiza toggleComplete() para que acepte una sola tarea pendiente o un array de tareas pendientes. Esto incluye lo que mueve filter() para que esté dentro de update(), en lugar de afuera.

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

Descartar todas las tareas pendientes

Hay un método más en store.js con localStorage:

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

No se llama a este método en la app actual. Por lo tanto, si quieres un desafío adicional, intenta implementarlo por tu cuenta. Sugerencia: Consulta chrome.storage.local.clear().

Inicia tu app de tareas pendientes terminada

Has completado el paso 2. Vuelve a cargar la app. Ahora deberías tener una versión empaquetada de Chrome que funcione correctamente. de TodoMVC.

Más información

Para obtener información más detallada sobre algunas de las APIs presentadas en este paso, consulta:

¿Todo listo para continuar con el siguiente paso? Ve al Paso 3: Agrega alarmas y notificaciones »