Como evitar o cache do navegador no site Angular 2?

104

No momento, estamos trabalhando em um novo projeto com atualizações regulares que está sendo usado diariamente por um de nossos clientes. Este projeto está sendo desenvolvido usando o angular 2 e estamos enfrentando problemas de cache, ou seja, nossos clientes não estão vendo as alterações mais recentes em suas máquinas.

Principalmente os arquivos html / css para os arquivos js parecem ser atualizados corretamente sem dar muitos problemas.

Rikku121
fonte
2
Ótima pergunta. Eu tenho o mesmo problema. Qual é a melhor forma de resolver esse problema? Isso é possível com o gulp ou qualquer ferramenta semelhante para publicar o aplicativo Angular 2?
jump4791
2
@ jump4791 A melhor maneira é usar o webpack e compilar o projeto usando as configurações de produção. No momento, estou usando este repositório, basta seguir as etapas e você deve ser bom: github.com/AngularClass/angular2-webpack-starter
Rikku121
Eu também tenho o mesmo problema.
Ziggler
3
Sei que essa é uma questão antiga, mas queria acrescentar a solução que encontrei, para quem passar por cima disso. Ao construir com ng build, adicionar a -prodtag adiciona um hash aos nomes de arquivo gerados. Isso força a recarga de tudo menos index.html. Este post no github deu algumas dicas sobre como recarregar.
Tiz
2
index.html é a causa raiz. Por não ter hashcode, quando está em cache, todo o resto é usado a partir do cache.
Fiona

Respostas:

178

angular-cli resolve isso fornecendo um --output-hashingsinalizador para o comando build (versões 6/7, para versões posteriores, veja aqui ). Exemplo de uso:

ng build --output-hashing=all

Bundling & Tree-Shaking fornece alguns detalhes e contexto. Em execução ng help build, documenta a bandeira:

--output-hashing=none|all|media|bundles (String)

Define the output filename cache-busting hashing mode.
aliases: -oh <value>, --outputHashing <value>

Embora isso seja aplicável apenas a usuários do angular-cli , ele funciona de maneira brilhante e não requer nenhuma alteração de código ou ferramentas adicionais.

Atualizar

Vários comentários indicaram de forma útil e correta que essa resposta adiciona um hash aos .jsarquivos, mas não faz nada para isso index.html. Portanto, é perfeitamente possível que index.htmlpermaneça em cache após o ng buildcache bloquear os .jsarquivos.

Neste ponto, irei passar para Como controlamos o cache de páginas da web em todos os navegadores?

Jack
fonte
14
Esta é a maneira correta de fazer isso e deve ser a resposta selecionada!
jonesy827
1
Isso não funcionou para nosso aplicativo. É uma pena que templateUrl com um parâmetro de string de consulta não funcione com CLI
DDiVita de
8
Isso não funcionará se o seu index.html for armazenado em cache pelo navegador, portanto, não verá novos nomes em hash para seus recursos javascript. Eu acho que uma combinação disso e a resposta que @Rossco deu faria sentido. Também faz sentido tornar isso consistente com os cabeçalhos HTTP enviados.
stryba 01 de
2
@stryba É por isso que o cache html deve ser tratado de forma diferente. Você deve especificar os cabeçalhos de resposta Cache-Control, Pragma e Expires para que nenhum cache ocorra. Isso é fácil se você estiver usando uma estrutura de back-end, mas eu acredito que você também pode lidar com isso em arquivos .htaccess para Apache (idk como funciona no nginx).
OzzyTheGiant
3
Essa resposta adiciona um hash aos arquivos js, o que é ótimo. Mas, como stryba disse, você também precisa ter certeza de que index.html não está armazenado em cache. Você não deve fazer isso com metatags html, mas com o cabeçalho de resposta cache-control: no-cache (ou outros cabeçalhos para estratégias de cache mais sofisticadas).
Noppey
34

Encontrou uma maneira de fazer isso, basta adicionar uma string de consulta para carregar seus componentes, assim:

@Component({
  selector: 'some-component',
  templateUrl: `./app/component/stuff/component.html?v=${new Date().getTime()}`,
  styleUrls: [`./app/component/stuff/component.css?v=${new Date().getTime()}`]
})

Isso deve forçar o cliente a carregar a cópia do modelo do servidor em vez da do navegador. Se desejar que ele seja atualizado somente após um determinado período, você pode usar este ISOString:

new Date().toISOString() //2016-09-24T00:43:21.584Z

E substring de alguns caracteres para que só mude depois de uma hora, por exemplo:

