Usar o Google Base e o Google Gears para uma experiência off-line eficiente

Primeiro artigo da série "Como criar aplicativos Ajax melhores com as APIs do Google".

Dion Almaer e Pamela Fox, Google
Junho de 2007

Observação do editor:a API Google Gears não está mais disponível.

Introdução

Combinando o Google Base com o Google Gears, demonstramos como criar um aplicativo que pode ser usado off-line. Depois de ler este artigo, você vai conhecer melhor a API Google Base e entender como usar o Google Gears para armazenar e acessar preferências e dados do usuário.

Noções básicas sobre o app

Para entender esse app, primeiro você precisa conhecer o Google Base, que é basicamente um grande banco de dados de itens em várias categorias, como produtos, avaliações, receitas, eventos e muito mais.

Cada item é anotado com um título, uma descrição, um link para a fonte original dos dados (se houver) e outros atributos que variam de acordo com o tipo de categoria. O Google Base aproveita o fato de que itens na mesma categoria compartilham um conjunto comum de atributos. Por exemplo, todas as receitas têm ingredientes. Os itens do Google Base também aparecem ocasionalmente nos resultados da Pesquisa Google na Web ou de produtos.

Nosso app de demonstração, Base com Gears, permite armazenar e mostrar pesquisas comuns que você pode fazer no Google Base, como encontrar receitas com "chocolate" (hummm) ou anúncios pessoais com "caminhadas na praia" (romântico!). É como um "Leitor do Google Base" que permite assinar pesquisas e ver os resultados atualizados quando você volta ao app ou quando ele procura feeds atualizados a cada 15 minutos.

Os desenvolvedores que quiserem estender o app podem adicionar mais recursos, como alertar visualmente o usuário quando os resultados da pesquisa contiverem novos resultados, permitir que o usuário adicione aos favoritos (estrela) itens favoritos (off-line e on-line) e permitir que o usuário faça pesquisas de atributos específicos da categoria, como o Google Base.

Como usar feeds da API Google Base Data

O Google Base pode ser consultado de forma programática com a API de dados do Google Base, que está em conformidade com a estrutura da API Google Data. O protocolo da API Google Data oferece um protocolo simples para leitura e gravação na Web e é usado por muitos produtos do Google: Picasa, Planilhas, Blogger, Agenda, Notebook e muito mais.

O formato da API Google Data é baseado em XML e no protocolo Atom Publishing. Portanto, a maioria das interações de leitura/gravação é em XML.

Exemplo de um feed do Google Base baseado na API Google Data:
http://www.google.com/base/feeds/snippets/-/products?bq=digital+camera

O tipo de feed snippets nos dá o feed de itens disponível publicamente. O -/products permite restringir o feed à categoria de produtos. O parâmetro bq= permite restringir ainda mais o feed, mostrando apenas os resultados que contêm a palavra-chave "câmera digital". Se você abrir esse feed no navegador, vai ver XML contendo nós <entry> com resultados correspondentes. Cada entrada contém os elementos típicos de autor, título, conteúdo e link, mas também vem com atributos adicionais específicos da categoria (como "preço" para itens na categoria de produtos).

Devido à restrição entre domínios do XMLHttpRequest no navegador, não é possível ler diretamente um feed XML de www.google.com no nosso código JavaScript. Podemos configurar um proxy do lado do servidor para ler o XML e devolvê-lo em um local no mesmo domínio do nosso app, mas queremos evitar a programação do lado do servidor. Felizmente, existe uma alternativa.

Assim como as outras APIs de dados do Google, a API de dados do Google Base tem uma opção de saída JSON, além do XML padrão. A saída do feed que vimos antes no formato JSON estaria neste URL:
http://www.google.com/base/feeds/snippets/-/products?bq=digital+camera&alt=json

JSON é um formato de troca leve que permite o aninhamento hierárquico e vários tipos de dados. Mas, mais importante, a saída JSON é o próprio código JavaScript nativo. Portanto, ela pode ser carregada na sua página da Web apenas fazendo referência a ela em uma tag de script, ignorando a restrição entre domínios.

