如何防止 Angular 2 站点上的浏览器缓存?
How to prevent Browser cache on Angular 2 site?
我们目前正在开发一个新项目,我们的一位客户每天都在使用该项目进行定期更新。该项目正在使用 angular 2 开发,我们面临缓存问题,即我们的客户没有在他们的机器上看到最新的更改。
主要是 js 文件的 html/css 文件似乎可以正确更新,不会造成太多麻烦。
找到了一种方法,只需添加一个查询字符串来加载您的组件,如下所示:
@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
希望对您有所帮助
在每个 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 文件中的无缓存规则设置。
angular-cli resolves this by providing an --output-hashing
flag for the build command (versions 6/7, for later versions see here)。用法示例:
ng build --output-hashing=all
Bundling & Tree-Shaking 提供了一些细节和上下文。 运行 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
文件后仍然被缓存。
在这一点上我会推迟 How do we control web page caching, across all browsers?
我有类似的问题 index.html 被浏览器缓存或更棘手的中间 cdn/proxies (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 文件
- 创建一个 angular 服务 VersionCheckService 检查 version.json 并在需要时重新加载。
- 请注意,部署后的 js 脚本 运行ning 会为您创建 version.json 并替换 angular 服务中的哈希值,因此无需手动操作,但 运行宁post-build.js
因为 Henrik Peinar 的解决方案是针对 angular 4 的,所以有一些小的改动,我也把固定的脚本放在这里:
版本检查服务:
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 脚本,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 文件夹 运行 使用 ng build --prod
[=] 构建 dist 文件夹后使用 node ./build/post-build.js
的脚本
您可以使用 HTTP headers 控制客户端缓存。这适用于任何网络框架。
您可以设置这些指令 headers 以精细控制如何以及何时启用|禁用缓存:
Cache-Control
Surrogate-Control
Expires
ETag
(很好)
Pragma
(如果你想支持旧浏览器)
好的缓存很好,但是很复杂,在所有的计算机系统中。查看 https://helmetjs.github.io/docs/nocache/#the-headers 了解更多信息。
结合@Jack 的回答和@ranierbit 的回答应该可以解决问题。
为 --output-hashing 设置 ng build 标志:
ng build --output-hashing=all
然后将此 class 添加到服务或您的 app.module
@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
]
这应该可以防止客户端计算机在实时站点上出现缓存问题
将此添加到您的 nginx
location ~ /index.html|.*\.json$ {
expires -1;
add_header Cache-Control 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0';
}
location ~ .*\.css$|.*\.js$ {
add_header Cache-Control 'max-age=31449600'; # one year
}
location / {
try_files $uri $uri/ /index.html?$args;
add_header Cache-Control 'max-age=86400'; # one day
}
当您使用 ng build 构建应用程序时,您应该使用以下标志:
--outputHashing=all
这是为了启用缓存清除。
Cache-busting 通过使用唯一的文件版本标识符告诉浏览器该文件的新版本可用,从而解决了浏览器缓存问题。因此浏览器不会从缓存中检索旧文件,而是向源服务器请求新文件。
因此,一种方法是 运行 这个:
示例(旧版本)
ng build --prod --aot --output-hashing=all
以下是您可以传入的选项--output-hashing
none: 没有执行散列
媒体:仅向通过 [url|file]-loaders 处理的文件添加哈希
bundles:仅将哈希添加到输出包
all:向媒体和捆绑包添加哈希
更新
对于 angular 的较新版本(例如 Angular 10),命令现已更新:
ng build --prod --aot --outputHashing=all
您可以在此处阅读有关构建选项标志的更多信息。 https://angular.io/cli/build
如果您不希望在 运行 ng 构建时附加标志,您应该在 angular.json 文件的配置中设置它们。
"configurations": {
"production": {
"optimization": true,
"outputHashing": "all",
.
.
.
}
}
我们目前正在开发一个新项目,我们的一位客户每天都在使用该项目进行定期更新。该项目正在使用 angular 2 开发,我们面临缓存问题,即我们的客户没有在他们的机器上看到最新的更改。
主要是 js 文件的 html/css 文件似乎可以正确更新,不会造成太多麻烦。
找到了一种方法,只需添加一个查询字符串来加载您的组件,如下所示:
@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
希望对您有所帮助
在每个 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 文件中的无缓存规则设置。
angular-cli resolves this by providing an --output-hashing
flag for the build command (versions 6/7, for later versions see here)。用法示例:
ng build --output-hashing=all
Bundling & Tree-Shaking 提供了一些细节和上下文。 运行 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
文件后仍然被缓存。
在这一点上我会推迟 How do we control web page caching, across all browsers?
我有类似的问题 index.html 被浏览器缓存或更棘手的中间 cdn/proxies (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 文件
- 创建一个 angular 服务 VersionCheckService 检查 version.json 并在需要时重新加载。
- 请注意,部署后的 js 脚本 运行ning 会为您创建 version.json 并替换 angular 服务中的哈希值,因此无需手动操作,但 运行宁post-build.js
因为 Henrik Peinar 的解决方案是针对 angular 4 的,所以有一些小的改动,我也把固定的脚本放在这里:
版本检查服务:
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 脚本,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 文件夹 运行 使用 ng build --prod
[=] 构建 dist 文件夹后使用 node ./build/post-build.js
的脚本
您可以使用 HTTP headers 控制客户端缓存。这适用于任何网络框架。
您可以设置这些指令 headers 以精细控制如何以及何时启用|禁用缓存:
Cache-Control
Surrogate-Control
Expires
ETag
(很好)Pragma
(如果你想支持旧浏览器)
好的缓存很好,但是很复杂,在所有的计算机系统中。查看 https://helmetjs.github.io/docs/nocache/#the-headers 了解更多信息。
结合@Jack 的回答和@ranierbit 的回答应该可以解决问题。
为 --output-hashing 设置 ng build 标志:
ng build --output-hashing=all
然后将此 class 添加到服务或您的 app.module
@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
]
这应该可以防止客户端计算机在实时站点上出现缓存问题
将此添加到您的 nginx
location ~ /index.html|.*\.json$ {
expires -1;
add_header Cache-Control 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0';
}
location ~ .*\.css$|.*\.js$ {
add_header Cache-Control 'max-age=31449600'; # one year
}
location / {
try_files $uri $uri/ /index.html?$args;
add_header Cache-Control 'max-age=86400'; # one day
}
当您使用 ng build 构建应用程序时,您应该使用以下标志:
--outputHashing=all
这是为了启用缓存清除。
Cache-busting 通过使用唯一的文件版本标识符告诉浏览器该文件的新版本可用,从而解决了浏览器缓存问题。因此浏览器不会从缓存中检索旧文件,而是向源服务器请求新文件。
因此,一种方法是 运行 这个:
示例(旧版本)
ng build --prod --aot --output-hashing=all
以下是您可以传入的选项--output-hashing
none: 没有执行散列 媒体:仅向通过 [url|file]-loaders 处理的文件添加哈希 bundles:仅将哈希添加到输出包 all:向媒体和捆绑包添加哈希 更新
对于 angular 的较新版本(例如 Angular 10),命令现已更新:
ng build --prod --aot --outputHashing=all
您可以在此处阅读有关构建选项标志的更多信息。 https://angular.io/cli/build
如果您不希望在 运行 ng 构建时附加标志,您应该在 angular.json 文件的配置中设置它们。
"configurations": {
"production": {
"optimization": true,
"outputHashing": "all",
.
.
.
}
}