Nesta etapa, você vai aprender:
- Como adaptar um aplicativo da Web existente para a plataforma de apps do Chrome.
- Como deixar os scripts do app em conformidade com a Política de Segurança de Conteúdo (CSP).
- Como implementar o armazenamento local usando chrome.storage.local.
Tempo estimado para concluir esta etapa: 20 minutos.
Para conferir o que você vai concluir nesta etapa, vá até o final desta página ↓.
Importar um app do app Todo
Como ponto de partida, importe a versão JavaScript básica do TodoMVC, um app de comparação comum, para o projeto.
Incluímos uma versão do app TodoMVC no zip do código de referência na pasta todomvc. Copie todos os arquivos (incluindo pastas) de todomvc para a pasta do projeto.
Será solicitado que você substitua index.html. É só aceitar.
A pasta do aplicativo terá a seguinte estrutura de arquivos:
Os arquivos destacados em azul são da pasta todomvc.
Atualize o app agora (clique com o botão direito do mouse > Recarregar app). Você verá a interface básica, mas não será possível adicionar tarefas.
Deixar os scripts em conformidade com a Política de Segurança de Conteúdo (CSP)
Abra o Console do DevTools (clique com o botão direito do mouse > Inspecionar elemento e selecione a guia Console). Você verá um erro sobre se recusar a executar um script in-line:
Para corrigir esse erro, garanta que o app obedeça à Política de Segurança de Conteúdo. Uma das não
conformidades mais comuns da CSP é causada pelo JavaScript in-line. Exemplos de JavaScript inline incluem
manipuladores de eventos como atributos DOM (por exemplo, <button onclick=''>
) e tags <script>
com conteúdo
dentro do HTML.
A solução é simples: mova o conteúdo inline para um novo arquivo.
1. Próximo à parte de baixo de index.html, remova o JavaScript inline e inclua 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. Crie um arquivo na pasta js com o nome bootstrap.js. Mova o código inline anteriormente para ficar neste arquivo:
// Bootstrap app data
window.app = {};
Se você recarregar o app do app Todo agora, ele está chegando perto disso.
Converter localStorage para chrome.storage.local
Se você abrir o Console do DevTools agora, o erro anterior desaparecerá. No entanto, há um novo erro
quando window.localStorage
não está disponível:
Os apps do Chrome não são compatíveis com o localStorage
porque o localStorage
é síncrono. O acesso síncrono
a recursos de bloqueio (E/S) em um ambiente de execução de linha de execução única pode fazer com que o app não responda.
Os apps do Chrome têm uma API equivalente que pode armazenar objetos de forma assíncrona. Isso ajudará a evitar o processo de serialização objeto->string->objeto às vezes caro.
Para resolver a mensagem de erro no nosso aplicativo, você precisa converter localStorage
em chrome.storage.local.
Atualizar as permissões de aplicativos
Para usar chrome.storage.local
, é necessário solicitar a permissão storage
. No
manifest.json, adicione "storage"
à matriz permissions
:
"permissions": ["storage"],
Saiba mais sobre local.storage.set() e local.storage.get()
Para salvar e recuperar itens de tarefas, você precisa conhecer os métodos set()
e get()
da API chrome.storage
.
O método set() aceita um objeto de pares de chave-valor como o primeiro parâmetro. Uma função de callback opcional é o segundo parâmetro. Exemplo:
chrome.storage.local.set({secretMessage:'Psst!',timeSet:Date.now()}, function() {
console.log("Secret message saved");
});
O método get() aceita um primeiro parâmetro opcional para as chaves do armazenamento de dados que você quer recuperar. Uma única chave pode ser transmitida como uma string. Várias chaves podem ser organizadas em uma matriz de strings ou um objeto de dicionário.
O segundo parâmetro, que é obrigatório, é uma função de callback. No objeto retornado, use as chaves solicitadas no primeiro parâmetro para acessar os valores armazenados. Exemplo:
chrome.storage.local.get(['secretMessage','timeSet'], function(data) {
console.log("The secret message:", data.secretMessage, "saved at:", data.timeSet);
});
Se você quiser usar get()
em tudo o que está em chrome.storage.local
, omita o primeiro
parâmetro:
chrome.storage.local.get(function(data) {
console.log(data);
});
Ao contrário de localStorage
, não será possível inspecionar itens armazenados localmente usando o painel
Recursos do DevTools. No entanto, é possível interagir com chrome.storage
no Console JavaScript da seguinte maneira:
Visualizar as mudanças necessárias na API
A maioria das etapas restantes para converter o app Todo são pequenas mudanças nas chamadas de API. É necessário mudar
todos os locais em que o localStorage
está sendo usado, embora seja demorado e propenso a
erros.
As principais diferenças entre localStorage
e chrome.storage
vêm da natureza assíncrona de
chrome.storage
:
Em vez de gravar em
localStorage
usando a atribuição simples, você precisa usarchrome.storage.local.set()
com callbacks opcionais.var data = { todos: [] }; localStorage[dbName] = JSON.stringify(data);
versus
var storage = {}; storage[dbName] = { todos: [] }; chrome.storage.local.set( storage, function() { // optional callback });
Em vez de acessar
localStorage[myStorageName]
diretamente, você precisa usarchrome.storage.local.get(myStorageName,function(storage){...})
e analisar o objetostorage
retornado na função de callback.var todos = JSON.parse(localStorage[dbName]).todos;
versus
chrome.storage.local.get(dbName, function(storage) { var todos = storage[dbName].todos; });
A função
.bind(this)
é usada em todos os callbacks para garantir quethis
se refira aothis
do protótipoStore
. Mais informações sobre funções vinculadas podem ser encontradas nos documentos do 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();
Lembre-se dessas principais diferenças à medida que abordamos como recuperar, salvar e remover itens de tarefas nas seções a seguir.
Recuperar itens de tarefas
Vamos atualizar o app Todo para recuperar itens de tarefas:
1. O método construtor Store
cuida da inicialização do app Todo com todos os itens de tarefas existentes do repositório de dados. O método verifica primeiro se o armazenamento de dados existe. Caso contrário, ele
vai criar uma matriz vazia de todos
e salvar no armazenamento de dados para que não haja erros de leitura no ambiente de execução.
Em js/store.js, converta o uso de localStorage
no método construtor 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. O método find()
é usado ao ler tarefas do Model. Os resultados retornados mudam de acordo com o tipo de filtragem: "Todos", "Ativos" ou "Concluído".
Converta 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. Semelhante a find()
, findAll()
recebe todas as tarefas do modelo. Converta 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));
};
Salvar itens das tarefas
O método save()
atual apresenta um desafio. Ela depende de duas operações assíncronas (get e set)
que operam em todo o armazenamento JSON monolítico todas as vezes. Qualquer atualização em lote em mais de um item de tarefa, como "marcar todas as tarefas como concluídas", resultará em um risco de dados conhecido como leitura após gravação. Esse problema não aconteceria se estivéssemos usando um armazenamento de dados mais apropriado,
como o IndexedDB, mas estamos tentando minimizar o esforço de conversão para este codelab.
Há várias maneiras de corrigir isso. Portanto, usaremos essa oportunidade para refatorar ligeiramente save()
pegando uma matriz de IDs de tarefas para serem atualizados de uma só vez:
1. Para começar, una tudo que já está dentro de save()
com um callback
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. Converta todas as instâncias de localStorage
com 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. Em seguida, atualize a lógica para operar em uma matriz em vez de um único item:
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 itens de tarefas como concluídos
Agora que o app está operando em matrizes, é necessário mudar a forma como ele processa um usuário que clica no botão Clear complete (#):
1. No controller.js, atualize toggleAll()
para chamar toggleComplete()
apenas uma vez com uma matriz de todas as tarefas, em vez de marcar uma tarefa como concluída uma a uma. Exclua também a chamada para _filter()
,
já que você vai ajustar o 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. Agora, atualize toggleComplete()
para aceitar uma única tarefa ou uma matriz de todas. Isso inclui
mover filter()
para dentro do update()
, em vez de para fora.
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();
};
Soltar todos os itens de tarefas
Há mais um método em store.js usando localStorage
:
Store.prototype.drop = function (callback) {
localStorage[this._dbName] = JSON.stringify({todos: []});
callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
};
Esse método não está sendo chamado no app atual. Portanto, se você quiser um desafio extra, tente
implementá-lo por conta própria. Dica: dê uma olhada no chrome.storage.local.clear()
.
Iniciar o app Todo concluído
Você concluiu a Etapa 2! Atualize seu app para ter uma versão empacotada do Chrome totalmente funcional do TodoMVC.
Mais informações
Para informações mais detalhadas sobre algumas das APIs introduzidas nesta etapa, consulte:
- Política de Segurança de Conteúdo ↑
- Declarar permissões ↑
- chrome.storage ↑
- chrome.storage.local.get() ↑
- chrome.storage.local.set() ↑
- chrome.storage.local.remove() ↑
- chrome.storage.local.clear() ↑
Tudo pronto para passar à próxima etapa? Vá para a Etapa 3: adicionar alarmes e notificações »