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

Raymond Camden
Raymond Camden

Introdução

A IndexedDB é uma maneira eficiente de armazenar dados no lado do cliente. Se você ainda não leu, recomendo que leia os tutoriais do MDN sobre o assunto. Este artigo presume algum conhecimento básico das APIs e dos recursos. Mesmo que você nunca tenha visto o IndexedDB antes, espero que a demonstração neste artigo dê uma ideia do que pode ser feito com ele.

Nossa demonstração é um aplicativo de prova de conceito simples para uma empresa. O aplicativo permite que os funcionários pesquisem outros funcionários. Para fornecer uma experiência mais rápida e ágil, o banco de dados do funcionário é copiado para a máquina do cliente e armazenado com o IndexedDB. A demonstração simplesmente fornece uma pesquisa e exibição de estilo de preenchimento automático de um único registro de funcionário, mas o 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. Em sua maioria, isso é simples, mas fazê-lo funcionar no Chrome e no Firefox demonstra ser um pouco complicado.
  2. Precisamos ver se temos dados e, em caso negativo, fazer o download deles. Geralmente, isso seria feito por chamadas AJAX. Para nossa demonstração, criamos uma classe utilitária simples para gerar dados falsos rapidamente. O aplicativo precisará reconhecer quando está criando esses dados e impedir que o usuário use os dados até então. Essa operação só precisa ser realizada 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 processaria operações de sincronização entre o cliente e o servidor, mas esta demonstração está mais focada nos aspectos da interface.
  3. Quando o aplicativo estiver pronto, poderemos usar o controle de preenchimento automático da interface do jQuery para sincronizar com o IndexedDB. Embora o controle de preenchimento automático permita listas básicas e matrizes de dados, ele tem uma API que permite qualquer fonte de dados. Vamos mostrar como usar isso para se conectar aos dados do IndexedDB.

Primeiros passos

Temos várias partes nesta demonstração. Para começar de forma simples, vamos analisar a 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. Ela é carregada desativada e será ativada mais tarde pelo 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 autocompletar.

Agora vamos conferir o JavaScript. Há muita coisa para digerir aqui, então vamos fazer isso passo a passo. O código completo vai estar disponível no final para que você possa conferir.

Primeiro, há alguns problemas de prefixo que precisamos considerar entre os navegadores compatíveis com o IndexedDB. Confira alguns códigos da documentação da Mozilla modificados para fornecer aliases simples para os componentes principais da IndexedDB necessários para o aplicativo.

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 vamos usar durante a demonstração:

var db;
var template;

Agora começaremos com o bloco pronto para o documento jQuery:

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

Nossa demonstração usa Handlebars.js para mostrar os detalhes do funcionário. Isso não será usado até mais tarde, mas podemos compilar nosso modelo agora e deixá-lo de lado. 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>

Em seguida, ele é 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 o IndexedDB. Primeiro, abrimos.

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

Abrir uma conexão com a IndexedDB nos dá acesso para ler e gravar dados, mas, antes disso, precisamos garantir que temos uma objectStore. Um objectStore é como uma tabela de banco de dados. Um IndexedDB pode ter muitos objectStores, cada um contendo uma coleção de objetos relacionados. Nossa demonstração é simples e precisa apenas de um objectStore chamado "employee". Quando o IndexedDB é aberto pela primeira vez ou quando você muda a versão no código, um evento onupgradeneeded é executado. Podemos usar isso para configurar o 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 repositórios de objetos, para saber se ele contém "employee". Caso contrário, simplesmente fazemos isso. A chamada createIndex é importante. É necessário informar ao IndexedDB quais métodos, fora das chaves, usaremos para recuperar os dados. Vamos usar uma chamada "chave de pesquisa". Isso será explicado em breve.

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

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

Gimme Some Data!

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 envolve a criação de uma API baseada em servidor que pode retornar uma contagem de funcionários e fornecer uma maneira de recuperar lotes de registros. Imagine um serviço simples que aceita uma contagem inicial e retorna 100 pessoas por vez. Isso pode ser executado de forma assíncrona em segundo plano enquanto o usuário faz outras coisas.

Para nossa demonstração, vamos fazer algo simples. Vamos conferir quantos objetos, se houver, temos no IndexedDB. Se o número for menor, vamos criar usuários falsos. Caso contrário, consideramos que a parte de semente foi concluída e podemos ativar a parte de preenchimento automático da demonstração. Vejamos 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 conectadas entre si. Vamos explicar:

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

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

objectStore("employee");

Acesse o repositório de objetos do funcionário.

count()

Execute a API de contagem, que, como você pode imaginar, realiza uma contagem.

onsuccess = function(e) {

E, quando terminar, execute este callback. No callback, podemos receber o valor do resultado, que é o número de objetos. Se a contagem for zero, iniciaremos o processo de semeadura.

Usamos a div de status mencionada anteriormente para informar ao usuário que vamos começar a receber dados. Devido à natureza assíncrona da IndexedDB, configuramos uma variável simples, "done", que vai rastrear as adições. Reproduzimos e inserimos as pessoas falsas. A origem dessa função está disponível no download, mas ela 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"
}

Isso por si só é suficiente para definir uma pessoa. Mas temos um requisito especial para 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". Se você se lembra, essa é a chave que dissemos que precisa ser criada como um índice para nossos dados.

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

Como essa é uma modificação específica do cliente, ela é feita aqui, e não no servidor de back-end (ou, no nosso caso, no servidor de 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 em disco para cada transação, o que vai prejudicar o desempenho ao adicionar muitos itens (pense em "1 minuto para gravar 1.000 objetos").

Quando a semente é concluída, a próxima parte do nosso aplicativo é acionada: setupAutoComplete.

Como criar o preenchimento automático

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

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 do jQuery UI permite definir uma propriedade de origem que pode ser personalizada para atender a qualquer necessidade, até mesmo nossos dados IndexedDB. A API fornece a solicitação (basicamente o que foi digitado no campo do formulário) e um callback de resposta. Você é responsável por enviar uma matriz de resultados de volta para esse callback.

A primeira coisa que fazemos é ocultar a div displayEmployee. Ela é usada para mostrar um funcionário individual e, se um já foi carregado, para limpar. Agora podemos começar a pesquisar.

Começamos criando uma transação somente leitura, uma matriz chamada "result" e um gerenciador oncomplete que simplesmente transmite o resultado ao controle de preenchimento automático.

Para encontrar itens que correspondem à 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 superior. Também usamos letras minúsculas para corresponder aos dados que você digitou.

Depois disso, podemos abrir um cursor (como se fosse uma consulta de banco de dados) e iterar os resultados. O controle de preenchimento automático do jQuery UI permite retornar qualquer tipo de dados, mas requer no mínimo uma chave de valor. Definimos o valor como uma versão formatada corretamente do nome. Também devolvemos a pessoa inteira. Você verá o porquê daqui a pouco. Primeiro, confira uma captura de tela do preenchimento automático em ação. Estamos usando o tema Vader para o jQuery UI.

Isso por si só é suficiente para retornar os resultados das nossas correspondências do IndexedDB para o preenchimento automático. Mas também queremos oferecer suporte para mostrar uma visualização detalhada da partida quando uma for selecionada. Especificamos um manipulador de seleção ao criar o preenchimento automático que usa o modelo Handlebars anterior.