Skip to content

HTTP 缓存(附 Express 实现代码) #4

@lilins

Description

@lilins

缓存是一种保存资源副本并在下次请求时直接使用该副本的技术,对于提高前端响应速度,缩短页面加载有十分显著的效果。前端缓存技术也有非常多,例如 CDN 缓存、DNS 缓存、NGINX 缓存、反向代理缓存和浏览器缓存等。今天要说明的是 HTTP 缓存是浏览器缓存中的一种。

常见的 HTTP 缓存只能缓存 GET 响应,一般会通过在请求 (Request) 和响应 (Response) 的 HTTP 报文 Header 上添加不同的字段以达到不同的缓存策略。

Header 上和缓存有关的字段

以下是 RFC2616 规定的 47 种 HTTP 报文首部字段中与缓存相关的字段:

字段名 Request/Response 说明
Cache-Control Request/Response 可设置不同的缓存策略
Pragma Request/Response HTTP/1.0 的字段,可忽略
If-Match Request 与 ETag 一起使用,服务器会比较 ETag 是否一致
If-None-Match Request 同上,结果取反
If-Modified-Since Request 与 Last-Modified 一起使用,服务器会比较最后更新的时间是否一致
If-Unmodified-Since Request 同上,结果取反
ETag Response 资源的标识符
Expires Response 值为时间,在此时间之后 Response 过期,可忽略
Last-Modified Response 资源最后更新的时间

Cache-Control

最为丰富的是 Cache-Control 的使用,所使用的值可参考语法。这里列举一下例子:

完全禁用缓存

Cache-Control: no-cache, no-store, must-revalidate

no-cache 表示不直接使用缓存,而是先向服务器进行验证(是否过期)

no-store 表示所有的内容都不允许缓存

must-revalidate 表示必须与服务器进行验证,若请求失败返回 504

缓存静态资源

Cache-Control:public, max-age=3600

public 表示可以被任何人缓存

max-age=3600 表示缓存最大周期为 3600 秒(1小时),即从接受到资源后的 3600 秒之后才会过期

Cache-Control 也有一些问题,在上例中,如果超过1小时后,这个请求将会失效,那么浏览器会从服务器强制更新,而此时很可能这个静态资源没有任何改动,这就浪费了时间和带宽。

为了解决这个问题,在 Cache-Control: max-age:0 的基础上,出现了新的 Header 字段来处理这件事,即 Last-ModifiedETag

Last-Modified

Last-Modified 的格式为 <day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT

在服务器的 Response Header 中设置 Last-Modified 后,浏览器会自动在下次发送 Request 附带 If-Modified-SinceIf-Unmodified-Since

服务器接收到请求后,就会根据 Header 判断是否符合资源最后更新的时间,如果符合的话,会返回 304 状态码。此时,并不会发送完整的资源 Response,浏览器根据返回的 304 状态码从缓存中返回资源以达到节省带宽、加快页面响应速度的效果。

ETag

ETag 的格式为 W/<etag_value>

W/ 是可选项,表示使用弱验证器。弱验证器容易生成,但不利于比较。<etag_value> 则通常使用时间戳的哈希值。ETag 配合 If-MatchIf-Not-Match 可以同样作为判断资源是否需要缓存的依据。

服务器会在 Response Header 中设置 ETag ,浏览器会在下次发送 Request 附带 If-MatchIf-Not-Match 进行匹配,如果匹配成功则返回 304 状态码告诉浏览器资源依然可以用。

优先级

在上面提到的三种缓存策略,也可以分为两种:强缓存(expires, Cache-Control)和对比缓存(也被称为协商缓存,Last-ModifiedETag)。同时存在各种缓存时,各缓存优先级:

1、强缓存和对比缓存同时存在,如果强缓存还在生效期则强制缓存覆盖对比缓存,对比缓存不生效;如果强缓存不在有效期,对比缓存生效。即:强缓存优先级 > 对比缓存优先级

2、强缓存 expiresCache-control 同时存在时,则 Cache-control 会覆盖 ExpiresExpires 无论有没有过期,都无效。 即:Cache-control 优先级 > Expires 优先级。

3、对比缓存 EtagLast-Modified 同时存在时,则 Etag 会覆盖 Last-ModifiedLast-Modified 不会生效。即:ETag 优先级 > Last-Modified 优先级。

补充:Pragma 优先级 > cache-control 优先级,Pragma 通常不使用,有限的使用场景是在 Chrome 的 devtools 中启用 disable cache 时,会在所有的 Request Header 上添加 Pragma: no-cache

在 Express 上的实践

我们可以通过 Express 来进行 Cache 的试验,以下是文件结构:

/static
--/index.html
--/cache.js
index.js

代码文件可见 Example

index.js 是用来启动 Express 的,static 文件夹中存放着启动页面和一个用来测试 Cache 的 JS 资源文件,启动页面中引入了 JS 资源文件。

首先是 Cache-Control,为了实现效果,我们需要暂时把 Last-ModifiedETag 停用:

// index.js
const express = require('express')
const path = require('path')
const app = express()

const opts = {
  etag: false,
  lastModified: false,
  maxAge: 86400000
}
app.use('/static', express.static(path.join(__dirname, 'static'), opts))

在浏览器中输入 localhost:3000/static/index.html,在第一次获取中可以看到返回 200,刷新页面发现 JS 资源文件被缓存:

1555772893898

并且 max-age 设置也完全正确。

1555772931069

接着我们来看 Last-Modified ,要注意的是,这里必须设置 maxAge: 0 ,因为 Last-ModifiedETag 是在 Cache-Control: max-age=0 的基础上生效的。

// index.js
const express = require('express')
const path = require('path')
const app = express()

const opts = {
  etag: false,
-  lastModified: false,
-  maxAge: 86400000
+  lastModified: true,
+  maxAge: 0
}
app.use('/static', express.static(path.join(__dirname, 'static'), opts))

使用 Ctrl+F5 强制刷新后,再普通刷新一次,我们可以发现,JS 资源已经被缓存,显示 304 状态码。

1555773340933

// index.js
const express = require('express')
const path = require('path')
const app = express()

const opts = {
-  etag: false,
+  etag: true,
   lastModified: false,
-  maxAge: 86400000
+  maxAge: 0
}
app.use('/static', express.static(path.join(__dirname, 'static'), opts))

同样状态下修改 etag,也成功显示缓存。

1555822403717

(本文完)

参考

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions