Elementos da IU de vinculação de dados com o IndexedDB

Raymond camden
Raymond Camden

Introdução

O IndexedDB é uma maneira eficiente de armazenar dados no lado do cliente. Se você ainda não analisou esse assunto, recomendo que leia os tutoriais da MDN sobre o assunto. Para usar este artigo, é necessário ter algum conhecimento básico das APIs e dos recursos. Mesmo que você nunca tenha visto o IndexedDB, a demonstração deste artigo vai dar uma ideia do que pode ser feito com ele.

Nossa demonstração é uma simples prova de conceito de um aplicativo da Intranet para uma empresa. O aplicativo vai permitir que os funcionários procurem outros funcionários. Para proporcionar uma experiência mais rápida e ágil, o banco de dados do funcionário é copiado para a máquina do cliente e armazenado usando o IndexedDB. A demonstração fornece simplesmente uma pesquisa no estilo preenchimento automático de um único registro de funcionário, mas o que é bom é que, quando esses dados estão disponíveis no cliente, também podemos usá-los de várias outras maneiras. Aqui está um esboço básico do que nosso aplicativo precisa fazer.

  1. Precisamos configurar e inicializar uma instância de um IndexedDB. Na maioria das vezes, isso é simples, mas fazê-lo funcionar no Chrome e no Firefox é um pouco complicado.
  2. Precisamos ver se há dados e, se não, fazer o download deles. Normalmente, isso seria feito por meio de chamadas AJAX. Para nossa demonstração, criamos uma classe de utilitários simples para gerar rapidamente dados falsos. O aplicativo precisará reconhecer quando esses dados estão sendo criados e impedir que o usuário os use até lá. Essa operação só precisa ser feita uma vez. Na próxima vez que o usuário executar o aplicativo, ele não precisará passar por esse processo. Uma demonstração mais avançada trataria de operações de sincronização entre o cliente e o servidor, mas esta demonstração tem mais foco nos aspectos da interface.
  3. Quando o aplicativo estiver pronto, poderemos usar o controle de preenchimento automático da interface do jQuery para fazer a sincronização com o IndexedDB. Embora o controle de preenchimento automático permita listas básicas e matrizes de dados, ele tem uma API para permitir qualquer fonte de dados. Vamos demonstrar como usá-lo para nos conectar aos dados do IndexedDB.

Vamos começar

Há várias partes nesta demonstração, então, para começar, vamos dar uma olhada na parte de HTML.

<form>
  <p>
    <label for="name">Name:</label> <input id="name" disabled> <span id="status"></span>
    </p>
</form>

<div id="displayEmployee"></div>

Não muito, certo? Há três aspectos principais nessa interface que são importantes para nós. O primeiro é o campo “name”, que será usado para o preenchimento automático. O carregamento está desativado e será ativado mais tarde via JavaScript. O período ao lado dele é usado durante a semente inicial para fornecer atualizações ao usuário. Por fim, o div com o ID displayEmployee será usado quando você selecionar um funcionário na sugestão automática.

Agora, vamos conferir o JavaScript. Há muito para digerir aqui, então vamos seguir em frente. O código completo estará disponível no final para que você possa visualizá-lo por completo.

Em primeiro lugar, há alguns problemas de prefixo com os quais nos devemos preocupar entre os navegadores compatíveis com o IndexedDB. Veja a seguir um código da documentação do Mozilla que foi modificado para fornecer aliases simples para os principais componentes do IndexedDB de que nosso aplicativo precisa.

window.indexedDB = window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB;
var IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction;
var IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange;

A seguir, algumas variáveis globais que usaremos na demonstração:

var db;
var template;

Agora vamos começar com o bloco "jQuery document Ready":

$(document).ready(function() {
  console.log("Startup...");
  ...
});

Nossa demonstração usa o Handlebars.js para mostrar os detalhes do funcionário. Isso não será usado até mais tarde, mas podemos compilar nosso modelo agora e tirá-lo do caminho. Temos um bloco de script configurado como um tipo reconhecido pelo Handlebars. Não é muito sofisticado, mas facilita a exibição do HTML dinâmico.

<h2>, </h2>
Department: <br/>
Email: <a href='mailto:'></a>

Isso é compilado novamente no JavaScript da seguinte forma:

//Create our template
var source = $("#employeeTemplate").html();
template = Handlebars.compile(source);

Agora, vamos começar a trabalhar com nosso IndexedDB. Primeiro, nós abrimos.

var openRequest = indexedDB.open("employees", 1);

A abertura de uma conexão com o IndexedDB nos dá acesso para ler e gravar dados, mas, antes de fazer isso, precisamos garantir que temos um objectStore. Um objectStore é como uma tabela de banco de dados. Um IndexedDB pode ter muitos objectStores e cada um contém uma coleção de objetos relacionados. Nossa demonstração é simples e só precisa de um objectStore que chamamos de "employee". Quando o indexadoDB é aberto pela primeira vez ou quando você altera a versão no código, um evento onupgradeneeded é executado. Podemos usar isso para configurar nosso objectStore.