As APIs de dados do Google também permitem especificar uma saída "json-in-script" com uma função de callback para ser executada quando o JSON for carregado. Isso facilita ainda mais o trabalho com a saída JSON, já que podemos anexar dinamicamente tags de script à página e especificar diferentes funções de callback para cada uma.

Portanto, para carregar dinamicamente um feed JSON da API Base na página, podemos usar a seguinte função, que cria uma tag de script com o URL do feed (acrescentado com valores altcallback) e a adiciona à página.

function getJSON() {
  var script = document.createElement('script');

  var url = "http://www.google.com/base/feeds/snippets/-/products?bq=digital+camera";
  script.setAttribute('src', url + "&alt=json-in-script&callback=listResults");
  script.setAttribute('type', 'text/JavaScript');
  document.documentElement.firstChild.appendChild(script);
}

Assim, nossa função de callback listResults agora pode iterar pelo JSON transmitido como o único parâmetro e mostrar informações sobre cada entrada encontrada em uma lista com marcadores.

  function listTasks(root) {
    var feed = root.feed;
    var html = [''];
    html.push('<ul>');
    for (var i = 0; i < feed.entry.length; ++i) {
      var entry = feed.entry[i];
      var title = entry.title.$t;
      var content = entry.content.$t;
      html.push('<li>', title, ' (', content, ')</li>');
    }
    html.push('</ul>');

    document.getElementById("agenda").innerHTML = html.join("");
  }

Adicionar o Google Gears

Agora que temos um aplicativo capaz de se comunicar com o Google Base pela API Google Data, queremos permitir que ele seja executado off-line. É aí que o Google Gears entra em cena.

Há várias opções de arquitetura ao escrever um aplicativo que pode ficar off-line. Você vai se perguntar como o aplicativo deve funcionar on-line e off-line (por exemplo, ele funciona exatamente da mesma forma? Alguns recursos estão desativados, como a pesquisa? Como você vai lidar com a sincronização?)

No nosso caso, queríamos garantir que os usuários em navegadores sem o Gears ainda pudessem usar o app, oferecendo aos usuários que têm o plug-in os benefícios do uso off-line e uma interface mais responsiva.

Nossa arquitetura é assim:

  • Temos um objeto JavaScript responsável por armazenar suas consultas de pesquisa e retornar resultados delas.
  • Se você tiver o Google Gears instalado, vai receber uma versão que armazena tudo no banco de dados local.
  • Se você não tiver o Google Gears instalado, vai receber uma versão que armazena as consultas em um cookie e não armazena os resultados completos (por isso a resposta é um pouco mais lenta), já que os resultados são muito grandes para serem armazenados em um cookie.
O bom dessa arquitetura é que você não precisa fazer verificações de if (online) {} em toda a loja. Em vez disso, o aplicativo tem uma verificação do Gears, e o adaptador correto é usado.


Como usar um banco de dados local do Gears

Um dos componentes do Gears é o banco de dados SQLite local incorporado e pronto para uso. Há uma API de banco de dados simples que seria familiar para você se já tiver usado APIs para bancos de dados do lado do servidor, como MySQL ou Oracle.

As etapas para usar um banco de dados local são bem simples:

  • Inicializar os objetos do Google Gears
  • Receber um objeto de fábrica de banco de dados e abrir um banco de dados
  • Começar a executar solicitações SQL

Vamos analisar cada uma delas rapidamente.


Inicializar os objetos do Google Gears

O aplicativo precisa ler o conteúdo de /gears/samples/gears_init.js diretamente ou colando o código no seu próprio arquivo JavaScript. Depois que o <script src="..../gears_init.js" type="text/JavaScript"></script> estiver funcionando, você terá acesso ao namespace google.gears.


Receber um objeto de fábrica de banco de dados e abrir um banco de dados
var db = google.gears.factory.create('beta.database', '1.0');
db.open('testdb');

