轻松理解浏览器缓存(Koa缓存源码解析)

1、缓存

缓存技术一直一来在WEB技术体系中扮演很是重要角色,是快速且有效地提高性能的手段。 css

缓存
如上图,在网页展现出来的过程当中,各个层面均可以进行缓存。

以前在学习缓存的过程当中,一直没有实践过,有些概念常常会忘记。 今天主要经过Node实践的方式学习浏览器缓存,顺便分析一下Koa处理缓存的源码。前端

2、浏览器缓存

首先咱们看一下浏览器请求缓存过程。 java

缓存的处理过程

  • 发出请求后,会先在本地查找缓存。node

  • 查到有缓存,要判断缓存是否新鲜(是否过时)。浏览器

  • 没有过时,直接返回给客户端。缓存

  • 若是缓存过时了,就要再次去服务器请求最新的资源,返回给客户端,并从新进行缓存。安全

3、新鲜度检测

可能不少同窗看其余博客,提到都是“强缓存/协商缓存”等说法,这个我会放到后面讲。 上图中新鲜一词比较少见,来自《HTTP权威指南》。服务器

由于HTTP会将资源缓存一段时间,在这个时间内,这个缓存就是“新鲜的”。 因此检查缓存是否过时就被称为,新鲜度检测框架

那么接下来就经过Node来实战一下,看看:koa

  1. 浏览器是如何进行缓存的?
  2. 如何进行新鲜度检测?

4、Node实战

上述提到缓存一段时间,那么HTTP提供了通用首部字段(就是请求报文和响应报文都能用上的字段),来控制缓存时间。

1. Pragma/Expires介绍

1.0

Pragma 是HTTP/1.0标准中定义的一个header属性,请求中包含Pragma的效果跟在头信息中定义Cache-Control: no-cache相同,可是HTTP的响应头没有明肯定义这个属性,因此它不能拿来彻底替代HTTP/1.1中定义的Cache-control头。一般定义Pragma以向后兼容基于HTTP/1.0的客户端。

Expires会返回一个绝对时间,若是请求时间在Expires指定的时间以前,就能命中缓存。可是由于客户端能够修改本地时间,会和和服务器时间不一致,容易出现差错,不推荐使用。

Pragma/Expires

2. Cache-Control介绍

1.1

Cache-Control是如今常见的缓存方式,上述字段不少,初学者能够只看max-age,避免混乱,也是最有意义的属性。

max-age
Cache-Control描述的是一个相对时间,在进行缓存命中的时候,都是利用客户端时间进行判断,因此相比较 ExpiresCache-Control的缓存管理更有效,安全一些。

3. Cache-Control实战

经过Koa框架,简单搭建一个Node服务。并经过koa-static管理静态资源。

代码结构参考以下,maxage缓存设置10秒。

代码
启动服务,就能够看到本身的页面了~

node index.js // server is starting at port 8001
复制代码

代码目录
在代码截图上,能够看到给 koa-static传了 maxage: 10 * 1000

koa-static源码中引入了koa-send库。截取部分koa-send源码,只要传入maxage,就会设置Cache-Controlmax-age。为符合前端开发者习惯传入为毫秒,其实是用秒为单位的。

经过 NetWork能够观察到已经成功设置 Cache-Control: max-age=10
缓存设置10s

访问测试以下图:

缓存验证

  1. 在10s内再次请求,能够看到js/css均来自缓存memory cache

  2. 10s后缓存过时,不走缓存,便再次从服务器获取。

4. HTML为什么如此特殊?

4.1 现象

通过上面的实验能够看出,在Js/Css都走本地缓存的时候,HTML是依旧从服务端获取的。

HTML为什么如此特殊
查看请求信息以后,发现请求头中默认加上了 Cache-Control: max-age=0
HTML为什么如此特殊
通过测试,发现若是单独请求 Js资源,也会出现此类现象。所以得出结论,这个是浏览器默认加的,应该是为了 保证直接请求的资源最新
客户端的缓存限制

