¿Cómo evitar la caché del navegador en el sitio de Angular 2?

104

Actualmente estamos trabajando en un nuevo proyecto con actualizaciones periódicas que uno de nuestros clientes utiliza a diario. Este proyecto se está desarrollando utilizando angular 2 y estamos enfrentando problemas de caché, es decir, nuestros clientes no están viendo los últimos cambios en sus máquinas.

Principalmente, los archivos html / css para los archivos js parecen actualizarse correctamente sin causar muchos problemas.

Rikku121
fuente
2
Muy buena pregunta. Tengo el mismo problema. ¿Cuál es la mejor forma de solucionar este problema? ¿Es esto posible con gulp o cualquier herramienta similar para publicar la aplicación Angular 2?
jump4791
2
@ jump4791 La mejor manera es usar webpack y compilar el proyecto usando la configuración de producción. Actualmente estoy usando este repositorio, solo siga los pasos y debería estar bien: github.com/AngularClass/angular2-webpack-starter
Rikku121
También tengo el mismo problema.
Ziggler
3
Sé que esta es una pregunta antigua, pero quería agregar la solución que encontré, para cualquiera que pase por esto. Al construir con ng build, agregar la -prodetiqueta agrega un hash a los nombres de archivo generados. Esto obliga a recargar todo menos index.html. Esta publicación de github tenía algunas sugerencias para que se recargue.
Tiz
2
index.html es la causa principal. Debido a que no tiene código hash, cuando se almacena en caché, todo lo demás se usa desde el caché.
Fiona

Respuestas:

178

angular-cli resuelve esto proporcionando una --output-hashingbandera para el comando de compilación (versiones 6/7, para versiones posteriores, consulte aquí ). Uso de ejemplo:

ng build --output-hashing=all

Bundling & Tree-Shaking proporciona algunos detalles y contexto. Corriendo ng help build, documenta la bandera:

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

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

Aunque esto solo es aplicable a los usuarios de angular-cli , funciona de manera brillante y no requiere ningún cambio de código ni herramientas adicionales.

Actualizar

Varios comentarios han señalado útil y correctamente que esta respuesta agrega un hash a los .jsarchivos pero no hace nada index.html. Por lo tanto, es muy posible que index.htmlpermanezca en caché después de que el ng buildcaché destruya los .jsarchivos.

En este punto, me referiré a ¿Cómo controlamos el almacenamiento en caché de la página web, en todos los navegadores?

Jack
fuente
14
¡Esta es la forma correcta de hacer esto y debería ser la respuesta seleccionada!
jonesy827
1
Esto no funcionó para nuestra aplicación. Es una lástima que templateUrl con un parámetro de cadena de consulta no funciona con CLI
DDiVita
8
Esto no funcionará si el navegador almacena en caché su index.html, por lo que no verá nuevos nombres hash para sus recursos javascript. Creo que esta combinación de esto y la respuesta que dio @Rossco tendría sentido. También tiene sentido hacer esto consistente con los encabezados HTTP enviados.
stryba
2
@stryba Esta es la razón por la que el almacenamiento en caché html debe manejarse de manera diferente. Debe especificar los encabezados de respuesta Cache-Control, Pragma y Expires para que no se realice el almacenamiento en caché. Esto es fácil si está utilizando un marco de backend, pero creo que también puede manejar esto en archivos .htaccess para Apache (aunque no sé cómo funciona en nginx).
OzzyTheGiant
3
Esta respuesta agrega un hash a los archivos js, lo cual es genial. Pero como dijo stryba, también debe asegurarse de que index.html no esté almacenado en caché. No debe hacer esto con metaetiquetas html, sino con el encabezado de respuesta cache-control: no-cache (u otros encabezados para estrategias de almacenamiento en caché más sofisticadas).
Noppey
34

Encontré una manera de hacer esto, simplemente agregue una cadena de consulta para cargar sus componentes, así:

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

Esto debería obligar al cliente a cargar la copia de la plantilla del servidor en lugar de la del navegador. Si desea que se actualice solo después de un cierto período de tiempo, puede usar esta ISOString en su lugar:

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

Y subcadena algunos caracteres para que solo cambie después de una hora, por ejemplo:

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