Essa única chamada vai fornecer um objeto de banco de dados que permite abrir um esquema de banco de dados. Quando você abre bancos de dados, eles são definidos pelas mesmas regras de política de origem. Assim, seu "testdb" não vai entrar em conflito com meu "testdb".


Começar a executar solicitações SQL

Agora estamos prontos para enviar solicitações SQL ao banco de dados. Quando enviamos solicitações "select", recebemos um conjunto de resultados que podemos iterar para os dados desejados:

var rs = db.execute('select * from foo where name = ?', [ name ]);

É possível operar no conjunto de resultados retornado com os seguintes métodos:

booleanisValidRow()
voidnext()
voidclose()
intfieldCount()
stringfieldName(int fieldIndex)
variantfield(int fieldIndex)
variantfieldByName(string fieldname)

Para mais detalhes, consulte a documentação da API do módulo de banco de dados. (Observação do editor: a API Google Gears não está mais disponível).


Como usar o GearsDB para encapsular a API de baixo nível

Queremos encapsular e facilitar algumas das tarefas comuns do banco de dados. Por exemplo,

  • Queríamos uma maneira agradável de registrar o SQL gerado ao depurar o aplicativo.
  • Queríamos processar exceções em um só lugar, em vez de ter que try{}catch(){} em todos os lugares.
  • Queríamos trabalhar com objetos JavaScript em vez de conjuntos de resultados ao ler ou gravar dados.

Para lidar com esses problemas de maneira genérica, criamos o GearsDB, uma biblioteca de código aberto que encapsula o objeto de banco de dados. Agora vamos mostrar como usar o GearsDB.

Configuração inicial

No nosso código window.onload, precisamos garantir que as tabelas de banco de dados em que confiamos estejam no lugar certo. Se o usuário tiver o Gears instalado quando o código a seguir for executado, ele vai criar um objeto GearsBaseContent:

content = hasGears() ? new GearsBaseContent() : new CookieBaseContent();

Em seguida, abrimos o banco de dados e criamos tabelas, se elas ainda não existirem:

db = new GearsDB('gears-base'); // db is defined as a global for reuse later!

if (db) {
  db.run('create table if not exists BaseQueries' +
         ' (Phrase varchar(255), Itemtype varchar(100))');
  db.run('create table if not exists BaseFeeds' + 
         ' (id varchar(255), JSON text)');
}

Neste momento, temos certeza de que temos uma tabela para armazenar as consultas e os feeds. O código new GearsDB(name) vai encapsular a abertura de um banco de dados com o nome indicado. O método run encapsula o método de nível inferior execute, mas também processa a saída de depuração para um console e captura exceções.


Adicionar um termo de pesquisa

Quando você executa o app pela primeira vez, não há nenhuma pesquisa. Se você tentar pesquisar um Nintendo Wii em produtos, vamos salvar esse termo de pesquisa na tabela "BaseQueries".

A versão do Gears do método addQuery faz isso pegando a entrada e salvando-a via insertRow:

var searchterm = { Phrase: phrase, Itemtype: itemtype };
db.insertRow('BaseQueries', searchterm); 

insertRow usa um objeto JavaScript (searchterm) e processa a inserção dele na tabela. Ele também permite definir restrições (por exemplo, inserção de bloco de exclusividade de mais de um "Bob"). No entanto, na maioria das vezes, você vai lidar com essas restrições no próprio banco de dados.


Como receber todos os termos de pesquisa

Para preencher sua lista de pesquisas anteriores, usamos um wrapper de seleção chamado selectAll:

GearsBaseContent.prototype.getQueries = function() {
  return this.db.selectAll('select * from BaseQueries');
}

Isso vai retornar uma matriz de objetos JavaScript que correspondem às linhas no banco de dados (por exemplo, [ { Phrase: 'Nintendo Wii', Itemtype: 'product' }, { ... }, ...]).

Nesse caso, não há problema em retornar a lista completa. Mas se você tiver muitos dados, provavelmente vai querer usar um callback na chamada de seleção para poder operar em cada linha retornada à medida que ela chega:

 db.selectAll('select * from BaseQueries where Itemtype = ?', ['product'], function(row) {
  ... do something with this row ...
});