4.2 缘由

针对request请求,若是有Cache-Control限制,那么缓存系统就会先校验Cache-Control。不符合规则就直接请求服务端,具体规则以下:

客户端的新鲜度限制
客户端的缓存限制
上述来自《HTTP权威指南》。 同理浏览器中 Network中的 disable-cache也是如此,发出请求时,表示不须要走缓存,必定要服务端最新的。
浏览器disable cache
no-cache

5. 服务端再验证(新鲜度检测)

上述不管是http1.0仍是1.1的方案,都是在本地缓存中存放一段时间。过时后就须要去服务端从新请求一遍。这个也被称之为强缓存

可是,缓存中过时并不意味服务端资源改变

所以请求发现本地缓存过时,能够去服务端咨询一下,这个资源还新鲜吗?还能够继续使用吗?常见的方法就是携带字段If-Modified-SinceIf-None-Match。若是验证资源是新鲜的,没有改变。那只须要返回一个标识,也就是咱们常说的304,不须要返回数据,加速请求时间。

这个过程就是新鲜度检测,那实现这个缓存的方式就是咱们常说的协商缓存

下面看下Node实战协商缓存。

6. Last-Modified 和 If-Modified-Since

携带If-Modified-Since的前提是,缓存中存储了Last-Modified字段。 每一个请求返回时,response中能够携带字段Last-Modified,是服务端资源修改的最后日期。

下次发起请求时携带 If-Modified-Since就是缓存中的 Last-Modified,和服务端资源最后修改时间进行比较,就知道资源是否新鲜了。

6.1 代码验证

每一个请求返回时,response中能够携带字段Last-Modified,是由于咱们使用的koa-static会默认给咱们的返回头加上Last-Modified

发出的请求也会自动携带 If-Modified-Since
If-Modified-Since
可是验证发现,10s后缓存过时,再次发出请求并无返回304,仍是200。
缘由是须要配置中间件 koa-conditional-get

6.2 koa-conditional-get

配置中间件`koa-conditional-get`
简单看下 koa-conditional-get作了什么,让协商缓存生效。 能够看出源码很是简单,判断是否新鲜便可。

ctx.fresh如何计算,会在后面讲。但很明显是校验了If-Modified-SinceLast-Modified

koa-conditional-get源码

6.3 Last-Modified测试

  1. 在10s内请求
  2. 10s过时后请求
    304

测试结果,10s内Js/Css走强缓存。HTML因为请求默认加max-age为0,走协商缓存返回304,不须要返回数据,Size由484B降至163B。

304
10s后 Js/Css缓存到期,所有走协商缓存,因为 Last-Modified一直没有改变,均返回304,不须要返回数据, Size降至163B。 返回304后,会重置 max-age,10s内请求无需请求服务器,依然是强缓存。

  1. 修改Js内容

修改 Js内容测试结果, Css没有修改依旧返回304。 Js修改致使 Last-Modified大于请求中的 If-Modified-Since,资源不够新鲜,返回200并返回最新数据。

6.4 总结

Last-Modified工做流程以下:

通常来讲,在没有调整服务器时间和篡改客户端缓存的状况下,这两个header配合起来管理协商缓存是很是可靠的,可是有时候也会服务器上资源其实有变化,可是最后修改时间却没有变化的状况,而这种问题又很不容易被定位出来,而当这种状况出现的时候,就会影响协商缓存的可靠性。因此就有了另一对header来管理协商缓存,这对header就是【ETagIf-None-Match

7. ETag 和 If-None-Match

7.1 ETag

这个header是服务器根据当前请求的资源生成的一个惟一标识,这个惟一标识是一个字符串,只要资源有变化这个串就不一样,因此能很好的补充Last-Modified的问题。 避免干扰,能够注释Last-modified逻辑。

ETag的验证也很是简单,只须要再加入一个中间件 koa-etag,重启服务测试。

7.2 ETag实践

发出请求,response已有Etag

