大家好,我是侃如,最近在学习node.js,涉及到缓存,发现自己有很多不清晰的地方,在这里对涉及到的知识点简单做一下总结。
缓存
缓存是一种保存资源的技术,保存当前请求资源以便下次请求时可以直接使用,而不需要再次访问源服务器下载。 缓存的好处:缓解服务器压力,减少数据请求次数;提高资源加载速度,提升性能,降低网络负荷等;
缓存的种类:浏览器缓存、网关缓存、CDN、反向代理缓存等。
本文主要对浏览器缓存和HTTP缓存做一下总结。
浏览器缓存和HTTP缓存的区别
之前有同事将浏览器缓存和HTTP缓存视为同一概念,也可以,但感觉不是很合理,在这里简单说一下他们的区别:
HTTP缓存:在HTTP请求传输时用到的缓存,通常是在服务端设置的,由缓存规则决定缓存方式;
浏览器缓存:使用js设置,主要涉及本地资源的存储方式。
HTTP缓存
HTTP缓存一般应用于get请求,主要分为强缓存(强制缓存)和协商缓存。 可以对照这张流程图理解。
强缓存
顾名思义,强制进行缓存,当客户端去请求某个资源文件时,服务端返回资源文件的同时还会携带一个响应头(Expires/Cache-Control)字段,这个字段就表示缓存的规则,当Expires和Cache-Control同时存在时,Cache-Control优先级更高。
Expires:用一个格林尼治时间表示缓存的过期时间,浏览器请求时如超过该时间,表示过期,需要重新请求服务器;如未超过该时间,直接使用缓存。
Expires使用客户端的时间与服务端的时间做对比,它有很大的局限性,如果两种时间有误差,那么强缓存直接失效,因此,在HTTP1.1(当前浏览器默认版本) Expires被Cache-Control取代。
Cache-Control:针对Expires的问题,Cache-Control引入了新的更强大、更灵活的缓存规则。Cache-Control不仅可以在服务端响应中使用,还可以在客户端 HTTP 请求中使用。
Cache-Control客户端参数:
Cache-Control: max-age=<seconds> //设置缓存存储的最大时间,超过这个时间缓存被认为过期
Cache-Control: max-stale[=<seconds>] //表明客户端愿意接收一个已经过期的资源
Cache-Control: min-fresh=<seconds> //表示客户端希望获取一个能在指定的秒数内保持其最新状态的响应。
Cache-control: no-cache //强制要求缓存把请求提交给协商缓存进行验证
Cache-control: no-store //不使用任何缓存
Cache-control: no-transform //不得对资源进行转换或转变。Content-Encoding、Content-Range、Content-Type等 HTTP 头不能由代理修改。
Cache-control: only-if-cached //表明客户端只接受已缓存的响应
Cache-Control服务端参数:
Cache-control: must-revalidate //一旦资源过期(比如已经超过max-age),在成功向原始服务器验证之前,缓存不能用该资源响应后续请求。
Cache-control: no-cache //强制要求缓存把请求提交给协商缓存进行验证
Cache-control: no-store //不使用任何缓存
Cache-control: no-transform //不得对资源进行转换或转变。Content-Encoding、Content-Range、Content-Type等 HTTP 头不能由代理修改。
Cache-control: public //共享缓存,表明响应可以被任何对象(包括:发送请求的客户端,代理服务器,等等)缓存,即使是通常不可缓存的内容。
Cache-control: private //私有缓存,表明响应只能被单个用户缓存(本地浏览器),不能作为共享缓存(即代理服务器不能缓存它)
Cache-control: proxy-revalidate //与 must-revalidate 作用相同,但它仅适用于共享缓存
Cache-Control: max-age=<seconds> //设置缓存存储的最大周期,超过这个时间缓存被认为过期 (单位秒)
Cache-control: s-maxage=<seconds> //覆盖max-age,但是仅适用于共享缓存 (比如各个代理),私有缓存会忽略它。
简单使用express框架搭一个服务器,使用setHeader方法设置Cache-Control字段即可:
app.get('/index.html',(req, res)=>{
let getPath = path.resolve(__dirname,'index.html');
res.setHeader('Cache-Control', 'max-age=2')
res.end(fs.readFileSync(getPath))
})
协商缓存
在强缓存失效(过期等原因,不是不存在)后,客户端会携带缓存标识向服务端发起请求,服务端会根据缓存标识决定是否使用缓存。
协商缓存的标识字段有两组,Last-Modified / If-Modified-Since和Etag / If-None-Match,其中Etag / If-None-Match的优先级比Last-Modified / If-Modified-Since高。和强缓存一样,协商缓存的标识也是在HTTP的响应头中返回给浏览器的。
在协商缓存生效时,状态码为304,并且请求时间和资源的大小都会极大的减少。因为服务端在比较了缓存标识后,只返回header部分,不需要再将请求的资源主体返回给客户端。
Last-Modified / If-Modified-Since:
Last-Modified:表示服务器返回资源的上次修改时间。参数如下:
Last-Modified:<day-name>,<day><month><year><hour>:<minute>:<second>GMT
Last-Modified: Wed, 21 Oct 2015 07:28:00 GMT
If-Modified-Since :表示服务器返回资源的修改时间,与Last-Modified格式相同。服务器会将两者时间进行对比,一致时,表示资源未经修改,返回一个不带有消息主体的 304 响应,浏览器直接从缓存中读取资源;不一致时,表示资源修改,服务器返回请求资源,状态码为 200 。
同样,Last-Modified / If-Modified-Since存在很大的局限性:它的时间精度只能到秒,一秒内进行多次改动,服务器是无法辨别的;还有就是”Ctrl+Z“——修改了文件但是又撤回了修改,实际内容并没有发生改变,但是修改时间还是会变化。也就是为什么它的优先级低于Etag / If-None-Match。
app.get('/index.html',(req, res)=>{
let getPath = path.resolve(__dirname,'index.html');
let status = fs.statSync(getPath)
if(status.mtime.toUTCString() === req.headers['if-modified-since']){
res.writeHead(304, 'Not Modified')
res.end()
} else {
res.setHeader('Cache-Control', 'max-age=2')
res.setHeader('Last-Modified', status.mtime.toUTCString())
res.writeHead(200, 'OK')
res.end(fs.readFileSync(getPath))
}
})
Etag / If-None-Match:
Etag表示请求资源特定版本的标识符,只有请求资源的内容发生变化时,Etag才会改变。浏览器会把首次请求到Etag作为If-None-Match,在下次请求时携带。服务器会把接收到的If-None-Match和当前版本的资源的 ETag 进行较,如果匹配(即资源未更改),服务器会返回不带资源的304状态,浏览器直接从缓存中读取资源。不匹配时服务器再次返回请求资源和ETag,状态码为 200。 ETag需要设置加密模块生成,可以引入MD5,这里就不贴代码了,在请求头判断Etag 和If-None-Match匹配即可。
浏览器缓存
cookie
Cookie:服务器传输到本地浏览器的一组数据,这组数据通常用于辨别用户身份、保持登录状态,告知服务器请求是否来源于同一浏览器。它始终在同源http请求中携带,因此它的大小大小被限制在4KB。 Cookie在浏览器关闭后会被删除,当然,浏览器恢复会话时Cookie会被保留,它的生命周期会被延长,这取决于Expires或Max-Age的有效期。它的实际应用场景主要有三个:
- 网页会话状态(用户的登录状态等)
- 用户个性化设置(用户自定义主题等)
- 浏览器行为跟踪(用户行为、操作等) 创建Cookie :当服务器收到 HTTP 请求时,会响应头里面添加一个 Set-Cookie 选项。浏览器收到后会保存下 Cookie,保存下来的Cookie信息会在之后的每一次请求携带,传递给服务器。
HTTP/1.0 200 OK
Content-type: text/html
Set-Cookie: yummy_cookie=choco
Set-Cookie: tasty_cookie=strawberry
js获取Cookie:js可通过document.cookie获取并设置Cookie,类似于getter、setter
oldCookies = document.cookie; //getter
document.cookie = newCookie; //setter
LocalStorage/SessionStorage
sessionStorage:为每一个给定的资源(键值对)维持一个独立的存储区域,该存储区域在页面会话期间可用(浏览器关闭失效),存储大小为5M。
localStorage:与sessionStorage功能相同,区别就是只要不主动删除会一直存在(浏览器关闭无影响)。
// 保存数据
sessionStorage.setItem('key', 'value');
localStorage.setItem('key', 'value');
// 获取数据
let data = sessionStorage.getItem('key');
let data = localStorage.getItem('key');
// 删除保存的数据
sessionStorage.removeItem('key');
localStorage.removeItem('key');
// 删除所有保存的数据
sessionStorage.clear();
localStorage.clear();
IndexedDB
IndexedDB:一种可以让你在用户的浏览器内持久化存储数据的方法,为解决前端无法存储大容量数据而生,它的存储容量非常大,取决与你的硬盘。它的基本模式如下:
- 打开数据库。
- 在数据库中创建一个对象仓库(object store)。
- 启动一个事务,并发送一个请求来执行一些数据库操作,像增加或提取数据等。
- 通过监听正确类型的 DOM 事件以等待操作完成。
- 在操作结果上进行一些操作(可以在 request 对象中找到) 特点如下:
- 异步
- 同源
- 事务型数据库
- 支持二进制 直接看代码(添加数据):
const dbName = "dbname";
var request = indexedDB.open(dbName, 2); //打开数据库,参数为名称和版本号,不存在则自动创建
request.onerror = function(event) { // 错误处理函数
};
// 数据库创建或升级的时候会触发onupgradeneeded事件
request.onupgradeneeded = function(event) {
var db = event.target.result;
// 建立对象仓库,keyPath为键路径,不可重复;
var objectStore = db.createObjectStore("customers", { keyPath: "akey" });
// 创建索引,unique表示是否唯一,name不唯一,id唯一
objectStore.createIndex("name", "name", { unique: false });
objectStore.createIndex("id", "id", { unique: true });
// 事务的 oncomplete 事件确保对象仓库已经创建完毕
objectStore.transaction.oncomplete = function(event) {
// 将数据保存到新创建的对象仓库,transaction接收的两个参数第一个为事务希望跨越的对象存储空间的列表为,第二个为操作权限(只读或读写)
var customerObjectStore = db.transaction("customers", "readwrite").objectStore("customers"); // 仓库对象
customerObjectStore.add( {
"akey":1,
"id": '25802580qwer', // 必须且值唯一
"name": '张三'
});
};
};
可以打开控制台来查看我们建造的仓库、添加的数据;
当然,除了添加indexedDB还可以实现删、改、查等操作,使用对应的方法即可,感兴趣的同学可以自己实现下。
写在最后
感谢各位小伙伴的阅读,如果这篇文章对你有帮助,麻烦点一个赞吧!有什么错误,也欢迎指正。