Como usar o Google Base e o Google Gear para uma experiência off-line de alto desempenho

Primeiro artigo da série "Building Better Ajax Applications with Google APIs".

Dion Almaer e Pamela Fox, Google
Junho de 2007

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

Introdução

Combinando o Google Base com o Google Gear, demonstramos como criar um aplicativo que pode ser usado off-line. Depois de ler este artigo, você estará mais familiarizado com a API do Google Base e entenderá como usar o Google Gear para armazenar e acessar as preferências e os dados dos usuários.

Noções básicas sobre o app

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

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

Com nosso app de demonstração, o Base with Gear, você armazena e exibe pesquisas comuns, como receitas de "chocolate" (yum) ou anúncios com "passeios na praia" (românticos). Pense nele como um "leitor de base do Google", que permite que você assine pesquisas e veja os resultados atualizados ao acessar o app novamente ou quando ele buscar feeds atualizados a cada 15 minutos.

Os desenvolvedores que desejam expandir o aplicativo podem adicionar mais recursos, como alertar visualmente o usuário quando os resultados da pesquisa contêm novos resultados, permitir que o usuário adicione aos favoritos os itens favoritos (off-line + on-line) e permitir que o usuário faça pesquisas de atributos específicos da categoria, como o Google Base.

Como usar os feeds de API de dados do Google Base

O Google Base pode ser consultado programaticamente com a API de dados do Google Base, que está em conformidade com o framework da API de dados do Google. O protocolo da API de dados do Google fornece um protocolo simples de leitura e gravação na web, sendo usado por muitos produtos do Google: Picasa, Planilhas, Blogger, Agenda, Notebook, entre outros.

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

Um exemplo de 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 oferece um feed de itens disponível publicamente. O -/products permite restringir o feed à categoria dos produtos. O parâmetro bq= permite restringir ainda mais o feed a resultados com a palavra-chave "câmera digital". Se você visualizar esse feed no navegador, verá XML contendo nós <entry> com resultados correspondentes. Cada entrada contém o autor, o título, o conteúdo e os elementos de link mais comuns, mas também vem com atributos adicionais específicos da categoria (como "preço" para itens na categoria de produtos).

Devido à restrição de vários domínios de XMLHttpRequest no navegador, não podemos ler diretamente em um feed XML de www.google.com em nosso código JavaScript. Poderíamos configurar um proxy do lado do servidor para ler no XML e executar em um local no mesmo domínio do nosso aplicativo, mas queremos evitar a programação do lado do servidor. Felizmente, há 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 anteriormente 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 com uma 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 a ser executada quando o JSON for carregado. Isso facilita o trabalho com a saída JSON, já que podemos anexar tags de script dinamicamente à 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 função a seguir, que cria uma tag de script com o URL do feed (anexada a valores altcallback) e a anexa à 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);
}

Portanto, nossa função de callback listResults agora pode iterar pelo JSON transmitido como a única informação de parâmetro e exibição em 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("");
  }

Como adicionar o Google Gear

Agora que temos um aplicativo capaz de se comunicar com o Google Base pela API de dados do Google, queremos permitir que ele seja executado off-line. É aqui que entra o Google Gear.

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

Em nosso caso, queremos garantir que os usuários de navegadores sem o Google Lens ainda possam usar o app, oferecendo aos usuários que têm o plug-in os benefícios do uso off-line e uma IU mais responsiva.

Nossa arquitetura tem esta aparência:

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


Como usar um banco de dados local do Google Tarefas

Um dos componentes do Gear é o banco de dados SQLite local que está incorporado e pronto para uso. Há uma API simples de banco de dados que seria interessante se você 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 bastante simples:

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

Vamos analisá-las rapidamente.


Inicializar os objetos do Google Gear

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. Quando tiver <script src="..../gears_init.js" type="text/JavaScript"></script>, você terá acesso ao namespace google.gears.


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

Essa 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 têm o escopo definido pelas mesmas regras de política de origem. Portanto, o "testdb" não entrará em conflito com o "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 ]);

Você pode operar no conjunto de resultados retornado usando 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 Database Module. Observação do editor: a API Google Gear não está mais disponível.


Como usar o GearDB para encapsular a API de nível inferior

Queremos encapsular e tornar mais convenientes algumas das tarefas comuns do banco de dados. Por exemplo:

  • Queríamos uma maneira boa de registrar o SQL gerado durante a depuração do aplicativo.
  • Queríamos lidar com as exceções em um só lugar, em vez de precisarmos try{}catch(){} em todo o local.
  • Queríamos lidar 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 Database. Agora vamos mostrar como usar o GearDB.

Configuração inicial

No código window.onload, precisamos garantir que as tabelas de banco de dados dependem das nossas tabelas. Se o usuário tiver o Beam instalado quando o código abaixo for executado, ele criará um objeto GearsBaseContent:

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

Em seguida, abrimos o banco de dados e criamos tabelas, caso ainda não existam:

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 ponto, temos certeza de que temos uma tabela para armazenar as consultas e os feeds. O código new GearsDB(name) encapsula a abertura de um banco de dados com o nome informado. O método run encapsula o método execute de nível inferior, mas também processa a depuração de saída para um console e captura exceções.