Confira outros métodos de seleção úteis no GearsDB:

selectOne(sql, args)Retornar o primeiro/um objeto JavaScript correspondente
selectRow(table, where, args, select)Normalmente usado em casos simples para ignorar o SQL.
selectRows(table, where, args, callback, select)Igual a "selectRow", mas para vários resultados.

Carregar um feed

Quando recebemos o feed de resultados do Google Base, precisamos salvá-lo no banco de dados:

content.setFeed({ id: id, JSON: json.toJSONString() });

... which calls ...

GearsBaseContent.prototype.setFeed = function(feed) {
  this.db.forceRow('BaseFeeds', feed);
}

Primeiro, pegamos o feed JSON e o retornamos como uma string usando o método toJSONString. Em seguida, criamos o objeto feed e o transmitimos para o método forceRow. O forceRow vai INSERIR uma entrada se ela não existir ou ATUALIZAR uma entrada existente.


Como mostrar resultados da pesquisa

Nosso app mostra os resultados de uma determinada pesquisa no painel à direita da página. Veja como recuperamos o feed associado ao termo de pesquisa:

GearsBaseContent.prototype.getFeed = function(url) {
  var row = this.db.selectRow('BaseFeeds', 'id = ?', [ url ]);
  return row.JSON;
}

Agora que temos o JSON de uma linha, podemos eval() para recuperar os objetos:

eval("var json = " + jsonString + ";");

Vamos começar a inserir conteúdo do JSON na nossa página usando innerHTML.


Como usar um Resource Store para acesso off-line

Como estamos recebendo conteúdo de um banco de dados local, esse app também deve funcionar off-line, certo?

Não. O problema é que, para iniciar esse app, você precisa carregar os recursos da Web dele, como JavaScript, CSS, HTML e imagens. No momento, se o usuário seguir estas etapas, o app ainda poderá funcionar: iniciar on-line, fazer algumas pesquisas, não fechar o navegador e ficar off-line. Isso pode funcionar porque os itens ainda estariam no cache do navegador. Mas e se não for esse o caso? Queremos que nossos usuários possam acessar o app do zero, após uma reinicialização etc.

Para isso, usamos o componente LocalServer e capturamos nossos recursos. Quando você captura um recurso (como o HTML e o JavaScript necessários para executar o aplicativo), o Gears salva esses itens e também intercepta as solicitações do navegador para retorná-los. O servidor local vai agir como um agente de trânsito e retornar o conteúdo salvo da loja.

Também usamos o componente ResourceStore, que exige que você informe manualmente ao sistema quais arquivos quer capturar. Em muitos cenários, você quer versionar seu aplicativo e permitir upgrades de maneira transacional. Um conjunto de recursos define uma versão, e quando você lança um novo conjunto, é importante que os usuários tenham um upgrade dos arquivos sem problemas. Se esse for o caso, você vai usar a API ManagedResourceStore.

Para capturar nossos recursos, o objeto GearsBaseContent vai:

  1. Configurar uma matriz de arquivos que precisam ser capturados
  2. Criar um LocalServer
  3. Abra ou crie um novo ResourceStore
  4. Chame para capturar as páginas na loja
// Step 1
this.storeName = 'gears-base';
this.pageFiles = [
  location.pathname,
  'gears_base.js',
  '../scripts/gears_db.js',
  '../scripts/firebug/firebug.js',
  '../scripts/firebug/firebug.html',
  '../scripts/firebug/firebug.css',
  '../scripts/json_util.js',    'style.css',
  'capture.gif' ];

// Step 2
try {
  this.localServer = google.gears.factory.create('beta.localserver', '1.0');
} catch (e) {
  alert('Could not create local server: ' + e.message);
  return;
}

// Step 3
this.store = this.localServer.openStore(this.storeName) || this.localServer.createStore(this.storeName);

// Step 4
this.capturePageFiles();

