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, desplázate hasta la parte inferior de esta página ↓.
Cómo importar una app de listas de tareas existente
Como punto de partida, importa la versión de JavaScript sin modificaciones de TodoMVC, una app de comparativas común, a tu proyecto.
Incluimos una versión de la app TodoMVC en el código postal de referencia de la carpeta todomvc. Copia todos los archivos (incluidas las carpetas) de todomvc en la carpeta de tu proyecto.
Se te pedirá que reemplaces index.html. Adelante, acepta.
Ahora deberías tener la siguiente estructura de archivos en la carpeta de la aplicación:
Los archivos destacados en azul pertenecen a la carpeta todomvc.
Vuelve a cargar la app ahora (haz clic con el botón derecho > Vuelve a cargar la app). Deberías ver la IU básica, pero no podrás añadir tareas.
Haz que las secuencias de comandos cumplan con la Política de Seguridad del Contenido (CSP)
Abre la consola de Herramientas para desarrolladores (haz clic con el botón derecho > Inspeccionar elemento y, luego, selecciona la pestaña Consola). Verás un error sobre la negativa a ejecutar una secuencia de comandos intercalada:
Para corregir este error, haz que la app cumpla con la Política de Seguridad de Contenido. Uno de los incumplimientos del CSP más comunes es causado por el código JavaScript intercalado. Los ejemplos de JavaScript intercalado incluyen controladores de eventos como atributos DOM (p.ej., <button onclick=''>
) y etiquetas <script>
con contenido dentro del 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, incluye 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 intercalado anterior para que esté en este archivo:
// Bootstrap app data
window.app = {};
Si vuelves a cargar la app ahora, seguirá sin funcionar, pero estarás más cerca de hacerlo.
Convierte localStorage a chrome.storage.local
Si abres la consola de Herramientas para desarrolladores ahora, el error anterior debería desaparecer. Sin embargo, hay un error nuevo sobre la falta de disponibilidad de window.localStorage
:
Las apps de Chrome no admiten localStorage
, ya que localStorage
es síncrono. El acceso síncrono a recursos de bloqueo (E/S) en un entorno de ejecución de subproceso único podría hacer que tu app deje de responder.
Las apps para Chrome tienen una API equivalente que puede almacenar objetos de forma asíncrona. Esto ayudará a evitar el proceso de serialización de objeto a cadena a objeto, que a veces es costoso.
Para abordar el mensaje de error en nuestra app, debes convertir localStorage
en chrome.storage.local.
Actualiza los permisos de la app
Para usar chrome.storage.local
, debes solicitar el permiso storage
. En manifest.json, agrega "storage"
al array permissions
:
"permissions": ["storage"],
Obtén 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 primer parámetro. Una función de devolución de llamada opcional 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 de Datastore que deseas recuperar. Se puede pasar una sola clave como una cadena. Se pueden organizar varias claves 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 las claves 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 quieres get()
todo lo que está actualmente en chrome.storage.local
, omite el primer 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 el panel Recursos de Herramientas para desarrolladores. Sin embargo, puedes interactuar con chrome.storage
desde la consola de JavaScript de la siguiente manera:
Obtén una vista previa de los cambios obligatorios en la API
La mayoría de los pasos restantes para convertir la app de Todo son pequeños cambios en las llamadas a la API. Es necesario cambiar todos los lugares donde se usa localStorage
actualmente, aunque esto requiera tiempo y sea propenso a errores.
Las diferencias clave entre localStorage
y chrome.storage
provienen de la naturaleza asíncrona de chrome.storage
:
En lugar de escribir en
localStorage
con una asignación simple, debes usarchrome.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 usarchrome.storage.local.get(myStorageName,function(storage){...})
y, luego, analizar el objetostorage
que se muestra 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 quethis
haga referencia althis
del prototipoStore
. (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 abarcamos la recuperación, el guardado y la eliminación de elementos de tareas pendientes en las siguientes secciones.
Cómo recuperar elementos de la lista de tareas pendientes
Actualicemos la app de tareas pendientes para recuperar los elementos de tareas pendientes:
1. El método del constructor Store
se encarga de inicializar la app de Todo con todos los elementos de tareas existentes del almacén de datos. Primero, el método verifica si existe el almacén de datos. De lo contrario, creará un array vacío de todos
y lo guardará en el almacén de datos para que no haya errores de lectura del tiempo de ejecución.
En js/store.js, convierte el uso de localStorage
en el método del constructor para usar
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 tareas del modelo. Los resultados que se muestran cambian según si filtras por "Todos", "Activos" o "Completados".
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 usar 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 cada vez. Cualquier actualización por lotes en más de un elemento de lista de tareas pendientes, como "marcar todas las tareas pendientes como completadas", generará un riesgo de datos conocido como lectura después de la escritura. 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.
Hay varias formas de solucionarlo, por lo que aprovecharemos esta oportunidad para refactorizar ligeramente save()
tomando un array de IDs de todo para actualizarlos todos a la vez:
1. Para comenzar, une todo lo que ya esté dentro de save()
con una devolución de llamada 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. 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));
};
Cómo marcar elementos de la lista de tareas pendientes como completados
Ahora que la app funciona en arrays, debes cambiar la forma en que la app controla que un usuario haga clic en el botón Clear completed (#):
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. También borra la llamada a _filter()
, ya que ajustarás el 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 mover 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:
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();
};
Cómo descartar todos los elementos de tareas pendientes
Hay un método más en store.js que usa 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 que, si quieres un desafío adicional, intenta implementarlo por tu cuenta. Sugerencia: Consulta chrome.storage.local.clear()
.
Inicia tu app de tareas pendientes terminada
Completaste el paso 2. Vuelve a cargar la app. Ahora deberías tener una versión empaquetada de Chrome de TodoMVC que funcione correctamente.
Más información
Para obtener información más detallada sobre algunas de las APIs que se presentan en este paso, consulta los siguientes recursos:
- Política de Seguridad del Contenido ↑
- Declara los permisos ↑
- chrome.storage ↑
- chrome.storage.local.get() ↑
- chrome.storage.local.set() ↑
- chrome.storage.local.remove() ↑
- chrome.storage.local.clear() ↑
¿Todo listo para continuar con el siguiente paso? Ve al Paso 3: Agrega alarmas y notificaciones ».