Fork me on GitHub

Você está aqui: Home Dive Into HTML5


O Passado, Presente & Futuro de Armazenamento Local para Aplicações Web

 

Mergulhando

AArmazenamento local persistente é uma das áreas onde aplicações nativas de cliente têm mantido uma vantagem sobre aplicações web. Para aplicações nativas, o sistema operacional tipicamente provê uma camada de abstração para armazenar e recuperar dados específicos do aplicativo como preferências ou estado de tempo de execução. Estes valores podem ser armazenados no registro, arquivos INI, arquivos XML ou algum outro lugar de acordo com a convenção da plataforma. Se o seu aplicativo cliente nativo necessita de armazenamento local diferente dos pares chave/valor, você pode inserir o seu próprio banco de dados, inventar o seu próprio formato de arquivo ou qualquer número de outras soluções.

Historicamente, aplicações web não tiveram nenhum desses luxos. Cookies foram inventados no início da história da web e, de fato, eles podem ser usados para armazenamento local persistente de pequenas quantidades de dados. Mas eles têm três desvantagens potencialmente [potentially dealbreaking downsides]:

O que nós realmente queremos é

Antes da HTML5, todas as tentativas de obtenção foram fundamentalmente insatisfatórias de diferentes maneiras.

Uma Breve História dos Hacks de armazenamento locais antes HTML5

No início, existia apenas o Internet Explorer. Ou ao menos, era isso que a Microsoft gostaria que o mundo acreditasse. Para esse fim, como parte da Primeira Grande Guerra de Browsers, a Microsoft inventou uma grande quantidade de coisas e as incluiu no seu "vencedor de todas as batalhas", o Internet Explorer. Uma dessas coisas foi conhecida como DHTML Comportamentos, e um desses comportamentos foi conhecido como userData.

userData possibilita que páginas web armazenem até 64KB de dados por domínio, em uma estrutura hierárquica baseada em XML. (Domínios confiáveis, tais como sites de intranet, podem armazenar 10 vezes esta quantidade. E, 640 KB deveria ser o suficiente para qualquer um.) O IE não apresenta qualquer forma de diálogo de permissões e não há previsão para o aumento da quantidade de armazenamento disponível.

Em 2002, a Adobe introduziu uma funcionalidade no Flash 6 que ganhou o infeliz e enganoso nome de “Flash cookies.” Dentro do ambiente do Flash, a funcionalidade é propriamente conhecida como Local Shared Objects - Objetos de local compartilhado. Resumidamente, isso permitia que objetos Flash armazenassem até 100KB de dados por domínio. Brad Neuberg desenvolveu um pré protótipo de uma ponte Flash-to-JavaScript chamada AMASS (AJAX Massive Storage System), porém ficou limitado por algumas peculiaridades de design do Flash. Em 2006, com o advento da ExternalInterface presente no Flash 8, acessar LSOs a partir do JavaScript tornou-se uma coisa de magnitude mais fácil e rápida. Brad reescreveu o AMASS e o integrou dentro do popular Dojo Toolkit com o nome de dojox.storage. Flash garante a cada domínio 100 KB de armazenamento “livre.” Além disso, ele solicita ao usuário para cada ordem de magnitude um aumento no armazenamento de dados (1 Mb, 10 Mb, e assim por diante).

Em 2007, o Google lançou o Gears, um plugin de código aberto com o objetivo de providenciar capacidades adicionais nos navegadores. (Nós já tínhamos discutido sobre Gears no contexto de providenciar uma API de geolocalização para o Internet Explorer.) Gears possibilita uma API para uma base de dados SQL icorporada baseada em SQLite. Após obter a permissão de um usuário uma vez, Gears pode armazenar ilimitada quantidade de dados por um domínio em uma tabela de banco de dados SQL.

