Fork me on GitHub

JS-浏览器缓存

Cookie、Session 和 localStorage、以及 SessionStorage 之间的区别

浏览器缓存机制

性能优化中最简单高效的方式,可以显著减少网络传输所带来的损耗。

缓存位置

  1. Service Worker
    Service Worker 的缓存与浏览器其他内建的缓存机制不同,它可以让我们自由控制缓存哪些文件,如何匹配缓存、如何读取缓存,并且缓存是持续性的。
    如果我们没有在 Service Worker 命中缓存的话,会根据缓存查找优先级去查找数据。但是不管我们是从 Memory Cache 中还是从网络请求中获取的数据,浏览器都会显示我们是从 Service Worker 中获取的内容。

  2. Memory Cache
    内存中的缓存。读取内存中的数据肯定比磁盘快,但是内存缓存虽然读取高效,但是缓存持续性很短,会随着进程的释放而释放。一旦关闭 Tab 页面,内存中的缓存也就被释放。
    内存一定比硬盘容量小得多,所以操作系统需要精打细算内存的使用。

  3. Disk Cache
    硬盘中的缓存,读取速度慢,但是什么都能存储到磁盘中,比之 Memory Cache 胜在容量和存储时效性上。

  4. Push Cache
    HTTP/2 中的内容,当以上三种缓存都没有命中时,它才会被使用。并且缓存时间也很短暂,只在会话(session)中存在,一旦会话结束就被释放。
    未来的趋势,现在资料很少,有时间看看

    • 所有的资源都能被推送,但是 Edge 和 Safari 浏览器兼容性不怎么好
    • 可以推送 no-cache 和 no-store 的资源
    • 一旦连接被关闭,Push Cache 就被释放
    • 多个页面可以使用相同的 HTTP/2 连接,也就是说能使用同样的缓存
    • Push Cache 中的缓存只能被使用一次
    • 浏览器可以拒绝接受已经存在的资源推送
    • 你可以给其他域名推送资源
  5. 网络请求
    如果所有缓存都没有命中的话,那么只能发起请求来获取资源了。

缓存策略

强缓存和协商缓存

如果什么缓存策略都没设置,那么浏览器会怎么处理?
对于这种情况,浏览器会采用一个启发式的算法,通常会取响应头中的 Date 减去 Last-Modified 值的 10% 作为缓存时间。

实际应用

  • 频繁变动的资源
  1. 使用Cache-Control: no-cache使浏览器每次都请求服务器,
  2. 配合ETag或者Last-Modified来验证资源是否有效,这种方法不能节省请求数量,但是能显著减少响应数据大小。
  • 代码文件
    这里特指 HTML 之外的代码文件,因为 HTML 文件一般不缓存或者缓存时间很短。
    一般来说,现在都会使用工具来打包代码,那么我们就可以对文件名进行哈希处理,只有当代码修改后才会生成新的文件名。基于此,我们就可以给代码文件设置缓存有效期一年 Cache-Control: max-age=31536000,这样只有当 HTML 文件中引入的文件名发生了改变才会去下载最新的代码文件,否则就一直使用缓存。

本地缓存

Cookie,localStorage,SessionStorage

  • 共同点:都是保存在浏览器端,且遵循同源策略
  • 不同点:生命周期与作用域不同

作用域:localStorage 只要在相同的协议、相同的主机名、相同的端口下,就能读取/修改到同一份 localStorage 数据。
SessionStorage 比 localStorage 更严苛一点,除了协议、主机名、端口外,还要求在同一窗口(也就是浏览器的标签页)下

1、Cookie
一般限制在 4kb 以下,Cookie 缓存浏览器和服务器间来回传递,所以一般只将用户登录状态或权限验证放在 Cookie 中,避免影响请求传输效率。在设置的过期时间之前一直有效。
2、localStorage
大小 5mb 以下,存储在浏览器缓存中直到代码删除或手动清除浏览器缓存。
方法:

  • 存储:localStorage.setItem(key,value),存储或更新
  • 获取:localStorage.getItem(key),如果 key 不存在返回 null
  • 删除:localStorage.removeItem(key)一旦删除,key 对应的数据将会全部删除
  • 全部删除:localStorage.clear()

