前言: 今天来聊一聊浏览器与服务器端如何配合进行数据缓存这件事。
Tips: 如果你不太明白前后端交互这件事的本质,建议你先阅读下面的文章
🚀:# 技术面🧑🏫:前端代码是如何与服务器交互的
一. 用 express 搭建一个简单的 web 服务器
-
这是你的目录结构,你需要在
static
文件下放置两张图片用来接下来的演示效果。 -
server.js
的内容如下,在此之前别忘了npm i express
const express = require('express'); const http = require('http'); const fs = require('fs'); const path = require('path');const html_path = path.resolve(__dirname, 'index.html'); const html = fs.readFileSync(html_path); const app = express(); // req 和 res 是经过 express 二次封装后的对象 app.get('/', (req, res) => { res.setHeader('Content-Type', 'text/html'); res.send(html); res.end(); }); app.use(express.static(path.resolve(__dirname, 'static'))); const server = http.createServer(app); server.listen(5008);
-
这是你的
index.html 文件
,这里我们用两张图片来演示两种缓存的差别。 Document浏览器的强缓存和协商缓存
二. 协商缓存
-
当浏览器首次访问服务器这两张图片资源的时候,我们来观察一下 chrome network 选项卡里是如何展示的。(注意,我这里勾选了只显示 img 类型的文件)可以看到第一次请求的时候返回的
Status
是一个深颜色的200
并且size
字段有值。
-
当我们浏览器左上角的刷新按钮后,你会发现
status
变为了304
。 -
经过查阅
MDN
我们会发现这个状态码标志着服务端无需重新发送数据(注意其中的一段话:该响应必须不包含主体),意味着浏览器这端可以直接使用当前的缓存数据。
-
但是你可能会疑问,why? 我明明什么都没配置,怎么就莫名其妙走了缓存了呢?这是因为我们此时使用
express.static()
方法托管静态资源的时候,并没有传递第二个选项参数。经过查阅
express
的文档可以得知。etag
和lastModified
这两个响应头信息都默认为true
,而这两个字段就是用来控制协商缓存的。 -
我们在
network
选项卡中也能看到对应的响应头信息,Eatg
的值是根据文件内容生成的hash
值,Last-Modified
的值是当前文件最后一次修改的信息。 -
我们打开文件系统看一下这张图片的信息,因为我当前的时区是中国时区,所以是
GMT+8
,而express
返回的是不带时区偏移的值。18:50 -8:00
也就是10:50
,这就是Last-Modified
字段的默认值,但是一定要记住,这些响应头都是可以通过setHeader
人为修改的,不过一般不会这样做。 -
这两个值其实分别对应了浏览器端的
if-Modified-Since
和if-None-Match
字段 -
我们强刷一遍浏览器,再来体会整个流程。右键刷新按钮,点击第三个按钮。
-
此时是浏览器首次请求该资源,所以浏览器的请求头信息里是不会带上面提到的
if-Modified-Since
和if-None-Match
这两个字段的。取而代之的是Cache-Control:no-cache
,意思是客户端本次请求不使用缓存,后续期望使用协商缓存。
-
当我们进行一个普通刷新后,你会发现这一次的请求头信息里就包含了之前我们提到的这两个数据。
-
之后浏览器请求相同路径的资源后,浏览器会带着这两个请求头字段去请求服务器,服务器通过计算当前文件的
hash
和读取当前文件的最后修改日期来和请求头里的这两个字段做对比,如果没有变化,则返回给浏览器304
状态码,且不会包含响应体。此时浏览器就知道:“哦,我可以直接使用缓存”。 -
当服务端检测到这两个数据信息不一致的时候,会更新
Etag
和Last-Modified
这两个字段,然后设置状态码为200
并且响应体返回最新的数据,然后浏览器重新更新if-Modified-Since
和if-None-Match
为最新值。 -
协商缓存可以用在比如说用户头像,官网
html
文件更新,不希望用户看到的还是老的数据,界面更新这样的场景,通常这些资源都是可能会发生变化,但是不确定什么时间变化。协商缓存既避免了重复请求获取相同资源,又保障了用户可以看到最新数据。 -
也可以在前端构建打包的时候,将第三方库文件和业务逻辑文件分别进行 chunk 打包,第三方库文件的打包生成的 hash 改动通常不会那么频繁,而我们的业务逻辑文件是改动相对频繁,打包生成的 hash 也就会不一样。将这两种文件区分开来,再使用协商缓存控制,既减少了资源的浪费,也不影响用户体验。
三. 强缓存
-
有了上面协商缓存的前置知识,我们就知道在开始前需要在调用
express.static(path,[option])
时添加第二个参数,将etag
和lastModified
设置为false
,这样就取消了服务器与客户端之间的协商缓存。
可以看到,此时点击刷新都会重新获取一次服务端资源。
-
此时观察响应头信息,
Cache-Control:
字段被设置为public,max-age=0
,其中public
字段的意思是可以被所有用户缓存,此处的所有用户包括终端用户或者中间的 CDN 服务器。与之对应的还有private
,意思是只运行终端用户缓存,不允许中间服务器进行缓存。这里的关键点就在于响应头字段的max-age=0
(单位秒)。这个值代表的含义是,服务器告诉浏览器,从现在开始(指第一次请求获取资源成功后),浏览器你都不需要重新再请求我了。当浏览器下次请求相同资源的时候,会将当前时间与该字段的时间进行差值对比,如果当前时间 - 第一次请求时间 < max-age 的值
, 说明浏览器需要使用强制缓存,但由于max-age
的值为 0,就代表着浏览器每次都需要请求服务器获取来获取当前数据。 -
此时我们再去更改服务端的相关配置,将响应头的
cache-control
的max-age
字段设置稍微大一点,设置为 10 秒。浏览器首次请求成功后,会记录本次请求时间节点,当下次请求相同路径时,同样做对比,如果发现本次请求时间未超过上一次请求时max-age
设置的值,则不再发起请求。(与协商缓存不同,强缓存是真正意义上的不再向服务器发起请求)
此时可以看到,浏览器在 10 秒内的请求都会直接在本地获取(size 为 memory cache),而在 10 秒之后,又重新触发了新的请求。
-
强缓存的特点就是不再对服务器端发送一次新的请求,不需要和服务器进行协商,浏览器这边直接使用缓存。可以很直观的感受到,强缓存并不适合使用在一些不知道何时会发生变化的资源上,比如用户头像,你无法得知用户什么时候会重新上传新的头像。但是像官网的公司 Logo 这种静态资源,就很适合将
max-age
设置为一个较大的值来进行强缓存
四. 强缓存和协商缓存一同协作公示
-
看到这里,你可能会想到,我同时开启协商缓存和强缓存会怎样呢?
-
实践出真知,我们来测试一下,可以看到浏览器会优先采用强缓存策略,那协商缓存就没有了吗?
-
为了方便演示,我将
max-age
的值修改为5秒,接下来我们看整个流程会是怎样。
-
我们就可以发现浏览器首先使用强缓存,5秒过后触发协商缓存,此处有个非常重要的逻辑我们需要理清。协商缓存浏览器是会真正发送一个请求到服务器的,而此时服务器又响应了
max-age
,就会触发浏览器重新记录当前时间戳。于是在之后的 5 秒内又会触发强缓存,依此循环。
五. 额外补充
现在再去观察京东和淘宝这些购物平台,你就会发现它们都是利用强缓存+协商缓存的方案来保存静态资源文件的。
响应头信息包含:
Expires
字段是 http1.0
提出的方案,是一个精确的时间戳,它表示的含义和 max-age
是类似的,都是告知浏览器,在过期时间之内,都使用强缓存 。显而易见,这里有个明显的弊端,由于这个值是响应头,也就是服务器设置的值,那么客户端就需要和服务器内的系统时间保持一致,才可以让 Expires
正常工作。于是在 http1.1
的时候 max-age
被提出用来代替 expires
,当响应头中包含 chache-control:max-age
时,因为 max-age
的值是一个相对时间,是用来和客户端首次发送请求时做比较的,客户端无需关系服务器内部系统时间。当二者同时存在的时候expires
会被忽略。
阅读到这里,我相信你对这两种缓存模式已经有了自己的认知。