协商缓存
二者关系
协商缓存可以看作是强制缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程。
浏览器启用协商缓存的前提是强缓存失效,但是反过来强缓存失效并不一定导致浏览器启用协商缓存。
# 协商缓存的生效流程
# last-Modified/ETag
除了强制缓存失效以外,还需要借助服务器响应请求时返回的报头首部:last-modified 和etag(缓存标识)。
etag的优先级要高于last-modified,当两者同时出现时,只有etag会生效,只要有这两个缓存标识之一,在强缓存失效后浏览器便会携带它们向服务器发起请求,携带方式如下所示:
if-modified-since: Wed, 11 May 2022 03:50:47 GMT
if-none-match: "700f049716443285878653598e"
- if-modified-since: 对应last-modified的值
- if-none-match: 对应etag的值 服务器根据优先级的缓存标识的值进行判断
如果 eTag 对应的 if-none-match 不存在,那么服务器会将 last-modified 对应的 if-modified-since 的时间值与服务器该资源的最后修改时间进行对比,最后判断是否走协商缓存。
# 二者区别
精确度: last-modified是一个时间,单位最小为秒,如果资源修改快到毫秒级别,服务器会任务该资源没有更新,导致资源在浏览器没有及时更新。
性能上:Etag要逊于Last-Modified,因为Last-Modified只需要记录时间,而Etag需要服务器通过算法来计算出一个hash值。
优先级上,服务器校验优先考虑Etag
# etag原理和实现
node下的etag (opens new window)为例:
# 第一种方式:使用文件大小和修改时间
图中当判断所要处理的内容是文件 stats 对象时,将会采用上述方法生成 eTag 值,最后返回的值是由文件大小和文件最后一次修改时间组成的字符串。
/**
* Generate a tag for a stat.
*
* @param {object} stat
* @return {string}
* @private
*/
function stattag (stat) {
var mtime = stat.mtime.getTime().toString(16)
var size = stat.size.toString(16)
return '"' + size + '-' + mtime + '"'
}
# 第二种方式:使用文件内容的 hash 值和内容长度。
/**
* Generate an entity tag.
*
* @param {Buffer|string} entity
* @return {string}
* @private
*/
function entitytag (entity) {
if (entity.length === 0) {
// fast-path empty
return '"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk"'
}
// compute hash of entity
var hash = crypto
.createHash('sha1')
.update(entity, 'utf8')
.digest('base64')
.substring(0, 27)
// compute length of entity
var len = typeof entity === 'string'
? Buffer.byteLength(entity, 'utf8')
: entity.length
return '"' + len.toString(16) + '-' + hash + '"'
}
通过对内容的 hash 转化和截取,最终返回内容长度与其 hash 值组合成的字符串。
通过上述方法生成的 eTag 也被称为强 eTag 值,其不论实体发生多么细微的变化都会改变它的值。那么与其对立的便是弱 eTag 值,在 eTag 包源码中我们可以发现通过传递第二个参数 weak 值为 true 时便可启用弱校验。
function etag (entity, options) {
if (entity == null) {
throw new TypeError('argument entity is required')
}
// support fs.Stats object
var isStats = isstats(entity)
var weak = options && typeof options.weak === 'boolean'
? options.weak
: isStats
// validate argument
if (!isStats && typeof entity !== 'string' && !Buffer.isBuffer(entity)) {
throw new TypeError('argument entity must be string, Buffer, or fs.Stats')
}
// generate entity tag
var tag = isStats
? stattag(entity)
: entitytag(entity)
return weak
? 'W/' + tag
: tag
}
弱 ETag
弱 ETag 值只适用于提示资源是否相同。只有资源发生了根本改变,产生差异时才会改变 ETag 值。这时会在字段值最开始处附加 W/。
ETag: W/"29322-09SpAhH3nXWd8KIVqB10hSSz66"
# 启发式缓存
强缓存新鲜度的公式为:缓存新鲜度 = max-age || (expires - date)。响应报头中没有 max-age(s-maxage) 和 expires 这两个关键的字段值,浏览器还是会走强缓存
date: Thu, 02 Sep 2021 13:28:56 GMT
age: 10467792
cache-control: public
last-modified: Mon, 26 Apr 2021 09:56:06 GMT
虽然有与协商缓存相关的 last-modified 首部,但并不会走协商缓存,反而浏览器会触发启发式缓存。启发式缓存对于缓存新鲜度计算公式如下所示:
缓存新鲜度 = max(0,(date - last-modified)) * 10%
根据响应报头中 date 与 last-modified 值之差与 0 取最大值后取其值的百分之十作为缓存时间。