介绍
web缓存主要指的是两部分:浏览器缓存(localStorage,sessionStorage,cookie等等)和http缓存。
一、浏览器缓存
1. localStorage
localStorage将数据存储在本地,不参与和服务器的通讯,localStorage的值的类型限定为字符串类型。
有效期:永久有效,即使保存的数据超出了浏览器所规定的大小,也不会把旧数据清空而只会报错。
作用域:同源内共享,并且不同子域名之间不共享
存储:一般为5MB
window.localStorage.setItem('key',value) //存储
window.localStorage.getItem('key') //获取
window.localStorage.removeItem('key') //删除
window.localStorage.clear() //清空
注意:在移动设备上的浏览器或各Native App用到的WebView里,localStorage都是不可靠的,可能会因为各种原因(比如说退出App、网络切换、内存不足等原因)被清空。
2. sessionStorage
有效期:一旦窗口或者标签页被关闭了,存储的数据也就失效了。
作用域:同源内,并且相同标签内共享。注意:如果相同文档源的页面渲染在不同的标签中,sessionStorage的数据是无法共享的。
存储:一般为5MB
window.sessionStorage.setItem('key','value') //存储
window.sessionStorage.getItem('key') //获取
window.sessionStorage.removeItem('key') //删除
window.sessionStorage.clear() //清空
3. cookie
有效期:需要手动设置,默认状体-关闭浏览器即失效
作用域:同源
存储:一般4k
//读取cookie
let x = document.cookie; //以字符串的方式返回所有cookie,类型格式:cookie1=value; cookie2=value; cookie3=value;
//修改cookie 修改 cookie 类似于创建 cookie
document.cookie="username=John Smith; expires=Thu, 18 Dec 2043 12:00:00 GMT; path=/";
//删除 cookie 只需要设置 expires 参数为以前的时间即可
document.cookie="username=John Smith; expires=Thu, 18 Dec 2043 12:00:00 GMT; path=/";
document.cookie 属性看起来像一个普通的文本字符串,其实它不是。
即使您在 document.cookie 中写入一个完整的 cookie 字符串, 当您重新读取该 cookie 信息时,cookie 信息是以名/值对的形式展示的。
如果您设置了新的 cookie,旧的 cookie 不会被覆盖。 新 cookie 将添加到 document.cookie 中;
//使用 path 参数告诉浏览器 cookie 的路径。默认情况下,cookie 属于当前页面。
document.cookie="username=John Doe; expires=Thu, 18 Dec 2043 12:00:00 GMT; path=/";
常用函数
//设置cookie
function setCookie(cname,cvalue,exdays){
var d = new Date();
d.setTime(d.getTime()+(exdays*24*60*60*1000));
var expires = "expires="+d.toGMTString();
document.cookie = cname+"="+cvalue+"; "+expires;
}
//读取coolie
function getCookie(cname){
var name = cname + "=";
var ca = document.cookie.split(';');
for(var i=0; i<ca.length; i++) {
var c = ca[i].trim();
if (c.indexOf(name)==0) { return c.substring(name.length,c.length); }
}
return "";
}
//检测cookid
function checkCookie(){
var user=getCookie("username");
if (user!=""){
alert("欢迎 " + user + " 再次访问");
}
else {
user = prompt("请输入你的名字:","");
if (user!="" && user!=null){
setCookie("username",user,30);
}
}
}
二、HTTP缓存
HTTP缓存:是可以自动保存常见文档副本的 HTTP 设备。当 Web 请求抵达缓存时, 如果本地有“已缓存的”副本,就可以从本地存储设备而不是原始服务器中提取这个文档。具体流程如下
缓存的优点:减少网络传输,节约带宽,更快的加载网页,减少服务器负载
缓存的缺点:占内存,更新不及时
http缓存又分为两种,强制缓存和协商缓存
1. 强缓存
从强制缓存的角度触发,如果浏览器判断请求的目标资源有效命中强缓存,如果命中,则可以直接从内存中读取目标资源,无需与服务器做任何通讯。
基于 Cache-Control实现强缓存
Cache-control这个字段在http1.1中被增加,Cache-control完美解决了Expires本地时间和服务器时间不同步的问题,使用方法如下:
//往响应头中写入需要缓存的时间
res.writeHead(200,{
'Cache-Control':'max-age=10'
});
Cache-Control:max-age=N,N就是需要缓存的秒数。从第一次请求资源的时候开始,往后N秒内,资源若再次请求,则直接从磁盘(或内存中读取),不与服务器做任何交互。中因为max-age后面的值是一个滑动时间,从服务器第一次返回该资源时开始倒计时。所以也就不需要比对客户端和服务端的时间,解决了Expires所存在的巨大漏洞。
Cache-control有max-age、s-maxage、no-cache、no-store、private、public这六个属性。
- max-age决定客户端资源被缓存多久。
- s-maxage决定代理服务器缓存的时长。
- no-cache表示是强制进行协商缓存。
- no-store是表示禁止任何缓存策略。
- public表示资源即可以被浏览器缓存也可以被代理服务器缓存。
- private表示资源只能被浏览器缓存。
2. 协商缓存
所谓“协商”,可以理解为:客户端和服务端双方商量着来。
客户端检查资源超过有效期、强缓存命中失败的情况下,则发出请求“询问”服务器是否资源真的过期了,询问的同时在请求头要携带着资源的「上次更新时间」或者「唯一实体标识」(不同http版本导致的共存问题)。
服务端核对客户端要请求的资源的「上次更新时间」或者「唯一实体标识」:
- 若一致,说明命中协商缓存,只返回304;
- 若不一致,说明资源有更新,则返回200、新资源,同时响应头返回「资源修改时间」后者「资源最新的实体标识」。同时,客户端拿到新的资源及其修改时间与标识后,重新进行缓存。
基于last-modified的协商缓存
实现方式如下:
- 首先需要在服务器端读出文件修改时间,
- 将读出来的修改时间赋给响应头的
last-modified字段。 - 最后设置
Cache-control:no-cache
const http = require('http');
const fs = require('fs');
http.createServer((req, res)=>{
if(req.url === '/bg2.png'){
cosnt data = fs.readFileSync
const {mtime} = fs.statSync('./bg2.png'); //读取修改时间
res.setHeader('last-modified', mtime.toUTCtring());//设置文件最后修改时间
res.setHeader('Catch-Control', 'no=cache'); //跳过强缓存,直接进行协商缓存
res.end(data)
}
})
当客户端读取到last-modified的时候,会在下次的请求标头中携带一个字段:If-Modified-Since,就是服务器第一次修改时候给他的时间。
那么之后每次对该资源的请求,都会带上If-Modified-Since这个字段,而服务端就需要拿到这个时间并再次读取该资源的修改时间,让他们两个做一个比对来决定是读取缓存还是返回新的资源。
const http = require('http');
const fs = require('fs');
http.createServer((req, res)=>{
if(req.url === '/bg2.png'){
const data = fs.readFileSync
const {mtime} = fs.statSync('./bg2.png'); //读取修改时间
const ifModifiedSince = req.headers['if-modified-since'];
if(ifModifiedSince === mtime.toUTCtring()){
res.statusCode = 304; //如果一致,说明文件没有被修改过。则返回304
res.end(); //因为缓存一件生效了,这里不需要返回资源data
return
}
res.setHeader('last-modified', mtime.toUTCtring());//设置文件最后修改时间
res.setHeader('Catch-Control', 'no=cache'); //跳过强缓存,直接进行协商缓存
res.end(data)
}
})
这样,就是协商缓存的所有操作了。 使用以上方式的协商缓存已经存在两个非常明显的漏洞。这两个漏洞都是基于文件是通过比较修改时间来判断是否更改而产生的。
- 因为是更具文件修改时间来判断的,所以,在文件内容本身不修改的情况下,依然有可能更新文件修改时间(比如修改文件名再改回来),这样,就有可能文件内容明明没有修改,但是缓存依然失效了。
- 当文件在极短时间内完成修改的时候(比如几百毫秒)。因为文件修改时间记录的最小单位是秒,所以,如果文件在几百毫秒内完成修改的话,文件修改时间不会改变,这样,即使文件内容修改了,依然不会 返回新的文件。
为了解决上述的这两个问题。从http1.1开始新增了一个头信息,ETag(Entity 实体标签)
为了解决上述的这两个问题。从http1.1开始新增了一个头信息,ETag(Entity 实体标签)
基础ETag的协商缓存
ETag就是将原先协商缓存的比较时间戳的形式修改成了比较文件指纹。
文件指纹:根据文件内容计算出的唯一哈希值。文件内容一旦改变则指纹改变。
具体流程是这样的
- 第一次请求某资源的时候,服务端读取文件并计算出文件指纹,将文件指纹放在响应头的
etag字段中跟资源一起返回给客户端。 - 第二次请求某资源的时候,客户端自动从缓存中读取出上一次服务端返回的
ETag也就是文件指纹。并赋给请求头的if-None-Match字段,让上一次的文件指纹跟随请求一起回到服务端。 - 服务端拿到请求头中的
if-None-Match字段值(也就是上一次的文件指纹),并再次读取目标资源并生成文件指纹,两个指纹做对比。如果两个文件指纹完全吻合,说明文件没有被改变,则直接返回304状态码和一个空的响应体并return。如果两个文件指纹不吻合,则说明文件被更改,那么将新的文件指纹重新存储到响应头的ETag中并返回给客户端
const http = require('http');
const fs = require('fs');
const etag = require('etag');
http.createServer((req, res)=>{
if(req.url === '/bg2.png'){
const data = fs.readFileSync
const etagContent = etag(data); //根据文件生成唯一标识符
const ifNoneMatch = req.headers['if-none-match']; //获取请求头中上一次的文件指纹
if(ifNoneMatch === etagContent){
res.statusCode = 304; //如果一致,说明文件没有被修改过。则返回304
res.end(); //因为缓存一件生效了,这里不需要返回资源data
return
}
res.setHeader('etag', etagContent);//设置文件最后修改时间
res.setHeader('Catch-Control', 'no=cache'); //跳过强缓存,直接进行协商缓存
res.end(data)
}
})
从校验流程上来说,协商缓存的修改时间比对和文件指纹比对,几乎是一样的。
ETag的缺点
- ETag需要计算文件指纹这样意味着,服务端需要更多的计算开销。如果文件尺寸大,数量多,并且计算频繁,那么ETag的计算就会影响服务器的性能。显然,ETag在这样的场景下就不是很适合。
- ETag有强验证和弱验证,所谓将强验证,ETag生成的哈希码深入到每个字节。哪怕文件中只有一个字节改变了,也会生成不同的哈希值,它可以保证文件内容绝对的不变。但是,强验证非常消耗计算量。ETag还有一个弱验证,弱验证是提取文件的部分属性来生成哈希值。因为不必精确到每个字节,所以他的整体速度会比强验证快,但是准确率不高。会降低协商缓存的有效性。
值得注意的一点是,不同于
cache-control是expires的完全替代方案。ETag并不是last-modified的完全替代方案。而是last-modified的补充方案,这两个没有谁更好谁更坏,却决于业务场景。
三、哪些文件应该使用哪种缓存
有哈希值的文件设置强缓存。没有哈希值的文件(比如index.html)设置协商缓存
现在大多数打包工具打完包之后都会给我文件名加上hash值,从而保持每次打包之后文件名都是最新的;因此这类文件名带hash值的用强缓存即可;
已vue项目为例
文件打包之后,css、js、img都会给文件名上新增hash值,而index.html不会;
支持 css、js、img应该设置强缓存,而index.html应该设置协商缓存