浏览器第一次向一个web服务器发起 http
请求后,服务器会返回请求的资源,并且在响应头中添加一些有关缓存的字段如: Cache-Control
、 Expires
、 Last-Modified
、 ETag
、 Date
等等。之后浏览器再向该服务器请求该资源就可以视情况使用 强缓存 和 协商缓存 。
下面假定浏览器已经访问了服务器,服务器返回了缓存相关的头部字段且浏览器已对相关资源做好缓存。通过下图来分析强缓存和协商缓存:
强缓存由两个http响应头部字段控制, Expires
和 Cache-Control
,其中 Cache-Control
的优先级比 Expires
高。
一、 Cache-Control :
max-age
(单位为s)指定设置缓存最大的有效时间,定义的是时间长短。当浏览器向服务器发送请求后,在max-age这段时间里浏览器就不会再向服务器发送请求了。
max-age>0 max-age<=0
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请求时告诉浏览器在过期时间前浏览器可以直接从浏览器缓存取数据,而无需再次请求。
当浏览器发现缓存过期后,缓存并不一定不能使用了,因为服务器端的资源可能仍然没有改变,所以需要与服务器协商,让服务器判断本地缓存是否还能使用。
当第一次请求响应头中有 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
; If-Modified-Since
能检查到的粒度是s级的,这种修改无法判断(或者说 UNIX
记录 MTIME
只能精确到秒); 这时,利用 ETag
能够更加准确的控制缓存,因为 ETag
是服务器自动生成的资源在服务器端的唯一标识符,资源每次变动,都会生成新的 ETag
值。 Last-Modified
与 ETag
是可以一起使用的,但服务器会优先验证 ETag
。
先举一个例子,先在linux服务器上安装tomcat,然后上传一个文件到服务器导航,向服务器请求这个静态资源
刷新再次请求
我们并没有配置响应头 ETag
和 Last-Modified
,为什么会进行协商缓存呢?我们来查看一下 tomcat
源码如何处理 http
缓存的,在 servlet-api.jar
包中有一个 HttpServlet.class
字节码文件,我们用 idea
打开可以看到反编译后的源码。
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...
其实,在很多业务中都有不需要使用缓存的情况,主要因为缓存会导致资源不是最新的,比如在 html
页面中使用 script
引入第三方插件。在客户端常有以下几种处理方式:
<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" />
<link rel="stylesheet" type="text/css" href="./reverse.css?v=2019060301"> <script src="./reverse.js?v=2019060301"></script>
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...