3、SessionStorage
5mb,生命周期维持到页面窗口关闭

Cookie 使基于无状态的 HTTP 协议可以记录稳定的状态信息。
Cookie 存储在客户端:服务器发送到用户浏览器并保存在本地,下次向服务器发起请求被携带。通常用于告知服务端两个请求是否来自同一浏览器,如保存用户的登录状态。
Cookie 不可以跨域:每个 Cookie 都会绑定单一的域名,无法在别的域名下获取使用,一级域名和二级域名之间是允许共享使用的(靠的是 domain)
Cookie 数据为键值对,键、值都必须是字符串类型。
如果值为 Unicode 字符,需要为字符编码。
如果值为二进制数据,则需要使用 BASE64 编码。
属性见《-HTTP 协议》

问题:

  • 存储在客户端,容易被客户端篡改。
  • 使用 httpOnly 在一定程度上提高安全性。
  • 一个浏览器针对一个网站最多存 20 个 Cookie,浏览器一般只允许存放 300 个 Cookie
  • 设置正确的 domain 和 path,减少数据传输

Session

记录服务器和客户端会话状态的机制。
保存在服务器,有一个唯一标识。在服务端保存 Session 的方法很多,内存、数据库、文件都有。
session
Session 认证流程:

  • 用户第一次请求服务器的时候,服务器根据用户提交的相关信息,创建对应的 Session
  • 请求返回时将此 Session 的唯一标识信息 SessionID 返回给浏览器
  • 浏览器接收到服务器返回的 SessionID 信息后,会将此信息存入到 Cookie 中,同时 Cookie 记录此 SessionID 属于哪个域名
  • 当用户第二次访问服务器的时候,请求会自动判断此域名下是否存在 Cookie 信息,如果存在自动将 Cookie 信息也发送给服务端,服务端会从 Cookie 中获取 SessionID,再根据 SessionID 查找对应的 Session 信息,如果没有找到说明用户没有登录或者登录失效,如果找到 Session 证明用户已经登录可执行后面操作。

问题:

  • 当用户同时在线量比较多时,这些 session 会占据较多的内存,需要在服务端定期的去清理过期的 session。
  • 当网站采用集群部署的时候,会遇到多台 web 服务器之间如何做 session 共享的问题。
  • sessionId 是存储在 cookie 中的,假如浏览器禁止 cookie 或不支持 cookie 怎么办?一般会把 sessionId 跟在 url 参数后面即重写 url,所以 session 不一定非得需要靠 cookie 实现。
  • 安全性: Session 比 Cookie 安全,Session 是存储在服务器端的,Cookie 是存储在客户端的。
  • 存取值的类型不同: Cookie 只支持存字符串数据,其他类型都要转换为字符串,Session 可以存任意数据。
  • 有效期不同: Cookie 可设置为长时间保存,Session 一般有效时间较短,客户端关闭或者 Session 超时都会失效。
  • 存储大小不同: 单个 Cookie 保存的数据不能超过 4K,Session 可以存储数据远高于 Cookie,但是访问量过多,会占用过多的服务器资源。

Token

  • 访问资源接口(API)时所需的资源凭证。
  • 简单 Token 的组成:uid(用户唯一的身份标识)、time(当前时间的时间戳,用于控制登录过期)、sign(签名、Token 的前几位以哈希算法压缩成的一定长度的十六进制字符串)

特点

  • 服务端无状态、可扩展性好
  • 支持移动端设备
  • 安全
  • 支持跨域程序调用

Token 的身份验证流程
Token

  1. 客户端使用用户名跟密码请求登录
  2. 服务端收到请求,去验证用户名与密码
  3. 验证成功后,服务端会签发一个 Token 并把这个 Token 发送给客户端
  4. 客户端收到 Token 以后,会把它存储起来,比如放在 Cookie 里或者 LocalStorage 里
  5. 客户端每次向服务端请求资源的时候需要带着服务端签发的 Token
  6. 服务端收到请求,然后去验证客户端请求里面带着的 Token ,如果验证成功,就向客户端返回请求的数据

