npm 模块 fresh 源码总结

534 阅读4分钟

该篇文章是个人学习 fresh 总结,如果理解有误请评论区指出或留言。

转载请标明出处

fresh 的应用案例 fresh-cache

fresh 做什么?

在深入学习 fresh 之前,需要对 HTTP 缓存及服务端实现 有一定的了解(前置知识)。

fresh 从单词翻译可知道和新鲜有关。它主要用在 HTTP 协商缓存中,通过 HTTP 的请求头和响应头字段,来校验客户端缓存是否新鲜。

fresh 怎么做的?

如果开启协商缓存 Cache-Control: no-cache 或 Cache-Control: max-age=0。服务器响应静态资源请求时,响应头会携带 Last-Modified、ETag 头字段。客户端再次请求这个资源时,请求头会携带 If-Modified-Since、If-None-Match 字段发给服务器。

响应头字段请求头字段
Last-ModifiedIf-Modified-Since
ETagIf-None-Match


fresh 内部主要做的是:

先检查请求头 If-None-Match 和响应头 ETag 字段值是否相同。如果相同,则服务器文件内容与客户端缓存文件内容一致。再检查请求头 If-Modified-Since 和 响应头 Last-Modified 字段是否相同,如果相同,则文件在服务器最后修改时间和客户端缓存文件最后修改时间相同。

所以,认为客户端缓存的资源是新鲜的,直接使用客户端缓存即可。否则,上述检查有一个不符,即客户端缓存文件资源相比服务器文件比较不是最新鲜的,需要服务器将最新鲜的文件发送给客户端。

新鲜度的定义:
服务器文件资源的内容、最后一次修改时间和客户端缓存文件内容及最后一次修改时间一致时,则认为客户端资源时新鲜的,否则认为客户端资源不新鲜。

客户端资源新鲜:服务器响应状态码为 304
客户端资源不新鲜:服务器响应状态码为 200,并返回最新的文件

fresh 源码分析

fresh 源码文件

是否开启协商缓存

// fields
var modifiedSince = reqHeaders['if-modified-since']
var noneMatch = reqHeaders['if-none-match']

// unconditional request
if (!modifiedSince && !noneMatch) {
  return false
}

如果开启了 HTTP 协商缓存,当客户端再次请求资源文件时,HTTP 请求携带 If-Modified-Since 和 If-None-Match 头字段。如果没有这些字段,则直接返回 false,每次请求都响应最新的服务器资源。

var CACHE_CONTROL_NO_CACHE_REGEXP = /(?:^|,)\s*?no-cache\s*?(?:,|$)/

var cacheControl = reqHeaders['cache-control']
if (cacheControl && CACHE_CONTROL_NO_CACHE_REGEXP.test(cacheControl)) {
  return false
}

当 Cache-Control: no-cache 支持端到端重新加载请求时,返回 false,始终响应最新的服务器资源。

If-None-Match 和 ETag 校验

if (noneMatch && noneMatch !== '*') {
  var etag = resHeaders['etag']

  if (!etag) {
    return false
  }

  var etagStale = true
  var matches = parseTokenList(noneMatch)
  for (var i = 0; i < matches.length; i++) {
    var match = matches[i]
    if (match === etag || match === 'W/' + etag || 'W/' + match === etag) {
      etagStale = false
      break
    }
  }

  if (etagStale) {
    return false
  }
}

对比请求头字段 If-None-Match 和响应头字段 ETag 是否一致。如果一致,说明文件内容一样,再校验最后修改时间是否相同。否则,直接返回 false,客户端缓存资源不新鲜。

我认为 parseTokenList 函数解析请求头字段 If-None-Match 对应的值,过滤了值前面的空格,还处理了通过逗号( , )可能携带多个 ETag 值的情况,其中一个与响应头的 ETag 相同,则进行后续校验,否则返回 false。

parseTokenList 函数自行了解,给出这个函数的 输入和输出结果。

  const IfNoneMatch1 = "  etagValue1 etagValue2"
  const IfNoneMatch2 = "etagValue1,etagValue2"
  const IfNoneMatch3 = "etagValue1"

parseTokenList(IfNoneMatch1)     => ["etagValue1 etagValue2"]
parseTokenList(IfNoneMatch2)     => ["etagValue1", "etagValue2"]
parseTokenList(IfNoneMatch3)     => ["etagValue1"]

If-Modified-Since 和 Last-Modified 校验

if (modifiedSince) {
  var lastModified = resHeaders['last-modified']
  var modifiedStale = !lastModified || !(parseHttpDate(lastModified) <= parseHttpDate(modifiedSince))

  if (modifiedStale) {
    return false
  }
}

parseHttpDate 函数用于将 Wed, 21 Oct 2015 07:28:00 GMT 时间格式转换为时间戳。

通过比较 Last-Modified 和 If-Modified-Since 时间戳的大小。
如果服务器资源文件的 Last-Modified 小于或等于客户端资源文件的 If-Modified-Since,则客户端资源新鲜。
如果服务器资源文件的 Last-Modified 大于客户端资源文件的 If-Modified-Since,则客户端资源不新鲜,需响应最新服务器资源文件。

fresh 完整代码

function fresh (reqHeaders, resHeaders) {
  // fields
  var modifiedSince = reqHeaders['if-modified-since']
  var noneMatch = reqHeaders['if-none-match']

  // unconditional request
  if (!modifiedSince && !noneMatch) {
    return false
  }

  var cacheControl = reqHeaders['cache-control']
  if (cacheControl && CACHE_CONTROL_NO_CACHE_REGEXP.test(cacheControl)) {
    return false
  }

  // if-none-match
  if (noneMatch && noneMatch !== '*') {
    var etag = resHeaders['etag']

    if (!etag) {
      return false
    }

    var etagStale = true
    var matches = parseTokenList(noneMatch)
    for (var i = 0; i < matches.length; i++) {
      var match = matches[i]
      if (match === etag || match === 'W/' + etag || 'W/' + match === etag) {
        etagStale = false
        break
      }
    }

    if (etagStale) {
      return false
    }
  }

  // if-modified-since
  if (modifiedSince) {
    var lastModified = resHeaders['last-modified']
    var modifiedStale = !lastModified || !(parseHttpDate(lastModified) <= parseHttpDate(modifiedSince))

    if (modifiedStale) {
      return false
    }
  }

  return true
}

当 if-none-match 和 if-modified-since 都校验通过,则返回 true,客户端缓存新鲜。

fresh 简单应用

var reqHeaders = { 'if-none-match': '"foo"' }
var resHeaders = { 'etag': '"bar"' }
fresh(reqHeaders, resHeaders)
// => false

var reqHeaders = { 'if-none-match': '"foo"' }
var resHeaders = { 'etag': '"foo"' }
fresh(reqHeaders, resHeaders)
// => true

总结

  1. 了解客户端资源新鲜度概念和定义
  2. 了解响应头字段 Last-Modified、ETag 和请求头字段 If-Last-Modified、If-None-Match 的关系
  3. 了解服务器缓存策略的比较方法