// Handle setup.
openRequest.onupgradeneeded = function(e) {

  console.log("running onupgradeneeded");
  var thisDb = e.target.result;

  // Create Employee
  if(!thisDb.objectStoreNames.contains("employee")) {
    console.log("I need to make the employee objectstore");
    var objectStore = thisDb.createObjectStore("employee", {keyPath: "id", autoIncrement: true});
    objectStore.createIndex("searchkey", "searchkey", {unique: false});
  }

};

openRequest.onsuccess = function(e) {
  db = e.target.result;

  db.onerror = function(e) {
    alert("Sorry, an unforseen error was thrown.");
    console.log("***ERROR***");
    console.dir(e.target);
  };

  handleSeed();
};

No bloco do manipulador de eventos onupgradeneeded, verificamos objectStoreNames, uma matriz de armazenamentos de objetos, para ver se ele contém employee. Caso contrário, simplesmente faremos isso. A chamada createIndex é importante. Precisamos informar ao IndexedDB quais métodos, fora das chaves, usaremos para recuperar dados. Usaremos uma chamada Searchkey. Isso será explicado mais adiante.

O evento onungradeneeded será executado automaticamente na primeira vez que executarmos o script. Depois que ele for executado ou ignorado nas execuções futuras, o gerenciador onsuccess será executado. Temos um gerenciador de erros simples (e feio) definido e, em seguida, chamamos handleSeed.

Antes de continuar, vamos revisar rapidamente o que está acontecendo. Abrimos o banco de dados. Verificamos se o armazenamento de objetos existe. Se não tiver, nós o criamos. Por fim, chamamos uma função chamada handleSeed. Agora vamos nos concentrar na parte de propagação de dados da demonstração.

Quero alguns dados.

Como mencionado na introdução deste artigo, esta demonstração recria um aplicativo no estilo de intranet que precisa armazenar uma cópia de todos os funcionários conhecidos. Normalmente, isso envolveria a criação de uma API com base em servidor que poderia retornar uma contagem de funcionários e fornecer uma maneira de recuperar lotes de registros. Imagine um serviço simples que ofereça suporte a uma contagem inicial e retorne cem pessoas por vez. Isso pode ser executado de forma assíncrona em segundo plano enquanto o usuário está realizando outras tarefas fora de casa.

Para nossa demonstração, fazemos algo simples. Vemos quantos objetos, se houver, temos no IndexedDB. Se estiver abaixo de um certo número, simplesmente criaremos usuários falsos. Caso contrário, vamos considerar que já terminou a parte de sugestão e podemos ativar a parte de preenchimento automático da demonstração. Vamos analisar o handleSeed.

function handleSeed() {
  // This is how we handle the initial data seed. Normally this would be via AJAX.

  db.transaction(["employee"], "readonly").objectStore("employee").count().onsuccess = function(e) {
    var count = e.target.result;
    if (count == 0) {
      console.log("Need to generate fake data - stand by please...");
      $("#status").text("Please stand by, loading in our initial data.");
      var done = 0;
      var employees = db.transaction(["employee"], "readwrite").objectStore("employee");
      // Generate 1k people
      for (var i = 0; i < 1000; i++) {
         var person = generateFakePerson();
         // Modify our data to add a searchable field
         person.searchkey = person.lastname.toLowerCase();
         resp = employees.add(person);
         resp.onsuccess = function(e) {
           done++;
           if (done == 1000) {
             $("#name").removeAttr("disabled");
             $("#status").text("");
             setupAutoComplete();
           } else if (done % 100 == 0) {
             $("#status").text("Approximately "+Math.floor(done/10) +"% done.");
           }
         }
      }
    } else {
      $("#name").removeAttr("disabled");
      setupAutoComplete();
    }
  };
}

A primeira linha é um pouco complexa, porque temos várias operações encadeadas entre si, então vamos dividi-la:

db.transaction(["employee"], "readonly");

Isso cria uma nova transação somente leitura. Todas as operações de dados com IndexedDB exigem alguma transação.

objectStore("employee");

Acessa o repositório de objetos de funcionário.

count()

Execute a API count, que faz uma contagem.

