浏览器缓存

3 阅读4分钟

浏览器缓存

一、核心概念:三层缓存体系

  • 浏览器缓存通常分为三层,你需要清楚它们的分工: 缓存类型 | 控制方 | 特点 | 缺点 Memory Cache​ | 浏览器 | 内存缓存,读取最快,关闭 Tab 即失 | 容量极小,不可控 Disk Cache​ | HTTP Header | 强缓存/协商缓存,存硬盘 | 不可编程,只能靠 HTTP 头 Service Worker Cache​ | 开发者​ | 可编程缓存,完全控制请求/响应 | 需 HTTPS,代码复杂

  • Cache API​ 就是第三层,它允许你像操作数据库一样操作缓存。

二、Service Worker:背后的“幕后黑手”

  • 什么是 Service Worker
    1. 它是一个独立于网页主线程的 JavaScript 脚本。
      • 角色:充当 Web 应用与网络之间的代理服务器。
      • 能力:拦截所有 fetch请求,决定是从缓存读、网络读,还是自己造一个响应。
      • 限制:必须在 HTTPS 或 localhost 下运行。
    2. Service Worker 的生命周期 (核心考点)
      • 这是面试必问的,理解它才能处理好缓存更新。
        1. Registering (注册):主线程注册 SW。
        2. Installing (安装):SW 首次被下载并执行。通常在这里预缓存(Cache Static Assets)。
        3. Waiting (等待):等待旧版本的 SW 关闭。
        4. Activating (激活):新 SW 接管页面。通常在这里清理旧缓存。
        5. Activated (激活后):开始拦截 fetch事件。

三、Cache API 核心操作

  • Cache API 是一个 Key-Value 存储,Key 是 Request,Value 是 Response。
    1. 打开缓存
          // 打开一个名为 'v1' 的缓存空间
          const cache = await caches.open('v1');
      
    2. 添加资源 (Add / AddAll)
          // 缓存单个文件
          await cache.add('/styles.css');
      
          // 批量缓存(常用于安装阶段)
          await cache.addAll([
              '/',
              '/index.html',
              '/app.js',
              '/logo.png'
          ]);
      
    3. 匹配与读取
          // 查找缓存中是否有匹配的请求
          const cachedResponse = await caches.match(request);
          if (cachedResponse) {
              return cachedResponse; // 返回缓存
          }
      
    4. 删除
          // 删除整个缓存空间
          await caches.delete('old-v1');
      

四、实战:Fetch 配合 Service Worker 实现离线缓存

  • 最经典的 App Shell​ 架构
  • 步骤 1:主线程注册 Service Worker
// main.js
if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/sw.js')
      .then(registration => {
        console.log('SW registered: ', registration);
      });
  });
}   
  • 步骤 2:编写 Service Worker 脚本 (sw.js)
// sw.js

const CACHE_NAME = 'app-shell-v1';
const urlsToCache = [
  '/',
  '/index.html',
  '/styles/main.css',
  '/scripts/app.js',
  '/images/logo.svg'
];

// 1. 安装阶段:预缓存 App Shell
self.addEventListener('install', event => {
  console.log('Installing Service Worker...');
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(cache => {
        console.log('Caching app shell');
        return cache.addAll(urlsToCache);
      })
      // 强制跳过等待,直接进入 activate
      .then(() => self.skipWaiting())
  );
});

// 2. 激活阶段:清理旧缓存
self.addEventListener('activate', event => {
  console.log('Activating Service Worker...');
  event.waitUntil(
    caches.keys().then(cacheNames => {
      return Promise.all(
        cacheNames.map(cache => {
          if (cache !== CACHE_NAME) {
            console.log('Deleting old cache:', cache);
            return caches.delete(cache);
          }
        })
      );
    }).then(() => self.clients.claim()) // 立即接管所有页面
  );
});

// 3. 拦截 Fetch 请求 (最核心)
self.addEventListener('fetch', event => {
  // 如果是导航请求(HTML页面),采用 Network First 策略
  if (event.request.mode === 'navigate') {
    event.respondWith(
      fetch(event.request).catch(() => caches.match('/offline.html'))
    );
    return;
  }

  // 其他资源(CSS/JS/图片)采用 Cache First 策略
  event.respondWith(
    caches.match(event.request)
      .then(response => {
        // 如果缓存中有,直接返回
        if (response) {
          return response;
        }
        // 否则去网络请求,并缓存结果
        return fetch(event.request).then(networkResponse => {
          // 克隆响应(因为响应流只能读一次)
          const responseToCache = networkResponse.clone();
          caches.open(CACHE_NAME)
            .then(cache => {
              cache.put(event.request, responseToCache);
            });
          return networkResponse;
        });
      })
  );
});

五、缓存策略详解

  • 不同的资源需要不同的策略,这是架构能力的体现。 策略名称 逻辑 适用场景 Cache First​ 先查缓存,无则网络。 静态资源​ (图片、CSS、JS)。最快,离线可用。 Network First​ 先请求网络,失败则缓存。 HTML 页面、API 数据。保证最新,网络不行才兜底。 Stale-While-Revalidate​ 先返回缓存,后台悄悄更新缓存。 新闻列表、用户头像。速度快,数据略旧但很快更新。 Network Only​ 只用网络。 支付接口、实时数据(股票)。 Cache Only​ 只用缓存。 很少用,除非是完全离线的 App。

六、Fetch 与 Cache API 的配合细节

  1. 为什么需要 clone()?
    • 在 Service Worker 中,Response 是一个 Readable Stream。
      1. 规则:流只能被消费一次。
      2. 如果你在 fetch中读了一次流,又想把它放进缓存,就会报错。
      3. 解决:event.respondWith(fetch(req).then(res => res.clone()))。
  2. 如何更新缓存?
    • 版本号法:修改 CACHE_NAME = 'v2'。
    • 在 activate事件中删除所有不等于 v2的旧缓存。
    • 刷新页面后,新的 SW 会接管并缓存新资源。

七、调试技巧

  • 打开 Chrome DevTools -> Application​ -> Service Workers。
  • 查看 Cache Storage,可以看到具体缓存了哪些文件。
  • 勾选 Update on reload,方便开发时每次刷新都更新 SW。
  • 使用 Offline​ 开关模拟断网环境,测试离线缓存是否生效。

八、总结

  • Fetch + Service Worker + Cache API​ 构成了现代 Web 的“超级武器”:
    1. Fetch​ 负责发起请求。
    2. Service Worker​ 作为中间人拦截请求。
    3. Cache API​ 负责存储响应。
  • 这套机制让你的网站不仅能秒开(Cache First),还能在地铁里、电梯里(离线)继续运行。