Grails: lutando contra o cache do navegador (CSS e Ajax)

Um problema no desenvolvimento de aplicações web é o cache do navegador. É muito comum situações nas quais alteramos arquivos CSS em nosso projeto, enviamos a aplicação para o ambiente de produção e mesmo assim nossos clientes continuam vendo o layout da versão anterior. Isto ocorre porquê o browser cacheia os arquivos baixados da Internet para melhorar a performance de navegação.

Há duas soluções para este problema: uma porca e outra elegante. A porca é pedir a seus clientes que esvaziem o cache do navegador, expondo assim a sua incompetência e queimando seu filme. A elegante é tirar proveito do número de versão do seu projeto.

CSS – evitando o cache

A técnica é bastante simples: o navegador identifica os recursos cacheados pela URL que usou para acessá-los. A solução para quem programa em Grails é composta de apenas dois passos.

1. Altere a versão da sua aplicação

Na raiz do seu projeto Grails há um arquivo chamado application.properties. É nele que ficam armazenadas meta-informações sobre sua aplicação, como por exemplo a versão do Grails e do seu projeto. A chave app.version contém este valor. Novamente, há três modos de alterar esta configuração. Uma é horrível, a feia e a terceira, a prática.

Opção horrível: altere o arquivo manualmente. Busque a chave application.properties e a altere de acordo com sua vontade.

Opção feia: use o comando grails set-version. É um script do Grails feito especificamente para esta tarefa.

O problema com as duas opções acima é que exigem que você, desenvolvedor, se lembre de executar estes passos. Por esta razão, recomendo a opção prática: interceptar o evento de compilação do seu projeto.

Opção prática

Você deverá incluir um evento no script de compilação do seu projeto. Isto é feito criando-se o arquivo _Events.groovy dentro do diretório scripts da sua aplicação. Toda vez que o seu código for compilado pelo Grails, ele buscará este arquivo que pode conter uma série de eventos a serem disparados pelo mecanismo de build do Framework (neste link há mais detalhes sobre este poderoso recurso).

Sem mais esperas, segue o evento que escrevi que deverá ser executado antes da compilação do projeto:


eventCompileStart = { kind ->
 println "Incrementando o número da versão"
 def versao = metadata.'app.version'

 if (!versao)
 versao = 0
 else
 versao = Double.valueOf(versao) + 0.01

 metadata.'app.version' = versao.toString()

 try {
 metadata.persist()
 println "Versão alterada para ${versao}"
 } catch (Exception ex) {
 ex.printStackTrace()
 }

 println "Nova versão: ${versao}"
}

(nota: o número de versão neste exemplo será algo horrível como 0.01000000003. A idéia do post é só passar a idéia básica ok? Sendo assim, lembre-se de “embelezar” seu número de versão)

2. Modificando a URL dos seus arquivos CSS

Esta é a parte fácil, e pode ser aplicada a qualquer recurso que sofra alterações entre uma versão e outra do seu projeto. Basta incluir o número da versão após o caractere ? na URL que identifica o seu arquivo CSS, tal como no exemplo abaixo.


<link rel="stylesheet" href="${resource(dir:'css',file:'main.css')}?${grailsApplication.metadata['app.version']}" />

O caractere “?” é usado para separar o nome do recurso dos parâmetros a serem passados ao servidor. Como se trata de um arquivo estático, o servidor irá ignorar os parâmetros passados e simplesmente retornar o arquivo. O navegador, por sua vez, irá cachear o arquivo CSS com um nome que varia de acordo com a versão do seu projeto, evitando assim o problema de cache que mencionei acima.

Ajax – como evitar problemas com o maldito cache

Em aplicações que acessem dados alterados com frequência, é muito comum o navegador (especialmente o Internet Explorer, cuja principal função é tornar sua vida um inferno) retornar dados cacheados ao navegador, ao invés das informações mais atualizadas. Neste caso, a solução é basicamente a mesma que apresentei para os arquivos CSS. Basta incluir um parâmetro a mais na sua requisição assíncrona que seja randômico.

Assim o navegador sempre acreditará estar buscando um novo recurso, mesmo que o resultado seja o mesmo. Abaixo segue um exemplo usando jQuery.


$("#elemento").load("/aplicacao/controlador/action",
 {produto:idProduto, colaborador: idColaborador, noCache:new Date()},
 function() {
 onChangePreAtividade()
 })

Repare que passo um atributo chamado noCache. Meu controlador simplesmente irá ignorá-lo. No caso, eu passei apenas uma nova instância da hora da requisição, representada pelo objeto Date. Mas poderia também usar Math.random() para obter o mesmo resultado.

Concluindo

É isto. Agora você pode respirar tranquilo tendo a certeza de que seus usuários sempre terão uma resposta Ajax atualizada e o CSS mais atual carregado em seu navegador. Até a próxima!

