Etapa 2: importar um app da Web existente

Nesta etapa, você vai aprender:

  • Como adaptar um aplicativo da Web existente para a plataforma de Apps do Chrome.
  • Como deixar a Política de Segurança de Conteúdo (CSP) dos scripts de app compatível.
  • Como implementar o armazenamento local usando chrome.storage.local.

Tempo estimado para concluir esta etapa: 20 minutos.
Para visualizar o que você concluirá nesta etapa, vá para a parte inferior desta página ↓.

Importar um app Todo

Como ponto de partida, importe a versão JavaScript básica do TodoMVC, um comparativo de mercado comum. em seu projeto.

Incluímos uma versão do app TodoMVC no zip do código de referência no todomvc (link em inglês). do Compute Engine. Copie todos os arquivos (incluindo as pastas) do todomvc para a pasta do seu projeto.

Copiar a pasta todomvc para a pasta do codelab

Você será solicitado a substituir index.html. É só aceitar.

Substituir index.html

Agora você terá a seguinte estrutura de arquivos na pasta do seu aplicativo:

Nova pasta do projeto

Os arquivos destacados em azul são da pasta todomvc.

Atualize seu aplicativo agora (clique com o botão direito do mouse > Recarregar aplicativo). Você verá a interface básica, mas não adicionar tarefas.

Tornar 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, depois selecione a guia Console). Você verá um erro sobre recusar a execução de um script in-line:

Erro de registro do app Todo com o console do CSP

Para corrigir esse erro, deixe o app em conformidade com a Política de Segurança de Conteúdo. Um dos mais não conformidades comuns de CSP são causadas pelo JavaScript inline. Exemplos de JavaScript inline incluem manipuladores de eventos como atributos DOM (por exemplo, <button onclick=''>) e tags <script> com conteúdo no HTML.

A solução é simples: mova o conteúdo in-line para um novo arquivo.

1. Na parte inferior 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. Mover o código inline anterior neste arquivo:

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

Se você recarregar o app agora, mas estiver se aproximando, o app Todo não vai funcionar.

Converter localStorage em chrome.storage.local

Se você abrir o Console do DevTools agora, o erro anterior vai desaparecer. Há um novo erro, No entanto, sobre window.localStorage não estar disponível:

App Todo com erro de registro do console localStorage

Os apps do Chrome não são compatíveis com a localStorage porque a localStorage é síncrona. Acesso síncrono ao bloqueio de recursos (E/S) em um ambiente de execução de thread único pode fazer com que seu aplicativo não responda.

Os apps do Chrome têm uma API equivalente que pode armazenar objetos de forma assíncrona. Isso ajudará a evitar um processo caro de serialização de objetos>strings->.

Para lidar com a mensagem de erro no nosso app, é necessário converter localStorage em chrome.storage.local (link em inglês).

Atualizar as permissões de aplicativos

Para usar chrome.storage.local, você precisa solicitar a permissão storage. Em 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() do API chrome.storage.

O método set() aceita um objeto de pares de chave-valor como o primeiro parâmetro. Um parâmetro opcional da função callback é 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 repositório de dados que você quer recuperação. Uma única chave pode ser transmitida como uma string. várias chaves podem ser organizadas em uma matriz strings de caracteres ou um objeto de dicionário.

O segundo parâmetro, que é obrigatório, é uma função de retorno de chamada. No objeto retornado, use o método 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);
});

Para get() tudo o que está atualmente em chrome.storage.local, omita a primeira :

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 DevTools Recursos. No entanto, é possível interagir com chrome.storage no Console JavaScript da seguinte forma: Portanto:

Usar o console para depurar o chrome.storage

Visualizar as mudanças necessárias da API

A maioria das etapas restantes na conversão do app Todo são pequenas mudanças nas chamadas de API. Mudança todos os lugares em que o localStorage está sendo usado, embora seja demorado e propenso a erros, é obrigatório.

As principais diferenças entre localStorage e chrome.storage vêm da natureza assíncrona do chrome.storage:

  • Em vez de escrever para localStorage usando uma atividade simples, você precisa usar chrome.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 usar chrome.storage.local.get(myStorageName,function(storage){...}) e analisar o Objeto storage 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 que this se refira ao this do Protótipo Store. (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 diferenças importantes à medida que abordamos como recuperar, salvar e remover itens de tarefas da 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 inicializa o app Todo com todo o itens de tarefas do repositório de dados. O método verifica primeiro se o armazenamento de dados existe. Caso contrário, crie uma matriz vazia de todos e salve-a no repositório 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 todas as tarefas do modelo. Os resultados retornados mudam com base de acordo com a filtragem por "Todos", "Ativos" ou "Concluídos".

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. Assim como find(), findAll() recebe todas as tarefas do modelo. Converter 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 todos os itens

O método save() atual apresenta um desafio. Depende de duas operações assíncronas (get e set) que operam em todo o armazenamento JSON monolítico todas as vezes. Atualizações em lote em mais de um 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, então vamos usar esta oportunidade para refatorar um pouco o save() até pegando uma matriz de IDs de tarefas a serem atualizadas de uma só vez:

1. Para começar, una tudo que já está dentro de save() com um 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. 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 em 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, você precisa mudar a forma como o app lida com um usuário que clica no botão Botão Limpar concluídas (#):

1. No controller.js, atualize toggleAll() para chamar toggleComplete() apenas uma vez com uma matriz. em vez de marcar uma tarefa como concluída uma a uma. Excluir 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 tarefas. Isso inclui Movendo a filter() para dentro do update(), e não 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:

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 todos os itens de tarefas

Existe mais um método na 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);
};

Esse método não está sendo chamado no app atual, então, se quiser um desafio extra, tente implementá-los por conta própria. Dica: dê uma olhada em chrome.storage.local.clear().

Iniciar o app Todo concluído

Você concluiu a Etapa 2! Atualize seu aplicativo para ter uma versão empacotada totalmente em funcionamento com o Chrome. da TodoMVC.

Mais informações

Para informações mais detalhadas sobre algumas das APIs introduzidas nesta etapa, consulte:

Tudo pronto para passar para a próxima etapa? Acesse a Etapa 3: adicionar alarmes e notificações »