onsuccess = function(e) {

Quando terminar, execute este callback. Dentro do callback, podemos obter o valor do resultado, que é o número de objetos. Se a contagem for zero, vamos iniciar nosso processo de sugestão.

Usamos o div de status mencionado anteriormente para passar uma mensagem de que vamos começar a coletar dados. Devido à natureza assíncrona do IndexedDB, configuramos uma variável simples, concluída, que acompanhará as adições. Fazemos a repetição e inserimos as pessoas falsas. A origem dessa função está disponível no download, mas retorna um objeto semelhante a este:

{
  firstname: "Random Name",
  lastname: "Some Random Last Name",
  department: "One of 8 random departments",
  email: "first letter of firstname+lastname@fakecorp.com"
}

Por si só, isso é suficiente para definir uma pessoa. No entanto, temos um requisito especial para poder pesquisar nossos dados. O IndexedDB não oferece uma maneira de procurar itens sem diferenciar maiúsculas de minúsculas. Portanto, fazemos uma cópia do campo lastname em uma nova propriedade, searchkey. Lembre-se, essa é a chave que dissemos que deveria ser criada como um índice para nossos dados.

// Modify our data to add a searchable field
person.searchkey = person.lastname.toLowerCase();

Como esta é uma modificação específica do cliente, ela é feita aqui, não no servidor back-end (ou, em nosso caso, no servidor back-end imaginário).

Para realizar as adições ao banco de dados de maneira eficiente, reutilize a transação para todas as gravações em lote. Se você criar uma nova transação para cada gravação, o navegador poderá causar uma gravação de disco para cada transação, e isso prejudicará seu desempenho ao adicionar muitos itens (pense em "1 minuto para gravar 1.000 objetos" - péssimo).

Depois que a sugestão for concluída, a próxima parte do aplicativo será acionada: setupAutoComplete.

Criar o Autocomplete

Agora, a parte divertida: conexão com o plug-in Autocomplete da interface do jQuery. Como acontece com a maior parte da interface do jQuery, começamos com um elemento HTML básico e o aprimoramos chamando um método construtor nele. Resumimos todo o processo em uma função chamada setupAutoComplete. Vamos conferir esse código.

function setupAutoComplete() {

  //Create the autocomplete
  $("#name").autocomplete({
    source: function(request, response) {

      console.log("Going to look for "+request.term);

      $("#displayEmployee").hide();

      var transaction = db.transaction(["employee"], "readonly");
      var result = [];

      transaction.oncomplete = function(event) {
        response(result);
      };

      // TODO: Handle the error and return to it jQuery UI
      var objectStore = transaction.objectStore("employee");

      // Credit: http://stackoverflow.com/a/8961462/52160
      var range = IDBKeyRange.bound(request.term.toLowerCase(), request.term.toLowerCase() + "z");
      var index = objectStore.index("searchkey");

      index.openCursor(range).onsuccess = function(event) {
        var cursor = event.target.result;
        if(cursor) {
          result.push({
            value: cursor.value.lastname + ", " + cursor.value.firstname,
            person: cursor.value
          });
          cursor.continue();
        }
      };
    },
    minLength: 2,
    select: function(event, ui) {
      $("#displayEmployee").show().html(template(ui.item.person));
    }
  });

}

A parte mais complexa desse código é a criação da propriedade de origem. O controle de preenchimento automático da interface do jQuery permite definir uma propriedade de origem que pode ser personalizada para atender a qualquer necessidade possível, até mesmo nossos dados do IndexedDB. A API fornece a solicitação (basicamente o que foi digitado no campo do formulário) e um retorno de chamada de resposta. Você é responsável por enviar uma matriz de resultados de volta para esse callback.

A primeira coisa que devemos fazer é ocultar o div displayEmployee. Isso é usado para exibir um funcionário individual e, se um já tiver sido carregado, para limpá-lo. Agora podemos começar a pesquisar.

Começamos criando uma transação somente leitura, uma matriz chamada result e um manipulador oncomplete que simplesmente passa o resultado para o controle de preenchimento automático.

Para encontrar itens que correspondam à nossa entrada, vamos usar uma dica do usuário do StackOverflow, Fong-Wan Chau: usamos um intervalo de índice com base na entrada como um limite inferior e a entrada mais a letra z como um limite de intervalo superior. Observe também que colocamos o termo em letras minúsculas para corresponder aos dados em minúsculas que inserimos.

Depois de fazer isso, podemos abrir um cursor (pense nisso como executar uma consulta no banco de dados) e iterar sobre os resultados. O controle de preenchimento automático da interface do jQuery permite retornar qualquer tipo de dados que você quiser, mas exige, no mínimo, uma chave de valor. Definimos o valor como uma versão do nome formatada corretamente. Também retornamos a pessoa inteira. Logo você entenderá o porquê. Primeiro, aqui está uma captura de tela do preenchimento automático em ação. Estamos usando o tema do Vader para a interface jQuery.

Por si só, isso é suficiente para retornar os resultados das correspondências do IndexedDB ao preenchimento automático. No entanto, também queremos oferecer a exibição de detalhes da partida quando um deles é selecionado. Especificamos um gerenciador de seleção ao criar o preenchimento automático que usa o modelo de Handlebars anterior.