Jak zablokować pamięć podręczną przeglądarki w witrynie Angular 2?


104

Obecnie pracujemy nad nowym projektem z regularnymi aktualizacjami, z którego codziennie korzysta jeden z naszych klientów. Ten projekt jest rozwijany przy użyciu angular 2 i mamy problemy z pamięcią podręczną, czyli nasi klienci nie widzą najnowszych zmian na swoich maszynach.

Wydaje się, że głównie pliki html / css dla plików js są aktualizowane prawidłowo, bez większych problemów.


2
Bardzo dobre pytanie. Mam ten sam problem. Jaki jest najlepszy sposób rozwiązania tego problemu? Czy jest to możliwe za pomocą gulp lub innego podobnego narzędzia do publikowania aplikacji Angular 2?
— jump4791

2
@ jump4791 Najlepszym sposobem jest użycie pakietu webpack i skompilowanie projektu przy użyciu ustawień produkcyjnych. Obecnie używam tego repozytorium, po prostu postępuj zgodnie z instrukcjami i powinno być dobrze: github.com/AngularClass/angular2-webpack-starter
— Rikku121

Mam też ten sam problem.
— Ziggler

3
Wiem, że to stare pytanie, ale chciałem dodać rozwiązanie, które znalazłem, dla każdego, kto się z tym zdarzy. Podczas budowania z ng build, dodanie -prodtagu dodaje hash do wygenerowanych nazw plików. Wymusza to ponowne załadowanie wszystkiego oprócz index.html. Ten post na githubie zawierał kilka wskazówek, jak go przeładować.
— Tiz

2
index.html jest główną przyczyną. Ponieważ nie ma kodu skrótu, gdy jest buforowany, wszystko inne jest używane z pamięci podręcznej.
— Fiona

Odpowiedzi:


179

angular-cli rozwiązuje ten problem, udostępniając --output-hashingflagę dla polecenia kompilacji (wersje 6/7, późniejsze wersje patrz tutaj ). Przykładowe użycie:

ng build --output-hashing=all

Wiązanie i potrząsanie drzewem zawiera pewne szczegóły i kontekst. Running ng help build, dokumentuje flagę:

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

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

Chociaż ma to zastosowanie tylko do użytkowników angular-cli , działa znakomicie i nie wymaga żadnych zmian w kodzie ani dodatkowych narzędzi.

Aktualizacja

Wiele komentarzy było pomocnych i słusznych wskazań, że ta odpowiedź dodaje skrót do .jsplików, ale nic nie robi index.html. Jest zatem całkowicie możliwe, że pliki index.htmlpozostaną w pamięci podręcznej po ng buildzniszczeniu pamięci podręcznej .js.

W tym momencie odłożę się do pytania Jak kontrolować buforowanie stron internetowych we wszystkich przeglądarkach?


14
To jest właściwy sposób na zrobienie tego i powinna to być wybrana odpowiedź!
— jonesy827

1
To nie zadziałało w przypadku naszej aplikacji. Szkoda, że ​​templateUrl z parametrem ciągu zapytania nie działa z CLI
— DDiVita

8
To nie zadziała, jeśli Twój plik index.html jest buforowany przez przeglądarkę, dlatego nie zobaczysz nowych zaszyfrowanych nazw zasobów javascript. Myślę, że to połączenie tego i odpowiedzi, której udzielił @Rossco, miałoby sens. Sensowne jest również zapewnienie spójności z wysłanymi nagłówkami HTTP.
— stryba

2
@stryba Dlatego buforowanie html powinno być obsługiwane inaczej. Należy określić nagłówki odpowiedzi Cache-Control, Pragma i Expires, aby nie było buforowania. Jest to łatwe, jeśli używasz frameworka zaplecza, ale wydaje mi się, że możesz sobie z tym poradzić również w plikach .htaccess dla Apache (chociaż nie wiem, jak to działa w nginx).
— OzzyTheGiant

3
Ta odpowiedź dodaje skrót do plików js, co jest świetne. Ale jak powiedział Stryba, musisz również upewnić się, że index.html nie jest buforowany. Nie powinieneś tego robić z metatagami html, ale z kontrolą pamięci podręcznej nagłówka odpowiedzi: no-cache (lub innymi nagłówkami dla bardziej wymyślnych strategii buforowania).
— Noppey

34

Znalazłem sposób, aby to zrobić, po prostu dodaj querystring, aby załadować komponenty, na przykład:

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

Powinno to zmusić klienta do załadowania kopii szablonu z serwera, a nie przeglądarki. Jeśli chcesz, aby odświeżał się dopiero po pewnym czasie, możesz zamiast tego użyć tego ISOString:

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