13 thoughts on “Grails: lutando contra o cache do navegador (CSS e Ajax)

  1. Prática a saída, “engana” o browser e evita o cache :)

    Vale dar uma boa olhada no plugin ui-performance, que faz isto de maneira automatizada! Ele tb concatena e minifica arquivos css e js automagicamente!

    Ele trata essa questão da versão também, não só para css, mas pra js e imagens, inclusive, quando o pacote é gerado, ele altera as referencias das imagens que estavam até dentro de arquivos CSS (tipo um background: url(…) )

    Além disso, na hora de servir os arquivos, ele adiciona headers no response para cachear ao “infinito” aquela url, ou seja, como ele cacheia forever, e a url é sempre diferente, ele consegue dar a segurança que virá sempre pegar os novos, mas nunca duas vezes o mesmo arquivo!

    É bem prático, estou usando em projetos em produção e quebrou um galho e tanto!

    Diria que é um dos top-5 plugins mais legais e importantes!

    Abração!!

    Responda

    Lucas Teixeira Reply:

    Ahh, esqueci de dizer, ele inclusive trabalha com imagens para fazer css-sprites!

    Pra isso usa o SmartSprites (csssprites.org)!!

    []s!

    Responda

    admin Reply:

    Oi Lucas, quanto tempo!

    Valeu pela dica!

    Responda

  2. Você pode também configurar o JQuery para evitar o cache em todas as requisições AJAX

    $.ajaxSetup({ cache: false });

    Responda

    admin Reply:

    Vivendo e aprendendo. Valeu pela dica!

    Responda

  3. Esta solução que você apresentou resolve o problema de cache do browser mas não o da versão, caso a pessoa queira que a versão seja incrementada automaticamente. Pois o evento compilar pega todo script que compile, ou seja, quando você der grails run-app para testar a aplicação ele vai incrementar a versão.

    Eu descobri uma forma pra resolver este problema. Basta ao invés de por o nome do evento “eventCompileStart”, usar o nome “eventWarStart”. Assim toda vez que você quiser testar a aplicação antes de levar a produção pode fazê-lo sem alterar a versão. E quando for gerar um arquivo war para levar a um servidor java ele irá incrementar a versão.

    Esta ainda não é a melhor das soluções pois não se restringe ao ambiente de produção. Ele atinge a todos os ambientes. Mas pra quem lida com versão, não busca somente driblar o cache do browser e não quer ficar fazendo manutenção na mão, esta solução já facilita as coisas.

    Responda

    admin Reply:

    Valeu Daniel! Muito bem observado.

    Realmente, a solução que propus incrementa a versão o tempo inteiro. Como nesta situação o objetivo era apenas evitar o problema do cache, e o número da versão não tinha muita função para mim além disto, eu fiz vista grossa ao problema.

    Vou modificar meu aplicativo pra que fique tal como você falou.

    Novamente, valeu!

    Responda

    Daniel Costa - Yarkhs Reply:

    De nada. Bem, tudo vai depender da necessidade do desenvolvedor. Sua necessidade era apenas a de driblar o browser. Já no caso de precisar da versão para lançar para a produção a solução que propus é melhor.

    Agora precisamos descobrir como fazer para a versão ser incrementada somente se for acionado o ambiente produção :P. Pq pode ser que a pessoa gere um war para testar e não para lançar em produção. Neste caso minha solução também é furada.

    Responda

  4. Oi,
    Estou usando a versão 1.3.5 do GRAILS, e o evento eventWarStart não funcionou.
    Verifiquei no _GRAILS_WAR.groovy que existem os eventos eventCreateWarStart e eventCreateWarEnd. Ambos os eventos funcionam, e alteram a versão APÓS gerar o WAR. O WAR é gerado com a versão antiga ainda.

    Para captar o evento somente em produção pode-se utilizar o
    ** if (grails.util.Environment.current == grails.util.Environment.PRODUCTION) **

    Para solucionar o problema de ponto flutuante 0.799999999 usei o seguinte algoritmo
    versao = versao.split(“\\.”)
    versao[1] = versao[1].toInteger() + 1
    versao = versao.join(“.”)

    Obs. Não consegui fazer esse script GLOBAL para todas as aplicações. Somente funcionou na pasta scripts da aplicação e não na scripts de $GRAILS_HOME

    Segue o arquivo _Events.groovy final

    eventCreateWarEnd = { warName, stagingDir ->

    if (grails.util.Environment.current == grails.util.Environment.PRODUCTION) {
    println “Incrementando o número da versão”
    def versao = metadata.’app.version’

    if (!versao)
    versao = 0
    else {
    versao = versao.split(“\\.”)
    versao[1] = versao[1].toInteger() + 1
    versao = versao.join(“.”)
    }

    metadata.’app.version’ = versao.toString()

    try {
    metadata.persist()
    println “Versão alterada para ${versao}”
    } catch (Exception ex) {
    ex.printStackTrace()
    }

    println “Nova versão: ${versao}”
    }
    }

    Responda

    admin Reply:

    Oi Daniel, pelo blog fica complicado te responder. Tem como você se registrar no Grails Brasil para que eu te atenda por lá?

    O endereço é http://www.grailsbrasil.com.br

    É que por lá, fica muito mais fácil pra mim postar as respostas e, além disto, mais pessoas que tenham o mesmo problema que você podem ser ajudadas também ok? Te aguardo lá.

    Grande abraço!

    Responda

    Daniel Costa (Yarkhs) Reply:

    Obrigado pela solução Chará. Uma linha de código e o problema de rodar o script em produção foi resolvido xD.

    Eu vi uma solução aqui na minha empresa fascinante. Resolvo todos os problemas citados aqui. Ao invés de incrementar 0.1 na versão, faz versão com data e hora da compilação.

    Fica assim:
    yyyyMMdd THHmm

    Exemplo
    de 11/02/2011 16:00
    para 20110211 T1600

    Responda

  5. Como fazer isso quando se usa bundles para CSS e JavaScript?

    Responda

    Kico (Henrique Lobo Weissmann) Reply:

    Se já estiver usando o plugin Asset ou Resource, não precisa fazer nada. :)

    Responda

Leave a Reply

Your email address will not be published. Required fields are marked *