Skip to content

web客户端缓存机制

Readme

如果你已经熟悉客户端的缓存机制,可以直接跳过,看最后一节的代码规范。

参考资料:

Bug 场景

比如我们在服务器发布了一个页面:http://domain.com/cate/page.html

在这个页面当中我们引入了一个样式文件 style.css

    <link rel="stylesheet" type="text/css" href="./css/style.css" />

这时候产品经理告诉你,页面需要把背景换成灰色。于是你迅速把修改后的 style.css 交给了发布,等发布的同事说OK了之后你欣喜雀跃的告诉产品:改完了!

产品打开浏览器,刷新了 page.html 并没有看到修改后的效果。

于是你又不得不告诉产品说:Ctrl+F5试试?

如果这是一个移动端页面,那就没有办法给产品经理的iPhone上装一个 Ctrl 和 F5 键了。

问题原因

简单来讲,当你访问一个页面的时候,页面引入的资源(assets)以及页面自身都会被浏览器下载到你自己的电脑上。当你再次访问同一个页面,你电脑的浏览器会先从你电脑中查找上一次的“访问痕迹”,如果有这个文件记录,就不会去服务器请求这个文件。

页面中的图片,音视频,外引js脚本、css文件都会被缓存。

当然,服务器端也会有缓存,这时候通过 Ctrl+F5 就无效了(本文不做讨论)。

为什么要缓存

  1. 用户不需要每次浏览都重新去服务器取文件,为服务器减轻了半数以上的压力。

  2. 从用户角度,大大缩短了网页的加载时间。

缓存规则

问题来了,如果浏览器缓存了文件,服务器文件有了更新,怎么通知浏览器更新文件呢?

1 缓存时效

我们可以在 http 头信息(header)里面设定该文件缓存的有效期。

打开 Chrome 的控制台,查看网络请求,我们会看到如下信息。

image

其中,Expires 和 Cache-Control 字段控制着这个文件在浏览器缓存的失效。Expires 指在这个时间点以前,这个文件缓存都是有效的。Cache-Control 中的 max-age 是说从 Date 字段的时间开始算,43200 秒以内,这个文件缓存是有效的。

Expires 是 HTTP1.0 就有的,max-age 是 HTTP1.1 提出的,所以低版本浏览器不支持 max-age。max-age 比 Expires 更好(因为一个时间长度比写死一个时间点更有灵活性,也更符合“有效期”的逻辑),优先级更高。

Expires 详情

Cache-Control 详情

这两个值在 html 页面中都是可以设置的。在 html 的 meta http-equiv 中可以设置文件的头信息。如:

<meta http-equiv="expires" content="1 Nov 2017" />

我们也可以设置不缓存这个页面,如:

<meta http-equiv="cache-control" content="no-cache">

后端语言在输出 html 的时候也可以设置相应的头信息(header)来控制缓存。下面是一个 JSP 示例:

<%
response.setHeader("expires","sat,6 May 1995 12:00:00 GMT"); //将expire时间设置为一个过去时间或0,-1等
response.setHeader("cache-control","no-store,no-cache,must-revalidadate"); //设置HTTP/1.1 cache-control头
response.addHeader("cache-control", "post-check=0,pre-check=0"); //设置IE 扩展HTTP/1.1 no-cache header
response.setHeader("Pragma", "no-cache"); //设置标准HTTP/1.0 no-cache header
%>

但是如果是其他资源文件(css、img、js等),我们可以通过web服务器配置来设置,比如 Nginx 可以通过在 .conf 文件中设置 Expires。下面代码分别为图片设置30天缓存、为 js 和 css 设置12小时缓存:

location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$ {
    expires      30d;
}

location ~ .*\.(js|css)?$ {
    expires      12h;
}

那么,是不是缓存过期了,浏览器就会去服务器重新请求一份新的文件呢?其实也不是的。当缓存过期,浏览器会携带头信息去与服务器的该文件进行比对。如果有更新,就返回状态码 200,重新请求文件。如果服务器文件没有更新,就会返回状态码 304,浏览器依旧使用缓存中的文件,这样就不需要再从服务器下载一份 http 的主体信息(body)。

2 状态码 304

如果缓存已经过期,浏览器在 Request 的时候,携带 ETag 和 Last-Modified 与服务器的文件进行比对。

image

如果服务器的文件在 Last-Modified(上次修改)之后更新了服务器端的文件,就会重新下载文件主体信息,如果没有修改,服务器返回状态码 304,依旧从缓存读取文件。

ETag 是服务器资源的唯一标识符,比对的优先级高于 Last-Modified。

ETag 详情

无论是 Last-Modified 还是 Etag,可以减少传输成本,但是不会减少 http 请求数,也就是说,服务器的并发数不会少。