... which calls ...

GearsBaseContent.prototype.capturePageFiles = function() {
  this.store.capture(this.pageFiles, function(url, success, captureId) {
    console.log(url + ' capture ' + (success ? 'succeeded' : 'failed'));
  });
}

É importante observar que você só pode capturar recursos no seu próprio domínio. Encontramos essa limitação quando tentamos acessar o arquivo JavaScript GearsDB diretamente do arquivo original "gears_db.js" no tronco SVN. A solução é simples: baixe todos os recursos externos e coloque-os no seu domínio. Redirecionamentos 302 ou 301 não funcionam, já que o LocalServer só aceita códigos de servidor 200 (sucesso) ou 304 (não modificado).

Isso tem implicações. Se você colocar as imagens em images.yourdomain.com, não será possível capturá-las. www1 e www2 não podem se ver. Você pode configurar proxies do lado do servidor, mas isso prejudicaria o objetivo de dividir o aplicativo em vários domínios.

Como depurar o aplicativo off-line

A depuração de um aplicativo off-line é um pouco mais complicada. Agora há mais cenários para testar:

  • Estou on-line com o app totalmente em execução no cache
  • Estou on-line, mas não acessei o app e não há nada no cache
  • Estou off-line, mas já acessei o app
  • Estou off-line e nunca acessei o app (não é uma boa situação!)

Para facilitar, usamos o seguinte padrão:

  • Desativamos o cache no Firefox (ou no navegador de sua preferência) quando precisamos garantir que o navegador não esteja apenas pegando algo do cache.
  • Depuramos usando o Firebug (e o Firebug Lite para testes em outros navegadores). Usamos console.log() em todos os lugares e detectamos o console por precaução.
  • Adicionamos um código JavaScript auxiliar a:
    • permitem limpar o banco de dados e começar do zero
    • remova os arquivos capturados para que, ao recarregar, eles sejam buscados novamente na Internet (útil quando você está iterando no desenvolvimento ;)

O widget de depuração aparece no lado esquerdo da página apenas se você tiver o Gears instalado. Ele tem indicadores para limpar o código:

GearsBaseContent.prototype.clearServer = function() {
  if (this.localServer.openStore(this.storeName)) {
    this.localServer.removeStore(this.storeName);
    this.store = null;
  }
}

GearsBaseContent.prototype.clearTables = function() {
  if (this.db) {
    this.db.run('delete from BaseQueries');
    this.db.run('delete from BaseFeeds');
  }
  displayQueries();
}

Conclusão

Como você pode ver, trabalhar com o Google Gears é bem simples. Usamos o GearsDB para facilitar ainda mais o componente Database e o ResourceStore manual, que funcionou bem no nosso exemplo.

A área em que você passa mais tempo é definindo a estratégia de quando coletar dados on-line e como armazená-los off-line. É importante dedicar tempo à definição do esquema do banco de dados. Se você precisar mudar o esquema no futuro, terá que gerenciar essa mudança, já que os usuários atuais já terão uma versão do banco de dados. Isso significa que você precisará enviar o código de script com qualquer upgrade do banco de dados. Obviamente, é bom minimizar isso. Você pode testar o GearShift, uma pequena biblioteca que ajuda a gerenciar revisões.

Também poderíamos ter usado ManagedResourceStore para acompanhar nossos arquivos, com as seguintes consequências:

  • Seríamos bons cidadãos e versionaríamos nossos arquivos para permitir upgrades futuros limpos.
  • Há um recurso do ManagedResourceStore que permite criar um alias de um URL para outro conteúdo. Uma opção de arquitetura válida seria ter gears_base.js como uma versão não Gears e criar um alias para que o Gears faça o download de gears_base_withgears.js, que teria todo o suporte off-line.
Para nosso app, achamos mais fácil ter apenas uma interface e implementá-la de duas maneiras.

Esperamos que você tenha achado o curso "Preparando os aplicativos" divertido e fácil! Participe do fórum do Google Gears se tiver dúvidas ou um app para compartilhar.