Skip to content

缓存

缓存的目的:减少网络请求,减轻服务器负担,提高页面加载速度

强缓存和协商缓存的主要区别:是否会向服务器发送请求

  • 强缓存:不会向服务器发送请求
  • 协商缓存:会向服务器发送请求,通过一些字段与服务器进行验证,来判断是否需要更新缓存。

若缓存生效,强缓存返回状态码200,协商缓存返回状态码304。他们缓存的内容:静态资源(HTML、CSS、JS、图片、字体)

强缓存

Expires

Expires: 服务器返回一个时间戳(资源的过期时间),由客户端时间决定是否失效

服务端给响应头追加一些字段(expires),在 expires 截止失效时间之前,无论如何刷新页面,都不会重新请求

js
// 响应头添加
ctx.set('Expires', new Date(Date.now() + 10 * 1000).toUTCString())

Cache-Control

Cache-Control:资源缓存的有效时间,在有效时间内,只要客户端发送请求,服务端不会返回新的资源,客户端会直接使用本地缓存。

取值

  • no-store:禁止缓存。表示不应存储请求或响应的任何部分。
  • no-cache:需要重新验证缓存。客户端需要向服务器发送一个请求来确认缓存的有效性。
  • max-age=<seconds>:指定资源在缓存中的最大存储时间,单位为秒。
  • s-maxage=<seconds>:类似于 max-age,但仅适用于代理服务器缓存,而不适用于浏览器缓存。
  • public:表示响应可以被任何缓存(包括代理服务器)缓存,即响应是公共资源。
  • private:表示响应只能被浏览器缓存,不允许代理服务器缓存。适用于包含用户特定信息的响应。
  • must-revalidate:表示客户端必须在使用已缓存的响应之前重新验证该响应的有效性。
  • proxy-revalidate:类似于 must-revalidate,但仅适用于代理服务器缓存。
  • max-stale[=seconds]:表示客户端愿意接受已过期的响应,可指定最长过期时间(可选)。
  • min-fresh=seconds:表示客户端希望获取一个在指定时间内不会过期的响应。
  • immutable:指示响应不会随时间的推移而发生更改,适用于长期缓存的不变资源。
  • no-transform:禁止代理服务器对响应进行任何形式的转换,例如,不要压缩或修改内容。
  • only-if-cached:表示客户端只接受缓存的响应,不要向服务器发送请求。
js
// 响应头添加
ctx.set('Cache-Control', 'public, max-age=10')
js
app.use(
  express.static('public', {
    setHeaders: (res, path) => {
      // 设置强缓存,这里设置缓存时间为 1 小时(3600 秒)
      res.setHeader('Cache-Control', 'public, max-age=3600')
    }
  })
)

协商缓存

浏览器通过一些字段与服务器进行验证,来判断是否需要更新缓存。如果服务器有更新的资源,就会返回状态码304,然后浏览器会向服务器拿最新的资源,去替换之前的本地缓存。

协商缓存(两组)

  • 第一组:

    • 响应状态码:Last-Modified(时间戳,资源最后修改时间)

    初次加载时,服务器返回一个时间

    • 请求状态码:If-Modified-Since

    第一次响应后,客户端发送请求时,会在 If-Modified-Since 携带 Last-Modified 的值,后端去判断是否改变,变了就返回最新数据,没变就返回状态码 304,走缓存

    js
    // 服务器
    async lastModifiedTest(ctx) {
      // 模拟资源数据
      const resource = { id: 1, name: 'Example Resource' };
    
      // 模拟最后修改时间
      const lastModifiedTime = 1689253117727;
    
      // 检查客户端发送的 if-modified-since
      const ifModifiedSince = ctx.get('If-Modified-Since');
      if (ifModifiedSince && new Date(ifModifiedSince).getTime() > lastModifiedTime) {
          // 如果请求的 If-Modified-Since 大于最后修改时间,则返回缓存状态码
          ctx.status = 304;
          return;
      }
    
      // 模拟3秒请求
      await new Promise(resolve => {
          setTimeout(() => {
              resolve();
          }, 3000);
      });
    
      // 设置响应头中的 ETag
      ctx.set('Last-Modified', new Date(lastModifiedTime).toUTCString());
      this.success(ctx, resource);
    }
  • 第二组:(升级)

    • 响应状态码:Etag(文件hash,资源的唯一资源)

    文件 hash 值赋给 Etag,服务端响应的是 Etag

    • 请求状态码:If-None-Match

    去判断 Etag 是否改变,没变就返回状态码 304,走缓存

    js
    // 服务器
    async etagTest(ctx) {
      // 模拟资源数据
      const resource = { id: 1, name: 'Example Resource' };
    
      // 生成 ETag
      const resourceETag = md5(`${resource.id}${resource.name}`);
    
      // 检查客户端发送的 If-None-Match 头
      const requestETag = ctx.get('If-None-Match');
      if (requestETag === resourceETag) {
          // 如果请求的 ETag 与资源的 ETag 匹配,则返回缓存状态码
          ctx.status = 304;
          return;
      }
    
      // 模拟3秒请求数据
      await new Promise(resolve => {
          setTimeout(() => {
              resolve();
          }, 3000);
      });
    
      // 设置响应头中的 ETag
      ctx.set('ETag', resourceETag);
      this.success(ctx, resource);
    }

启发式缓存

启发式缓存是浏览器默认的缓存方式,当浏览器没有从服务器获取到明确的缓存指令(如Cache-ControlExpires等头部信息)时,它会基于资源的类型、大小、上次访问时间等因素,运用内置的启发式算法来决定将资源在本地缓存多长时间。

启发式缓存时间公式:缓存时间 = (当前时间 - Last-Modified) * 10%

Last-Modified:服务器中资源最后被修改的日期

策略缓存

所用技术 service worker,是一个 web worker,它运行在浏览器后台的脚本,与页面分离,不会影响页面的加载速度。

service worker 功能:

  • 拦截请求,缓存资源
  • 离线功能(PWA),离线本质就是不与服务器发生请求,直接从缓存中获取数据,service worker 可以拦截请求,并优先从缓存中获取数据

策略缓存可以拦截前端资源请求,并约定请求缓存策略

  • 仅限缓存:仅缓存,不请求网络
  • 仅限网络:仅请求网络,不缓存
  • 先缓存,然后回退到网络:(一般常用)
    1. 请求命中缓存。如果资源在缓存中,请从缓存中提供。
    2. 如果请求不在缓存中,请转到网络。
    3. 在网络请求完成后,将其添加到缓存中, 然后从网络返回响应。
  • 先网络,回退到缓存
    1. 您首先要访问网络以获取请求,然后将响应放入缓存中。
    2. 如果您稍后处于离线状态, 则您会在缓存中回退到该响应的最新版本。

面试题 1:如果要缓存接口数据,怎样缓存?

  • http 缓存头:Cache-ControlExpiresLast-ModifiedETag,去缓存接口响应的数据
    • 请求缓存头:If-Modified-SinceIf-None-Match
    • 响应端缓存头:Last-ModifiedETag
  • localStorage、sessionStorage 小规模数据
  • indexedDB 较大规模数据
  • service worker 进行请求的拦截,缓存数据

面试题 2:如果使用 localStorage 缓存数据,可能会有安全问题,会怎么做?

localStorage 存储的数据是明文,不安全

解决方法

  • 敏感数据加密后存储
  • 避免敏感数据存储到 localStorage 中
  • HttpOnlySecure 标记 cookie 替换(标记 cookie 替换通常是指在网络环境中,对与特定标记相关的 cookie 进行更新、修改或替换的操作。)