在我们创建网站的时候,经常需要利用HTTP Get的方法从服务器加载数据,例如一个在售图书列表程序,会将图书信息以JSON的形式从服务器端加载到前端Web,这是现代网站、浏览器和互联网的常见工作模式。但是这种基于HTTP Get 的模式存在一个令人头痛的问题:前端网页无法知道什么时候需要继续使用浏览器的缓存文件,什么时候需要重新下载新的版本。
以下面的Angular程序为例:
@Component({
selector: 'app-root',
standalone: true,
imports: [],
template: `
<button (click)="getData()">Get Data</button>
<p>{{message}}</p>`
})
export class AppComponent {
message : string = '';
constructor(private http: HttpClient){}
getData() : void{
const headers = new HttpHeaders({ 'Content-Type': 'application/json'});
this.http.get<any>('http://127.0.0.1/test.json', { headers: headers})
.subscribe({next : (data) => {
this.message = data.message;
}});
}
}
我们的网页程序通过http get请求从服务器端(127.0.0.1)加载了一个名为test.json的文件。在完成第一次请求后,随后再次点击Get Data按钮,我们始终会得到disk cache中的内容(注意灰色的Status 200和Size一栏的提示):
如果服务器端test.json文件的内容发生改变,用户将无法得到最新版本的文件。
对于这个恼人的情况,一种解决方案是在进行HTTP请求的时候,给网址后面加上一个版本标签,当我们的文件发生改变时,就改变这个版本标签:
getData(version : string) : void{
…
this.http.get<any>('http://127.0.0.1/test.json?ver=' + version, { headers: headers})
…
}
但是我们马上会遇到新的问题:如何通知客户端我们的文件发生了改变呢?尤其是我们有无数个*.json文件的时候,我们需要维护一个庞大的版本系统,然后在初始化时下载这个版本信息文件。
另一个解决方案,也是前端始终使用缓存内容的原因,就是我们并没有使用Cache-Control标头来指定缓存的最大有效时间,这很好理解,因为我们在大多数情况下都不清楚一个文件到底应该缓存多少时间。服务器端文件内容的改变,往往都是不可预知的。
如果我们把Cache-Control设置为no-cache(就像我们需要对版本信息文件做的那样),我们不仅会失去缓存这个提升用户体验的工具,还会增加服务器端的开销。
为了解决这个问题,我们将设计一个基于ETag的缓存服务,用来接管浏览器的缓存方案。虽然我们使用Angular进行演示,但只要按照本文的思路,你可以做出适合各种前端工具,包括适合jQuery的解决方案。
ETag是服务器根据文件的信息生成的一个字符串,每当所依赖的信息发生改变时,这个标签就会相应的发生变化。对于Apache服务器,我们需要在.htaccess中设置服务器向客户端发送这个标签:
Header set Access-Control-Expose-Headers "ETag"
如果你使用AWS的S3,需要在Permissions页面的CORS配置数组中加上对ETag的支持:
"ExposeHeaders": ["ETag"],
现在我们略微修改getData的代码,让服务器返回ETag数据:
getData() : void{
const headers = new HttpHeaders({ 'Content-Type': 'application/json'});
this.http.get<any>('http://127.0.0.1/test.json', { headers: headers, observe: 'response'})
.subscribe({next : (data) => {
console.log(data.headers.get('ETag'));
}});
}