1、http缓存
浏览器第一次向一个web服务器发起http
请求后,服务器会返回请求的资源,并且在响应头中添加一些有关缓存的字段如:Cache-Control
、Expires
、Last-Modified
、ETag
、Date
等等。之后浏览器再向该服务器请求该资源就可以视情况使用强缓存和协商缓存。
- 强缓存:浏览器直接从本地缓存中获取数据,不与服务器进行交互。
- 协商缓存:浏览器发送请求到服务器,服务器判定是否可使用本地缓存。
- 联系与区别:两种缓存方式最终使用的都是本地缓存;前者无需与服务器交互,后者需要。
下面假定浏览器已经访问了服务器,服务器返回了缓存相关的头部字段且浏览器已对相关资源做好缓存。通过下图来分析强缓存和协商缓存:
1.1、强缓存
强缓存由两个http响应头部字段控制,Expires
和Cache-Control
,其中Cache-Control
的优先级比Expires
高。
一、Cache-Control:
-
max-age
(单位为s)指定设置缓存最大的有效时间,定义的是时间长短。当浏览器向服务器发送请求后,在max-age这段时间里浏览器就不会再向服务器发送请求了。-
max-age>0
表示在设置时间内请求直接从浏览器缓存中读取,使用强缓存 -
max-age<=0
表示请求到服务器,服务器需要判断文件是否已更新,进而返回200还是304
-
-
no-cache
:设置了no-cache之后并不代表浏览器不缓存,而是在缓存前要向服务器确认资源是否被更改。-
Cache-Control: no-cache, max-age=2000
表示在2000秒内使用强缓存,超过2000秒使用协商缓存
-
-
no-store
:禁用缓存。 -
public
:表明其他用户也可使用缓存,适用于公共缓存服务器的情况。如果没有指定public还是private,则默认为public。
-
private
:表明只有特定用户才能使用缓存,适用于公共缓存服务器的情况。
-
s-maxage
:适用于多用户使用的公共缓存服务器,比如CDN。比如,当s-maxage=60时,在这60秒中,即使更新了CDN的内容,浏览器也不会进行请求。也就是说max-age用于普通缓存,而s-maxage用于代理缓存。如果存在s-maxage,则会覆盖掉max-age和Expires header。
二、Expires:
缓存过期时间,用来指定资源到期的时间,是服务器端的具体的时间点。也就是说,Expires=max-age + 请求时间,需要和Last-modified结合使用。但在上面我们提到过,cache-control的优先级更高。 Expires是Web服务器响应消息头字段,在响应http请求时告诉浏览器在过期时间前浏览器可以直接从浏览器缓存取数据,而无需再次请求。
1.2、协商缓存
当浏览器发现缓存过期后,缓存并不一定不能使用了,因为服务器端的资源可能仍然没有改变,所以需要与服务器协商,让服务器判断本地缓存是否还能使用。
当第一次请求响应头中有ETag
或Last-Modified
字段,那么第二次请求的请求头中就会携带If-None-Match
和If-Modified-Since
字段,服务器收到请求后会判断ETag
与If-None-Match
以及Last-Modified
与If-Modified-Since
是否一致,如果一致就表示请求资源没有被修改,服务器返回304状态码,使用浏览器缓存资源。如果不一致,则服务器处理请求,返回新资源,状态码为200。
一、ETag
和If-None-Match
二者的值都是服务器为每份资源分配的唯一标识字符串,相当于hash。
- 浏览器请求资源,服务器会在响应报文头中加入
ETag
字段。资源更新时,服务器端的ETag
值也随之更新; - 浏览器再次请求资源时,会在请求报文头中添加
If-None-Match
字段,它的值就是上次响应报文中的ETag
的值; - 服务器会比对
ETag
与If-None-Match
的值是否一致,如果不一致,服务器则接受请求,返回更新后的资源;如果一致,表明资源未更新,则返回状态码为304
的响应,可继续使用本地缓存,要注意的是,此时响应头会加上ETag
字段,即使它没有变化。
二、Last-Modified
和If-Modified-Since
二者的值都是GMT格式的时间字符串。
- 浏览器第一次向服务器请求资源后,服务器会在响应头中加上
Last-Modified
字段,表明该资源最后一次的修改时间; - 浏览器再次请求该资源时,会在请求报文头中添加
If-Modified-Since
字段,它的值就是上次服务器响应报文中的Last-Modified
的值; - 服务器会比对
Last-Modified
与If-Modified-Since
的值是否一致,如果不一致,服务器则接受请求,返回更新后的资源;如果一致,表明资源未更新,则返回状态码为304
的响应,可继续使用本地缓存,与ETag
不同的是:此时响应头中不会再添加Last-Modified
字段。
三、ETag
较之Last-Modified
的优势
以下内容引用于:http协商缓存VS强缓存
你可能会觉得使用Last-Modified
已经足以让浏览器知道本地的缓存副本是否足够新,为什么还需要ETag
呢?HTTP1.1
中ETag
的出现主要是为了解决几个Last-Modified
比较难解决的问题:
- 一些文件也许会周期性的更改,但是他的内容并不改变(仅仅改变的修改时间),这个时候我们并不希望客户端认为这个文件被修改了,而重新
GET
; - 某些文件修改非常频繁,比如在秒以下的时间内进行修改,(比方说1s内修改了N次),
If-Modified-Since
能检查到的粒度是s级的,这种修改无法判断(或者说UNIX
记录MTIME
只能精确到秒); - 某些服务器不能精确的得到文件的最后修改时间。
这时,利用ETag
能够更加准确的控制缓存,因为ETag
是服务器自动生成的资源在服务器端的唯一标识符,资源每次变动,都会生成新的ETag
值。Last-Modified
与ETag
是可以一起使用的,但服务器会优先验证ETag
。
2、tomcat服务的静态资源缓存机制
先举一个例子,先在linux服务器上安装tomcat,然后上传一个文件到服务器导航,向服务器请求这个静态资源
我们并没有配置响应头ETag
和Last-Modified
,为什么会进行协商缓存呢?我们来查看一下tomcat
源码如何处理http
缓存的,在servlet-api.jar
包中有一个HttpServlet.class
字节码文件,我们用idea
打开可以看到反编译后的源码。
HttpServlet的功能
HttpServlet
首先必须读取Http
请求的内容。Servlet
容器负责创建HttpServlet
对象,并把Http
请求直接封装到HttpServlet
对象中,大大简化了HttpServlet
解析请求数据的工作量。HttpServlet
容器响应Web
客户请求流程如下:
-
Web
客户向Servlet
容器发出Http
请求; -
Servlet
容器解析Web
客户的Http
请求; -
Servlet
容器创建一个HttpRequest
对象,在这个对象中封装Http
请求信息; -
Servlet
容器创建一个HttpResponse
对象; -
Servlet
容器调用HttpServlet
的service
方法,把HttpRequest
和HttpResponse
对象作为service
方法的参数传给HttpServlet
对象; -
HttpServlet
调用HttpRequest
的有关方法,获取HTTP
请求信息; -
HttpServlet
调用HttpResponse
的有关方法,生成响应数据; -
Servlet
容器把HttpServlet
的响应结果传给Web
客户。
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package javax.servlet.http;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.text.MessageFormat;
import java.util.Enumeration;
import java.util.ResourceBundle;
import javax.servlet.DispatcherType;
import javax.servlet.GenericServlet;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
public abstract class HttpServlet extends GenericServlet {
private static final long serialVersionUID = 1L;
private static final String METHOD_DELETE = "DELETE";
private static final String METHOD_HEAD = "HEAD";
private static final String METHOD_GET = "GET";
private static final String METHOD_OPTIONS = "OPTIONS";
private static final String METHOD_POST = "POST";
private static final String METHOD_PUT = "PUT";
private static final String METHOD_TRACE = "TRACE";
private static final String HEADER_IFMODSINCE = "If-Modified-Since";
private static final String HEADER_LASTMOD = "Last-Modified";
private static final String LSTRING_FILE = "javax.servlet.http.LocalStrings";
private static final ResourceBundle lStrings = ResourceBundle.getBundle("javax.servlet.http.LocalStrings");
public HttpServlet() {
}
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String protocol = req.getProtocol();
String msg = lStrings.getString("http.method_get_not_supported");
if (protocol.endsWith("1.1")) {
resp.sendError(405, msg);
} else {
resp.sendError(400, msg);
}
}
protected long getLastModified(HttpServletRequest req) {
return -1L;
}
protected void doHead(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
if (DispatcherType.INCLUDE.equals(req.getDispatcherType())) {
this.doGet(req, resp);
} else {
NoBodyResponse response = new NoBodyResponse(resp);
this.doGet(req, response);
response.setContentLength();
}
}
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String protocol = req.getProtocol();
String msg = lStrings.getString("http.method_post_not_supported");
if (protocol.endsWith("1.1")) {
resp.sendError(405, msg);
} else {
resp.sendError(400, msg);
}
}
protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String protocol = req.getProtocol();
String msg = lStrings.getString("http.method_put_not_supported");
if (protocol.endsWith("1.1")) {
resp.sendError(405, msg);
} else {
resp.sendError(400, msg);
}
}
protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String protocol = req.getProtocol();
String msg = lStrings.getString("http.method_delete_not_supported");
if (protocol.endsWith("1.1")) {
resp.sendError(405, msg);
} else {
resp.sendError(400, msg);
}
}
private static Method[] getAllDeclaredMethods(Class> c) {
if (c.equals(HttpServlet.class)) {
return null;
} else {
Method[] parentMethods = getAllDeclaredMethods(c.getSuperclass());
Method[] thisMethods = c.getDeclaredMethods();
if (parentMethods != null && parentMethods.length > 0) {
Method[] allMethods = new Method[parentMethods.length + thisMethods.length];
System.arraycopy(parentMethods, 0, allMethods, 0, parentMethods.length);
System.arraycopy(thisMethods, 0, allMethods, parentMethods.length, thisMethods.length);
thisMethods = allMethods;
}
return thisMethods;
}
}
protected void doOptions(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
Method[] methods = getAllDeclaredMethods(this.getClass());
boolean ALLOW_GET = false;
boolean ALLOW_HEAD = false;
boolean ALLOW_POST = false;
boolean ALLOW_PUT = false;
boolean ALLOW_DELETE = false;
boolean ALLOW_TRACE = true;
boolean ALLOW_OPTIONS = true;
Class clazz = null;
try {
clazz = Class.forName("org.apache.catalina.connector.RequestFacade");
Method getAllowTrace = clazz.getMethod("getAllowTrace", (Class[])null);
ALLOW_TRACE = (Boolean)getAllowTrace.invoke(req, (Object[])null);
} catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | ClassNotFoundException var14) {
}
for(int i = 0; i < methods.length; ++i) {
Method m = methods[i];
if (m.getName().equals("doGet")) {
ALLOW_GET = true;
ALLOW_HEAD = true;
}
if (m.getName().equals("doPost")) {
ALLOW_POST = true;
}
if (m.getName().equals("doPut")) {
ALLOW_PUT = true;
}
if (m.getName().equals("doDelete")) {
ALLOW_DELETE = true;
}
}
String allow = null;
if (ALLOW_GET) {
allow = "GET";
}
if (ALLOW_HEAD) {
if (allow == null) {
allow = "HEAD";
} else {
allow = allow + ", HEAD";
}
}
if (ALLOW_POST) {
if (allow == null) {
allow = "POST";
} else {
allow = allow + ", POST";
}
}
if (ALLOW_PUT) {
if (allow == null) {
allow = "PUT";
} else {
allow = allow + ", PUT";
}
}
if (ALLOW_DELETE) {
if (allow == null) {
allow = "DELETE";
} else {
allow = allow + ", DELETE";
}
}
if (ALLOW_TRACE) {
if (allow == null) {
allow = "TRACE";
} else {
allow = allow + ", TRACE";
}
}
if (ALLOW_OPTIONS) {
if (allow == null) {
allow = "OPTIONS";
} else {
allow = allow + ", OPTIONS";
}
}
resp.setHeader("Allow", allow);
}
protected void doTrace(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String CRLF = "\r\n";
StringBuilder buffer = (new StringBuilder("TRACE ")).append(req.getRequestURI()).append(" ").append(req.getProtocol());
Enumeration reqHeaderEnum = req.getHeaderNames();
while(reqHeaderEnum.hasMoreElements()) {
String headerName = (String)reqHeaderEnum.nextElement();
buffer.append(CRLF).append(headerName).append(": ").append(req.getHeader(headerName));
}
buffer.append(CRLF);
int responseLength = buffer.length();
resp.setContentType("message/http");
resp.setContentLength(responseLength);
ServletOutputStream out = resp.getOutputStream();
out.print(buffer.toString());
out.close();
}
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String method = req.getMethod();
long lastModified;
if (method.equals("GET")) {
lastModified = this.getLastModified(req);
if (lastModified == -1L) {
this.doGet(req, resp);
} else {
long ifModifiedSince;
try {
ifModifiedSince = req.getDateHeader("If-Modified-Since");
} catch (IllegalArgumentException var9) {
ifModifiedSince = -1L;
}
if (ifModifiedSince < lastModified / 1000L * 1000L) {
this.maybeSetLastModified(resp, lastModified);
this.doGet(req, resp);
} else {
resp.setStatus(304);
}
}
} else if (method.equals("HEAD")) {
lastModified = this.getLastModified(req);
this.maybeSetLastModified(resp, lastModified);
this.doHead(req, resp);
} else if (method.equals("POST")) {
this.doPost(req, resp);
} else if (method.equals("PUT")) {
this.doPut(req, resp);
} else if (method.equals("DELETE")) {
this.doDelete(req, resp);
} else if (method.equals("OPTIONS")) {
this.doOptions(req, resp);
} else if (method.equals("TRACE")) {
this.doTrace(req, resp);
} else {
String errMsg = lStrings.getString("http.method_not_implemented");
Object[] errArgs = new Object[]{method};
errMsg = MessageFormat.format(errMsg, errArgs);
resp.sendError(501, errMsg);
}
}
private void maybeSetLastModified(HttpServletResponse resp, long lastModified) {
if (!resp.containsHeader("Last-Modified")) {
if (lastModified >= 0L) {
resp.setDateHeader("Last-Modified", lastModified);
}
}
}
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
HttpServletRequest request;
HttpServletResponse response;
try {
request = (HttpServletRequest)req;
response = (HttpServletResponse)res;
} catch (ClassCastException var6) {
throw new ServletException(lStrings.getString("http.non_http"));
}
this.service(request, response);
}
}
可以看到在调用service
方法时会处理GET
请求(静态资源都是通过get
请求),调用getLastModified
来获取响应内容最后修改时间,service
方法可以根据这个返回值在响应消息中自动生成Last-Modified
头字段,所以在向tomcat
服务器请求静态资源时会使用协商缓存。这里解释一下为什么HttpServlet
类中getLastModified
方法返回-1
呢?其实,在HttpServlet
子类中可以对这个方法进行覆盖,以便返回一个代表当前输出的响应内容的修改时间。参考:https://blog.csdn.net/andydev...
3、客户端处理缓存
其实,在很多业务中都有不需要使用缓存的情况,主要因为缓存会导致资源不是最新的,比如在html
页面中使用script
引入第三方插件。在客户端常有以下几种处理方式:
3.1、使用meta标签中http-equiv
3.2、在请求url加上版本号
3.3、webpack打包后文件带上hsah
entry: {
main: path.join(__dirname, './main.js'),
vendor: ['react', 'antd']
},
output: {
path: path.join(__dirname,'./dist'),
publicPath: '/dist/',
filname: 'bundle.[chunkhash].js'
}
webpack
给我们提供了三种哈希值计算方式,分别是hash
、chunkhash
和contenthash
。那么这三者有什么区别呢?
-
hash
:跟整个项目的构建相关,构建生成的文件hash
值都是一样的,只要项目里有文件更改,整个项目构建的hash
值都会更改。 -
chunkhash
:根据不同的入口文件(Entry
)进行依赖文件解析、构建对应的chunk,生成对应的hash
值。 -
contenthash
:由文件内容产生的hash
值,内容不同产生的contenthash
值也不一样。
显然,我们是不会使用第一种的。改了一个文件,打包之后,其他文件的hash
都变了,缓存自然都失效了。这不是我们想要的。
那chunkhash
和contenthash
的主要应用场景是什么呢?在实际在项目中,我们一般会把项目中的css
都抽离出对应的css
文件来加以引用。如果我们使用chunkhash
,当我们改了css
代码之后,会发现css
文件hash
值改变的同时,js
文件的hash
值也会改变。这时候,contenthash
就派上用场了。
参考:
https://juejin.im/entry/56f0e...
https://segmentfault.com/a/11...
https://blog.csdn.net/qq_2995...
https://www.xp.cn/c.php/28750...
https://blog.csdn.net/andydev...
https://juejin.im/post/5c136b...