注意

  • 每一次请求都需要携带 Token,需要把 Token 放到 HTTP 的 Header 里
  • 基于 Token 的用户认证是一种服务端无状态的认证方式,服务端不用存放 Token 数据。用解析 Token 的计算时间换取 session 的存储空间,从而减轻服务器的压力,减少频繁的查询数据库
  • Token 完全由应用管理,所以它可以避开同源策略

JWT

JSON Web Toke 目前最流行的跨域认证解决方案。一种认证授权机制。
JWT 的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源。
原理: 服务器认证后,生成一个 JSON 对象,发给用户,之后用户与服务器通信都要发回这个 JSON 对象。为了防止用户篡改数据,服务器在生成这个对象的时候会加上签名。
组成部分: Header(头部),Payload(消息体),Signature(签名)
Header.Payload.Signature

  • Header
    JSON 对象,描述 JWT 的元数据

    1
    2
    3
    4
    {
    "alg":"HS256"
    "typ":"JWT"
    }

    alg:签名算法(algorithm),默认是 HMAC SHA256(HS256);
    typ:表示这个令牌(token)的类型,JWT 统一写为JWT

  • Payload
    用来存放实际需要传递的数据,官方规定 7 个字段供选用

    • iss(issuer):签发人
    • exp(expiration time):过期时间
    • sub(subject):主题
    • aud(audience):受众
    • nbf(Not Before):生效时间
    • iat(Issued At):签发时间,令牌生成的时间
    • jti(JWT ID):编号
      除了官方字段,你还可以在这个部分定义私有字段
1
2
3
4
5
{
"sub":"1234567890",
"name":"John Doe",
"admin":true
}

注意,JWT 默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分。
这个 JSON 对象也要使用 Base64URL 算法转成字符串。

  • Signature
    对前两部分的签名,防止数据篡改。
    指定一个密钥(secret),只能服务器知道。使用 Header 里面指定的签名算法(默认 HMAC SHA256),按照下面的公式产生签名。
    签名计算如下:

    1
    2
    3
    4
    5
    6
    const base64Header = encodeBase64(header)
    const base64Payload = encodeBase64(payload)
    const unsignedToken = `${base64Header}.${base64Payload}`
    const key = '服务器私钥'

    signature = HMAC(key, unsignedToken)

    最后,Token 计算如下:

    1
    2
    3
    4
    5
    const base64Header = encodeBase64(header)
    const base64Payload = encodeBase64(payload)
    const base64Signature = encodeBase64(signature)

    token = `${base64Header}.${base64Payload}.${base64Signature}`

使用方式
客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage。

此后,客户端每次与服务器通信,都要带上这个 JWT。你可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 请求的头信息 Authorization 字段里面。
另一种做法是,跨域的时候,JWT 就放在 POST 请求的数据体里面。
服务器在判断 Token 时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const [base64Header, base64Payload, base64Signature] = token.split('.')

const signature1 = decodeBase64(base64Signature)
const unsignedToken = `${base64Header}.${base64Payload}`
const signature2 = HMAC('服务器私钥', unsignedToken)

if(signature1 === signature2) {
return '签名验证成功,token 没有被篡改'
}

const payload = decodeBase64(base64Payload)
if(new Date() - payload.iat < 'token 有效期'){
return 'token 有效'
}

特点

  • 默认不加密,但是也可以加密,生成原始 Token 以后,可以用密钥加密一次。
  • JWT 不加密的情况下,不能将秘密数据写入 JWT。
  • JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。
  • JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。
  • JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。
  • 为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。

Service Worker(整理的有点乱,没看懂)

运行在浏览器背后的独立线程,一般可以用来实现缓存功能。使用Service Worker的话,传输协议必须为HTTPS。因为Service Worker中涉及到请求拦截,所以必须HTTPS协议来保障安全。
Service Worker 的初衷是极致优化用户体验,是用来锦上添花的,技术只是技术,但实际应用前,应考虑成本和收益。