new Date().toISOString().substr(0,13) //2016-09-24T00

Espero que isto ajude

Rikku121
fonte
3
Portanto, minha implementação realmente não funcionou. o cache é um problema estranho. às vezes funciona e às vezes não. ah, a beleza dos problemas intermitentes. Na verdade, adaptei sua resposta para:templateUrl: './app/shared/menu/menu.html?v=' + Math.random()
Rossco
Estou recebendo 404 para meu templateUrls. Por exemplo: GET localhost: 8080 / app.component.html /? V = 0.0.1-alpha 404 (não encontrado) Alguma ideia do porquê?
Shenbo
@ Rikku121 Não, não importa. Na verdade, está sem o / no url. Posso ter adicionado acidentalmente quando postar o comentário
Shenbo
14
Qual é o ponto de armazenamento em cache quando você está impedindo o cache sempre, mesmo quando não há alteração de código?
Apurv Kamalapuri
1
ng build --aot --build-optimizer = true --base-href = / <url> / dá erro --- Não foi possível resolver o recurso ./login.component.html?v=${new Date (). getTime ()}
Pranjal Successena
23

Em cada modelo html, acabei de adicionar as seguintes metatags no topo:

<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">

No meu entendimento, cada modelo é independente, portanto, ele não herda a configuração de regras meta no cache no arquivo index.html.

Rossco
fonte
4
Nós mudamos para o webpack há algum tempo e ele cuida do bloqueio do cache de nossos aplicativos angulares. É bom saber que sua solução funciona. Obrigado
Rikku121
Para mim também
iniravpatel
4

Uma combinação da resposta de @Jack e da resposta de @ranierbit deve resolver o problema.

Defina o sinalizador de construção ng para --output-hashing para:

ng build --output-hashing=all

Em seguida, adicione essa classe em um serviço ou em seu app.moudle

@Injectable()
export class NoCacheHeadersInterceptor implements HttpInterceptor {
    intercept(req: HttpRequest<any>, next: HttpHandler) {
        const authReq = req.clone({
            setHeaders: {
                'Cache-Control': 'no-cache',
                 Pragma: 'no-cache'
            }
        });
        return next.handle(authReq);    
    }
}

Em seguida, adicione isso aos seus provedores em seu app.module:

providers: [
  ... // other providers
  {
    provide: HTTP_INTERCEPTORS,
    useClass: NoCacheHeadersInterceptor,
    multi: true
  },
  ... // other providers
]

Isso deve evitar problemas de cache em sites ativos para máquinas clientes

NiallMitch14
fonte
3

Eu tive um problema semelhante com o index.html sendo armazenado em cache pelo navegador ou mais complicado por meio de cdn / proxies (F5 não ajudará você).

Procurei uma solução que verifique 100% se o cliente tem a última versão index.html, felizmente encontrei esta solução de Henrik Peinar:

https://blog.nodeswat.com/automagic-reload-for-clients-after-deploy-with-angular-4-8440c9fdd96c

A solução resolve também o caso em que o cliente fica com o navegador aberto por dias, o cliente verifica se há atualizações em intervalos e recarrega se uma versão mais recente for implantada.

A solução é um pouco complicada, mas funciona perfeitamente:

  • use o fato de que ng cli -- prodproduz arquivos hash com um deles chamado main. [hash] .js
  • crie um arquivo version.json que contenha esse hash
  • crie um serviço angular VersionCheckService que verifica a versão.json e recarrega se necessário.
  • Observe que um script js executado após a implantação cria para você o version.json e substitui o hash no serviço angular, portanto, nenhum trabalho manual é necessário, mas executando post-build.js

Como a solução de Henrik Peinar era para angular 4, houve pequenas mudanças, coloco também os scripts fixos aqui:

VersionCheckService:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Injectable()
export class VersionCheckService {
    // this will be replaced by actual hash post-build.js
    private currentHash = '{{POST_BUILD_ENTERS_HASH_HERE}}';

    constructor(private http: HttpClient) {}

    /**
     * Checks in every set frequency the version of frontend application
     * @param url
     * @param {number} frequency - in milliseconds, defaults to 30 minutes
     */
    public initVersionCheck(url, frequency = 1000 * 60 * 30) {
        //check for first time
        this.checkVersion(url); 

        setInterval(() => {
            this.checkVersion(url);
        }, frequency);
    }

