Как предотвратить кеширование браузера на сайте Angular 2?

104

В настоящее время мы работаем над новым проектом с регулярными обновлениями, которым ежедневно пользуется один из наших клиентов. Этот проект разрабатывается с использованием angular 2, и мы сталкиваемся с проблемами кеширования, то есть наши клиенты не видят последних изменений на своих машинах.

В основном файлы html / css для файлов js, кажется, обновляются должным образом, не доставляя особых проблем.

Rikku121
источник
2
Очень хороший вопрос. У меня та же проблема. Как лучше всего решить эту проблему? Возможно ли это с помощью gulp или любого аналогичного инструмента для публикации приложения Angular 2?
jump4791
2
@ jump4791 Лучший способ - использовать webpack и скомпилировать проект с использованием производственных настроек. В настоящее время я использую это репо, просто следуйте
инструкциям,
У меня такая же проблема.
Ziggler
3
Я знаю, что это старый вопрос, но я хотел добавить решение, которое я нашел, для всех, кто сталкивается с этим. При построении с ng buildдобавлением -prodтега к сгенерированным именам файлов добавляется хеш. Это заставляет перезагрузить все, кроме index.html. В этом сообщении на github было несколько советов по перезагрузке.
Tiz
2
index.html - это основная причина. Поскольку у него нет хэш-кода, когда он кешируется, все остальное используется из кеша.
Фиона

Ответы:

179

angular-cli решает эту проблему , предоставляя --output-hashingфлаг для команды сборки (версии 6/7, более поздние версии см. здесь ). Пример использования:

ng build --output-hashing=all

Объединение и встряхивание дерева предоставляет некоторые детали и контекст. Идет ng help build, документирует флаг:

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

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

Хотя это применимо только к пользователям angular-cli , оно работает блестяще и не требует каких-либо изменений кода или дополнительных инструментов.

Обновить

В ряде комментариев полезно и правильно указано, что этот ответ добавляет хеш к .jsфайлам, но ничего не делает для них index.html. Поэтому вполне возможно, что файлы index.htmlостанутся кешированными после того, как ng buildкеш- .jsфайлы будут заблокированы.

На этом этапе я остановлюсь на том, как мы контролируем кеширование веб-страниц во всех браузерах?

разъем
источник
14
Это правильный способ сделать это, и он должен быть выбранным ответом!
jonesy827
1
Это не сработало для нашего приложения.
Жаль,
8
Это не сработает, если ваш index.html кэширован браузером, следовательно, вы не увидите новые хешированные имена для ваших ресурсов javascript. Я думаю, что это сочетание этого и ответа @Rossco имело бы смысл. Также имеет смысл согласовать это с отправляемыми заголовками HTTP.
stryba 01
2
@stryba Вот почему кеширование HTML следует обрабатывать иначе. Вы должны указать заголовки ответа Cache-Control, Pragma и Expires, чтобы не было кэширования. Это легко, если вы используете бэкэнд-фреймворк, но я считаю, что вы также можете справиться с этим в файлах .htaccess для Apache (хотя я знаю, как это работает в nginx).
OzzyTheGiant
3
Этот ответ добавляет хеш к файлам js, и это здорово. Но, как сказал Стрыба, вам также необходимо убедиться, что index.html не кеширован. Вы должны делать это не с метатегами html, а с заголовком ответа cache-control: no-cache (или другими заголовками для более сложных стратегий кэширования).
Noppey
34

Нашел способ сделать это, просто добавьте строку запроса для загрузки ваших компонентов, например:

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

Это должно заставить клиента загрузить серверную копию шаблона, а не браузер. Если вы хотите, чтобы он обновлялся только через определенный период времени, вы можете использовать вместо этого ISOString:

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

И подставьте несколько символов, чтобы они изменились только через час, например:

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

Надеюсь это поможет

Rikku121
источник
3
Так что моя реализация на самом деле не сработала. кеширование - странная проблема. иногда работает, а иногда нет. о красоте прерывистых проблем. Так что я фактически адаптировал ваш ответ как таковой:templateUrl: './app/shared/menu/menu.html?v=' + Math.random()
Rossco
Я получаю 404 для моего templateUrls. Например: GET localhost: 8080 / app.component.html /? V = 0.0.1-alpha 404 (не найдено) Есть идеи, почему?
Shenbo
@ Rikku121 Нет, это не так. На самом деле это без / в URL-адресе. Я мог бы случайно добавил его в том, когда я отправляю комментарий
Shenbo
15
В чем смысл кеширования, если вы каждый раз очищаете кеш, даже если код не меняется?
Апурв Камалапури
1
ng build --aot --build-optimizer = true --base-href = / <url> / выдает ошибку --- Не удалось разрешить ресурс ./login.component.html?v=${new Date (). GetTime ()}
Pranjal Successena
23

В каждом шаблоне html я просто добавляю следующие метатеги вверху:

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

Насколько я понимаю, каждый шаблон является автономным, поэтому он не наследует мета-правила кеширования, установленные в файле index.html.

Росско
источник
4
Мы уже некоторое время перешли на webpack, и он позаботится о том, чтобы наши приложения angular перебирали кеш. Однако хорошо знать, что ваше решение работает. Спасибо
Rikku121 01
Для меня это тоже
подействовало
4

Комбинация ответа @Jack и ответа @ ranierbit должна помочь.

Установите флаг сборки ng для --output-hashing так:

ng build --output-hashing=all

Затем добавьте этот класс в службу или в приложение.

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

Затем добавьте это к своим поставщикам в app.module:

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

Это должно предотвратить проблемы с кешированием на живых сайтах для клиентских машин.

НайлМитч14
источник
3

У меня была аналогичная проблема с кешированием index.html браузером или с более сложными средними cdn / прокси (F5 вам не поможет).

Я искал решение, которое на 100% проверяет наличие у клиента последней версии index.html, к счастью, я нашел это решение от Хенрика Пейнара:

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

Решение также решает случай, когда клиент остается с открытым браузером в течение нескольких дней, клиент проверяет наличие обновлений через определенные промежутки времени и перезагружается, если развернута более новая версия.

Решение немного сложное, но работает как шарм:

  • используйте тот факт, что ng cli -- prodсоздает хешированные файлы, один из которых называется main. [hash] .js
  • создайте файл version.json, содержащий этот хеш
  • создайте угловую службу VersionCheckService, которая проверяет version.json и при необходимости перезагружает.
  • Обратите внимание, что js-скрипт, запускаемый после развертывания, создает для вас как version.json, так и заменяет хэш в службе angular, поэтому ручная работа не требуется, но выполняется post-build.js

Поскольку решение Хенрика Пейнара было для angular 4, были внесены незначительные изменения, я также помещаю сюда исправленные скрипты:

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

изменить на основной компонент приложения:

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

}

Скрипт после сборки, который делает волшебство, 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);
  });

просто поместите скрипт в (новую) папку сборки, запустите скрипт, используя node ./build/post-build.jsпосле создания папки dist, используяng build --prod

Aviko
источник
1

Вы можете управлять кешем клиента с помощью заголовков HTTP. Это работает в любой веб-платформе.

Вы можете установить директивы этих заголовков, чтобы иметь точный контроль над тем, как и когда включать | отключать кеш:

  • Cache-Control
  • Surrogate-Control
  • Expires
  • ETag (очень хороший)
  • Pragma (если вы хотите поддерживать старые браузеры)

Хорошее кэширование - это хорошо, но очень сложно во всех компьютерных системах . Взгляните на https://helmetjs.github.io/docs/nocache/#the-headers для получения дополнительной информации.

Ranieribt
источник