Espero que esto ayude

Rikku121
fuente
3
Entonces mi implementación en realidad no terminó funcionando. el almacenamiento en caché es un problema extraño. a veces funciona y otras no. oh la belleza de los problemas intermitentes. Así que de hecho adapté tu respuesta como tal:templateUrl: './app/shared/menu/menu.html?v=' + Math.random()
Rossco
Obtengo 404 para mis templateUrls. Por ejemplo: GET localhost: 8080 / app.component.html /? V = 0.0.1-alpha 404 (No encontrado) ¿Alguna idea de por qué?
Shenbo
@ Rikku121 No, no lo hace. En realidad, no tiene / en la URL. Podría haberlo agregado accidentalmente cuando
publiqué
14
¿Cuál es el punto de almacenar en caché cuando está destruyendo el caché cada vez, incluso cuando no hay cambio de código?
Apurv Kamalapuri
1
ng build --aot --build-optimizer = true --base-href = / <url> / da error --- No se pudo resolver el recurso ./login.component.html?v=${new Date (). getTime ()}
Pranjal Successena
23

En cada plantilla html, solo agrego las siguientes metaetiquetas en la parte superior:

<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">

Según tengo entendido, cada plantilla es independiente, por lo que no hereda la configuración de reglas meta sin caché en el archivo index.html.

Rossco
fuente
4
Hemos cambiado al paquete web desde hace algún tiempo y se encarga de eliminar el caché de nuestras aplicaciones angulares. Sin embargo, es bueno saber que su solución funciona. Gracias
Rikku121
También lo hizo por mí
iniravpatel
4

Una combinación de la respuesta de @ Jack y la respuesta de @ ranierbit debería funcionar.

Establezca la marca ng build para --output-hashing así:

ng build --output-hashing=all

Luego agregue esta clase en un servicio o en su aplicación.

@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);    
    }
}

Luego agregue esto a sus proveedores en su módulo de aplicación:

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

Esto debería evitar problemas de almacenamiento en caché en sitios activos para máquinas cliente.

NiallMitch14
fuente
3

Tuve un problema similar con index.html siendo almacenado en caché por el navegador o más complicado por medio de cdn / proxies (F5 no lo ayudará).

Busqué una solución que verifique al 100% que el cliente tiene la última versión index.html, afortunadamente encontré esta solución de Henrik Peinar:

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

La solución también resuelve el caso en el que el cliente permanece con el navegador abierto durante días, el cliente busca actualizaciones en los intervalos y recarga si se implementa una versión más nueva.

La solución es un poco complicada pero funciona a las mil maravillas:

  • use el hecho de que ng cli -- prodproduce archivos hash con uno de ellos llamado main. [hash] .js
  • crea un archivo version.json que contenga ese hash
  • cree un servicio angular VersionCheckService que verifique version.json y vuelva a cargar si es necesario.
  • Tenga en cuenta que un script js que se ejecuta después de la implementación crea para usted version.json y reemplaza el hash en el servicio angular, por lo que no se necesita trabajo manual, pero se ejecuta post-build.js

Como la solución de Henrik Peinar era para angular 4, hubo cambios menores, coloco también los scripts fijos aquí:

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;
    }
}

cambiar al 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);
        }
    }

}

El script posterior a la compilación que hace que la magia, posterior a la compilación.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);
  });

simplemente coloque el script en la carpeta de compilación (nueva) ejecute el script usando node ./build/post-build.jsdespués de construir la carpeta dist usandong build --prod

Aviko
fuente
1

Puede controlar la caché del cliente con encabezados HTTP. Esto funciona en cualquier marco web.

Puede configurar las directivas de estos encabezados para tener un control detallado sobre cómo y cuándo habilitar | deshabilitar el caché:

  • Cache-Control
  • Surrogate-Control
  • Expires
  • ETag (muy buena)
  • Pragma (si desea admitir navegadores antiguos)

Un buen almacenamiento en caché es bueno, pero muy complejo, en todos los sistemas informáticos . Eche un vistazo a https://helmetjs.github.io/docs/nocache/#the-headers para obtener más información.

ranieribt
fuente