Como adicionar um termo de pesquisa

Na primeira vez que você executar o app, não terá nenhuma pesquisa. Se você tentar pesquisar um Nintendo Wii nos produtos, salvaremos esse termo na tabela BaseConsultas.

A versão do método addQuery do Beam faz isso salvando a entrada em insertRow:

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

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


Como ver todos os termos de pesquisa

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

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

Isso 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. No entanto, se você tiver muitos dados, convém usar um callback na chamada selecionada para que possa operar em cada linha retornada conforme ela é recebida:

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

Veja outros métodos de seleção úteis no GearDB:

selectOne(sql, args)Retornar 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)O mesmo que selectRow, mas para vários resultados.

Como 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 transmitemos para o método forceRow. O forceRow vai INSERIR uma entrada, se ainda não existir, ou ATUALIZAR uma entrada existente.


Como mostrar resultados da pesquisa

Nosso app exibe os resultados de uma determinada pesquisa no painel direito da página. Veja como recuperar 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 recuperá-los:

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

Começamos com tudo e podemos começar a mostrar o conteúdo interno em HTML do JSON na nossa página.


Como usar um armazenamento de recursos para acesso off-line

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

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

Para fazer 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 Google engrenagem salva esses itens e também captura as solicitações do navegador para devolvê-los. O servidor local vai atuar como uma chave de tráfego 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 controlar a versão do aplicativo e permitir upgrades de maneira transacional. Juntos, você define uma versão e, ao lançar um novo conjunto de recursos, quer que os usuários tenham um upgrade contínuo dos arquivos. Se esse for o modelo, você vai usar a API ManagedResourceStore.

Para capturar nossos recursos, o objeto GearBaseContent:

  1. Configurar uma matriz de arquivos que precisam ser capturados
  2. Criar um LocalServer
  3. Abrir ou criar um novo ResourceStore
  4. Chame a atenção 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'));
  });
}

O importante aqui é que você só pode capturar recursos no seu próprio domínio. Tivemos essa limitação quando tentamos acessar o arquivo JavaScript do GearDB diretamente do arquivo "gears_db.js" original no entroncamento SVN. A solução é simples: claro que você precisa fazer o download de todos os recursos externos e colocá-los no seu domínio. Os redirecionamentos 302 ou 301 não vão funcionar, já que o LocalServer só aceita códigos de servidor 200 (Sucesso) ou 304 (Não modificado).

Isso tem implicações. Se você colocar suas imagens em images.seudominio.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 anula o propósito de dividir seu aplicativo em vários domínios.

Como depurar o aplicativo off-line

Depurar um aplicativo off-line é um pouco mais complicado. Agora há mais cenários para testar:

  • Estou on-line com o app em execução no cache
  • Estou on-line, mas não acessei o app e não há nada em cache
  • Estou off-line, mas já acessei o app
  • Estou off-line e nunca acessei o app (não é um bom lugar para chegar lá!)

Para facilitar a vida, usamos o seguinte padrão:

  • Desativamos o cache no Firefox (ou no navegador que você escolher) quando precisamos verificar se o navegador não está apenas captando algo do cache.
  • Nós depuramos com o Kubeflow e o Lite para testes em outros navegadores. Usamos console.log() em todo o lugar e detectamos o console apenas por precaução
  • Adicionamos o código JavaScript auxiliar para:
    • limpar o banco de dados e limpar as informações
    • remover os arquivos capturados, para que, quando você atualizar, ele acesse a Internet para pegá-los novamente (útil quando você está iterando no desenvolvimento;);

O widget de depuração só será exibido no lado esquerdo da página se o Google Apps estiver instalado. Ele tem frases de destaque 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, usar o Google Gear é bem simples. Usamos o GearDB 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 é definir a estratégia para quando acessar os dados on-line e como armazená-los off-line. É importante dedicar tempo para definir o esquema do banco de dados. Se você precisar alterar o esquema no futuro, precisará gerenciar essa alteração, uma vez que seus usuários atuais já têm uma versão do banco de dados. Isso significa que você precisará enviar o código do script com qualquer upgrade de banco de dados. Obviamente, isso ajuda a minimizar isso, e você pode testar o GearShift, uma pequena biblioteca que pode ajudar a gerenciar revisões.

Também podemos ter usado o ManagedResourceStore para monitorar nossos arquivos, com as seguintes consequências:

  • Seremos bons cidadãos e controlaremos os arquivos para permitir upgrades futuros limpos
  • Há um recurso do ManagedResourceStore que permite criar um alias de URL para outro conteúdo. Uma opção válida de arquitetura seria ter Gears_base.js como uma versão não Gears e alias para que o próprio Gear faça o download de Gears_base_withgears.js com suporte off-line.
Para o app, consideramos mais fácil ter apenas uma e implementar essa interface de duas maneiras.

Esperamos que você tenha achado os apps de engrenagem fáceis e divertidos. Participe do fórum do Google Gear caso tenha dúvidas ou um app para compartilhar.