前端缓存
前端缓存主要分为两个方面:
网络方面的缓存:DNS缓存
、浏览器缓存
、CDN缓存
。
本地存储:Cookie
、 localStorage
、sessionStorage
、IndexDB
。
DNS 缓存:
DNS
全称 Domain Name System ,即域名系统。
简单的说,通过域名,最终得到该域名对应的 IP 地址的过程叫做域名解析(或主机名解析)。
www.zuofc.com (域名) - DNS解析 -> 111.222.33.444 (IP地址)
有DNS
的地方,就有缓存。浏览器、操作系统、Local DNS、根域名服务器,它们都会对DNS
结果做一定程度的缓存。
DNS
查询过程如下:
- 首先搜索浏览器自身的
DNS
缓存,如果存在,则域名解析到此完成。 - 如果浏览器自身的缓存里面没有找到对应的条目,那么会尝试读取操作系统的 hosts 文件看是否存在对应的映射关系,如果存在,则域名解析到此完成。
- 如果本地 hosts 文件不存在映射关系,则查找本地
DNS
服务器(ISP
服务器,或者自己手动设置的DNS
服务器),如果存在,域名到此解析完成。 - 如果本地
DNS
服务器还没找到的话,它就会向根服务器发出请求,进行递归查询。
在任何一步找到就会结束查找流程,而整个过程客户端只发出一次查询请求。
浏览器缓存(HTTP 缓存):
浏览器中的缓存作用分为两种情况,一种是需要发送HTTP
请求(协商缓存),一种是不需要发送(强缓存)。
强缓存
第一次请求时,服务器把资源的过期时间通过响应头中的Expires
和Cache-Control
两个字段告诉浏览器,之后再请求这个资源的话,会判断有没有过期,没有过期就直接拿来用,不向服务器发起请求,这就是强缓存。
Expires
用来指定资源到期绝对时间,服务器响应时,添加在响应头中。
expires: Wed, 22 Nov 2021 08:41:00 GMT
注意:如果服务器和浏览器端时间不一致的话可能导致失败。比如现在时间是 8 月 1,expires 过期时间是 8 月 2,客户端把电脑时间改成了 8 月 3,那就用不了这个缓存。
Cache-Control
指定资源过期时间秒,如下,表示在这个请求正确返回后的 300 秒内,资源可以使用,否则过期
cache-control:max-age=300
Expires 和 Cache-Control 的区别
- Expires 是
HTTP/1.0
中的,Cache-Control 是HTTP/1.1
中的; - Expires 是为了兼容,在不支持
HTTP/1.1
的情况下才会发生作用 - 两者同时存在的话 Cache-Control 优先级高于 Expires;
Cache-Control请求头
常见属性
字段(单位秒) | 说明 |
---|---|
max-age=300 | 拒绝接受长于 300 秒的资源,为 0 时表示获取最新资源 |
max-stale=100 | 缓存过期之后的 100 秒内,依然拿来用 |
min-fresh=50 | 缓存到期时间还剩余 50 秒开始,就不给拿了,不新鲜了 |
no-cache | 协商缓存验证 |
no-store | 不使用缓存 |
only-if-chached | 只使用缓存,没有就报 504 错误 |
no-transform | 不得对资源进行转换或转变。Content-Encoding, Content-Range, Content-Type 等 HTTP 头不能由代理修改。 |
Cache-Control响应头
常见属性
字段(单位秒) | 说明 |
---|---|
max-age=300 | 缓存有效期 300 秒 |
s-maxage=500 | 有效期 500 秒,优先级高于 max-age,适用于共享缓存(如 CDN) |
public | 可以被任何终端缓存,包括代理服务器、CDN 等 |
private | 只能被用户的浏览器终端缓存(私有缓存) |
no-cache | 先和服务端确认资源是否发生变化,没有就使用 |
no-store | 不缓存 |
no-transform | 与上面请求指令中的一样 |
must-revalidate | 客户端缓存过期了就向源服务器验证 |
proxy-revalidate | 代理缓存过期了就去源服务器重新获取 |
强缓存的缺点
缓存过期之后,不管资源有没有变化,都会重新发起请求,重新获取资源
而我们希望的是在资源文件没有更新的情况下,即使过期了也不重新获取资源,继续使用旧资源
所以协商缓存它来了,在强缓存过期的情况下,再走协商缓存的流程,判断文件有没有更新。
协商缓存
强缓存失效之后,浏览器在请求头中携带相应的缓存tag
来向服务器发请求,由服务器根据这个 tag,来决定是否使用缓存,这就是协商缓存。
具体来说,这样的缓存 tag 分为两种: Last-Modified 和 ETag。这两者各有优劣,并不存在谁对谁有绝对的优势
,跟上面强缓存的两个 tag 不一样。
Last-Modified
第一次请求资源时,服务器除了会返回给浏览器上面说的过期时间,还会在响应头添加 Last-Modified
字段,告诉浏览器该资源的最后修改时间
last-modified: Fri, 27 Oct 2021 08:35:57 GMT
然后浏览器再次请求的时候就把这个时间再通过另一个字段If-Modified-Since
,发送给服务器
if-modified-since: Fri, 27 Oct 2021 08:35:57 GMT
服务器再把这两个字段的时间对比,如果是一样的,就说明文件没有被更新过,就返回状态码 304 和空响应体给浏览器,浏览器直接拿过期了的资源继续使用即可;如果对比不一样说明资源有更新,就返回状态码 200 和新的资源
ETag
第一次请求资源时,服务器除了会在响应头上返回Expires
、Cache-Control
、Last-Modified
,还在返回Etag
字段,表示当前资源文件的一个唯一标识。这个标识符由服务器基于文件内容编码生成,能精准感知文件的变化,只要文件内容不同,ETag
就会重新生成
etag: W / '132489-1627839023000';
然后浏览器再次请求的时候就把这个文件标识 再通过另一个字段 If-None-Match
,发送给服务器
if-none-match: W/"132489-1627839023000"
服务器再把这两个字段的时间对比,如果发现是一样的,就说明文件没有被更新过,就返回状态码 304 和空响应体给浏览器,浏览器直接拿过期了的资源继续使用;如果对比不一样说明资源有更新,就返回状态码 200 和新的资源。
两者对比
1.在精准度
上,ETag
优于Last-Modified
。优于 ETag 是按照内容给资源上标识,因此能准确感知资源的变化。而 Last-Modified 就不一样了,它在一些特殊的情况并不能准确感知资源变化,主要有两种情况:
编辑了资源文件,但是文件内容并没有更改,这样也会造成缓存失效。
Last-Modified 能够感知的单位时间是秒,如果文件在 1 秒内改变了多次,那么这时候的 Last-Modified 并没有体现出修改了。
2.在性能上,
Last-Modified
优于ETag
,也很简单理解,Last-Modified
仅仅只是记录一个时间点,而Etag
生成过程中需要服务器付出额外开销,会影响服务器端的性能。
另外,如果两种方式都支持的话,服务器会优先考虑ETag
。
强缓存与协商缓存的区别
- 优先查找强缓存,没有命中再查找协商缓存
- 强缓存不发请求到服务器,所以有时候资源更新了浏览器还不知道,但是协商缓存会发请求到服务器,资源是否有更新,服务器肯定知道
- 目前项目大多数使用缓存文案
- 协商缓存一般存储:
HTML
- 强缓存一般存储:
css
,image
,js
,文件名带上hash
- 协商缓存一般存储:
缓存位置
浏览器中的缓存位置一共有四种,按优先级从高到低排列分别是:
- Service Worker
- Memory Cache
- Disk Cache
- Push Cache
1. Service Worker
Service Worker
是运行 js 主线程之外的,在浏览器背后的独立线程,自然也无法访问DOM
,它相当于一个代理服务器,可以拦截用户发出的请求,修改请求或者直接向用户发出回应,不用联系服务器。比如加载 JS 和图片,这就让我们可以在离线的情况下使用网络应用
一般用于离线缓存
(提高首屏加载速度)、消息推送
、网络代理
等功能。使用 Service Worker 的话必须使用 https 协议,因为 Service Worker 中涉及到请求拦截,需要 https 保障安全
用 Service Worker 来实现缓存分三步:
- 一是注册
- 然后监听 install 事件后就可以缓存文件
- 下次再访问的时候就可以通过拦截请求的方式直接返回缓存的数据
// index.js 注册
if (navigator.serviceWorker) {
navigator.serviceWorker
.register('sw.js')
.then((registration) => {
console.log('service worker 注册成功');
})
.catch((err) => {
console.log('servcie worker 注册失败');
});
}
// sw.js 监听 `install` 事件,回调中缓存所需文件
self.addEventListener('install', (e) => {
// 打开指定的缓存文件名
e.waitUntil(
caches.open('my-cache').then((cache) => {
// 添加需要缓存的文件
return cache.addAll(['./index.html', './index.css']);
})
);
});
// 拦截所有请求事件 缓存中有请求的数据就直接用缓存,否则去请求数据
self.addEventListener('fetch', (e) => {
// 查找request中被缓存命中的response
e.respondWith(
caches.match(e.request).then((response) => {
if (response) {
return response;
}
console.log('fetch source');
})
);
});
2. Memory Cache(内存)
就是将资源存储在内存中,下次访问直接从内存中读取。例如刷新页面时,很多数据都是来自于内存缓存。一般存储脚本、字体、图片。
优点是读取速度快;缺点由于一旦关闭 Tab 标签页,内存中的缓存也就释放了,所以容量和存储时效上差些
3. Disk Cache(硬盘)
就是将资源存储在硬盘中,下次访问时直接从硬盘中读取。它会根据请求头中的字段判断哪些资源需要缓存,哪些资源可以不请求直接使用,哪些资源已经过期需要重新请求。并且即使是跨域站点的情况下,相同地址的资源一旦被硬盘缓存下来,就不会再次请求。
优点是缓存在硬盘中,容量大,并且存储时效性更长;缺点是读取速度慢些
4. Push Cache
这个是推送缓存,是HTTP/2
中的内容,当上面三种缓存都没有命中时才会,被使用。它只会存在于Session
中,一旦会话结束就会释放,所以缓存时间很短,而且 Push Cache 中的缓存只能被使用一次
CDN 缓存
全称 Content Delivery Network
,即内容分发网络。
当我们发送一个请求时,浏览器本地缓存失效的情况下,CDN
会帮我们去计算哪得到这些内容的路径短而且快。
比如在广州请求广州的服务器就比请求新疆的服务器响应速度快得多,然后向最近的CDN
节点请求数据
CDN
会判断缓存数据是否过期,如果没有过期,则直接将缓存数据返回给客户端,从而加快了响应速度。如果CDN
判断缓存过期,就会向服务器发出回源请求,从服务器拉取最新数据,更新本地缓存,并将最新数据返回给客户端。
CDN 优势:
- CDN 节点解决了跨运营商和跨地域访问的问题,访问延时大大降低。
- 大部分请求在 CDN 边缘节点完成,CDN 起到了分流作用,减轻了源服务器的负载。
本地存储
Cookie
Cookie
最开始被设计出来其实并不是来做本地存储的,而是为了弥补HTTP
在状态管理上的不足。
HTTP
协议是一个无状态协议,客户端向服务器发请求,服务器返回响应,故事就这样结束了,但是下次发请求如何让服务端知道客户端是谁呢?
这种背景下,就产生了 Cookie
.
Cookie 本质上就是浏览器里面存储的一个很小的文本文件,内部以键值对的方式来存储(在 chrome 开发者面板的Application
这一栏可以看到)。向同一个域名下发送请求,都会携带相同的 Cookie,服务器拿到 Cookie 进行解析,便能拿到客户端的状态。
Cookie 的作用很好理解,就是用来做状态存储的,但它也是有诸多致命的缺陷的:
- 容量缺陷。Cookie 的体积上限只有
4KB
,只能用来存储少量的信息。 - 性能缺陷。Cookie 紧跟域名,不管域名下面的某一个地址需不需要这个 Cookie ,请求都会携带上完整的 Cookie,这样随着请求数的增多,其实会造成巨大的性能浪费的,因为请求携带了很多不必要的内容。
- 安全缺陷。由于 Cookie 以纯文本的形式在浏览器和服务器中传递,很容易被非法用户截获,然后进行一系列的篡改,在 Cookie 的有效期内重新发送给服务器,这是相当危险的。另外,在
HttpOnly
为 false 的情况下,Cookie 信息能直接通过 JS 脚本来读取。
localStorage
和 Cookie 异同
localStorage
有一点跟Cookie
一样,就是针对一个域名,即在同一个域名下,会存储相同的一段localStorage。
不过它相对Cookie
还是有相当多的区别的:
- 容量。localStorage 的容量上限为5M,相比于
Cookie
的 4K 大大增加。当然这个 5M 是针对一个域名的,因此对于一个域名是持久存储的。 - 只存在客户端,默认不参与与服务端的通信。这样就很好地避免了 Cookie 带来的性能问题和安全问题。
- 接口封装。通过
localStorage
暴露在全局,并通过它的setItem
和getItem
等方法进行操作,非常方便。
操作方式
接下来我们来具体看看如何来操作localStorage
。
let obj = { name: 'sanyuan', age: 18 };
localStorage.setItem('name', 'sanyuan');
localStorage.setItem('info', JSON.stringify(obj));
接着进入相同的域名时就能拿到相应的值:
let name = localStorage.getItem('name');
let info = JSON.parse(localStorage.getItem('info'));
从这里可以看出,localStorage
其实存储的都是字符串,如果是存储对象需要调用JSON
的stringify
方法,并且用JSON.parse
来解析成对象。
应用场景
利用localStorage
的较大容量和持久特性,可以利用localStorage
存储一些内容稳定的资源,比如官网的logo
,存储Base64
格式的图片资源,因此利用localStorage
sessionStorage
特点
sessionStorage
以下方面和localStorage
一致:
- 容量。容量上限也为 5M。
- 只存在客户端,默认不参与与服务端的通信。
- 接口封装。除了
sessionStorage
名字有所变化,存储方式、操作方式均和localStorage
一样。
但sessionStorage
和localStorage
有一个本质的区别,那就是前者只是会话级别的存储,并不是持久化存储。会话结束,也就是页面关闭,这部分sessionStorage
就不复存在了。
应用场景
- 可以用它对表单信息进行维护,将表单信息存储在里面,可以保证页面即使刷新也不会让之前的表单信息丢失。
- 可以用它存储本次浏览记录。如果关闭页面后不需要这些记录,用
sessionStorage
就再合适不过了。事实上微博就采取了这样的存储方式。
IndexDB
IndexDB
是运行在浏览器中的非关系型数据库
, 本质上是数据库,绝不是和刚才 WebStorage 的 5M 一个量级,理论上这个容量是没有上限的。
关于它的使用,本文侧重原理,而且 MDN 上的教程文档已经非常详尽,这里就不做赘述了,感兴趣可以看一下使用文档。
接着我们来分析一下IndexDB
的一些重要特性,除了拥有数据库本身的特性,比如支持事务
,存储二进制数据
,还有这样一些特性需要格外注意:
- 键值对存储。内部采用
对象仓库
存放数据,在这个对象仓库中数据采用键值对的方式来存储。 - 异步操作。数据库的读写属于 I/O 操作, 浏览器中对异步 I/O 提供了支持。
- 受同源策略限制,即无法访问跨域的数据库。
区别
Cookie | SessionStorage | LocalStorage | indexDB | |
---|---|---|---|---|
存储大小 | 4k | 5M 或更大 | 5M 或更大 | 无限 |
存储时间 | 可指定时间,没指定关闭窗口就失效 | 浏览器窗口关闭就失效 | 永久有效 | 永久有效 |
作用域 | 同浏览器,所有同源标签页 | 当前标签页 | 同浏览器,所有同源标签页 | |
存在于 | 请求中来回传递 | 客户端本地 | 客户端本地 | 客户端本地 |
同源策略 | 同浏览器,只能被同源同路径页面访问共享 | 自己用 | 同浏览器,只能被同源页面访问共享 |
总结
浏览器中各种本地存储和缓存技术的发展,给前端应用带来了大量的机会,PWA 也正是依托了这些优秀的存储方案才得以发展起来。重新梳理一下这些本地存储方案:
cookie
并不适合存储,而且存在非常多的缺陷。Web Storage
包括localStorage
和sessionStorage
, 默认不会参与和服务器的通信。IndexDB
为运行在浏览器上的非关系型数据库,为大型数据的存储提供了接口。