HTTP缓存与离线缓存

11,599 阅读10分钟

HTTP缓存

做技术的对HTTP缓存应该都不陌生,本文除了对HTTP缓存概念上的理解,更有HTTP缓存的实际应用案例。

先从HTTP请求的流程图来了解HTTP缓存的运作

image.png

强缓存

顾名思义强制缓存,无需再次询问服务端。

优点

  1. 减少服务器负载:当客户端发起请求时,如果资源在强缓存有效期内,服务器可以直接返回缓存的资源,而无需再次处理请求,从而减少了服务器的负载。
  2. 提高页面加载速度:由于强缓存直接返回缓存资源,无需再进行网络请求和数据传输,因此可以大大提高页面加载速度。
  3. 减少网络流量:由于强缓存直接返回缓存资源,无需再进行网络请求和数据传输,因此可以减少网络流量消耗。

强缓存涉及的响应头有三个:Pragma > Expires > Cache-Control

Cache-Control字段(HTTP/1.1)

Cache-Control 是 HTTP/1.1 中新增的属性,在请求头和响应头中都可以使用,精确控制缓存时间,可以根据实际需求进行灵活配置。常用的属性值如有:

  • max-age:单位是秒,缓存时间计算的方式是距离发起的时间的秒数,超过间隔的秒数缓存失效。
  • no-cache:不使用强缓存,需要与服务器验证缓存是否新鲜
  • no-store:禁止使用缓存(包括协商缓存),每次都向服务器请求最新的资源
  • private:专用于个人的缓存,中间代理、CDN 等不能缓存此响应
  • public:响应可以被中间代理、CDN 等缓存
  • must-revalidate:在缓存过期前可以使用,过期后必须向服务器验证

Expires字段(HTTP/1.0)

Expires 的值是一个 HTTP 日期,在浏览器发起请求时,会根据系统时间和 Expires 的值进行比较,如果系统时间超过了 Expires 的值,缓存失效。

优点:兼容性好,支持所有HTTP版本。
缺点:由于和系统时间进行比较,所以当系统时间和服务器时间不一致的时候,会有缓存有效期不准的问题,无法精确控制缓存时间。

Pragma

Pragma 只有一个属性值,就是 no-cache ,效果和 Cache-Control 中的 no-cache 一致,不使用强缓存,需要与服务器验证缓存是否新鲜,在3个头部属性中的优先级最高。

强缓存的使用案例

// 距离发起请求后1周内使用强缓存
Cache-Control  max-age=604800

// 2099年12月31日前都生效的强缓存
Expires  Thu Dec 31 2099 08:00:00 GMT

协商缓存

每次使用缓存前,需要与服务端协商缓存是否过期,过期则返回新资源否则使用缓存。

  1. 实时更新:协商缓存通过与服务器进行通信来验证资源是否过期,并根据服务器返回的状态码来决定是否使用本地缓存。这样可以保证客户端始终获取到最新版本的资源。
  2. 灵活控制:通过设置合适的响应头字段(如Last-Modified、ETag),服务器可以灵活地控制客户端是否需要重新获取资源。如果资源未发生变化,则可以返回304 Not Modified状态码,并告知客户端使用本地缓存。

协商缓存涉及的响应头有两个:ETagh > Last-Modified

ETag/If-None-Match

ETag/If-None-Match 的值是一串 hash 码,代表的是一个资源的标识符,当服务端的文件变化的时候,它的 hash 码会随之改变,通过请求头中的 If-None-Match 和当前文件的 hash 值进行比较,如果相等则表示命中协商缓存。
ETag 又有强弱校验之分,如果 hash 码是以 "W/" 开头的一串字符串,说明此时协商缓存的校验是弱校验的,只有服务器上的文件差异(根据 ETag 计算方式来决定)达到能够触发 hash 值后缀变化的时候,才会真正地请求资源,否则返回 304 并加载浏览器缓存。

Last-Modified/If-Modified-Since

Last-Modified/If-Modified-Since 的值代表的是文件的最后修改时间,第一次请求服务端会把资源的最后修改时间放到 Last-Modified 响应头中,第二次发起请求的时候,请求头会带上上一次响应头中的 Last-Modified 的时间,并放到 If-Modified-Since 请求头属性中,服务端根据文件最后一次修改时间和 If-Modified-Since 的值进行比较,如果相等,返回 304 ,并加载浏览器缓存。

ETag/If-None-Match 的出现主要解决了 Last-Modified/If-Modified-Since 所解决不了的问题:

  • 如果文件的修改频率在秒级以下,Last-Modified/If-Modified-Since 会错误地返回 304。
  • 如果文件被修改了,但是内容没有任何变化的时候(如文件属性、权限被修改),Last-Modified/If-Modified-Since 会错误地返回 304,上面的例子就说明了这个问题。

应用场景

综上所述,强缓存、协商缓存在实际应用中如何使用呢?

回想webpack、vite等众多前端构建工具对静态资源的编译后都会以哈希值作为命名的一部分,这被称为文件内容哈希化。其原理是针对每个文件的内容进行哈希计算,通常根据文件内容的摘要算法计算得出唯一的哈希值,那就意味着对应的代码内容不变计算得出的哈希值不变。结合HTTP缓存的使用作出以下方案:

  • 除入口文件外(通常为index.html)的所有静态资源文件都使用强缓存。
  • 入口文件(通常为index.html)则使用协商缓存。

案例