Entretanto, Brad Neuberg e outros continuaram a trabalhar em cima do dojox.storage para providenciar uma interface unificada para todos esses diferentes plugins e APIs. Em 2009, dojox.storage podia auto-detectar (e providenciar uma interface unificada em cima de) Adobe Flash, Gears, Adobe AIR, e um pré protótipo da HTML5 storage que era apenas implementado em antigas versões do Firefox.

Quando você analisa essas soluções, um padrão surge: todos eles são específicos de um único navegador, ou dependentes de plugins de terceiros. Apesar do esforço heróico para deixar clara as diferenças (no dojox.storage), eles todos apresentam interfaces totalmente diferentes, têm diferentes limitações de armazenamento, e apresentam diferentes experiências de usabilidade. Portanto, esse é o problema que a HTML5 se propõe a resolver: providenciar uma padronizada API, implementada de forma nativa e consistente em vários navegadores, sem a necessidade e dependência de plugins de terceiros.

Introdução ao HTML5 Storage

O que me refiro como “HTML5 Storage” é a especificação nomeada de Web Storage, que estava na especificação da HTML5 por um tempo, mas era tratado separadamente por desinteresse e razões políticas. Certos navegadores se referenciavam como “Local Storage” ou “DOM Storage.” A nomeação ficava ainda mais complicada em navegadores relacionados com padrões emergentes, que vou discutir mais adiante neste capítulo.

Então o que é HTML5 Storage? Simplesmente armazenar, é a forma de páginas da web amazenarem pares de chave/valor localmente, dentro do navegador do cliente. Semelhante aos cookies, esses dados persistem mesmo depois de você sair do site, fechar a guia ou o navegador. Diferente dos cookies, esses dados nunca são transmitidos ao servidor (a menos que você queria enviá-los manualmente). Diferente de todas as tentativas anteriores de fornecimento de armazenamento local persistente, este é implementado de forma nativa nos navegadores, para que esteja disponível mesmo quando os plugins de terceiros não estiverem.

Quais navegadores? Bom, as últimas versões de quase todos os navegadores suportam HTML5 Storage… até o Internet Explorer!

Suporte ao HTML5 Storage
IEFirefoxSafariChromeOperaiPhoneAndroid
8.0+3.5+4.0+4.0+10.5+2.0+2.0+

Direto do seu código JavaScript, você acessa o HTML5 Storage através do objeto localStorage no objeto global window. Antes de usá-lo, você deve detectar se o navegador tem suporte.

Verificando suporte HTML5 Storage

function supports_html5_storage() {
  try {
    return 'localStorage' in window && window['localStorage'] !== null;
  } catch (e) {
    return false;
  }
}

Ao invés de escrever essa função, você pode usar Modernizr para detectar o suporte da HTML5 Storage.

if (Modernizr.localstorage) {
  // window.localStorage é suportado pelo seu navegador!
} else {
  // não há suporte nativo ao HTML5 storage no seu navegador! :(
  // tente talvez dojox.storage ou alguma solução de terceiros
}

Usando HTML5 Storage

HTML5 Storage é baseado em pares de chave/valor nomeadas. Você pode armazenar dados em uma chave nomeada, e com essa mesma chave, recuperar os dados armazenados. A chave nomeada é uma string. E seu valor, é qualquer um suportado pelo JavaScript, incluindo Strings, Booleanos, Inteiros, ou Flutuantes. Conteúdo, os dados são armazenados como uma sequência de strings. Se você estiver armazenando e recuperando qualquer outro tipo de dado diferente de Strings, você precisa usar funções como parseInt() ou parseFloat() para recuperar os dados em um tipo esperado pelo JavaScript.

interface Storage {
  getter any getItem(in DOMString key);
  setter creator void setItem(in DOMString key, in any data);
};

Chamando setItem() com o nome de uma chave que já existe, ela sobrescreve o valor anterior. Chamando getItem() com o nome de uma chave que não existe, retornará null ao invés de de uma exceção.

Como outros objetos JavaScript, você pode tratar o objeto localStorage como um array associativo. Ao invés de usar os métodos getItem() e setItem(), você pode usar simplesmentes colchetes. Por exemplo, no trecho seguinte:

