Skip to content

浏览器缓存 ​

缓存位置 ​

  • 内存缓存(memory cache)
    • 快速读取(内存缓存将编译解析后的文件,直接存入该进程的内存中,占据该进程一定的内存资源,以方便下次运行使用时的快速读取)
    • 进程关闭时数据会被清除
    • 不请求网络资源,资源存在内存中一般 JS 和图片文件会存入内存
    • 状态码:200(from memory cache)
  • 硬盘缓存(disk cache)
    • 写入硬盘文件进行 I/O 操作
    • 进程关闭时数据不会被清除
    • 速度比 memory cache 慢
    • 不请求网络资源,资源存在磁盘中一般非脚本会存在磁盘中,如 css
    • 状态码:200(from disk cache)
  • 代理服务器缓存(server worker)
    • 可以拦截处理页面的所有网络请求
    • 仅 HTTPS 下可用、存在兼容问题
    • 状态码:200(from service worker)

我们经常需要对业务中的一些数据进行存储,通常可以分为 短暂性存储 和 持久性储存。

  • 短暂性的时候,我们只需要将数据存在内存中,只在运行时可用

  • 持久性存储,可以分为 浏览器端 与 服务器端

    • 浏览器cookie 通常用于存储用户身份,登录状态等

      • http 中自动携带, 体积上限为 4K, 可自行设置过期时间

      • localStorage / sessionStorage: 长久储存/窗口关闭删除, 体积限制为 4~5M

      • indexDB

    • 服务器:

      • 分布式缓存 redis
    • 数据库

提示:如果平常有遇到过缓存的坑或者很好的利用缓存,可以讲解一下自己的使用场景。如果没有使用注意过缓存问题你也可以尝试讲解一下和我们息息相关的 Webpack 构建(每一次构建静态资源名称的hash值都会变化),它其实就跟缓存相关

缓存分为强缓存和协商缓存。强缓存不过服务器,协商缓存需要过服务器,协商缓存返回的状态码是 304。两类缓存机制可以同时存在,强缓存的优先级高于协商缓存。当执行强缓存时,如若缓存命中,则直接使用缓存数据库中的数据,不再进行缓存协商。

强缓存 ​

img

Expires(HTTP1.0):Exprires的值为服务端返回的数据到期时间。当再次请求时的请求时间小于返回的此时间,则直接使用缓存数据。但由于服务端时间和客户端时间可能有误差,这也将导致缓存命中的误差。另一方面,Expires 是 HTTP1.0 的产物,故现在大多数使用Cache-Control替代

缺点:使用的是绝对时间,如果服务端和客户端的时间产生偏差,那么会导致命中缓存产生偏差。

Cache-Control(HTTP1.1):有很多属性,不同的属性代表的意义也不同

  • private:客户端可以缓存
  • public:客户端和代理服务器都可以缓存
  • max-age=t:缓存内容将在 t 秒后失效
  • no-cache:需要使用协商缓存来验证缓存数据,可以在客户端存储资源,每次都必须去服务端做新鲜度校验,来决定从服务端获取新的资源(200)还是使用客户端缓存(304)
  • no-store:所有内容都不会缓存,永远都不要在客户端存储资源,永远都去原始服务器去获取资源

请注意no-cache指令很多人误以为是不缓存,这是不准确的,no-cache的意思是可以缓存,但每次用应该去向服务器验证缓存是否可用。no-store才是不缓存内容。当在首部字段Cache-Control 有指定 max-age 指令时,比起首部字段 Expires,会优先处理 max-age 指令。命中强缓存的表现形式:Firefox 浏览器表现为一个灰色的 200 状态码。Chrome 浏览器状态码表现为 200 (from disk cache)或是 200 OK (from memory cache)

协商缓存(对比缓存) ​

协商缓存需要进行对比判断是否可以使用缓存。浏览器第一次请求数据时,服务器会将缓存标识与数据一起响应给客户端,客户端将它们备份至缓存中。再次请求时,客户端会将缓存中的标识发送给服务器,服务器根据此标识判断。若未失效,返回 304 状态码,浏览器拿到此状态码就可以直接使用缓存数据了

  • Last-Modified:服务器在响应请求时,会告诉浏览器资源的最后修改时间
  • if-Modified-Since:浏览器再次请求服务器的时候,请求头会包含此字段,后面跟着在缓存中获得的最后修改时间。服务端收到此请求头发现有if-Modified-Since,则与被请求资源的最后修改时间进行对比,如果一致则返回 304 和响应报文头,浏览器只需要从缓存中获取信息即可
  • 如果真的被修改:那么开始传输响应一个整体,服务器返回:200 OK
  • 如果没有被修改:那么只需传输响应header,服务器返回:304 Not Modified
  • Etag:服务器响应请求时,通过此字段告诉浏览器当前资源在服务器生成的唯一标识(生成规则由服务器决定)
  • If-Match:条件请求,携带上一次请求中资源的ETag,服务器根据这个字段判断文件是否有新的修改
  • If-None-Match: 再次请求服务器时,浏览器的请求报文头部会包含此字段,后面的值为在缓存中获取的标识。服务器接收到次报文后发现If-None-Match则与被请求资源的唯一标识进行对比。

