“学过就忘”,一个知识点长时间不去读取,那么在你脑海中的基因片段会逐渐模糊,但是基因索引还是存在的。本篇文章通过实战理论想结合的方式来帮助大家更深刻的记忆常用知识点。
如有不足之处,恳请斧正。
一、什么是HTTP缓存 ?
http
缓存通俗来说: 当客户端向服务器请求资源时,不会直接向服务器获取资源(喂:服务器,给我一张最新的自拍图片)。而是先看下自己的存储里有没有上次保存的还在有效期内的资源。如果有的话,就省下了一笔运费(流量)。这里举一个简单的例子:
- 客户端需要
a.js
,于是发送一个请求头(1kb)
- 服务端响应后,返回
a.js(10kb)
+ 响应头(1kb)
- 如此反复每次就是
11kb
的流量传送
但是我们我们需要的文件a.js
的内容往往并没有发生变化,却仍然需要浪费流量,为了解决这个问题,于是人们提出了http
缓存这个概念。
HTTP缓存可以分为强缓存和协商缓存。
强缓存
- Expires
- Cache-Control
- Pragma
协商缓存
- Last-Modified
- If-Modified-Since
- ETag
- If-Not-Match
强缓存与协商缓存的区别:
用户大人
:我现在需要a.js
,你们帮我拿回来强缓存
: 稍等,我找下我这里有没有关于a.js
的备份,找到了。(消耗0kb
流量)协商缓存
: 我这里也有备份,不过我得问下服务端这个备份是不是最新款,发送请求头(1kb
流量)。服务端回复(1kb
流量)响应头则使用本地备份,若是返回a.js(10kb)
和响用头(1kb
)则使用服务器返回的最新数据。
强缓存
Expires
这是
HTTP 1.0
的字段,表示缓存到期时间,是一个绝对的时间 (当前时间 + 缓存时间)。在响应消息头中,请求资源前浏览器会用当前时间与其值对比,若是未过期则不需要再次请求。
新建cache.html
cache
cache
新建cache.js
let http = require('http'),
fs = require('fs'),
path = require('path'),
url = require("url")
http.createServer((req,res)=>{
let pathname = __dirname + url.parse(req.url).pathname; // 获取文件路径
let statusCode = 200 // 响应头状态码
let extname = path.extname(pathname) // 获取文件扩展名
let headType = 'text/html' // 内容类型
if( extname){
switch(extname){
case '.html':
headType = 'text/html'
break;
case '.js':
headType = 'text/javascript'
break;
}
}
fs.readFile(pathname, function (err, data) {
res.writeHead(statusCode, {
'Content-Type': headType,
'Expires':new Date(Date.now() + 10000).toUTCString() // 设置文件过期时间为10秒后
});
res.end(data);
});
}).listen(3000)
console.log("Server running at http://127.0.0.1:3000/");
启动
node cache.js
打开浏览器访问http://127.0.0.1:3000/cache.html
查看network
- 首次访问全部走网络请求资源
- 10s内再次刷新从内存中加载(
from memory cache
)
- 10s内关闭tab,重新打开请求的cache.html,从磁盘加载(
from disk cache
)
- 10s以后请求,缓存已经失效,重复第1步
缺点:
-
Expires
过期控制不稳定。因为如果我们修改电脑的本地时间,会导致浏览器判断缓存失效。
Cache-control
这是HTTP1.1
的字段,这是一个相对时间'Cache-Control':'max-age=20'
意思是20秒内使用缓存资源,不用请求服务器
// 省略其他代码
fs.readFile(pathname, function (err, data) {
res.writeHead(statusCode, {
'Content-Type': headType,
'Cache-Control':'max-age=20'
});
Cache-Control 除了max-age 相对过期时间以外,还有很多属性
1. no-store
:所有内容都不缓存
2. no-cache
:缓存,但是浏览器使用缓存前,都会请求服务器判断缓存资源是否是最新,它是个比较高贵的存在,因为它只用不过期的缓存。
3. public
客户端和代理服务器(CDN)都可缓存
4. private
只有客户端可以缓存
更多属性可参考 Cache-Control
Tips:也许大家在这里有一些小疑问,为什么对cache.html
设置Expires
或Cache-Control
在谷歌浏览器里不生效。这时候查看request header
发现Cache-Control: max-age=0
,浏览器强制不用缓存。
这是因为浏览器会针对的用户不同行为采用不同的缓存策略,这样会导致在不同的浏览器会产生不同的现象:
Chrome does something quite different: 'Cache-Control' is always set to 'max-age=0′, no matter if you press enter, f5 or ctrl+f5. Except if you start Chrome and enter the url and press enter
.
pragma
pragma是http/1.1之前版本的历史遗留字段,仅作为与http的向后兼容而定义。这里不做讨论。感兴趣的朋友可以点击了解 Pragma
node 热更新
这里如果大家觉得每次修改cache.js
文件都需要重新执行node run cache.js
,这里我们可以配置node 热更新
npm init
npm i -D nodemon
npm i -D
为npm install --save-dev
的缩写
修改package.json
"scripts": {
"dev": "nodemon ./bin/www"
},
下面我们只需要执行一次npm run dev
即可
对比缓存(协商缓存)
上面的强缓存依旧存在着很大的缺陷。当设置的时间过期后,不管文件内容有没有变化,我们不得不再次向服务器请求资源。这时候我们就需要用到协商缓存了。
Last-Modified
响应值,由服务器返回给客户端关于请求资源的最近修改时间 (GMT标准格式)
If-Modified-Since
请求值 , 由客户端发送给服务端上次返回的资源修改时间
首次请求时服务端会携带Last-Modified
返回给客户端,客户端将其数值保存起来,并重新命名为If-Modified-Since
再次请求时,客户端会先发送一个携带If-Modified-Since
的请求头发送到服务端,服务端会比较请求头的If-Modified-Since
和服务器请求资源上次的修改时间(Last-Modified)
.
- 如果资源已经被修改:那么返回响应资源
a.js(10kb)
+ 响应头(1kb)
,状态码:200 OK
- 如果没有被修改:那么只返回响应头
(1kb)
,状态码:304 Not Modified
// 省略其他代码
let stat = fs.statSync(pathname);
fs.readFile(pathname, function (err, data) {
// 判断请求头的文件修改时间是否等于服务端的文件修改时间
if(req.headers['if-modified-since'] === stat.mtime.toUTCString()) { // mtime为文件内容改变的时间戳
statusCode = 304;
}
res.writeHead(statusCode, {
'Content-Type': headType,
'Last-Modified':stat.mtime.toUTCString()
});
res.end(data);
});
-
last-modified
是以秒为单位的,如果资源在1s内修改多次,由于1s内last-modified
并未改变,客户端仍然会使用缓存。 - 如果在服务器上请求的资源(
a.js
)被修改了,但其实际内容根本没发生改变,会因为 Last-Modified 时间匹配不上而重新返回 a.js 给浏览器(举例:服务器动态生成文件)
为了解决上述问题,使用新的字段 ETag 和 If-None-Match
ETag
响应值,由服务器返回给客户端根据文件内容,算出的一个唯一的值 (GMT标准格式)
If-Not-Match
请求值 , 由客户端发送给服务端上次返回的资源唯一值
请求流程与Last-Modified一致
上面我们所述last_modified
一般由mtime
组成,而ETag
一般由last_modified
与content_length
组成
// 省略其他代码
fs.readFile(pathname, function (err, data) {
let Etag = `${data.length.toString(16)}${stat.mtime.toString(16)}`
if((req.headers['if-modified-since'] === stat.mtime.toUTCString()) || (req.headers['if-none-match'] === Etag)) {
statusCode = 304;
}
res.writeHead(statusCode, {
'Content-Type': headType,
Etag
});
res.end(data);
});
缓存优先级
当多种缓存同时存在时,浏览器改使用哪种缓存方式呢?这里就有了一个优先级关系
- 强缓存与协商缓存同时存在时,如果强缓存还在生效期则强制缓存,否则使用协商缓存。(强缓存优先级 > 对比缓存优先级)
- 强缓存
expires
和cache-control
同时存在时,cache-control
会覆盖expires
(cache-control
优先级 >expires
优先级。) - 对比缓存
Etag
和Last-Modified
同时存在时,Etag
会覆盖Last-Modified
效。(ETag
优先级 >Last-Modified
)优先级。
最佳实践
上面只是让大家更好的了解了http缓存的大概知识点,那么在实际开发中我们是如何如何利用缓存实现更好的用户体验的呢?
相信webpack
大家已经并不陌生,如果有过实际配置经验的同学一定记得我们在配置出口文件output
或者打包图片的filename
时往往会加上hash || chunkhash || contenthash
这些字段
module.exports = {
output:{
path:path.resolve(__dirname,'../dist'),
filename:'js/[name].[contenthash:8].js',
chunkFilename:'js/[name].[chunkhash:8].js'
},
loader:{
rules:[
{
test:/\.(jep?g|png|gif)$/,
use:{
loader:'url-loader',
options:{
limit:10240,
fallback:{
loader:'file-loader',
options:{
name:'img/[name].[hash:8].[ext]'
}
}
}
}
}
]
}
}
这样打包出来的文件名称往往如下
这样如果每次文件有变化,那么文件名称随即变化。浏览器则会重新请求这些文件资源。如果文件名称与上次一致,那么则会使用到http缓存策略。