7 Princípios de aplicativos web modernos - parte II
Continuando o artigo 7 Princípios aplicativos web modernos - parte I
3. Reaja à mudanças de dados
Quando os dados mudam no servidor, deixe os clientes saberem sem perguntar. Esta é uma forma de melhoria de performance que libera o usuário de ações manuais para atualizar (F5, puxar a tela para baixo). Novos desafios: controle de (re)conexão, reconciliação de estado.
O terceiro princípio é que a capacidade de resposta da UI (do inglês, "User Interface") em relação a mudanças de dados na fonte, tipicamente um ou mais servidores de database.
Servir um HTML instantâneo do dado que permanece estático até que o usuário atualize a página (websites tradicionais) ou interaja com ele (AJAX) está cada vez mais se tornando obsoleto.
Sua interface deve se auto-atualizar.
Isto é crucialmente importante num mundo com um número cada vez maior de pontos de informação, na forma de relógios, celulares, tablets e dispositivos que se possa usar no corpo que ainda serão inventados.
Considere o feed de notícias do Facebook no momento de sua criação, quando informações eram compartilhadas primariamente através de computadores pessoais. Renderizar de forma estática não era o ideal, mas fazia sentido se as pessoas estivessem atualizando seus perfis talvez uma vez por dia, se tanto.
Agora vivemos em um mundo em que você pode fazer upload de uma foto e seus amigos podem curti-la ou comentá-la quase que imediatamente. A necessidade de feedback em tempo real é natural devido ao uso muito maior da aplicação.
Seria errado, porém, assumir que os benefícios da capacidade de resposta são limitados a aplicações multi-usuário. Que é o motivo pelo qual eu gosto de falar sobre pontos de dados concorrentes como opostos a usuários. Considere o cenário comum de compartilhar uma foto que você tem no seu celular com o seu próprio laptop.
Uma aplicação single-user ainda pode se beneficiar da capacidade de reação
É útil pensar em toda a informação exposta ao usuário como reativa. Sincronização de estado de sessão e login são um exemplo de aplicação deste princípio de maneira uniforme. Se os usuários da sua aplicação tiverem múltiplas abas abertas simultaneamente, deslogar de uma delas invalidará todas as outras. Isto inevitavelmente resulta em melhorias de privacidade e segurança, especialmente em situações onde várias pessoas têm acesso ao mesmo dispositivo.
Cada página reage à sessão e ao estado de login
Uma vez que você crie a expectativa de que a informação na tela se atualizará automaticamente, é importante considerar uma nova necessidade: reconciliação de estado.
Quando recebemos atualizações de dados atômicos ordenados, é fácil de esquecer que sua aplicação deve ser capaz de atualizar apropriadamente mesmo depois de longos períodos desconectada. Considere o cenário de fechar a tampa do seu laptop e reabri-la dias depois. Como seu app se comporta então?
Exemplo do que ocorreria caso desconsiderássemos o tempo decorrido até a reconexão.
A capacidade da sua aplicação reconciliar estados deslocados no tempo também é relevante para o nosso primeiro princípio. Se você optar por enviar dados com o carregamento inicial da página, você deve considerar o tempo que o dado levará até que os seus scripts client-side carreguem. Este tempo é essencialmente equivalente a uma desconexão, e a conexão inicial dos seus scripts é a retomada da sessão.
4. Controle a troca de dados com o servidor
Agora nós podemos afinar a troca de dados com o servidor. Certifique-se de lidar com os erros, fazer uma nova tentativa pelo usuário, sincronizar dados em plano de fundo e manter caches offline.
Quando a WWW foi criada, troca de dados entre cliente e servidor era limitada a algumas poucas maneiras:
1. Clicar em um link resultaria (GET) uma nova página que seria renderizada.
2. Enviar um formulário iria postar (POST) ou obter (GET) e renderizar uma nova página.
3. Anexar uma imagem ou objeto iria enviá-lo (GET) de forma assíncrona e então renderizá-lo.
A simplicidade deste modelo é atrativa, e nós certamente temos uma curva de aprendizado maior hoje quando o assunto é entender como dados são enviados e recebidos.
As maiores limitações estavam relacionadas ao segundo ponto. A inabilidade de enviar dados sem necessariamente carregar uma nova página não era o ideal do ponto de vista de performance. Mas, mais importante, inviabilizava completamente o botão de voltar:
Possivelmente o artefato mais irritante da antiga web
A web como uma plataforma de aplicações foi, assim, inconcebível sem JavaScript. O AJAX constituiu um salto em termos de experiência de usuário acerca do envio de informação.
Agora temos uma variedade de APIs (XMLHttpRequest, WebSocket, EventSource para dar alguns exemplos) que nos dão um controle mais fino do fluxo de dados. Em adição à possibilidade de enviar dados que o usuário insira em um formulário, agora temos algumas novas oportunidades na melhoria de UX (User Experience).
Uma que é especialmente relevante ao princípio anterior é a habilidade de mostrar o estado de conexão. Se criarmos a expectativa de que a informação se atualiza automaticamente, devemos notificar o usuário sobre ser desconectado e sobre tentativas de reconexão.
Quando uma desconexão é detectada, é útil que se armazene dados na memória (ou até melhor, localStorage) para que possa ser enviada depois. Isto é especialmente importante em luz da introdução doc serial work,que possibilita que aplicações web JavaScript rodem em background. Se sua aplicação não estiver aberta, você ainda pode tentar sincronizar dados do usuário em background.
Considere timeouts e erros quando se envia dados e se faz uma nova tentativa em nome do usuário. Se uma conexão é restabelecida, se tenta enviar os dados novamente. Em caso de persistência do problema, entre em contato com o usuário.
É necessário lidar com certos erros de forma cautelosa. Por exemplo, um erro 403 inesperado pode significar que a sessão do usuário foi invalidada. Nestes casos, você tem a oportunidade de levar o usuário a retomá-la mostrando uma tela de login.
Também é importante ter certeza que o usuário não interrompa o fluxo de dados inadvertidamente. Isto pode acontecer em duas situações. A primeira e mais óbvia é fechando o browser ou aba, que você pode tentar evitar com o "beforeunload"
O aviso de beforeunload no browser
O outro (e menos óbvio) é capturar transições de página antes que elas aconteçam, como clicar em links que façam com que uma nova página seja carregada. Isto te dá uma chance de mostrar seus próprios avisos.
5. Não mude a história, faça melhorias
Sem o browser gerenciando URLs e histórico para nós, novos desafios são criados. Garanta que expectativas ligadas a rolar a página não serão quebradas. Mantenha seus caches para resposta rápida.
Envios de formulário de lado, se tivéssemos que desenvolver qualquer aplicação web moderna apenas com hiperlinks, acabaríamos com uma navegação completamente funcional no sentido de voltar e avançar.
Considere, por exemplo, o típico "cenário de paginação infinita". A forma típica que é implementado envolve capturar o clique com JavaScript, fazendo o pedido de dados / HTML, injetando. Fazendo com que a chamada history.pushState ou replaceState chamem seu passo opcional, que infelizmente não é dado pela maioria.
E esta é a razão pela qual sempre uso a palavra "quebra". Com o modelo mais simples que a web propôs inicialmente, esta situação não estava em jogo. Todo estado de transição necessitava de uma mudança de URL.
O outro lado disso é que surgem novas oportunidades para melhorar a história agora que podemos controlá-la com JavaScript.
Uma oportunidade é o que Daniel Pipius chamou de fast back.
Voltar deve ser rápido; usuários não esperam que os dados tenham mudado muito.
Isto é semelhante ao considerar o botão de voltar um botão em nível de aplicação e aplicar o princípio 2: aja imediatamente no input de usuário. A chave é que você agora pode decidir como fazer cache da página anterior e renderizar instantaneamente. Você pode então aplicar o princípio 3 e então informar o usuário das novas mudanças de dados que ocorreram à página.
Ainda existem alguns casos onde você não estará em controle do comportamento do cache. Por exemplo, se você renderizar uma página, e então navegar para um website de terceiros, e então o usuário pressiona o botão de voltar. Aplicações que renderizem o HTML no servidor e então o modifiquem no cliente estão em risco particular deste bug súbito:
Pressionar o botão de voltar incorretamente carrega o HTML inicial da página
Outra forma de quebrar a navegação é ignorando a memória de rolagem. Mais uma vez, páginas que não se apoiem em JS e gerenciamento de histórico manual muito provavelmente não tenham problemas em relação a isto. Mas as dinâmicas normalmente têm. Eu testei os dois mais populares newsfeeds JavaScript da web: Twitter e Facebook. Ambos exibiram amnésia de rolagem.
**********
Paginação infinita é geralmente suscetível a amnésia da barra de rolagem
Finalmente, esteja ciente de que mudanças de estado são relevantes apenas quando se está navegando pelo histórico. Considere este exemplo de quando contraímos as sub-árvores de comentários.
**********
A contração dos comentários deveria ser preservada quando navegamos pelo histórico.
Se a página fosse re-renderizada ao se seguir um link dentro da aplicação, a expectaria do usuário poderia ser de que todos os comentários aparecessem da forma que estavam. O estado era volátil e apenas associado com o histórico.
6. Atualizações de push code
Publicar dados sem um pushing code é insuficiente. Se seus dados se atualizam automaticamente, seu código também deve o fazer. Evite erros de API e melhore a performance. Use DOM estateles para evitar os efeitos colaterais.
Fazer sua aplicação reagir a mudanças de código é crucialmente importante.
Primeiro de tudo, isso reduz a possibilidade de erros e melhora a segurança. Se você fizer uma mudança nos seus APIs de backend, então o código do cliente deve ser atualizado. De outra maneira, eles podem não ser capazes de entender novos dados, ou podem enviar os dados em formato incompatível.
Outra razão igualmente importante tem a ver com a implementação do princípio #3. Se a sua UI (do inglês, "User Interface") se atualiza sozinha, existem poucos motivos para que os usuários atualizem a página.
Mantenha em mente que em um website tradicional, atualizar uma página tem duas partes: recarregar os dados e recarregar o código. Criar um mecanismo para carregar dados sem carregar o código não é suficiente, especialmente em um mundo em que uma única aba (sessão) pode ficar aberta por um longo período de tempo.
Depois disso, algumas aplicações web optam por atualizar a página pelo usuário quando for apropriado. Por exemplo, se a página não estivervisível e nenhum formulário for preenchido.
Uma abordagem melhor seria fazer um carregamento de hot code. Isso significa que não haveria necessidade de recarregar a página completamente. Ao invés disso, alguns módulos podem ser trocados em tempo real e seus códigos re-executados.
É certamente difícil fazer fazer recarregamento de hot code funcionar para muitos codebases existentes. Vale a pena discutir então um tipo de arquitetura que possa separar de maneira elegante o comportamento (código) dos dados (estado). Tal separação nos permitiria criar uma gama de patches de forma eficiente.
Considere por exemplo um módulo da sua aplicação que cria um event bus (e.g.: socket). Quando os eventos são recebidos, o estado de um certo componente é populado e então é feita a renderização para o DOM. Então você modifica o comportamento daquele componente, por exemplo, para que produza diferentes marcações de DOM para novos e existentes estados.
Mas o próximo desafio é que módulos devem ser capazes de serem capazes de serem re-avaliados sem introduzirem efeitos colaterais indesejáveis. Isso é quando uma arquitetura como a proposta pelo React se torna particularmente útil. Se um componente de código é atualizado, sua lógica pode ser re-executada de forma trivial e o DOM atualiza de forma eficiente. Uma exploração deste conceito por Dan Abramov pode ser encontradaaqui
Em essência, a idéia de que você renderiza para o DOM é o que ajuda de forma significante com a mudança de código. Se o estado fosse mantido no DOM, ou ouvintes do evento fossem criados manualmente pela sua aplicação, atualizar o código se tornaria uma tarefa muito mais complicada.
7. Preveja o comportamento
Latência negativa
Uma aplicação JavaScript rica pode ter mecanismos que consigam predizer o eventual input de usuário.
A aplicação mais comum desta idéia é a de preemptivamente requerer dados do servidor antes de uma ação ser consumada. Começando a buscar dados quando você passar o cursor por cima de um hiperlink para que esteja pronto quando for clivado é um exemplo disto.
Um método um pouco mais avançado é monitorar movimentos do mouse e analisar sua trajetória para detectar "colisões" com elementos acionáveis como botões. Um exemplo jQuery
Plugin jQuery que prediz a trajetória do mouse
Conclusão
A web continua sendo um dos meios mais versáteis para transmissão de informação. Enquanto continuamos adicionando mais dinamismo para nossas páginas, devemos garantir que vamos manter alguns de seus grandes benefícios históricos, enquanto nós incorporamos novos.
Páginas interconectadas por hiperlinks são um grande bloco de construção para qualquer tipo de aplicação. Carregamento progressivo de código, estilo e marcação enquanto o usuário navega através delas irá garantir ótima performance sem sacrificar a interatividade.
Novas oportunidades únicas foram possibilitadas pelo JavaScript que, uma vez que tenha sido universalmente adotado, irá garantir a melhor experiência de usuário possível para a maior e mais livre plataforma existente.