DistChen

浏览器缓存与访问方式

之前在做项目的过程中,有一件很痛苦的事情就是每次前端修改文件后(静态资源),必须得清空下浏览器缓存才能生效,所以就看了下浏览器缓存相关的知识点。

http 协议中有这样几个 header:Last-Modified、ETag、Cache-Control、Expires 、If-None-Match 、If-Modified-Since等。访问一个网页可以在地址栏回车,可以F5刷新,Ctrl+F5强制刷新、浏览器的刷新按钮等多种方式。让人想不到的是:不同的访问方式对于缓存的使用有很大区别。

这里就不介绍这些header是干什么用的了,网上资料很多,请自行查找。这里总结下几个容易忽略的点:

  • Last-Modified 、ETag 是设置在response里面的,两者可以一起用,也可以只用其中一个;
  • If-Modified-Since 、If-None-Match 是设置在request中的,由浏览器从资源的上次响应中提取Last-Modified 、ETag值(如果有,没有的话,服务器提取到的值是-1和null),然后发给服务器;
  • Last-Modified只能精确到秒级。当在服务端设置响应的Last-Modified值时,即使设置的值精确到了毫秒并返回给浏览器。但是浏览器下次请求时携带的If-Modified-Since值也会丢失毫秒值,因此如果用Last-Modified来校验文件是否变化,一般会在其基础上加上1000L来进行比较;
  • 不管Cache-Control是什么值,浏览器都会缓存资源。很多人以为Cache-Control设置了 no-cache 之类的值后,浏览器就不会缓存资源了。其实不然,不管该值为什么,浏览器都会缓存下来。

不同的访问方式对于缓存的处理以及请求的构造是不同的:

Ctrl+F5 强制刷新

浏览器请求时,不管有没有缓存,浏览器都会重新发起一次http请求,并且不会携带If-None-Match 、If-Modified-Since值。服务器收到这次请求后,因为没有相应的If-None-Match 、If-Modified-Since值,无法验证资源的状态,所以会重新返回内容,状态设为200。浏览器收到响应后,再重新缓存。
这里写图片描述

F5 刷新、刷新按钮

这种请求方式下,与上一个类似:不管有没有缓存,浏览器都会重新发起一次http请求。不同的是,如果资源之前有缓存过,就会设置相应的If-None-Match 、If-Modified-Since值(如果有)到 request中,服务器根据这些值与文件的状态来确定返回的状态码:200还是304。如果文件没有修改过,就返回304,并且不传输任何内容;如果文件修改过,就返回200,并将新的内容返回。不管返回什么状态,缓存文件都会持续更新,比如返回304的时候,Cache-Control 的值又设置了一个新的值,那么原有的缓存文件也会更新此值。
这里写图片描述

地址栏回车、前进、后退、新窗口、新标签页等

这种方式下,如果Cache-Control或者Expires 设置的值还有效,那么浏览器不会发起http请求,而是直接从缓存中取对应的内容(此时服务器打断点也进不去可以验证);如果Cache-Control或者Expires无效了,那么浏览器就会重新发起http请求并携带If-Modified-Since 、If-None-Match头(如果有),这时就与上面的处理过程类似了。
这里写图片描述

从上面可以看出,Last-Modified、ETag、Cache-Control、Expires、If-None-Match 、If-Modified-Since 这些header是没有任何冲突的,使用场景也划分的很清晰。Last-Modified、ETag设置在响应里面,用于浏览器在 request 的时候设置这些值到 If-Modified-Since、If-None-Match。Cache-Control、Expires 的值是在用地址栏回车等访问方式的情况下,浏览器用来判断是否发起http请求的依据,并不是用来决定是否进行缓存的依据。如果有效,就不发起请求;如果缓存过期或者no-cache等无效状态,则发起请求。

前面说过不管Cache-Control是什么值,浏览器都会缓存资源,怎样验证?假设第一次请求某个资源时,response 设置 Cache-Control 为 no-cache,并设置了ETag 或 Last-Modified;在资源不修改的情况下,第二次继续访问,通过携带的 If-Modified-Since 、If-None-Match ,服务器会返回304,而且可以不返回任何内容。如果第一次不缓存的话,这里又不返回任何内容,那么我们肯定无法显示资源,而实际上,第二次不返回任何内容,也是可以正常显示资源的,所以可以验证不管Cache-Control是什么值,浏览器都会缓存资源。

上面的这些,我是写了一个Filter 来进行验证的,Filter 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
public class GZIPFilterByETag implements Filter {
private ServletContext servletContext;
public void init(FilterConfig filterConfig) throws ServletException {
this.servletContext = filterConfig.getServletContext();
}
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)servletRequest;
HttpServletResponse response = (HttpServletResponse)servletResponse;
String url = request.getRequestURI();
String filePath = url.substring(request.getContextPath().length());
String lastETag = request.getHeader("If-None-Match");
String currentETag = getFileETage(filePath);
response.setHeader("Cache-Control", "max-age=10");//no-cache
response.setHeader("ETag", currentETag);
if(currentETag.equals(lastETag)){
response.setStatus(304);
} else {
ServletOutputStream servletOutputStream = response.getOutputStream();
servletOutputStream.write(getFileContent(filePath));
servletOutputStream.flush();
servletOutputStream.close();
}
}
public String getFileETage(String filePath) {
String value = null;
try {
File file = new File(this.servletContext.getRealPath("/")+File.separator+filePath);
FileInputStream in = new FileInputStream(file);
MappedByteBuffer byteBuffer = in.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, file.length());
MessageDigest md5 = MessageDigest.getInstance("MD5");
md5.update(byteBuffer);
BigInteger bi = new BigInteger(1, md5.digest());
value = bi.toString(16);
in.close();
} catch (Exception e) {
e.printStackTrace();
}
return value;
}
private byte[] getFileContent(String filePath) throws IOException{
InputStream fileInputStream = this.servletContext.getResourceAsStream(filePath);
ByteArrayOutputStream bytes = new ByteArrayOutputStream(1048576);
int length = 0;
byte[] buf = new byte[65536];
while (length >= 0) {
length = fileInputStream.read(buf);
if (length > 0) {
bytes.write(buf, 0, length);
}
}
fileInputStream.close();
return bytes.toByteArray();
}
public void destroy() {}
}

web.xml 配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
<filter>
<filter-name>GZipFilter</filter-name>
<filter-class>distchen.GZIPFilterByETag</filter-class>
</filter>
<filter-mapping>
<filter-name>GZipFilter</filter-name>
<url-pattern>*.js</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>GZipFilter</filter-name>
<url-pattern>*.css</url-pattern>
</filter-mapping>

需要注意的是,上面说的这些行为是针对浏览器发起的请求,不针对非浏览器发起的请求(比如 ajax 请求等)。

坚持原创技术分享,您的支持将鼓励我继续创作!