但是实际应用中由于Etag的计算是使用算法来得出的,而算法会占用服务端计算的资源,所有服务端的资源都是宝贵的,所以就很少使用Etag了

  • 浏览器地址栏中写入URL,回车浏览器发现缓存中有这个文件了,不用继续请求了,直接去缓存拿(最快)
  • F5就是告诉浏览器,别偷懒,好歹去服务器看看这个文件是否有过期了。于是浏览器就胆胆襟襟的发送一个请求带上If-Modify-since
  • Ctrl+F5告诉浏览器,你先把你缓存中的这个文件给我删了,然后再去服务器请求个完整的资源文件下来。于是客户端就完成了强行更新的操作

总结

  • 服务端判断客户端资源,是否和服务端资源一样。
  • 一致则返回 304,否则返回 200 和最新的资源

资源标识:

  • 在 Response Headers 中,有两种。
  • Last-Modified:资源的最后修改时间。
  • Etag:资源的唯一标识(一个字符串,类似于人类的指纹)

img

服务端拿到 if-Modified-Since 之后拿这个时间去和服务端资源最后修改时间做比较,如果一致则返回 304 ,不一致(也就是资源已经更新了)就返回 200 和新的资源及新的 Last-Modified

img

其实 Etag 和 Last-Modified 一样的,只不过 Etag 是服务端对资源按照一定方式(比如 contenthash)计算出来的唯一标识,就像人类指纹一样,传给客户端之后,客户端再传过来时候,服务端会将其与现在的资源计算出来的唯一标识做比较,一致则返回 304,不一致就返回 200 和新的资源及新的 Etag

两者比较:

  • 优先使用 Etag。
  • Last-Modified 只能精确到秒级。
  • 如果资源被重复生成,而内容不变,则 Etag 更精确。

img

三种刷新操作对 http 缓存的影响

  • 正常操作:地址栏输入 url,跳转链接,前进后退等
  • 手动刷新:f5,点击刷新按钮,右键菜单刷新
  • 强制刷新:ctrl + f5,shift+command+r
  • 正常操作:强制缓存有效,协商缓存有效
  • 手动刷新:强制缓存失效,协商缓存有效
  • 强制刷新:强制缓存失效,协商缓存失效

缓存场景 ​

对于大部分的场景都可以使用强缓存配合协商缓存解决,但是在一些特殊的地方可能需要选择特殊的缓存策

  • 对于某些不需要缓存的资源,可以使用 Cache-control: no-store ,表示该资源不需要缓存
  • 对于频繁变动的资源,可以使用 Cache-Control: no-cache 并配合 ETag 使用,表示该资源已被缓存,但是每次都会发送请求询问资源是否更新
  • 对于代码文件来说,通常使用 Cache-Control: max-age=31536000 并配合策略缓存使用,然后对文件进行指纹处理,一旦文件名变动就会立刻下载新的文件

讲讲 304 ​

如果客户端发送了一个带条件的 GET 请求且该请求已被允许,而文档的内容(自上次访问以来或者根据请求的条件)并没有改变,则服务器应当返回这个 304 状态码

强缓存、协商缓存什么时候用哪个 ​

因为服务器上的资源不是一直固定不变的,大多数情况下它会更新,这个时候如果我们 还访问本地缓存,那么对用户来说,那就相当于资源没有更新,用户看到的还是旧的资 源;所以我们希望服务器上的资源更新了浏览器就请求新的资源,没有更新就使用本地 的缓存,以最大程度的减少因网络请求而产生的资源浪费。

缓存总结 ​

缓存分为两种:强缓存和协商缓存,根据响应的 header 内容来决定。

获取资源形式状态码发送请求到服务器
强缓存从缓存取200(from cache)否,直接从缓存取
协商缓存从缓存取304(not modified)是,通过服务器来告知缓存是否可 用
  • 强缓存相关字段有 expires,cache-control。如果 cache-control 与 expires 同时存在的话, cache-control 的优先级高于 expires。
  • 协商缓存相关字段有 Last-Modified/If-Modified-Since,Etag/If-None-Match
特性cookielocalStoragesessionStorageindexDB
数据生命周期一般由服务器生成,可以设置过期时间除非被清理,否则一直存在页面关闭就清理除非被清理,否则一直存在
数据存储大小4K5M5M无限
与服务端通信每次都会携带在 header 中,对于请求性能影响不参与不参与不参与

