最近在对项目作 IE 11 兼容,由 IE 的缓存问题,引起我对于浏览器缓存策略的思考。javascript
web缓存主要能够分为下面几类:html
这里咱们主要关注客户端,也就是浏览器缓存。java
浏览器和服务器通讯是经过 HTTP 协议,浏览器向服务器发起 HTTP 请求,服务器做出响应。当再次发起请求的时候,能够直接读取缓存中的数据,减小网络带宽的消耗,提高页面的访问速度。git
根据是否从新发起 HTTP 请求,能够将浏览器缓存分为两种:强制缓存和协商缓存。github
与强制缓存有关的 HTTP 头部有 Expires 和 Cache-Controlweb
Expires 响应头包含一个 HTTP 日期(GMT 时间,非本地时间),表示资源过时的时间。chrome
当设置无效值,例如 0,表示资源当即过时,即不使用缓存。数据库
//...
const getGMT = () => `${moment().utc().add(1, 'm').format('ddd, DD MMM YYYY HH:mm:ss')} GMT`
app.get('/expries', (req, res) => {
res.setHeader('Expires', getGMT());
res.end('ok')
});
复制代码
这里使用 express 建立了一个 web 服务,在 header 中添加了 Expires 响应头,利用 moment 转化为相应的 GMT 格式,设置为 10s 后过时,能够看到首次请求时向服务端发起了 HTTP 请求,第二次则使用了缓存(disk cache),超过 10s 以后再请求时(第三次)缓存过时,从新向服务端发起 HTTP 请求。express
请求时带上 Expries 请求头:浏览器
Cache-Control 是一个通用首部,既能够设置在请求头中,也能够设置在响应头中,经常使用的取值包括如下几种:
Cache-Control 取值 | 含义 |
---|---|
no-store | 绝对禁止缓存 |
no-cache | 会被缓存,可是马上过时,要求将请求提交给原始服务器进行验证,至关于 max-age=0 |
private | 只有浏览器能够缓存,禁止代理服务器、CDN等中间人缓存 |
public | 资源能够被任何对象缓存 |
max-age | 表示资源被缓存的最大时间,单位秒;当设置该值时,Expries 头部会被忽略 |
其中private
、public
只能用于响应头部中
在强制缓存中,咱们根据时间来判断资源是否过时,这会存在必定弊端,当过时时间到了,即便服务端资源未改动,也会从新获取。由此咱们引进了协商缓存的概念,协商缓存须要浏览器和服务器共同实现,与协商缓存有关的响应头部字段主要为如下两组:
Last-Modified
和 If-Modified-Since
ETag
和 If-None-Match
Last-Modified
表示资源最后的修改时间(GMT 格式),具体过程以下:
Last-Modified
响应头部,告诉浏览器该资源的最后修改时间If-Modified-Since
这个请求头部,它的值即为上一次请求响应的 Last-Modified
,服务端比较两个字段的值,若是一致,说明资源未改动,返回 304,不然返回更改后的资源。能够看到再次请求时自动加上 If-Modified-Since
请求头部:
服务端实现以下:
const filePath = path.join(__dirname, '../static/index.html')
app.get('/lastModified', (req, res) => {
const stat = fs.statSync(filePath);
const file = fs.readFileSync(filePath);
const lastModified = stat.mtime.toUTCString();
res.setHeader('Cache-Control', 'public,max-age=10');
if (lastModified === req.headers['if-modified-since']) {
res.writeHead(304, 'Not Modified');
res.end();
} else {
res.setHeader('Last-Modified', lastModified);
res.writeHead(200, 'OK');
res.end(file);
}
});
复制代码
当资源发生屡次改动,可是资源内容未改变时,此时服务器仍须要从新返回资源。为了提高判断的精确度,引入 ETag 响应头部,表示资源特定版本的标识符,当文件内容未发生变化时,该标识符的值不会改变。具体过程以下:
ETag
响应头部,告诉浏览器该资源的特殊标识If-None-Match
这个请求头部,它的值即为上一次请求响应的 ETag
,服务端比较两个字段的值,若是一致,说明资源未改动,返回 304,不然返回更改后的资源。当文件发生变化时,响应头部的 ETag
和请求头部的 If-None-Match
不一致:
服务端实现以下:
const filePath = path.join(__dirname, '../static/index.html')
// 建立 md5 加密
const cryptoFile = (file) => {
const md5 = crypto.createHash('md5');
return md5.update(file).digest('hex');
}
app.get('/eTag', (req, res) => {
const file = fs.readFileSync(filePath);
const eTag = cryptoFile(file)
res.setHeader('Cache-Control', 'public,max-age=10');
if (eTag === req.headers['if-none-match']) {
res.writeHead(304, 'Not Modified');
res.end();
} else {
res.setHeader('ETag', eTag);
res.writeHead(200, 'OK');
res.end(file);
}
})
复制代码
在 HTTP/1.0 时期存在一个通用首部 Pragma
,当它的值为 no-cache
时,与 Cache-Control: no-cache
的行为一致。它在“请求-响应”链中可能会有不一样的效果,如今通常用于向后兼容只支持 HTTP/1.0 的客户端。
Chrome 下测试,在请求头部/响应头部中设置 Pragma: 'no-cache'
都可以实现禁用缓存:
但在 IE 11 下,当 Pragma
置于响应头部时并未生效,能够在 IE 11 下运行测试代码进行验证。
在 chrome 下控制台能够看到浏览器本地缓存分为两类:memory cache
和 disk cache
,即内存缓存和磁盘缓存。
那么浏览器是如何区分哪些资源存放在内存中,哪些又存在磁盘中呢?
其实这个问题没有一个标准答案,广泛认为和系统当前内存的使用状况有关,若是当前系统内存使用率高,那么会优先存储在磁盘中;另一个就是对于大文件,通常存储在磁盘中。
关于优先级,强制缓存的优先级老是大于协商缓存,只有在强制缓存失效后才会发起请求进行协商缓存;
而在协商缓存中,Last-Modified
表示的是一个 GMT 格式的时间,只能精确到秒,所以 ETag
的精确度要高于 Last-Modified
,但同时每次进行 hash 运算生成标识也会带来额外的开销。两者都存在时,服务端应以 ETag
为准。
总的优先级以下:
Pragma > Cache-Control > Expries > ETag > Last-Modified
在Chrome下验证,当 Pragma
为 no-cache,Cache-Control
设置 1000s 缓存时,浏览器会禁用缓存:
一样,设置响应头为 Cache-Control: 'no-cache'
和 Expries
为 1000s 后过时,浏览器依然禁用缓存:
总体的缓存过程以下:
兼容 IE 11 的过程当中踩过一些坑,在实际项目中遇到的印象比较深入的问题是下面这个:
因为 IE 对 GET 接口的缓存,当用户首次进入系统时,由于未登陆跳转至sso,登陆成功以后仍然返回的是缓存中的未登陆,致使登陆以后出现闪屏,在原系统和sso之间不停来回跳转。
另外,因为 IE 浏览器打开控制台以后默认开启始终从服务端刷新,在 debug 阶段着实给我形成了不小的困扰,后来放弃使用控制台,经过抓包工具Charles进行截取、分析,这才定位到问题。
究其缘由,是 IE 对于 GET 请求的缓存策略问题:
屡次发起 GET 请求时,若 url 未发生变化,IE 则认为这是非首次请求,直接读取缓存。
经过在 get 请求的 url 中加入随机标识,例如时间戳、随机数等,来达到变动 url 的目的,此时浏览器不会从缓存中读取数据
服务端设置响应头部禁止浏览器缓存
{
'Cache-Control': 'no-cache',
'Pragma': 'no-cache',
'Expires': -1,
}
复制代码
在实际项目中我采用的是这种解决方案
{
'Cache-Control': 'no-cache',
'Pragma': 'no-cache',
}
复制代码