var foo = localStorage.getItem("bar");
// ...
localStorage.setItem("bar", foo);

…pode ser reescrito para usar a sintaxe de colchetes ao invés:

var foo = localStorage["bar"];
// ...
localStorage["bar"] = foo;

Existem métodos para remover um valor já dado a uma chave, e limpar o armazenamento local inteiro (isto é, apagar todas as chaves/valor existentes de uma só vez).

interface Storage {
  deleter void removeItem(in DOMString key);
  void clear();
};

Chamando removeItem() com uma chave inexistente, não retornará nada.

Finalmente, há uma propriedade para obter o número total de chaves/valor na área de armazenamento local, e também, para percorrer todas as chaves pelo seu índice (e retornar o nome de cada uma).

interface Storage {
  readonly attribute unsigned long length;
  getter DOMString key(in unsigned long index);
};

Se você chamar key() com um índice que não está entre 0–(length-1), a função retornará null.

Controlando Alterações na Área do HTML5 Storage

Se quiser manter o controle das constantes mudanças no armazenamento local, você pode capturar o evento storage. O evento storage é ativado no objeto window sempre que setItem(), removeItem(), ou clear() é chamado e, geralmente, muda alguma coisa. Por exemplo, se você definir um valor a uma chave existente ou chamar clear() em chaves não nomeadas, o evento storage não será chamado, porque realmente, nada mudou na área de armazenamento local.

O evento storage é suportado em todos os lugares em que o objeto localStorage funciona, incluindo Internet Explorer 8. IE 8 não suporta o padrão W3C addEventListener (será adicionado, finalmente, no IE 9). Portanto, para pegar o evento storage , você vai precisar checar qual mecanismo de evento o navegador suporta. (Se você já fez isso antes, para outros eventos, pode pular essa seção. Pegando o evento storage , funciona da mesma forma como todos os outros eventos, você estará sempre preso. Se você preferir usar jQuery ou qualquer outra biblioteca JavaScript para registrar os eventos de manipulação, você pode fazer isso com o evento storage , também.)

if (window.addEventListener) {
  window.addEventListener("storage", handle_storage, false);
} else {
  window.attachEvent("onstorage", handle_storage);
};

A função de callback handle_storage é chamada junto ao objeto StorageEvent, exceto no Internet Explorer que os objetos de evento são armazenados no window.event.

function handle_storage(e) {
  if (!e) { e = window.event; }
}

Nesse ponto, a variável e será o objeto StorageEvent, que tem as seguintes propriedades:

Objeto StorageEvent
PropriedadeTipoDescrição
keystringo nome da chave que foi adicionada, removida ou alterada
oldValuequalquero valor antigo (agora atualizado), ou null se for um novo item adicionado
newValuequalquero novo valor, ou null se um item foi removido
url*stringa página que chamou o método que realizou a mudança
* Nota: a propriedade url é originalmente chamada de uri. Alguns navegadores lançaram essa propriedade antes das mudanças na especificação. Para uma compatibilidade máxima, você deve checar se a propriedade url existe, se não, checar se a propriedade uri existe.

O evento storage não é cancelável. Dentro da função callback handle_storage, não há maneira de parar essa mundaça. É uma simples forma do navegador dizer, “ei, isso já aconteceu. Não há nada que você possa fazer, eu só queria te avisar sobre isso.”

Limitações nos navegadores atuais

Ao falar sobre a história dos hacks no armazenamento local usando plugins de terceiros, fiz questão de mencionar a limitação de cada técnica, tal como limite de armazenamento. Apenas percebi que não tinha mencionado nada sobre as limitações do agora padronizado armazenamento da HTML5. Vou dar a vocês as respostas primeiro, em seguida explicarei elas. As respostas, em ordem de importância, são “5 megabytes,” “QUOTA_EXCEEDED_ERR,” e “Não.”

