Criando aplicativos HTML5

Uma aula sobre histórico (API)

Clark Sell

Baixar o código de exemplo

Histórico, no contexto de um navegador da Web, normalmente significou o botão Voltar, e nunca foi fácil de gerenciar Tornou-se mais ainda um desafio quando o AJAX se tornou significativo em aplicativos Web e começamos a ver o site da Web típico crescer em direção a um aplicativo Web mais rico. E à medida que o JavaScript tornou-se habilitado por padrão, em vez de desabilitado, aplicativos Web de página única apareceram os quais fizeram uso pesado de JavaScript do lado do cliente. Mas não havia uma boa maneira de lidar com armazenamento do lado do cliente, banco de dados ou mesmo o estado geral entre páginas.

Agora, no entanto, o HTML5 assumiu de frente o gerenciamento de estado do lado do cliente e introduziu um conjunto novo e completo de APIs, incluindo especificações como IndexedDB, Armazenamento local e a API de Histórico, na qual me concentrarei neste artigo.

Resumindo, o objetivo da API de Histórico é fornecer uma maneira de aplicativos JavaScript avançados poderem ser mais capazes de não apenas navegar no histórico da sessão, mas também gerenciar seu estado e oferecer suporte de primeira classe a URLs. Isso significa que com algumas chamadas à API e eventos simples você pode navegar, enviando e exibindo dados de estado para a pilha da sessão para afetar, por exemplo, como o botão Voltar opera, ao mesmo tempo que mantém uma excelente estrutura de URL.

O problema

Quando você está trabalhando em qualquer solução, é importante entender o problema. A API de Histórico não é apenas ter uma maneira mágica de persistir estado de página à medida que o usuário final bate nos botões Voltar e Avançar. Embora seja certamente verdadeiro, a história real está muito mais embaixo. Da perspectiva de um usuário, há dois recursos importantes em qualquer navegador: a URL e os botões de navegação (Avançar e Voltar). Juntos eles permitem ao usuário solicitar ou navegar através de uma série de documentos na Internet.

Embora esses recursos tenham permanecido essencialmente os mesmos ao longo dos anos, nos bastidores as coisas mudaram à medida que o AJAX se tornou popular. O que uma vez foi uma série de documentos tornou-se apenas um único arquivo com chamadas AJAX nos bastidores. Isso foi ótimo; poderíamos entregar uma experiência mais rica com apenas pouca assincronia, não mencionando os benefícios de desempenho e as melhorias na experiência do usuário. Mas isso trouxe alguns problemas; tornou-se muito mais desafiador controlar a URL, por exemplo, saber aonde o navegador deveria ir quando o usuário pressionasse o botão Voltar ou Atualizar.

Eu não acho que possa exagerar a importância da URL. As URLs são permanentes: os usuários fazem favoritos delas, mecanismos de pesquisa as indexam e as empresas as comercializam. Como você poderia gerenciá-las no mundo da Web em transformação?