以下图为例。a、b、c三份静态资源文件在多个版本中由于a、b文件内容没有变化导致哈希值不变继续使用强缓存。而文件c的哈希值变化能够重新加载的原因是index.html使用协商缓存,index.html则描述了对静态资源的引用导致了新版本的文件c被加载。

image.png

离线缓存

离线缓存,在初次访问静态资源后被缓存至设备本地,在离线的状态下通过读取本地的静态资源实现无感知的离线体验。

Service Worker

在上述HTTP缓存流程图中,红色标记的ServiceWorker是实现离线缓存的核心,当然是离线缓存实现的其中一种方式,大致原理是借助浏览器API在发出HTTP请求前拦截并且返回本地的资源。

先从缓存机制说起,Service Worker中的缓存可以基于浏览器的Cache API完成,大致的缓存策略有如下几种:

  • Cache First:首先尝试从缓存中获取资源,如果缓存中存在,则直接返回;如果缓存中不存在,则向网络请求资源,并将请求到的资源添加到缓存中。
  • Network First:首先尝试从网络请求资源,如果网络请求成功,则将请求到的资源添加到缓存中,并返回给页面;如果网络请求失败,则尝试从缓存中获取资源。
  • Cache Only:只从缓存中获取资源,如果缓存中不存在该资源,则返回错误。
  • Network Only:只从网络请求资源,不使用缓存。
  • Stale While Revalidate:首先尝试从缓存中获取资源,并立即返回给页面展示;同时向服务器发起请求验证该资源是否过期,如果过期则更新缓存,并在下次请求时使用新的资源。

除了以上常见的缓存策略外,你也可以根据自己的需求自定义任意设置。例如,可以根据URL、文件类型或其他条件来决定是否使用缓存、如何更新缓存等。

需要注意的是,在使用Cache API时,需要在Service Worker脚本中编写相应的逻辑来处理缓存策略和操作。还可以借助workbox-coreworkbox-expirationworkbox-routingworkbox-strategies等插件简化缓存使用。

const cacheName = 'cache-demo';
const filesToCache = [
  '/',
  '/styles/main.css',
  '/scripts/main.js'
];

// 在安装阶段缓存资源
self.addEventListener('install', (event) => {
  event.waitUntil(
    // 不同浏览器厂商对 cache storage 大小的限制不一样
    caches.open(cacheName).then((cache) => {
      return cache.addAll(filesToCache);
    })
  );
});

// 在 fetch 事件中返回缓存的资源
self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((response) => {
      return response || fetch(event.request);
    })
  );
});


// 监听 push 事件
self.addEventListener('push', function(event) {
    event.waitUntil(
       // 获取推送通知的内容
       event.data.text().then(function(message) {
          // 显示推送通知
          return self.registration.showNotification('新消息', {
             body: message,
             icon: 'icon.png'
          });
       })
    );
});

有关service worker的详细问题可通过以下链接查看:

Application Cache

Application Cache 是 HTML5 提供的一种浏览器缓存机制,基于 manifest 文件定义需要缓存的资源列表,用于将Web应用的静态资源保存在本地,实现离线访问。

manifest文件需要正确的配置MIME-type(描述该消息的媒体类型),即"text/cache-manifest"。manifest文件描述大致区分三部分:

  • CACHE:声明文件将在首次下载后进行缓存。
  • NETWORK:声明哪些文件不被缓存。可以使用通配符*,表示除CACHE 外的所有其他资源/文件都需要请求。
  • FALLBACK:声明当前页面无法访问时的替代页面。

ApplicationCache状态描述

状态码描述描述
0UNCACHED无缓存, 即没有与页面相关的应用缓存。
1IDLE闲置,即应用缓存未得到更新。
2CHECKING检查中,即正在下载描述文件并检查更新。
3DOWNLOADING下载中,即应用缓存正在下载描述文件中指定的资源。
4UPDATEREADY更新完成,所有资源都已下载完毕。

ApplicationCache事件

事件描述
checking每当应用程序载入的时候,都会检查该清单文件,也总会首先触发“checking”事件。
noupdate如果没有改动,同时应用程序也已经缓存了“noupdate”事件被触发,整个过程结束 。
downloading如果还未缓存应用程序,或者清单文件有改动,那么浏览器会下载并缓存清单中的所有资源 ,触发"downloading"事件,同时意味着下载过程开始。
progress在下载过程中会间断性触发“progress”事件,通常是在每个文件下载完毕的时候 。
cached下载完成并且首次将应用程序下载到缓存中时,浏览器会触发“cached“事件
updateready当下载完成并将缓存中的应用程序更新后,浏览器会触发”updaterady”事件。
error如果浏览器处于离线状态,检查清单列表失败,则会触发“error“事件,当一个未缓存的应用程序引用一个不存在的清单文件,也会触发此事件
obsolete如果一个缓存的应用程序引用一个不存在的清单文件,会触发“obsolete“,同时将应用从缓存中移除之后不会从缓存而是通过网络加载资源

案例

# demo.appcache
CACHE MANIFEST
#version 1.0
CACHE:
    img.jpg
NETWORK:
    *
FALLBACK:
    /demo/ /404.html
<!DOCTYPE html>
<html manifest="demo.manifest">
<head>
    <title>Application Cache Example</title>
</head>
<body>
    <script>
        const AppCache = window.applicationCache
        if (AppCache) {
            AppCache.addEventListener('updateready', function() {
                if (AppCache.status === AppCache.UPDATEREADY) {
                    AppCache.swapCache();
                    if (confirm('资源版本更新,是否应用?')) {
                        window.location.reload();
                    }
                }
            }, false);
        }
    </script>
</body>
</html>