title: 前端缓存详解 date: 2022-02-23 14:29:19 updated: 2022-02-23 14:29:19 photos:
前端缓存是我们日常开发中非常重要的一部分,利用缓存我们可以做很多的事情。例如:
了解完前端缓存能做的事情后,接下来了解一下前端缓存的组成和实现吧~
首先,前端缓存可以分为 HTTP缓存
和 浏览器缓存
。
约定
保存指定对应的数据。HTTP缓存又分为强缓存
和协商缓存
。Cookie
,localStorage
,sessionStorage
,IndexDB
。介绍完前端缓存的分类,接下来详细了解一下每一个缓存的作用和使用。
首先来看一下一个HTTP请求的流程
可以看到,在向服务器请求之前,会先向浏览器缓存查找,是否有这个缓存,查找缓存的根据是url
。
如果有缓存则立即返回
数据。如果没有找到缓存或者缓存已经过期,此时需要分情况讨论:
强缓存是通过设置HTTP请求头来设置的,具体的字段有:
Expires: HTTP/1.0 使用的字段,值为资源的过期时间。利用服务器时间与客户端作对比来判断缓存是否过期。(如果服务器与客户端时间不同步,则会造成缓存失效)
Cache-Control: Cache-Control字段同样也可以设置强缓存,而且优先级比Expires高。Cache-Control使用max-age来设置缓存过期时间,即在HTTP请求中返回一个资源有效的秒数,避免了Expires时间比较存在的问题。除此之外,Cache-Control还提供资源缓存策略。
Cache-Control
取值[1]:
字段名 | 作用 |
---|---|
public | 所有内容都将被缓存(客户端和代理服务器都可以缓存) |
private | 所有内容只有客户端可以缓存 (默认取值) |
no-cache | 客户端缓存内容,但是是否使用缓存则需要经过协商缓存决定 |
no-store | 所有内容都不会被缓存,即不使用强制缓存,也不使用协商缓存 |
max-age=x | 缓存内容将在 x 秒后失效 |
来看一个简单的🌰:
首先简单搭起一个服务
const http = require('http');
const fs = require('fs');
const path = require('path');
const filePath = path.resolve(__dirname, 'file', 'test.js');
const htmlPath = path.resolve(__dirname, 'file', 'test.html');
const imgPath = path.resolve(__dirname, 'file', 'image.jpg');
const app = http.createServer((req, res) => {
const path = req.url;
if (path === '/file.js') {
res.setHeader('Content-Type', 'text/javascript');
// 设置强缓存
res.setHeader('Cache-Control', 'max-age=600');
fs.createReadStream(filePath).pipe(res);
} else if (path === '/index.html') {
fs.createReadStream(htmlPath).pipe(res);
} else if (path === '/image.jpg') {
// 设置强缓存
res.setHeader('Cache-Control', 'max-age=600');
fs.createReadStream(imgPath).pipe(res);
} else {
res.end('Hello World');
}
});
app.listen('8888', () => {
console.log('server running at 8888');
});
前端声明一个简单的html文件,文档中有一张图片,而且调用ajax去请求/file
这个接口
<html>
<head>
<title>test</title>
</head>
<body>
<img src="/image.jpg" />
<script lang="text/javascript">
(() => {
const request = new XMLHttpRequest()
request.open("GET", "/file")
request.send()
request.onreadystatechange = (e) => {
if (e.target.status === 200 && e.target.readyState === 4) {
console.log(e.target.responseText)
}
}
})()
</script>
</body>
</html>
浏览器收到Cache-Control
后,就会存放在浏览器缓存中,下一次调用时如果未过期则会使用浏览器的缓存。如图所示
返回结果:
可以看到,后端返回了一个Cache-Control的字段,这个就是用于强缓存的字段,如果浏览器检测到有此标识,则将数据保存到浏览器缓存中。
那么我们要如何判别是否触发了强缓存呢?很简单,我们刷新一下页面看一下。
当我们再次刷新页面时,接口的size变成了disk cache
。表示这个缓存存放在硬盘中。此时,除非缓存时间已经过,或者手动请求浏览器缓存。访问改接口都会返回浏览器保存在本地的数据。
除了存放在硬盘中,强缓存还可以存放在 内存(memory cache) 中,可以看到图片的缓存为memory cache
看到上文我们可以知道,强缓存也分为disk cache和memory cache,那么什么时候会使用memory cache呢?
这取决于使用浏览器的缓存策略:通常一些小的图片,部分js文件会存放在内存中,css文件则存放在硬盘中。
除了上面两种缓存,还有下面几种缓存:
disk
,memory
,service worker
都没有被使用的时候才会触发,具体的Push Cache可以看这里缓存的执行顺序:Service Worker -> Memory Cache -> Disk Cache -> Push Cache
HTTP除了强缓存,还有协商缓存,当强缓存失效后,浏览器会携带缓存标识去请求资源,服务器根据缓存标识来决定是否使用缓存。协商缓存调用过程如下
可以看到,强缓存的优先级比协商缓存高,当强缓存失效时,才会触发协商缓存。而协商缓存原理是:通过缓存标识去请求服务器对比该资源是否有改变过,如果没有改变过,则可以继续使用强缓存失效的数据。
也就是说,协商缓存主要有两种情况:
使用协商缓存与强缓存一样,都是通过HTTP请求头实现。设置协商缓存的字段有两个,分别是:
Last-Modified
: 返回资源时,该资源的最后修改时间Etag
: 表示当前资源的唯一标识(由服务器生成)当浏览器接收到有协商缓存字段请求时,会将该标识保存在浏览器缓存中,并在下一次请求该资源时带上该标识,浏览器在带上请求时与服务器设置的字段的不一样,他们分别是:
If-Modified-Since
: 对应的是Last-Modifyed。通过对比时间判断资源是否有修改If-None-Match
: 对应的是Etag。通过对比该资源的唯一标识,判断资源是否有修改来看一下协商缓存的实现
// 服务器端
const http = require('http');
const fs = require('fs');
const path = require('path');
const filePath = path.resolve(__dirname, 'file', 'test.js');
const htmlPath = path.resolve(__dirname, 'file', 'test.html');
const imgPath = path.resolve(__dirname, 'file', 'image.jpg');
function formatTime(date) {
return `${date.getFullYear()}-${date.getMonth()}-${date.getDate()} ${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`;
}
const app = http.createServer((req, res) => {
const path = req.url;
if (path === '/file.js') {
const modifyDate = req.headers['if-modified-since'];
if (
modifyDate &&
formatTime(new Date(modifyDate)) ===
formatTime(new Date(fs.statSync(filePath).mtime))
) {
res.statusCode = 304;
res.end();
} else {
res.setHeader('Content-Type', 'text/javascript');
res.setHeader('Cache-Control', 'max-age=5');
// 使用 Last-Modified 触发协商缓存
// 通常是文件的最后修改时间
res.setHeader('Last-Modified', fs.statSync(filePath).mtime);
fs.createReadStream(filePath).pipe(res);
}
} else if (path === '/index.html') {
fs.createReadStream(htmlPath).pipe(res);
} else if (path === '/image.jpg') {
const getEtag = req.headers['if-none-match'];
if (getEtag && getEtag === '123') {
res.statusCode = 304;
res.end();
} else {
res.setHeader('Cache-Control', 'max-age=5');
// 使用Etag触发协商缓存
res.setHeader('Etag', '123');
fs.createReadStream(imgPath).pipe(res);
}
} else {
res.end('Hello World');
}
});
app.listen('8888', () => {
console.log('server running at 8888');
});
可以看到,在读取JS文件,我们使用Last-Modified
触发协商缓存,读取图片文件时,使用Etag
来触发协商缓存。对应地分别使用If-Modified-Since
和If-None-Match
来捕捉浏览器发送过来的标识。
访问 localhost:8888/index.html, 返回结果如下
可以看到这两个资源都触发了协商缓存,资源都是从浏览器缓存中得到的。
内存
或硬盘
中的缓存,通过HTTP请求头的Expires
和Cache-Control
设置强缓存。存储在内存的缓存速度非常快,但是空间有限。存储在硬盘的缓存读取时需要I/O操作,所以比内存缓存要慢Last-Modified
和Etag
设置协商缓存。服务器对应地需要获取请求头中的If-Modified-Since
和If-None-Match
来判断标识是否一致。一致则返回304
状态码,浏览器收到304状态吗后直接取浏览器缓存中的数据首先来了解一下什么是Cookie
HTTP协议本身是无状态的。什么是无状态呢,即服务器无法判断用户身份。Cookie实际上是一小段的文本信息(key-value格式)。客户端向服务器发起请求,如果服务器需要记录该用户状态,就使用response向客户端浏览器颁发一个Cookie。客户端浏览器会把Cookie保存起来。当浏览器再请求该网站时,浏览器把请求的网址连同该Cookie一同提交给服务器。服务器检查该Cookie,以此来辨认用户状态。[5]
上面引用提到:
举个🌰:
const http = require('http');
const fs = require('fs');
const path = require('path');
const filePath = path.resolve(__dirname, 'file', 'test.js');
const htmlPath = path.resolve(__dirname, 'file', 'test.html');
const app = http.createServer((req, res) => {
const path = req.url;
if (path === '/file.js') {
fs.createReadStream(filePath).pipe(res);
} else if (path === '/index.html') {
// 设置Cookie
res.setHeader('Set-Cookie', ['user=imuser', 'type=user']);
fs.createReadStream(htmlPath).pipe(res);
} else {
res.end('Hello World');
}
});
app.listen('8888', () => {
console.log('server running at 8888');
});
浏览器访问 index.html ,浏览器发现后Set-Cookie
字段后,将他保存到缓存中
在浏览器中也可以看到它保存在Cookies
中了
现在我们来请求同一个源中的/file.js
接口
const request = new XMLHttpRequest()
request.open("GET", "/file.js")
request.send()
request.onreadystatechange = (e) => {
if (e.target.status === 200 && e.target.readyState === 4) {
console.log(e.target.responseText)
}
}
可以看到Request Headers
中自动带上了Cookie
字段,我们再来修改一下服务端代码
const http = require('http');
const fs = require('fs');
const path = require('path');
const filePath = path.resolve(__dirname, 'file', 'test.js');
const htmlPath = path.resolve(__dirname, 'file', 'test.html');
const app = http.createServer((req, res) => {
const path = req.url;
if (path === '/file.js') {
// 获取cookie
const cookie = req.headers.cookie;
const cookieArr = cookie ? cookie.split(';') : [];
const cookieObj = {};
// 将cookie转化为键值对
cookieArr.forEach((val) => {
const tmp = val.split('=');
if (tmp.length === 2) cookieObj[tmp[0].trim()] = tmp[1].trim();
});
// 判断用户信息
if (cookieObj.user === 'imuser') {
fs.createReadStream(filePath).pipe(res);
} else {
res.end('404 not found');
}
} else if (path === '/index.html') {
res.setHeader('Set-Cookie', ['user=imuser', 'type=user']);
fs.createReadStream(htmlPath).pipe(res);
} else {
res.end('Hello World');
}
});
app.listen('8888', () => {
console.log('server running at 8888');
});
上面代码中,修改了一下/file.js
的逻辑:
可见,cookie可以保存用户状态。除了服务器端可以获取到cookie,客户端也可以拿到cookie。只需调用document.cookie
即可拿到cookie字符串。
属性名 | 作用 |
---|---|
Key=Value | 键值对 |
Path | 指定哪些路径可以接受Cookie |
Domain | 指定哪些域名可以接受Cookie |
Secure | 设置后 Cookie 只应通过被HTTPS协议加密过的请求发送给服务端 |
HttpOnly | 防止客户端修改和访问Cookie,避免XSS攻击 |
Expires | 指定Cookie过期时间 |
SameSite | Cookie 允许服务器要求某个cookie在跨站请求时不会被发送,防止CSRF攻击 |
Cookie很小,大约只有4KB
,所以通常只会用来存放一些用户信息,如果想用稍微大一点的浏览器缓存,可以使用浏览器提供的Storage
浏览器Storage是HTML5的新引入的,可以在浏览器端保存数据,分为LocalStorage
和SessionStorage
。
两者调用方法,大小和API都是一致的,大约有5MB。唯一不同的是SessionStorage
在关闭页面后就会消失,而LocalStorage
除非用户手动清除,不然会一致存在。
注意,Storage只在同源页面下共享。
IndexedDB 是一种底层 API,用于在客户端存储大量的结构化数据(也包括文件/二进制大型对象(blobs))。该 API 使用索引实现对数据的高性能搜索。虽然 Web Storage 在存储较少量的数据很有用,但对于存储更大量的结构化数据来说力不从心。[7]
IndexDB是在前端运行的一个数据库系统,想要了解更多点击这里
IndexDB存储数据举例:
var customerData = [
{ ssn: "111", name: "Bill", age: 35, email: "bill@test.com" },
{ ssn: "222", name: "Donna", age: 32, email: "donna@test.com" }
];
var request = indexedDB.open("testDB", 2) // 打开一个数据库
request.onerror = (err) => {
console.log(err)
}
request.onupgradeneeded = (e) => {
const db = e.target.result
const objectStore = db.createObjectStore("test", { keyPath: "ssn" }) // 创建一个数据对象
// 使用事务的 oncomplete 事件确保在插入数据前对象仓库已经创建完毕
objectStore.transaction.oncomplete = function(event) {
// 将数据保存到新创建的对象仓库
var customerObjectStore = db.transaction("test", "readwrite").objectStore("test");
// 循环插入数据
customerData.forEach(function(customer) {
customerObjectStore.add(customer);
});
// 读取数据
setTimeout(() => {
var transaction = db.transaction(['test']);
var getStore = transaction.objectStore('test');
var res = getStore.get("111")
res.onerror = () => {} // 异常捕捉
res.onsuccess = () => {
console.log(res.result)
}
}, 2000)
};
}
可以看到, 执行结果:
强缓存
expires
或cache-control
字段,cache-control
的优先级比expires
高硬盘
或内存
中协商缓存
last-modified
或Etag
字段。last-modified
通常是数据的最后更改时间;Etag
为数据在服务器的唯一标识if-modified-since
或if-none-match
字段。判断请求携带过来标识于对应数据的标识是否一致,一致则返回304。浏览器判断到304状态码后从浏览器缓存中读取数据Etag
优先级比last-modify
高Cookie
Set-Cookie
字段将数据发送给浏览器。浏览器收到后会将数据保存起来。下一次请求对应的域名后会自动把Cookie放到请求头的Cookie
字段中。Storage
sessionStorage
和localStorage
,sessionStorage
在页面关闭时会消失。localStorage
除非用户手动请求,不然一直会存在浏览器中。IndexDB