下一次请求也会携带 If-None-Match为缓存中的 Etag值:

修改Js资源测试,结果以下:

修改 Js资源测试后,致使 Etag改变。服务端再验证资源不新鲜, Js资源从新获取,返回200。
Css没有修改, Etag没变返回304。

Etag总体流程和Last-Modified保持一致。

7.3 koa-etag源码解析

koaetag生成主要2个方法,具体的能够直接去看源码。

(1)根据文件的修改时间和文件大小生成

function stattag (stat) {
  var mtime = stat.mtime.getTime().toString(16)
  var size = stat.size.toString(16)

  return '"' + size + '-' + mtime + '"'
}
复制代码

(2)使用crypto库加密生成

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 + '"'
}

复制代码

5、新鲜度检测(Koa源码解读)

1. koa-conditional-get

在前面看到koa-conditional-get可让协商缓存生效,缘由是对资源新鲜度作了304返回的处理。

koa-conditional-get源码
那么重点来看下 ctx.fresh是如何处理的?

2. koa

能够看到Koa在request中的fresh方法以下:

状态码200-300之间以及304调用 fresh方法,判断该请求的资源是否新鲜。

3. fresh方法源码解读

只保留核心代码,能够自行去看fresh的源码。

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

function fresh (reqHeaders, resHeaders) {
   // 1. 若是这2个字段,一个都没有,不须要校验
  var modifiedSince = reqHeaders['if-modified-since']
  var noneMatch = reqHeaders['if-none-match']
  if (!modifiedSince && !noneMatch) {
    console.log('not fresh')
    return false
  }

  // 2. 给端对端测试用的,由于浏览器的Cache-Control: no-cache请求
  // 是不会带if条件的 不会走到这个逻辑
  var cacheControl = reqHeaders['cache-control']
  if (cacheControl && CACHE_CONTROL_NO_CACHE_REGEXP.test(cacheControl)) {
    return false
  }

  // 3. 比较 etag和if-none-match
  if (noneMatch && noneMatch !== '*') {
    var etag = resHeaders['etag']

    if (!etag) {
      return false
    }
    // 部分代码
    if (match === etag) {
        return true;
    }
  }
  
  // 4. 比较if-modified-since和last-modified
  if (modifiedSince) {
    var lastModified = resHeaders['last-modified']
    var modifiedStale = !lastModified || !(parseHttpDate(lastModified) <= parseHttpDate(modifiedSince))
    if (modifiedStale) {
      return false
    }
  }
  
  return true
}
复制代码

fresh的代码判断逻辑总结以下,知足3种条件之一,freshtrue

fresh代码

6、总结

浏览器缓存总体流程以下:

浏览器缓存总体流程

  1. 发出请求后,会先在本地查找缓存。
  2. 没有缓存去服务端请求最新的资源,返回给客户端(200),并从新进行缓存。
  3. 查到有缓存,要判断缓存本地是否过时(max-age等)。
  4. 没有过时,直接返回给客户端(200 from cache)。
  5. 若是缓存过时了,看是否有配置协商缓存(etag/last-modified),去服务端再验证该资源是否更新,本地缓存是否能够继续使用。
  6. 若是发现资源可用,返回304,告知客户端能够继续使用缓存,并根据max-age等更新缓存时间。不须要返回数据,加速请求时间。
  7. 若是服务端再验证失败,请求最新的资源,返回给客户端(200),并从新进行缓存。

咱们常说的强缓存,其实就是直接在本地缓存获取,也就是Cache-Control: max-age等配置,不须要和服务端沟通。

而协商缓存是在强缓存的基础上,配置etag或last-modified等参数。本地缓存失效后,去服务端进行新鲜度检测。能够避免每次本地缓存过时后都返回最新的数据,形成请求缓慢。

7、参考资料

本文的源码分析围绕koa,不表明其余服务框架。对这块知识不了解建议实践一下。写错的地方,接受批评指正~

相关文章
相关标签/搜索