Insira o hash (#) e o hashbang (#!). Os navegadores consideram qualquer coisa depois de # como um identificador de fragmento de URL e nunca o manda para o servidor. O hash foi originalmente concebido como uma forma das marcas de ancoragem vincularem dentro da página, mas aconteceu delas serem usadas para atividades relacionadas ao AJAX. No início do AJAX, no entanto, as URLs com apenas um hash não eram indexadas por um mecanismo de pesquisa. A solução foi o hashbang, que deu aos aplicativos Web uma forma de usar e modificar uma URL e deixar que os mecanismos de pesquisa a indexassem, sem mesmo fazer uma solicitação de página de volta ao servidor. As URLs que contêm hashbangs, como twitter.com/#!replies, tornaram-se importantes com o advento de aplicativos Web.

A nova API de Histórico

Embora isso tenha sido um grande passo à frente, não houve realmente qualquer suporte formal do navegador. Os aplicativos Web apenas executavam jogos de script para fazer as coisas funcionarem. A nova API de Histórico se concentra em dar aos aplicativos Web a capacidade de “gerenciar o estado”, que significa controlar a sequência de documentos que constituem um histórico de sessão. Isso permite que você navegue no histórico da sessão e persista os dados de estado, e ainda lide com a URL adequadamente. Para começar a explorar essa API, vamos examinar a definição de interface real conforme especificada pelo World Wide Web Consortium, ou W3C (bit.ly/MU89iZ):

interface History {
  readonly attribute long length;
  readonly attribute any state;
  void go(optional long delta);
  void back();
  void forward();
  void pushState(any data, 
    DOMString title, 
    optional DOMString url);
  void replaceState(any data, 
    DOMString title, 
    optional DOMString url);
};

Não é de maneira alguma uma API complicada.

Para ter uma ideia de como as coisas realmente funcionam, vamos começar fazendo alguma navegação simples entre documentos. Suponha que você possua três documentos, a.cshtml, b.cshtml e c.cshtml, e você quer se mover por todos eles. Normalmente, você simplesmente criaria uma marca de ancoragem <a href=“http://foo.com/a.cshmtl”>goto a</a> na qual o usuário clica, forçando o navegador a seguir por seu ciclo de vida normal de página e servidor. Enquanto o usuário clica por um determinado site, ele cria o que é chamado o histórico da sessão.

No entanto, existem dois problemas potenciais com esse método. Primeiro, você está forçando o navegador a seguir por seu ciclo completo de página, e até chamar o servidor; e segundo, você precisa de um documento físico que represente a URL.

O AJAX resolve parte do problema permitindo que você solicite apenas partes de uma página, reduzindo o número de solicitações de página completa e atualizando manualmente a URL para algo como http://foo.com/\#\ or http://foo.com/\#. Para realizar algo similar com a API de Histórico, chame window.history.pushState(state, title, url), passando qualquer estado que você deseja que persista, junto com o título da página e a URL a ser exibida. Observe que não é necessário usar uma URL com um hash ou hashbang; você pode usar apenas http://foo.com/a, mesmo que “a” não exista fisicamente.

Chamando pushState, você estará criando o histórico da sessão para a sessão do usuário. O usuário poderá navegar conforme desejar e as coisas funcionarão como esperado. Ao pressionar o botão Voltar, ele é levado de volta à URL anterior, como esperado, e as URLs que estavam lá antes continuarão a existir, exatamente como com qualquer série normal de páginas.

Você também tem ganchos que permitem navegar e verificar o histórico da sessão do usuário. É possível mover o usuário dinamicamente pra frente e para trás na pilha, exatamente como se o próprio usuário estivesse clicando nos botões Voltar e Avançar.

Um exemplo real

Vamos tornar isso concreto com um exemplo real. Eu dirijo uma conferência de tecnologia chamada That Conference (thatconference.com). A conferência tem muitos oradores, mas não quero criar uma página para cada um deles. O que eu preferiria fazer é criar dinamicamente uma página para cada orador que parecer real. Posso fazer isso facilmente com a API de Histórico.

Como com qualquer aplicativo Web cheio de scripts, preciso de dados. Felizmente, That Conference tem uma API Representational State Transfer (REST) simples que posso chamar para obter os oradores, thatConference.com/api/person. Essa chamada resultará em uma matriz de oradores para o determinado ano em JSON ou XML. A Figura 1 mostra um item nessa matriz.

Figura 1 Um perfil de orador

<PersonViewModel>
  <FirstName>Scott</FirstName>
  <LastName>Hanselman</LastName>
  <Company>Microsoft</Company>
  <Bio>
    My name is Scott Hanselman. I work out of my home office for Microsoft as a Principal Program Manager, aiming to spread good information about developing software, usually on the Microsoft stack. Before this I was the Chief Architect at Corillian Corporation, now a part of Checkfree, for 6-plus years. I was also involved in a few Microsoft Developer things for many years like the MVP and RD programs and I'll speak about computers (and other passions) whenever someone will listen.
  </Bio>
  <Twitter>shanselman</Twitter>
  <WebSite>http://www.hanselman.com</WebSite>
  <Gravatar>/Images/People/2012Speakers/ScottHanselman.png</Gravatar>
</PersonViewModel>

Os dados não são bons se não houver uma maneira de vê-los. Preciso configurar um modelo de marcação simples que eu possa usar para criar dinamicamente uma página para cada orador. Para isso, usarei uma estrutura chamada Knockout (knockoutjs.com). Knockout é uma biblioteca JavaScript que ajuda os desenvolvedores a usar associações declarativas com o padrão Model-View-ViewModel. Não é necessário usar Knockout para a API de Histórico, mas eu vou, e vou me divertir com isso.

Como toda página de orador é a mesma, definirei um modelo de marcação simples em Knockout. Preciso criar um bloco de scripts e dizer à estrutura como preenchê-lo mais tarde:

<script type="text/html" id="person-template">
  <div>
    <p>
      <strong>Name:</strong>
        <span data-bind="text: FirstName"></span>
        <span data-bind="text: LastName"></span>
    </p>
    <p>Company: <strong data-bind="text: Company"></strong></p>
    <p>Bio: <strong data-bind="text: Bio"></strong></p>
  </div>
</script>

Em seguida, preciso preencher o modelo. Para isso, chamo ko.applyBindings(someData), e Knockout operará sua mágica em qualquer que seja o objeto que eu passar para applyBindings. Com isso, tenho o mecanismo básico instalado para tomar um objeto de orador e preencher a marcação com seus dados.

No entanto, meu objetivo é um pouco mais complexo. O que realmente quero é uma série de páginas pelas quais um usuário pode passar; um livro de oradores, por assim dizer. Eis o que é preciso acontecer na primeira vez que a página é carregada:

  1. Obter o JSON que representa os oradores.
  2. Associar o primeiro item da matriz ao modelo Knockout como padrão.
  3. Chamar window.pushState, passando os argumentos adequados.

Já abordei as duas primeiras etapas, assim, vamos falar sobre pushState. Chamando pushState, você estará criando um item no histórico da sessão do usuário. Vou chamar pushState e passar três itens:

  • Estado: Em meu caso, esses dados são o item da matriz que associei ao modelo Knockout.
  • Título: Esse é o título da página, que será o nome completo do orador.
  • URL: É a URL da página; nesse caso será algo como thatconference.com/speakers/speaker/SpeakerFullName.

Encapsulei toda essa lógica em um método que chamei de Bind:

function bind (speakerID) {
  var speakerVM = new speakerViewModel(speakerID);
  var fullName = speakers[speakerID].FirstName 
    + speakers[speakerID].LastName
  window.history.pushState(speakerVM, 
    fullName, "/speakers/" + fullName); 
  ko.applyBindings(speakerVM);
}

Agora, adicionarei alguns botões ao livro de oradores para permitir a movimentação pelos oradores:

<button id="prevSpeaker">previous speaker</button>
<button id="nextSpeaker">next speaker</button>

Certamente, preciso de alguns manipuladores de eventos para os botões nextSpeaker e prevSpeaker. Para controlar qual orador deve ser o próximo, vou criar um contador simples que manipularei à medida que o usuário navega. O valor do contador é o que passarei para o método Bind:

var counter = 0;
$('#nextSpeaker').click( function () {
  counter = counter + 1;   
  bind(counter);
});
$('#prevSpeaker').click( function () {
  counter = counter - 1;
  window.history.back();
});

Nesse ponto tenho uma página que é carregada com alguns dados padrão, e quando clico em Avançar, continuo a obter o próximo orador da matriz de oradores. No entanto, se eu chamar prevSpeaker, nada acontece. Algo mais é necessário.

Eventos

Quando o botão Voltar (ou um script) chama windows.history.back, o evento onpopstate é disparado. Esse é o gancho para voltar em um histórico de sessão de usuário. Quando onpopstate é disparado, ele passa junto os dados de estado para pushState; em meu caso, é aquele orador.

Agora, preciso pegar esses dados de estado e dizer a Knockout para associá-los:

window.onpopstate = function (event) {
  console.log('onpopstate event was fired');
  ko.applyBindings( event.state );
};

Com isso, posso mover para frente e para trás no histórico da sessão, como esperado. Você verá agora os oradores mudarem de maneira adequada, se você pressiona o botão Voltar ou o botão de orador anterior do navegador.

E agora?

Minha abordagem da API de Histórico foi apenas superficial. Não abordei o que fazer se um usuário faz um favorito de um orador e posteriormente visita o site ou, ainda, ele pressiona Atualizar enquanto está em uma dessas novas páginas de oradores que acabei de criar.

Explicitamente não abordei esse cenário, pois há muitas maneiras de fazer isso, mas elas dependem de como você estruturou seu site e das tecnologias que está usando. Se você assinar para usar # ou #!, chamar window.location.hash para obter o fragmento da URL e, então, chamar um serviço para recuperar os dados apropriados para esse hash e associá-lo à sua marcação talvez seja tudo do que precisa.

É importante observar que embora minha solução crie um página dinâmica inteira, também é possível usar a API de Histórico para parte de uma página existente, de modo que o principal da página aproveita o servidor, mas parte da página usa a API de Histórico. É possível encontrar um excelente exemplo detalhado disso em bit.ly/vOlB2U.

Também deve ser implementado a detecção de recursos em seu aplicativo Web. Em vez de basear as ações em agentes do usuário, você deve aproveitar uma ferramenta como o Modernizr (modernizr.com) para perguntar ao navegador o que ele pode fazer. Se o navegador de um usuário não der suporte a um determinado recurso, será possível usar um suporte retroativo, um shim que implementa o recurso para o navegador. Isso pode até mesmo ser feito para recursos como o CSS. Para obter mais informações sobre a detecção de recursos, verifique o artigo de setembro de 2011 de Brandon Satrom, “Nenhum navegador ficará para trás: uma estratégia de adoção do HTML5” (msdn.microsoft.com/magazine/hh394148).

O Ajax mudou a maneira que os sites interagem com a Internet, e os desenvolvedores da Web encontraram soluções criativas para transformar sites padrão em aplicativos Web avançados. A API de Histórico está aqui para ajudar esses aplicativos Web cheios de scripts a manter os fundamentos básicos de navegador intactos.

Tudo neste artigo foi feito no Windows 8 Release Preview usando o Microsoft WebMatrix. Todo o código pode ser encontrado em on.csell.net/msdn-historylesson e uma variedade de excelentes recursos para explorar a API de Histórico em on.csell.net/msdn-historylesson-linkstack.

Clark Sell trabalha como divulgador sênior da Web e do Windows 8 para a Microsoft fora de Chicago. Ele mantém um blog em csell.net, podcasts em DeveloperSmackdown.com e pode ser encontrado no Twitter em twitter.com/csell5.

Agradecemos aos seguintes especialistas técnicos pela revisão deste artigo: John Hrvatin, Mark Nichols, Tony Ross e Brandon Satrom