I podciąg niektóre znaki, aby zmienił się dopiero po godzinie, na przykład:

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

Mam nadzieję że to pomoże


3
Więc moja implementacja właściwie nie zadziałała. buforowanie to dziwny problem. czasami działa, a czasami nie. och, piękno sporadycznych problemów. Więc właściwie dostosowałem twoją odpowiedź jako taką:templateUrl: './app/shared/menu/menu.html?v=' + Math.random()
— Rossco

Otrzymuję 404 dla moich templateUrls. Na przykład: GET localhost: 8080 / app.component.html /? V = 0.0.1-alpha 404 (nie znaleziono) Masz jakiś pomysł, dlaczego?
— Shenbo

@ Rikku121 Nie, nie ma. W rzeczywistości nie ma / w adresie URL. Mogłem przypadkowo dodać to w komentarzu
— Shenbo

15
Jaki jest sens buforowania, gdy za każdym razem usuwasz pamięć podręczną, nawet jeśli nie ma zmiany kodu?
— Apurv Kamalapuri

1
ng build --aot --build-optimizer = true --base-href = / <url> / daje błąd --- Nie można rozwiązać zasobu ./login.component.html?v=${new Date (). getTime ()}
— Pranjal Successena

23

W każdym szablonie HTML po prostu dodaję następujące metatagi u góry:

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

W moim rozumieniu każdy szablon jest wolnostojący, dlatego nie dziedziczy konfiguracji meta bez buforowania w pliku index.html.


4
Od jakiegoś czasu przeszliśmy na pakiet webpack i dba on o niszczenie pamięci podręcznej naszych aplikacji kątowych. Dobrze jest jednak wiedzieć, że Twoje rozwiązanie działa. Dzięki
— Rikku121

Dla mnie to też
— zrobiło

4

Połączenie odpowiedzi @ Jacka i odpowiedzi @ ranierbit powinno załatwić sprawę.

Ustaw flagę kompilacji ng dla --output-hashing, więc:

ng build --output-hashing=all

Następnie dodaj tę klasę w usłudze lub w pliku 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);    
    }
}

Następnie dodaj to do swoich dostawców w swoim module app.module:

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

Powinno to zapobiec problemom z buforowaniem w aktywnych witrynach dla komputerów klienckich


3

Miałem podobny problem z cache'owaniem pliku index.html przez przeglądarkę lub bardziej skomplikowanym przez środkowe cdn / proxy (F5 ci nie pomoże).

Szukałem rozwiązania, które w 100% weryfikuje, czy klient posiada najnowszą wersję index.html, na szczęście znalazłem takie rozwiązanie autorstwa Henrika Peinara:

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

Rozwiązanie rozwiązuje również przypadek, gdy klient pozostaje z otwartą przeglądarką przez kilka dni, klient sprawdza aktualizacje w określonych odstępach czasu i przeładowuje, jeśli zainstalowano nowszą wersję.

Rozwiązanie jest nieco skomplikowane, ale działa jak urok:

  • użyj faktu, że ng cli -- prodtworzy zhaszowane pliki z jednym z nich o nazwie main. [hash] .js
  • utwórz plik version.json zawierający ten skrót
  • utwórz usługę kątową VersionCheckService, która sprawdza version.json i w razie potrzeby ładuje ponownie.
  • Zwróć uwagę, że skrypt js uruchomiony po wdrożeniu tworzy dla Ciebie zarówno version.json, jak i zastępuje skrót w usłudze kątowej, więc nie jest wymagana żadna praca ręczna, ale uruchamianie po kompilacji.js

Ponieważ rozwiązanie Henrika Peinara było dla kątowej 4, nastąpiły drobne zmiany, tutaj umieszczam również poprawione skrypty:

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

zmień na główny komponent aplikacji:

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

}

Skrypt post-build, który tworzy magię, 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);
  });

po prostu umieść skrypt w (nowym) folderze budowania, uruchom skrypt używając node ./build/post-build.jspo zbudowaniu folderu dist za pomocąng build --prod


1

Możesz kontrolować pamięć podręczną klienta za pomocą nagłówków HTTP. Działa to w każdym frameworku internetowym.

Możesz ustawić dyrektywy w tych nagłówkach, aby mieć szczegółową kontrolę nad tym, jak i kiedy włączyć | wyłączyć pamięć podręczną:

  • Cache-Control
  • Surrogate-Control
  • Expires
  • ETag (bardzo dobry)
  • Pragma (jeśli chcesz obsługiwać stare przeglądarki)

Dobre buforowanie jest dobre, ale bardzo złożone we wszystkich systemach komputerowych . Zajrzyj na https://helmetjs.github.io/docs/nocache/#the-headers, aby uzyskać więcej informacji.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.