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.
Se te pedirá que reemplaces index.html. Continúa y acepta.
Ahora deberías tener la siguiente estructura de archivos en la carpeta de tu aplicación:
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:
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:
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:
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 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, analiza los datos que se muestranstorage
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
de la 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 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:
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:
- 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 »