“5 megabytes” é a quantidade em espaço que cada origem tem por padrão. Isto é surpreendentemente consistente em todos os navegadores, embora seja formulada como não mais do que uma sugestão na especificação do Armazenamento local da HTML5. Um aspecto a ter em mente é que você está armazenando strings, não as informações no seu formato original. Se você está armazenando muitos inteiros ou floats, a diferença de representação pode realmente aumentar. Cada dígito naquele float está sendo armazenado como um caractere, não na representação usual de um ponto de número flutuante.

QUOTA_EXCEEDED_ERR” é a exceção que será lançada se você exceder sua cota de armazenamento de 5 megabytes. “Não” é a resposta para a próxima questão óbvia, “Eu posso pedir permissão do usuário para usar mais espaço de armazenamento?” No momento em que escrevo (fevereiro 2011), não há navegador que suporte qualquer mecanismo para desenvolvedores web requisitarem mais espaço de armazenamento. Alguns navegadores (como Opera) permitem que o usuário controle a cota de armazenamento de cada site, mas isto é uma ação puramente iniciada pelo usuário, não algo que você como desenvolvedor pode construir na sua aplicação web.

Armazenamento HTML5 em Ação

Vamos ver o Armazenamento da HTML5 em ação. Recorde o jogo Halma que nós construímos no capítulo do canvas. Existe um pequeno problema com o jogo: se você fechar o navegador no meio do jogo, você perderá seu progresso. Porém, com o armazenamento da HTML5, podemos salvar o progresso localmente, dentro do próprio navegador. Aqui está uma demonstração no ar. Faça alguns movimentos, depois feche a aba do navegador, em seguida reabra ela. Se o seu navegador suporta Armazenamendo da HTML5, a página de demonstração deve magicamente recordar a sua exata posição dentro do jogo, incluindo o número de movimentos que você fez, a posição de cada uma das peças do tabuleiro, e até mesmo se uma determinada peça está selecionada.

Como isto funciona? Cada momento que uma mudança acontece dentro do jogo, chamamos esta função:

function saveGameState() {
    if (!supportsLocalStorage()) { return false; }
    localStorage["halma.game.in.progress"] = gGameInProgress;
    for (var i = 0; i < kNumPieces; i++) {
	localStorage["halma.piece." + i + ".row"] = gPieces[i].row;
	localStorage["halma.piece." + i + ".column"] = gPieces[i].column;
    }
    localStorage["halma.selectedpiece"] = gSelectedPieceIndex;
    localStorage["halma.selectedpiecehasmoved"] = gSelectedPieceHasMoved;
    localStorage["halma.movecount"] = gMoveCount;
    return true;
}

Como você pode ver, ela usa o objeto localStorage para salvar se há um jogo em progresso (gGameInProgress, um booleano). Se assim for, percorre as peças (gPieces, um Array JavaScript) e salva o número de linha e coluna de cada uma. Em seguida ele salva alguns estados adicionais do jogo, incluindo a peça que está selecionada (gSelectedPieceIndex, um inteiro), se a peça está no meio de uma série potencialmente longa de saltos (gSelectedPieceHasMoved, um booleano), e o número total de movimentos feito até então (gMoveCount, um inteiro).

No carregamento da página, em vez de chamar automaticamente uma função newGame() que resetaria estas variáveis para valores "hard-coded", chamamos uma função resumeGame(). Usando Armazenamento HTML5, a função resumeGame() checa se um estado relacionado ao jogo em progresso está armazenado localmente. Se está, ele recupera esses valores usando o objeto do localStorage.

