前言
我们是否发现当我们第一次打开一个网站时内部资源加载速度比较缓慢,而当我们之后再一次去访问这个相同的网站时速度就会变得很快,这种手段其实就是我们今天要聊的主角--浏览器的缓存
1.当我们通过后端发送一些请求,资源等发送给前端时,后端都会传递响应头,请求头两部分,其实这两部分主要是提供一种内容协商机制,而我们在在后端传递资源时都要放入
res.writeHead(200,{'Content-type':'text/html';})
其实这段代码就是后端在请求头中告诉前端返回资源的数据类型是属于什么类型的资源,我们通过在后端代码对'Content-type':'text/html'内部进行更改就能传递我们想要传递的是数据类型资源。
正是我们通过请求头,和请求体两部分来对 HTTP 后端内容资源之间的数据类型等协商,这就是一种协商机制。
当我们已经了解到在 HTTP 中存在这样一种机制,那我们来看一下静态资源传输时又会有哪些操作。
我们通过静态内容传输将自身在后端写入的静态资源从服务器传入到浏览器的过程。例如我们在文件中添加www的文件夹,在此文件夹下放入一个index.html的文件,然后再向前端返回此文件
// www/index.html
<body>
<h1>你好 世界</h1>
<img src="assets/img.jpg" alt="">
</body>
我们通过获取路径,然后通过流式的方法将内容返回个前端
// index.js
const http = require('http')
const path = require('path')
const fs = require('fs')
const mime = require('mime') // npm i mine@3 安装
const server = http.createServer((req, res) => {
let filePath = path.resolve(__dirname, path.join('www', req.url)) // 将文件路径拼接
if (fs.existsSync(filePath)) { // 判断文件是否存在
const stat = fs.statSync(filePath) // 获取文件信息
const isDir = stat.isDirectory() // 判断是否为文件夹
if (isDir) {
filePath = path.join(filePath, 'index.html') // 如果是文件夹,默认返回index.html
}
if (!isDir || fs.existsSync(filePath)) { // 向前端返回文件
const { ext } = path.parse(filePath) // 获取文件后缀
res.writeHead(200, { 'Content-Type': mime.getType(ext) })
// 边加载边输出给前端
const readStream = fs.createReadStream(filePath) // 创建可读流
readStream.pipes(res) // 将可读流的数据,通过管道输出到前端
}
}
})
server.listen(3000)
最后我们来看浏览器的返回头里面
就会识别我们传入的是一份图片的静态资源。
HTTP 缓存
试想我们通过上面对 HTTP 给前端传递资源方式发现,当我们每次刷新页面时,资源都会重新加载,前端每次都会向后端发送资源的请求。那我们如果数据不会发生改变时,那当我们每次传入很大的数据时都会刷新,这样就会很多无效时耗,那我们该如何进行优化可以对页面上一直长时间不变的地方进行缓存,下次访问时会直接获取
- HTTP 缓存: 将页面上长时间不更新的资源缓存到浏览器上, 下次访问页面时该部分资源直接从缓存中获取,从而减少了网络请求次数 提高了页面加载速度。
强缓存
- 我们通过在响应头中设置 Cache-Control 字段,该字段的值为 max-age=xxx,表示缓存该资源的有效期 为 xxx 秒。
代码示例
const http = require('http')
const path = require('path')
const fs = require('fs')
const mime = require('mime')
const server = http.createServer((req, res) => {
let filePath = path.resolve(__dirname, path.join('www', req.url))
// console.log(filePath);
if (fs.existsSync(filePath)) { // 判断文件是否存在
const stats = fs.statSync(filePath) // 获取文件信息
const isDir = stats.isDirectory() // 判断是否为文件夹
// console.log(stats);
// console.log(isDir);
if (isDir) { // 如果是文件夹,默认返回index.html
filePath = path.join(filePath, 'index.html')
}
if (!isDir || fs.existsSync(filePath)) {
const { ext } = path.parse(filePath)
const timeStamp = req.headers['if-modified-since']
console.log(timeStamp);
console.log(stats.mtimeMs);
let status = 200
if (timeStamp && Number(timeStamp) === stats.mtimeMs) { // 文件没有发生更改
// 状态码设置为304会通知浏览器这个资源没有发生更改,直接缓存起来
status = 304
}
res.writeHead(status, {
'content-type': mime.getType(ext), // 根据后缀名生成对应响应头类型
'cache-control': 'max-age=86400', // 强缓存一天
})
const readStream = fs.createReadStream(filePath) // 创建可读流
readStream.pipe(res) // 将可读流的数据,通过管道,输出到前端
}
}
})
server.listen(3000)
浏览器运行实现
我们可以发现当我们每次刷新页面后页面中的静态资源
.png的图片资源并没有耗时这就表明浏览器直接使用缓存中的内容,没有向HTTP发送请求.但是我们可以根据上面图片发现,为什么html文件重新向后端请求了?
这就是我们要注意的一方面
-
通过浏览器 url 地址栏请求的资源, 请求头中就会携带 Cache-Control: max-age=0 字段, 也就意味着这种情况下请求的资源无法强缓存
-
被缓存的资源在浏览器的 cache-Storage 中, 本质还在硬盘上
-
强制刷新浏览器,会强制清空浏览器的 cache-Storage
协商缓存
强缓存对浏览器地址栏访问的资源无效,所以浏览器提供了协商缓存的机制
- 浏览器第一次访问资源时,响应头中携带 last-modified 字段, 值为该资源的最后修改时间, 当浏览器接收到响应头后,会在该资源再次被请求时,在请求头中自动携带if-modified-since 字段,值为 last-modified 字段的值,后端校验请求头中的 if-modified-since 和 last-modified 字段是否一致,一致则返回304,如果不一致则返回200,浏览器会重新请求该资源(可以简单的理解为当我们最后一次更改浏览器的内容是他会存储最后修改的时间搓,当我们之后刷新时如果时间搓没有发生改变,且没超过其强缓存时间时那么他就会直接从浏览器缓存中拿出资源使用不会再去向后端请求资源)
注意:
- last-modified + if-modified-since 存在的问题: 当文件被修改后带内容没有变更,last-modified 的值会更新,从而导致该资源重新求
对此问题的处理
-
文件指纹 etag + if-none-match
浏览器第一次访问资源时,响应头中携带 etag 字段, 值为该资源的最后修改签名, 当浏览器接收到响应头后,会在该资源再次被请求时,在请求头中自动携带if-none-match 字段,值为 etag 字段的值,后端校验请求头中的 if-none-match 和 etag 字段是否一致,一致则返回304,如果不一致则返回200,浏览器会重新请求该资源
总结
-
只用强缓存可以把除url地址栏访问的资源都缓存起来,但是资源更新了就无法第一时间让前端获取到,所以还需要协商缓存
-
只要命中了强缓存,就不会走协商缓存,只有强缓存到期,才会走协商缓存
-
为了保证资源更新前端能够及时获取到,一般会在文件名后加上文件指纹(用内容生成 hash 值), 这样文件指纹就会改变,从而触发资源文件更新