PWA

Progressive Web Apps,渐进式网络应用程序,是一种普通网页或网站架构起来的的网络应用程序。
Progressive 渐进式:由于浏览器对于 Web 标准的跟进会有不同程度的滞后(更有甚者不但不跟进还要乱搞),很多优秀的新特性老旧浏览器并不支持,所以开发者有时会采取渐进式的策略,充分利用新特性,为支持新特性的浏览器提供更完善的功能和更好的体验( 让一部分人先富起来?)。PWA 之 P,大约就是这个意思。

Service Worker 的生命周期

  • 注册
    如果要使用Service Worker,就要在指定页面的 JS 页面注册,注册后,浏览器会自动安装Service Worker;
  • 安装
    缓存一些静态资源,如果缓存成功,则Service Worker安装成功,相反,缓存失败,安装就会失败,此时Service Worker不会被激活,但是稍后它还是会继续安装的。

  • 激活
    激活成功后,service worker 会控制其范围内的所有页面。第一次注册 service worker 的页面,会等到页面加载成功后,才会接受控制。一旦service worker在控制页面,它只会有两种状态:要么停止运行,要么处理页面中的fetchmessage事件(当页面中有网络请求或消息时)。

Service Worker 的主要事件

  • install,安装时触发,通常在此时缓存文件
  • active,激活时触发,通常做一些重置的操作,例如处理旧版本 Service Worker 的缓存
  • fetch,浏览器发起HTTP请求时触发,通常在这个事件的回调函数中匹配缓存,是最常用的事件。
  • push,和推送通知功能相关
  • sync,和后台同步功能相关

应用

  • 缓存静态资源
  • 离线体验
    步骤

  • 注册Service Worker

  • 监听到install事件以后就可以缓存需要的文件;
  • 下次用户访问的时候,就可以通过拦截请求的方式查询是否存在缓存,存在缓存的话就可以直接读取缓存文件,否则就去请求数据。

注册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//防止报错,符合渐进式的要求
//如果浏览器不支持Service Worker,就不执行
if ('serviceWorker' in navigator) {
window.addEventListener('load', function () {
//所以Service Worker只是一个挂在navigator对象上的HTML5 API而已
navigator.serviceWorker.register('/sw.js').then(function (registration) {
// 注册成功
console.log('注册成功: ', registration.scope);
}, function (err) {
// 注册失败
console.log('注册失败: ', err);
});
});
}

检查Service Worker服务是否可用,页面加载完成后,注册/sw.js.
可以在页面加载后直接调用register(),浏览器会自动处理 service worker 是不是已经注册过并且根据情况处理。
register() 的一个需要注意的点是 service worker的文件位置。这个例子中,文件位置在域名的根位置底下。这说明 service worker的作用域是整个源。换句话说,service worker 会接受到这个域名下到所有 fetch 事件。如果我们注册到是/example/sw.js 这个位置,那 service worker 就只会接收到 URL 以/example/ 开头(比如 /example/page1/, /example/page2/)的 fetch 事件

安装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// sw.js
self.addEventListener("install", (e) => {
e.waitUntill(
catches.open("my-cache").then((cache) => {
return cache.addAll([ "./a.js"]);
})
);
});

self.addEventListener("fetch", function (event) {
if (/\.png$/.test(event.request.url)) {
event.respondWith(fetch("/bird.jpg"));
}
});

在 callback 函数内部,我们需要注意:

  • 开启缓存
  • 缓存一些文件
  • 检查文件是否缓存成功

self,类似于 windowglobal,代表该 Service Worker 自身。

调用caches.open()开启缓存,调用cache.addAll(),传入需要缓存的文件数组。
如果所有这些文件都被成功缓存,那service worker 就被成功安装上了。但是,只要有一个文件下载失败,整个安装步骤就失败了。

-------------本文结束感谢阅读-------------