另外,前面提到的 Cache-control 中 max-age=0 表示缓存立马过期,请求服务器会返回 304(有缓存的前提)。Cache-Control 值为 no cache 表示总是请求服务器最新文件,不会返回 304。

3 状态码 200

状态码 200 是请求成功,这里不是说服务器请求成功,也包含了从缓存文件请求成功。

在 Chrome 浏览器,我们还会发现,状态码是 200 的文件,也会有 from memory cache(缓存到内存) 和 from disk cache(缓存到本地硬盘) 两种情况:

image

缓存到内存(memory)中的文件,加载的时间几乎是瞬间。

至于 Chrome 如何区分 from memory cache 和 from disk cache,目前我也不知道。

3 为什么 html 页面很少遇到文件不更新的 bug?

我们遇到不更新的 bug 的时候,多数是页面外引的css和js,而页面本身做了修改,只需要刷新一下就可以更新,并不需要 Ctrl+F5 强制刷新。

这是因为点击浏览器的刷新按钮(包括微信浏览器->右上角->刷新),或者新开窗口,或者地址栏敲回车,都会重新请求 html 页面(IE8以下可能有出入,但是刷新按钮肯定会重请求)。

用户基本上已经习惯了刷新按钮,所以 html 极少遇到缓存bug(除非服务器端使用缓存),但是页面引入的其他资源就很难保证及时刷新了。

避免缓存

1 添加后缀

因为浏览器的 http 请求是基于 URL 来的,而且缓存机制不适用于 POST 方法,POST 是不会有缓存的。如果在请求文件的时候添加或修改携带的参数,浏览器就会认为这是一个新的文件,从而不考虑缓存,直接去请求。比如在地址栏请求新的 html 页面:

  • http://domain.com/sample/page.html?201711011801

或者在引入文件的时候使用:

<script src="./bundle.js?20171101114908"></script>
2 ajax

因为 ajax 可以设置头信息,所以在 ajax 中:

xmlHttpRequest.setRequestHeader(“If-modified-since”,”0″);
xmlHttpRequest.setRequestHeader(“Cache-Control”,”no-cache”);

jquery 使用 ajax 方法的时候设置 cache:false。

ajax 中也可以使用随机数参数。

3 后端

response.setHeader(“Cache-Control”,”no-cache,must-revalidate”);

4 POST

最后,因为浏览器的 http 请求是基于 URL 来的,缓存机制不适用于 POST,所以可以用 POST 代替 GET。

缓存负面影响

缓存损坏

image
image
image

有时候我们打开网站会发现页面失去了样式,这种情况多数原因是网络环境差(加载css失败)或者页面错误,也有极少可能是缓存在本地的 css 缓存被损坏了,浏览器使用了本地缓存,但是并不知道缓存已经失效,而是加载了一个无效的缓存文件。
当然,这种情况发生的概率小到可以忽略,就算发生了这种情况,我们可以把解决方案推给用户(用户只需要清除缓存或者使用 Ctrl+F5 强制刷新即可)。

不需要缓存的网站

个人观点认为:多数网站都被建议使用缓存,如果说要挑出一个例外,那应该是适用一条法则:
更新频率大于用户访问频率
比如股票实时行情页面,秒杀页面……用户对于页面数据请求要保持最新,这样就必须要避免数据缓存。

代码规范

因为我们可以用强制刷新(Ctrl+F5),但是用户可能不会。开发人员一旦习惯强制刷新之后,就很容易忽略缓存问题。所以强制刷新不是好习惯,因为不是最大限度模拟用户行为。

  1. 在 html 中引入资源,包括媒体、CSS、JS,如果引入的资源文件是再次发布的,建议添加该文件的版本号(没有约定版本号可以使用时间串),如:
<script src="./bundle.js?20171101114908"></script>
  1. 在移动端,务必这么做。

  2. 额外的,因为 https 越来越成为主流,为了更好兼容,在使用绝对地址引入外部脚本和样式文件,切勿限定协议类型。如:

<script src="http://domain.com/js/foo.js"></script>

应该替换为:

<script src="//domain.com/js/foo.js"></script>

测试环境的临时解决方案

在测试的时候,每次修改一下版本号(随机数)都太麻烦。因为刷新按钮可以每次都去服务器请求文件,所以我们只要将修改的文件直接放到当前浏览器的地址栏,然后刷新一下页面就可以更新了(注意是和html页面同一个浏览器)。移动端也可以直接打开css或js文件的URI地址刷新一下。

您的赞助将会鼓励作者技术文章创作以及支持本站运维。

发表评论

Your email is never published nor shared. Required fields are marked *


TOP