前言
大家好,我是林三心,用最通俗易懂的话讲最难的知识点 是我的座右铭,根底是进阶的前提 是我的初心
背景
无论是开发中或者是面试中,HTTP 缓存 都是十分重要的,这体现在了两个方面:
- 开发中 :正当利用
HTTP 缓存
能够进步前端页面的性能 - 面试中 :
HTTP 缓存
是面试中的高频问点
所以本篇文章,我不讲废话,我就通过 Nodejs
的简略实际,给大家讲最通俗易懂的HTTP 缓存,大家通过这篇文章肯定能理解把握它!!!
前置筹备
筹备
-
创立文件夹
cache-study
,并筹备环境npm init
-
装置
Koa、nodemon
npm i koa -D npm i nodemon -g
- 创立
index.js、index.html、static 文件夹
-
index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <link rel="stylesheet" href="./static/css/index.css"> </head> <body> <div class="box"> </div> </body> </html>
-
static/css/index.css
.box { width: 500px; height: 300px; background-image: url('../image/guang.jpg'); background-size: 100% 100%; color: #000; }
static/image/guang.jpg
-
index.js
const Koa = require('koa') const fs = require('fs') const path = require('path') const mimes = { css: 'text/css', less: 'text/css', gif: 'image/gif', html: 'text/html', ico: 'image/x-icon', jpeg: 'image/jpeg', jpg: 'image/jpeg', js: 'text/javascript', json: 'application/json', pdf: 'application/pdf', png: 'image/png', svg: 'image/svg+xml', swf: 'application/x-shockwave-flash', tiff: 'image/tiff', txt: 'text/plain', wav: 'audio/x-wav', wma: 'audio/x-ms-wma', wmv: 'video/x-ms-wmv', xml: 'text/xml', } // 获取文件的类型 function parseMime(url) { // path.extname 获取门路中文件的后缀名 let extName = path.extname(url) extName = extName ? extName.slice(1) : 'unknown' return mimes[extName] } // 将文件转成传输所需格局 const parseStatic = (dir) => {return new Promise((resolve) => {resolve(fs.readFileSync(dir), 'binary') }) } const app = new Koa() app.use(async (ctx) => { const url = ctx.request.url if (url === '/') { // 拜访根门路返回 index.html ctx.set('Content-Type', 'text/html') ctx.body = await parseStatic('./index.html') } else {const filePath = path.resolve(__dirname, `.${url}`) // 设置类型 ctx.set('Content-Type', parseMime(url)) // 设置传输 ctx.body = await parseStatic(filePath) } }) app.listen(9898, () => {console.log('start at port 9898') })
启动页面
当初你能够在终端中输出
nodemon index
,看到下方的显示,则代表胜利启动了服务
此时你能够在浏览器链接里输出 http://localhost:9898/
,关上看到如下页面,则代表页面拜访胜利!!!
HTTP 缓存品种
HTTP 缓存
常见的有两类:
-
强缓存
:能够由这两个字段其中一个决定expires
cache-control(优先级更高)
-
协商缓存
:能够由这两对字段中的一对决定Last-Modified,If-Modified-Since
Etag,If-None-Match(优先级更高)
强缓存
接下来咱们就开始讲 强缓存
expires
咱们只需设置响应头里 expires
的工夫为 以后工夫 + 30s
就行了
app.use(async (ctx) => {
const url = ctx.request.url
if (url === '/') {
// 拜访根门路返回 index.html
ctx.set('Content-Type', 'text/html')
ctx.body = await parseStatic('./index.html')
} else {const filePath = path.resolve(__dirname, `.${url}`)
// 设置类型
ctx.set('Content-Type', parseMime(url))
// 设置 Expires 响应头
const time = new Date(Date.now() + 30000).toUTCString()
ctx.set('Expires', time)
// 设置传输
ctx.body = await parseStatic(filePath)
}
})
而后咱们在前端页面刷新,咱们能够看到申请的资源的响应头里多了一个 expires
的字段
并且,在 30s 内,咱们刷新之后,看到申请都是走 memory
,这意味着,通过 expires
设置强缓存的时效是 30s,这 30s 之内,资源都会走本地缓存,而不会从新申请
留神点:有时候你 Nodejs 代码更新了时效工夫,然而发现前端页面还是在走上一次代码的时效,这个时候,你能够把这个
Disabled cache
打钩,而后刷新一下,再勾销打钩
cache-control
其实 cache-control
跟 expires
成果差不多,只不过这两个字段设置的值不一样而已,前者设置的是 秒数
,后者设置的是 毫秒数
app.use(async (ctx) => {
const url = ctx.request.url
if (url === '/') {
// 拜访根门路返回 index.html
ctx.set('Content-Type', 'text/html')
ctx.body = await parseStatic('./index.html')
} else {const filePath = path.resolve(__dirname, `.${url}`)
// 设置类型
ctx.set('Content-Type', parseMime(url))
// 设置 Cache-Control 响应头
ctx.set('Cache-Control', 'max-age=30')
// 设置传输
ctx.body = await parseStatic(filePath)
}
})
前端页面响应头多了 cache-control
这个字段,且 30s 内都走本地缓存,不会去申请服务端
协商缓存
与 强缓存
不同的是, 强缓存
是在时效工夫内,不走服务端,只走本地缓存;而 协商缓存
是要走服务端的,如果申请某个资源,去申请服务端时,发现 命中缓存
则返回 304
,否则则返回所申请的资源,那怎么才算 命中缓存
呢?接下来讲讲
Last-Modified,If-Modified-Since
简略来说就是:
- 第一次申请资源时,服务端会把所申请的资源的
最初一次批改工夫
当成响应头中Last-Modified
的值发到浏览器并在浏览器存起来 - 第二次申请资源时,浏览器会把刚刚存储的工夫当成申请头中
If-Modified-Since
的值,传到服务端,服务端拿到这个工夫跟所申请的资源的最初批改工夫进行比对 - 比对后果如果两个工夫雷同,则阐明此资源没批改过,那就是
命中缓存
,那就返回304
,如果不雷同,则阐明此资源批改过了,则没命中缓存
,则返回批改过后的新资源
// 获取文件信息
const getFileStat = (path) => {return new Promise((resolve) => {fs.stat(path, (_, stat) => {resolve(stat)
})
})
}
app.use(async (ctx) => {
const url = ctx.request.url
if (url === '/') {
// 拜访根门路返回 index.html
ctx.set('Content-Type', 'text/html')
ctx.body = await parseStatic('./index.html')
} else {const filePath = path.resolve(__dirname, `.${url}`)
const ifModifiedSince = ctx.request.header['if-modified-since']
const fileStat = await getFileStat(filePath)
console.log(new Date(fileStat.mtime).getTime())
ctx.set('Cache-Control', 'no-cache')
ctx.set('Content-Type', parseMime(url))
// 比对工夫,mtime 为文件最初批改工夫
if (ifModifiedSince === fileStat.mtime.toGMTString()) {ctx.status = 304} else {ctx.set('Last-Modified', fileStat.mtime.toGMTString())
ctx.body = await parseStatic(filePath)
}
}
})
第一次申请时,响应头中:
第二次申请时,申请头中:
因为资源并没批改,则命中缓存,返回 304:
此时咱们批改一下 index.css
.box {
width: 500px;
height: 300px;
background-image: url('../image/guang.jpg');
background-size: 100% 100%;
/* 批改这里 */
color: #333;
}
而后咱们刷新一下页面, index.css
变了,所以会 没命中缓存
,返回 200 和新资源,而 guang.jpg
并没有批改,则 命中缓存
返回 304:
Etag,If-None-Match
其实 Etag,If-None-Match
跟 Last-Modified,If-Modified-Since
大体一样,区别在于:
- 后者是比照资源最初一次批改工夫,来确定资源是否批改了
- 前者是比照资源内容,来确定资源是否批改
那咱们要怎么比对资源内容呢?咱们只须要读取资源内容,转成 hash 值,前后进行比对就行了!!
const crypto = require('crypto')
app.use(async (ctx) => {
const url = ctx.request.url
if (url === '/') {
// 拜访根门路返回 index.html
ctx.set('Content-Type', 'text/html')
ctx.body = await parseStatic('./index.html')
} else {const filePath = path.resolve(__dirname, `.${url}`)
const fileBuffer = await parseStatic(filePath)
const ifNoneMatch = ctx.request.header['if-none-match']
// 生产内容 hash 值
const hash = crypto.createHash('md5')
hash.update(fileBuffer)
const etag = `"${hash.digest('hex')}"`
ctx.set('Cache-Control', 'no-cache')
ctx.set('Content-Type', parseMime(url))
// 比照 hash 值
if (ifNoneMatch === etag) {ctx.status = 304} else {ctx.set('etag', etag)
ctx.body = fileBuffer
}
}
})
验证形式跟刚刚 Last-Modified,If-Modified-Since
的一样,这里就不反复阐明了。。。
总结
参考资料
- https://blog.csdn.net/qq_3243…
结语
我是林三心,一个热心的前端菜鸟程序员。如果你上进,喜爱前端,想学习前端,那咱们能够交朋友,一起摸鱼哈哈,摸鱼群,加我请备注【思否】