Web缓存是能够自动保存常见文档副本的HTTP设备。当Web请求抵达缓存时,若是本地有“已缓存的副本”,就能够从本地存储设备而不是原始服务器中提取这个文档。javascript
上面是《HTTP权威指南》中对Web缓存的定义,缓存的好处主要有如下几点:html
总结一下就是省流量,省带宽,还贼快。那么缓存是如何工做的呢?客户端和服务端是如何协调缓存的时效性的呢?下面咱们用代码来一步一步揭晓缓存的工做原理。java
当咱们在浏览器地址栏敲入localhost:8080/test.txt
并回车时,咱们是向指定的服务端发起对text.txt
文件的请求,git
服务端在接收到这个请求以后,找到了这个文件并准备返回给客户端,并经过设置Cache-Control
和Expires
两个response header
告诉客户端这个文件要缓存下来,在过时以前别跟我要了。github
首先咱们看一下项目目录:浏览器
|-- Cache |-- index.js |-- assets |-- index.html |-- test.txt
具体实现代码以下:缓存
<!-- index.html --> ... <a href="./test.txt">test.txt</a> ...
// index.js const http = require('http'); const path = require('path'); const fs = require('fs'); http.createServer((req, res) => { const requestUrl = path.join(__dirname, '/assets', path.normalize(req.url)); fs.stat(requestUrl, (err, stats) => { if (err || !stats.isFile) { res.writeHead(404, 'Not Found'); res.end(); } else { const readStream = fs.createReadStream(requestUrl); const maxAge = 10; const expireDate = new Date( new Date().getTime() + maxAge * 1000 ).toUTCString(); res.setHeader('Cache-Control', `max-age=${maxAge}, public`); res.setHeader('Expires', expireDate); readStream.pipe(res); } }); }).listen(8080);
那Cache-Control
和Expires
这个两个response header又表明什么意思呢?Cache-Control:max-age=500
表示设置缓存存储的最大周期为500秒,超过这个时间缓存被认为过时。Expires:Tue, 23 Feb 2021 01:23:48 GMT
表示在Tue, 23 Feb 2021 01:23:48 GMT
这个日期以后文档过时。服务器
启动server后,在浏览器访问localhost:8080/index.html
,这时是第一次访问,没有缓存,因此服务器返回完整的资源。网络
咱们点击超连接访问test.txt
:测试
由于是第一次访问,因此没有缓存,这个时候咱们点击返回按钮回到index.html
:
发现不一样了吗?这个时候NetWork中Size已经变成了disk cache
,说明命中了浏览器缓存,也就是强缓存,这个时候再点击超连接访问test.txt
,若是在设置的过时时间10s之内,就能看到命中浏览器缓存,若是超过10s,就会从新从服务器获取资源。
这里说明一点,浏览器的前进后退按钮会一直从缓存中读取资源,而忽略设置的缓存规则。也就是说刚才若是我从localhost:8080/test.txt
页面经过浏览器返回按钮回到localhost:8080/index.html
页面,会发现无论过多久Network都是disk cache
,一样再点击浏览器前进按钮进入localhost:8080/test.txt
页面,哪怕超过设置的过时时间也仍是from disk cache。
注意:
Cache-Control
的优先级大于Expires
,由于时差缘由还有服务端时间和客户端时间可能不一致会致使Expires
判断缓存有效性不许确。可是Expires
兼容http1.0,Cache-Control
兼容到http1.1,因此通常仍是两个都设置。
上面咱们设置过缓存时限后,若是缓存过时了怎么办呢?你可能会说,过时了就从新从服务端获取资源啊。可是也有可能缓存时间过时了,可是资源并无变化,因此咱们还要引入其余的策略来处理这种状况,那就是协商缓存也就是弱缓存。
咱们梳理一下协商缓存的流程:
当服务端第一次返回资源时,除了设置Cache-Control
和Expires
响应头以外,还会设置Last-Modified
(资源更新时间)和ETag
(资源摘要或资源版本)两个响应头,分别表明资源的最近一次变动时间和实体标签。当客户端没有命中强缓存时,会从新像服务端发起请求,并携带If-modified-Since
和If-None-Match
两个请求头,服务端拿到这两个请求头会跟以前设置的Last-Modified
和ETag
做比较,若是不匹配,说明缓存不可用,从新返回资源,反之说明缓存有效,返回304
响应码,告知缓存能够继续使用,并更新缓存有效时间。
下面咱们看一下具体代码实现:
const http = require('http'); const path = require('path'); const fs = require('fs'); const crypto = require('crypto'); // 生成entity digest function generateDigest(requestUrl) { let hash = '2jmj7l5rSw0yVb/vlWAYkK/YBwk'; let len = 0; fs.readFile(requestUrl, (err, data) => { if (err) { console.error(error); throw new Error(err); } else { len = Buffer.byteLength(data, 'utf8'); hash = crypto .createHash('sha1') .update(data, 'utf-8') .digest('base64') .substring(0, 27); } }); return '"' + len.toString(16) + '-' + hash + '"'; } // 响应文件 function responseFile(requestUrl, stats, res) { const readStream = fs.createReadStream(requestUrl); const maxAge = 10; const expireDate = new Date( new Date().getTime() + maxAge * 1000 ).toUTCString(); res.setHeader('Cache-Control', `max-age=${maxAge}, public`); res.setHeader('Expires', expireDate); res.setHeader('Last-Modified', stats.mtime); res.setHeader('ETag', generateDigest(requestUrl)); readStream.pipe(res); } // 判断新鲜度 function isFresh(requestUrl, stats, req) { const ifModifiedSince = req.headers['if-modified-since']; const ifNoneMatch = req.headers['if-none-match']; if (!ifModifiedSince && !ifNoneMatch) { //若是没有相应的请求头,应该返回全新的资源 return false; } else if (ifNoneMatch && ifNoneMatch !== generateDigest(requestUrl)) { //若是ETag不匹配(资源内容发生改变),表示缓存不新鲜 return false; } else if (ifModifiedSince && ifModifiedSince !== stats.mtime.toString()) { //若是资源更新时间不匹配,表示缓存不新鲜 return false; } return true; } http.createServer((req, res) => { const requestUrl = path.join(__dirname, '/assets', path.normalize(req.url)); fs.stat(requestUrl, (err, stats) => { if (err || !stats.isFile) { res.writeHead(404, 'Not Found'); res.end(); } else { if (isFresh(requestUrl, stats, req)) { // 缓存新鲜,告知客户端没有缓存可用,不返回响应实体 res.writeHead(304, 'Not Modified'); res.end(); } else { // 缓存不新鲜,从新返回资源 responseFile(requestUrl, stats, res); } } }); }).listen(8080);
从代码中能够看到ETag
和Last-Modified
都是用于协商缓存的校验的,ETag
基于实体标签,通常能够经过版本号,或者资源摘要来指定;Last-Modified
则是基于资源的最后修改时间。
这时访问localhost:8080/test.txt
文件,当命中强缓存后,等待10s钟,再次访问,服务器返回304
,而非200
,代表协商缓存生效。
此时修改test.txt文件,再次访问,服务器返回200
,页面展现最新的test.txt
文件内容。
总结一下:
ETag
能更精确地判断资源到底有没有变化,且优先级高于Last-Modified
;ETag
相对较慢,更占资源;Last-Modified
精确到秒,对亚秒级的资源更新的缓存新鲜度判断无能为力;ETag
兼容到http1.1
,Last-Modified
兼容到http1.0
。注意:本文中经过超连接访问
test.txt
是由于,若是直接在地址栏访问该资源,浏览器会在request headers
中设置cache-control:max-age=0
,这样永远不会命中浏览器缓存。本文测试浏览器:Chrome 版本 88.0.4324.192