    /**
     * Will do the call and check if the hash has changed or not
     * @param url
     */
    private checkVersion(url) {
        // timestamp these requests to invalidate caches
        this.http.get(url + '?t=' + new Date().getTime())
            .subscribe(
                (response: any) => {
                    const hash = response.hash;
                    const hashChanged = this.hasHashChanged(this.currentHash, hash);

                    // If new version, do something
                    if (hashChanged) {
                        // ENTER YOUR CODE TO DO SOMETHING UPON VERSION CHANGE
                        // for an example: location.reload();
                        // or to ensure cdn miss: window.location.replace(window.location.href + '?rand=' + Math.random());
                    }
                    // store the new hash so we wouldn't trigger versionChange again
                    // only necessary in case you did not force refresh
                    this.currentHash = hash;
                },
                (err) => {
                    console.error(err, 'Could not get version');
                }
            );
    }

    /**
     * Checks if hash has changed.
     * This file has the JS hash, if it is a different one than in the version.json
     * we are dealing with version change
     * @param currentHash
     * @param newHash
     * @returns {boolean}
     */
    private hasHashChanged(currentHash, newHash) {
        if (!currentHash || currentHash === '{{POST_BUILD_ENTERS_HASH_HERE}}') {
            return false;
        }

        return currentHash !== newHash;
    }
}

mude para AppComponent principal:

@Component({
    selector: 'app-root',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
    constructor(private versionCheckService: VersionCheckService) {

    }

    ngOnInit() {
        console.log('AppComponent.ngOnInit() environment.versionCheckUrl=' + environment.versionCheckUrl);
        if (environment.versionCheckUrl) {
            this.versionCheckService.initVersionCheck(environment.versionCheckUrl);
        }
    }

}

O script de pós-construção que faz a mágica, post-build.js:

const path = require('path');
const fs = require('fs');
const util = require('util');

// get application version from package.json
const appVersion = require('../package.json').version;

// promisify core API's
const readDir = util.promisify(fs.readdir);
const writeFile = util.promisify(fs.writeFile);
const readFile = util.promisify(fs.readFile);

console.log('\nRunning post-build tasks');

// our version.json will be in the dist folder
const versionFilePath = path.join(__dirname + '/../dist/version.json');

let mainHash = '';
let mainBundleFile = '';

// RegExp to find main.bundle.js, even if it doesn't include a hash in it's name (dev build)
let mainBundleRegexp = /^main.?([a-z0-9]*)?.js$/;

// read the dist folder files and find the one we're looking for
readDir(path.join(__dirname, '../dist/'))
  .then(files => {
    mainBundleFile = files.find(f => mainBundleRegexp.test(f));

    if (mainBundleFile) {
      let matchHash = mainBundleFile.match(mainBundleRegexp);

      // if it has a hash in it's name, mark it down
      if (matchHash.length > 1 && !!matchHash[1]) {
        mainHash = matchHash[1];
      }
    }

    console.log(`Writing version and hash to ${versionFilePath}`);

    // write current version and hash into the version.json file
    const src = `{"version": "${appVersion}", "hash": "${mainHash}"}`;
    return writeFile(versionFilePath, src);
  }).then(() => {
    // main bundle file not found, dev build?
    if (!mainBundleFile) {
      return;
    }

    console.log(`Replacing hash in the ${mainBundleFile}`);

    // replace hash placeholder in our main.js file so the code knows it's current hash
    const mainFilepath = path.join(__dirname, '../dist/', mainBundleFile);
    return readFile(mainFilepath, 'utf8')
      .then(mainFileData => {
        const replacedFile = mainFileData.replace('{{POST_BUILD_ENTERS_HASH_HERE}}', mainHash);
        return writeFile(mainFilepath, replacedFile);
      });
  }).catch(err => {
    console.log('Error with post build:', err);
  });

simplesmente coloque o script na (nova) pasta de construção, execute o script usando node ./build/post-build.jsdepois de construir a pasta dist usandong build --prod

Aviko
fonte
1

Você pode controlar o cache do cliente com cabeçalhos HTTP. Isso funciona em qualquer estrutura da web.

Você pode definir as diretivas desses cabeçalhos para ter controle refinado sobre como e quando ativar | desativar o cache:

  • Cache-Control
  • Surrogate-Control
  • Expires
  • ETag (muito bom)
  • Pragma (se você deseja oferecer suporte a navegadores antigos)

Um bom armazenamento em cache é bom, mas muito complexo, em todos os sistemas de computador . Dê uma olhada em https://helmetjs.github.io/docs/nocache/#the-headers para mais informações.

ranieribt
fonte