如何防止Angular 2网站上的浏览器缓存?


104

我们目前正在开发一个具有定期更新的新项目,我们的一位客户每天都会使用该更新。这个项目是使用angular 2开发的,我们面临着缓存问题,即我们的客户没有看到他们机器上的最新变化。

主要是js文件的html / css文件似乎已正确更新而不会带来太多麻烦。


2
很好的问题。我也有同样的问题。解决此问题的最佳方法是什么?gulp或任何类似的工具可以发布Angular 2应用程序吗?
jump4791

2
@ jump4791最好的方法是使用webpack并使用生产设置编译项目。我目前正在使用此存储库,只需按照以下步骤操作,您应该会很好:github.com/AngularClass/angular2-webpack-starter
Rikku121

我也有同样的问题。
Ziggler

3
我知道这是一个老问题,但我想为遇到此问题的任何人添加找到的解决方案。使用进行构建时ng build,添加-prod标签会将哈希添加到生成的文件名中。这迫使所有东西重新加载index.html这篇github帖子对重新加载有一些提示。
Tiz

2
index.html是根本原因。因为它没有哈希码,所以在缓存时,将从缓存中使用所有其他内容。
菲奥纳

Answers:


178

angular-cli通过为build 命令提供--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.htmlng build缓存破坏.js文件后仍然保留缓存。

至此,我将介绍如何在所有浏览器中控制网页缓存?


14
这是执行此操作的正确方法,应该是选择的答案!
jonesy827

1
这不适用于我们的应用程序。太糟糕了,带有查询字符串参数的templateUrl在CLI中不起作用
DDiVita

8
如果您的index.html被浏览器缓存了,则此方法将不起作用,因此不会看到JavaScript资源的新哈希名称。我认为将此与@Rossco给出的答案结合起来是有意义的。使它与发送的HTTP标头保持一致也很有意义。
stryba

2
@stryba这就是为什么html缓存应该区别对待的原因。您应该指定Cache-Control,Pragma和Expires响应头,以便不进行缓存。如果您使用的是后端框架,那么这很容易,但是我相信您也可以在.htaccess文件中为Apache处理此操作(不过,idk在nginx中是如何工作的)。
OzzyTheGiant

3
这个答案为js文件添加了一个哈希,这很棒。但是,正如stryba所说,您还需要确保未缓存index.html。您不应该使用html meta标记执行此操作,而应使用响应标头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

希望这可以帮助


3
所以我的实现实际上并没有奏效。缓存是一个奇怪的问题。有时有效,有时无效。断断续续的问题之美。因此,我实际上将您的答案修改为:templateUrl: './app/shared/menu/menu.html?v=' + Math.random()
Rossco '16

我的templateUrls收到404。例如:GET localhost:8080 / app.component.html /?v = 0.0.1-alpha 404(未找到)知道为什么吗?

@ Rikku121不,不是。网址中实际上没有/。我可能会在当我发布评论意外增加它
申博

14
每次即使不更改代码也要破坏高速缓存时,高速缓存有什么意义呢?
Apurv Kamalapuri

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文件中设置的meta no caching规则。


4
我们已经切换到Webpack已有一段时间了,它可以解决缓存破坏我们的角度应用程序的问题。很高兴知道您的解决方案是否有效。谢谢
Rikku121 '16

它也对我
有用

4

@Jack的答案和@ranierbit的答案应该可以解决问题。

为--output-hashing设置ng build标志,因此:

ng build --output-hashing=all

然后在服务或您的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);    
    }
}

然后将其添加到您的app.module中的提供程序中:

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

这样可以防止在客户端计算机的活动站点上缓存问题


3

我在浏览器中缓存index.html时遇到了类似的问题,或者在中间的CDN /代理服务器中比较棘手(F5不能帮助您)。

我正在寻找一种解决方案,该解决方案可100%验证客户端是否具有最新的index.html版本,幸运的是,我找到了Henrik Peinar的以下解决方案:

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

由于Henrik Peinar解决方案是针对角度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;
    }
}

更改为主AppComponent:

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

只需将脚本放置在(新)build文件夹中,然后使用以下命令node ./build/post-build.js构建dist文件夹即可运行脚本ng build --prod


1

您可以使用HTTP标头控制客户端缓存。这适用于任何Web框架。

您可以将指令设置为以下标头,以对如何以及何时启用|禁用缓存进行精细控制:

  • Cache-Control
  • Surrogate-Control
  • Expires
  • ETag (很好)
  • Pragma (如果您想支持旧的浏览器)

在所有计算机系统中,良好的缓存都是好的,但是非常复杂。请查看https://helmetjs.github.io/docs/nocache/#the-headers了解更多信息。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.