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.

Rikku121
źródło
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?

Jacek
źródło
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

Rikku121
źródło
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.

Rossco
źródło
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

NiallMitch14
źródło
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

Aviko
źródło
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.

ranieribt
źródło