function resumeGame() {
    if (!supportsLocalStorage()) { return false; }
    gGameInProgress = (localStorage["halma.game.in.progress"] == "true");
    if (!gGameInProgress) { return false; }
    gPieces = new Array(kNumPieces);
    for (var i = 0; i < kNumPieces; i++) {
	var row = parseInt(localStorage["halma.piece." + i + ".row"]);
	var column = parseInt(localStorage["halma.piece." + i + ".column"]);
	gPieces[i] = new Cell(row, column);
    }
    gNumPieces = kNumPieces;
    gSelectedPieceIndex = parseInt(localStorage["halma.selectedpiece"]);
    gSelectedPieceHasMoved = localStorage["halma.selectedpiecehasmoved"] == "true";
    gMoveCount = parseInt(localStorage["halma.movecount"]);
    drawBoard();
    return true;
}

A parte mais importante desta função é a ressalva que mencionei no início deste capítulo, que vou repetir aqui: Informações são armazenadas como string. Se você está armazenando algo diferente de string, você vai precisar forçar uma conversão quando você recuperá-la. Por exemplo, a bandeira para verificar se existe um jogo em progresso (gGameInProgress) é um booleano. Na função saveGameState(), nós apenas armazenamos e não nos preocupamos em relação ao datatype:

localStorage["halma.game.in.progress"] = gGameInProgress;

Pórem, na função resumeGame(), precisamos tratar o valor que pegamos da área de armazenamento local como uma string e construir manualmente o valor booleano apropriado:

gGameInProgress = (localStorage["halma.game.in.progress"] == "true");

Semelhantemente, o número de movimentos está armazenado no gMoveCount como um inteiro. Na função saveGameState(), apenas armazenamos:

localStorage["halma.movecount"] = gMoveCount;

Mas na função resumeGame(), precisamos forçar o valor para um inteiro, usando a função nativa do JavaScript parseInt():

gMoveCount = parseInt(localStorage["halma.movecount"]);

Além de Pares de Chaves-Valores Nomeados: Visões conflitantes

Enquanto o passado é cheio de truques e soluções alternativas, a condição presente do Armazenamendo da HTML5 é surpreendentemente otimista. Uma nova API vem sendo padronizada e implementada em todos os principais navegadores, plataformas e dispositivos. Como um desenvolvedor web, isto não é algo que você vê todo dia, não é verdade? Porém, há mais vida além dos “5 megabytes de pares de chaves/valores nomeados” e o futuro do armazenamento local persistente é…como poderei colocá-lo… bom, existem visões conflitantes.

Uma visão é uma sigla que você provavelmente já sabe: SQL. Em 2007, a Google lançou Gears, um plugin de código aberto que incluía um banco de dados incorporado com base de dados no SQLite. Esse protótipo inicial influenciou a criação da especificação do Web SQL Database. Web SQL Database (formalmente conhecida como “WebDB”) fornece uma fina camada em torno de uma base de dados SQL, permitindo você fazer coisas como essa pelo JavaScript:

código atual funcionando em 4 navegadores

openDatabase('documents', '1.0', 'Local document storage', 5*1024*1024, function (db) {
  db.changeVersion('', '1.0', function (t) {
    t.executeSql('CREATE TABLE docids (id, name)');
  }, error);
});

Você pode ver, maior parte da ação reside na sequência que você passa para o método executeSql. Esta string pode ser qualquer comando suportado pelo SQL, incluindo SELECT, UPDATE, INSERT and DELETE. É como programação de base de dados backend, exceto o fato de você está fazendo isto via JavaScript! Oh alegria!

A especificação da Base de Dados SQL vem sendo implementada por quatro navegadores e plataformas.

Suporte a Base de Dados SQL Web
IEFirefoxSafariChromeOperaiPhoneAndroid
··4.0+4.0+10.5+3.0+2.0+

Claro, se você já usou mais de um produto de banco de dados em sua vida, você está ciente que “SQL” é mais um termo de marketing do que um rígido e rápido padrão. (Alguns podem dizer o mesmo da “HTML5,” mas não se importe.) Claro, existe uma especificação atual do SQL (ela é chamada de SQL-92), mas não há nenhuma base de dados do servidor no mundo que está em conformidade com esta e apenas esta especificação. Existe o SQL da Oracle, SQL da Microsoft, SQL do MySQL, SQL do PostgreSQL e SQL do SQLite. De fato, cada um desses produtos adiciona novas funcionalidades ao SQL no passar do tempo, então mesmo dizendo “SQL do SQLite” não é suficiente para determinar exatamente o que você está falando. Você precisa dizer a versão do SQL que acompanha o SQLite versão X.Y.Z.”