从上表可以看到,cookie 已经不建议用于存储。如果没有大量数据存储需求的话,可以使用 localStorage和 sessionStorage 。对于不怎么改变的数据尽量使用 localStorage 存储,否则可以用 sessionStorage 存储。

对于 cookie,我们还需要注意安全性

属性作用
value如果用于保存用户登录态,应该将该值加密,不能使用明文的用户标识
http-only不能通过 JS访问 Cookie,减少 XSS攻击
secure只能在协议为 HTTPS 的请求中携带
same-site规定浏览器不能在跨域请求中携带 Cookie,减少 CSRF 攻击

IndexedDB ​

IndexedDB 是一个运行在浏览器上的非关系型数据库,用于在客户端存储大量结构化数据

IndexedDB 的特点

  • 存储空间大(一般来说不少于 250MB 甚至没有上限)
  • 支持存储二进制数据(ArrayBuffer 和 Blob)
  • 键值对储存
  • 同源限制
  • 执行的操作是异步执行,以免阻塞应用程序
  • 是一个事务型数据库系统

API 使用 ​

打开/创建一个 IndexedDB 数据库,并指定数据库的版本号 (版本号只能为整数)

js
const request = window.indexedDB.open('myDatabase', 1)
let db

// 成功回调
request.onsuccess = function (event) {
  // 获取 indexedDB 实例
  db = event.target.result
  // 也可以使用 request.result 获取 indexedDB 实例
  console.log('连接 IndexedDB 成功')
}

// 失败回调
request.onerror = function () {
  console.log('连接 IndexedDB 失败')
}
const request = window.indexedDB.open('myDatabase', 1)
let db

// 成功回调
request.onsuccess = function (event) {
  // 获取 indexedDB 实例
  db = event.target.result
  // 也可以使用 request.result 获取 indexedDB 实例
  console.log('连接 IndexedDB 成功')
}

// 失败回调
request.onerror = function () {
  console.log('连接 IndexedDB 失败')
}

创建一个对象仓库(类似于数据库中的表)

js
// upgradeneeded 事件会在初始化数据库或版本发生更新时被调用
request.onupgradeneeded = function (event) {
  const db = event.target.result
  // 创建对象仓库并指定主键
  const objectStore = db.createObjectStore('userInfo', {
    keyPath: 'id',
    autoIncrement: false,
  })
  console.log('创建对象仓库成功')

  /**
   * 定义存储对象的数据项
   * 第一个参数是创建的索引名称,可以为空
   * 第二个参数是索引使用的关键名称,可以为空
   * 第三个参数是可选配置参数,可以不传,常用参数之一就是 unique ,表示该字段是否唯一,不能重复
   */
  objectStore.createIndex('id', 'id', {
    unique: true,
  })
  objectStore.createIndex('name', 'name')
}
// upgradeneeded 事件会在初始化数据库或版本发生更新时被调用
request.onupgradeneeded = function (event) {
  const db = event.target.result
  // 创建对象仓库并指定主键
  const objectStore = db.createObjectStore('userInfo', {
    keyPath: 'id',
    autoIncrement: false,
  })
  console.log('创建对象仓库成功')

  /**
   * 定义存储对象的数据项
   * 第一个参数是创建的索引名称,可以为空
   * 第二个参数是索引使用的关键名称,可以为空
   * 第三个参数是可选配置参数,可以不传,常用参数之一就是 unique ,表示该字段是否唯一,不能重复
   */
  objectStore.createIndex('id', 'id', {
    unique: true,
  })
  objectStore.createIndex('name', 'name')
}

添加数据

js
// 创建事务指并定表格名称和读写权限
const transaction = db.transaction(['userInfo'], 'readwrite')
// 获取 Object Store 对象
const objectStore = transaction.objectStore('userInfo')

/* 添加数据 */
objectStore.add({ id: 1, name: 'test' })
// 创建事务指并定表格名称和读写权限
const transaction = db.transaction(['userInfo'], 'readwrite')
// 获取 Object Store 对象
const objectStore = transaction.objectStore('userInfo')

/* 添加数据 */
objectStore.add({ id: 1, name: 'test' })

获取数据

js
const transaction = db.transaction(['userInfo'], 'readonly')
const objectStore = transaction.objectStore('userInfo')

const objectStoreRequest = objectStore.get(1)
objectStoreRequest.onsuccess = function () {
  console.log('获取数据', objectStoreRequest.result)
}
const transaction = db.transaction(['userInfo'], 'readonly')
const objectStore = transaction.objectStore('userInfo')

const objectStoreRequest = objectStore.get(1)
objectStoreRequest.onsuccess = function () {
  console.log('获取数据', objectStoreRequest.result)
}

修改数据

js
const transaction = db.transaction(['userInfo'], 'readwrite')
const objectStore = transaction.objectStore('userInfo')