Tudo o que nos leva ao seguinte aviso, atualmente residindo no topo da especificação da Base de Dados SQL Web:

Esta especificação chegou a um impasse: todos os implementadores interessados ​​têm utilizado o mesmo backend do SQL (Sqlite), mas precisamos de múltiplas implementações independentes para prosseguir ao longo de um caminho de normalização. Até outro implementador estar interessado em implementar esta especificação, a descrição do dialeto SQL foi deixada como simplesmente uma referência para Sqlite, o que não é aceitável para um padrão.

Isto é contra esse cenário que vou apresentá-lo para uma outra visão concorrente avançada, persistente, armazenamento local para aplicações web: A API de "Indexed Database", formalmente conhecida como “WebSimpleDB,” agora carinhosamente conhecida como “IndexedDB.”

A API do “Indexed Database” expõe o que é chamado um objeto de armazenamento. Um objeto de armazenamento compartilha vários conceitos com o a base de dados SQL. Existem “base de dados” com “registros,” e cada registro tem um número definido de “campos.” Cada campo tem um “datatype” específico, que é definido quando a base de dados é criada. Você pode selecionar um subconjunto de registros, depois enumerá-los com um “índice.” Alterações no armazenamento de objetos são tratados dentro de “transações”.

Se você já fez alguma programação de banco de dados SQL, esses termos provavelmente serão familiares. A principal diferença é que o objeto de armazenamento não tem linguagem de consulta estruturada. Você não constrói uma declaração como "SELECT * from USERS where ACTIVE = 'Y'". Em vez disso, você usa métodos fornecidos pelo objeto de armazenamento para adicionar um índice na base de dados nomeada “USERS,” enumerar os registros, filtrar os registros de usuários inativos, e usar métodos de acesso para obter os valores de cada campo nos registros restantes. Uma visão geral do IndexedDB é um bom tutorial de como IndexedDB funciona, fazendo comparações lado a lado entre o IndexedDB e base dados SQL.

No momento que escrevo, IndexedDB está implementado apenas na versão beta do Firefox 4. (Em contraste, a Mozilla afirmou que eles nunca irão implementar Base de Dados SQL Web.) Google afirmou que estão considerando o suporte ao IndexedDB para o Chromium e o Google Chrome. E até mesmo a Microsoft disse que o IndexedDB “é uma ótima solução para a web.”

Então o que você, como desenvolvedor web, pode fazer com IndexedDB? Neste momento, praticamente nada além de algumas demonstrações técnicas. Um ano a frente? Talvez algo mais. Para iniciar, confira os links na seção “Leitura complementar” com alguns bons tutoriais.

Leitura complementar

HTML5 storage:

Trabalho prévio de Brad Neuberg et. al. (pré-HTML5):

Web SQL Database:

IndexedDB:

Isso foi “O Passado, Presente & Futuro de Armazenamento Local para Aplicações Web.” Consulte o Sumário, caso queira continuar com a leitura.

Você sabia?

Em associação a Google Press, O’Reilly está distribuindo este livro em variados formatos, incluindo papel, ePub, Mobi, DRM-free e PDF. A edição paga é chamada “HTML5: Up & Running” e está disponível agora. Este capítulo está incluído na versão paga.

Se você gostou deste capítulo e quer mostrar sua apreciação, basta comprar o livro “HTML5: Up & Running” com esse link afiliado ou comprar a edição eletrônica diretamente da O’Reilly. Você vai ganhar um livro, e eu vou ganhar um trocado. Atualmente, não aceito doações diretas.

Copyright MMIX–MMXI Mark Pilgrim