const objectStoreRequest = objectStore.get(1)
objectStoreRequest.onsuccess = function () {
  const data = objectStoreRequest.result
  data.name = 'maomao'
  objectStore.put(data)
}
const transaction = db.transaction(['userInfo'], 'readwrite')
const objectStore = transaction.objectStore('userInfo')

const objectStoreRequest = objectStore.get(1)
objectStoreRequest.onsuccess = function () {
  const data = objectStoreRequest.result
  data.name = 'maomao'
  objectStore.put(data)
}

删除数据

js
const transaction = db.transaction(['userInfo'], 'readwrite')
const objectStore = transaction.objectStore('userInfo')

const objectStoreRequest = objectStore.delete(1)
objectStoreRequest.onsuccess = function () {
  console.log('删除成功')
}
const transaction = db.transaction(['userInfo'], 'readwrite')
const objectStore = transaction.objectStore('userInfo')

const objectStoreRequest = objectStore.delete(1)
objectStoreRequest.onsuccess = function () {
  console.log('删除成功')
}

在日常开发中可以使用如下类库简化代码量

  • localForage 一个提供 name:value 的简单语法的客户端数据存储垫片,基于 IndexedDB 实现,并在不持支 IndexedDB 的浏览器中自动回退到 WebSQL 和 localStorage
  • Dexie.js 对 IndexedDB 的封装,通过提供更友好和简单语法进行快速的编码开发
  • PouchDB 对 IndexedDB 的封装,通过提供更友好和简单语法进行快速的编码开发
  • HTTP 是无状态的协议(对于事务处理没有记忆能力,每次客户端和服务端会话完成时,服务端不会保存任何会话信息):每个请求都是完全独立的,服务端无法确认当前访问者的身份信息,无法分辨上一次的请求发送者和这一次的发送者是不是同一个人。所以服务器与浏览器为了进行会话跟踪(知道是谁在访问我),就必须主动的去维护一个状态,这个状态用于告知服务端前后两个请求是否来自同一浏览器。而这个状态需要通过 cookie 或者 session 去实现
  • cookie 存储在客户端: cookie 是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上
  • cookie 是不可跨域的: 每个 cookie 都会绑定单一的域名,无法在别的域名下获取使用,一级域名和二级域名之间是允许共享使用的(靠的是 domain)
属性说明
name=value键值对,设置 Cookie 的名称及相对应的值,都必须是字符串类型。如果值为 Unicode 字符,需要为字符编码。如果值为二进制数据,则需要使用 BASE64 编码。
domain指定 cookie 所属域名,默认是当前域名
path指定 cookie 在哪个路径(路由)下生效,默认是 '/'。如果设置为 /abc,则只有 /abc 下的路由可以访问到该 cookie,如:/abc/read。
maxAgecookie 失效的时间,单位秒。如果为整数,则该 cookie 在 maxAge 秒后失效。如果为负数,该 cookie 为临时 cookie ,关闭浏览器即失效,浏览器也不会以任何形式保存该 cookie 。如果为 0,表示删除该 cookie 。默认为 -1。- 比 expires 好用。
expires过期时间,在设置的某个时间点后该 cookie 就会失效。一般浏览器的 cookie 都是默认储存的,当关闭浏览器结束这个会话的时候,这个 cookie 也就会被删除
secure该 cookie 是否仅被使用安全协议传输。安全协议有 HTTPS,SSL等,在网络上传输数据之前先将数据加密。默认为false。当 secure 值为 true 时,cookie 在 HTTP 中是无效,在 HTTPS 中才有效
httpOnly如果给某个 cookie 设置了 httpOnly 属性,则无法通过 JS 脚本 读取到该 cookie 的信息,但还是能通过 Application 中手动修改 cookie,所以只是在一定程度上可以防止 XSS 攻击,不是绝对的安全

什么是 Session ​

  • session 是另一种记录服务器和客户端会话状态的机制
  • session 是基于 cookie 实现的,session 存储在服务器端,sessionId 会被存储到客户端的cookie 中

img

session 认证流程:

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

根据以上流程可知,SessionID 是连接 Cookie 和 Session 的一道桥梁,大部分系统也是根据此原理来验证用户登录状态。

  • 安全性: Session 比 Cookie 安全,Session 是存储在服务器端的,Cookie 是存储在客户端的。
  • 存取值的类型不同:Cookie 只支持存字符串数据,想要设置其他类型的数据,需要将其转换成字符串,Session 可以存任意数据类型。
  • 有效期不同: Cookie 可设置为长时间保持,比如我们经常使用的默认登录功能,Session 一般失效时间较短,客户端关闭(默认情况下)或者 Session 超时都会失效。
  • 存储大小不同: 单个 Cookie 保存的数据不能超过 4K,Session 可存储数据远高于 Cookie,但是当访问量过多,会占用过多的服务器资源。