通过此方案,你可以实现:

1、自动缓存所有静态资源(本地 + 指定外部域)

2、版本化缓存管理(更新即清理)

3、用户友好的更新提示

4、跨域资源支持

5、生产级缓存策略

Astro 集成 Service Worker 教程

第 1 步

public目录下新建sw.js

// 定义版本标识符和缓存名称
const SW_VERSION = 'v1'; // 更新版本号以触发缓存更新
const CACHE_NAME = `${SW_VERSION}-static-cache`;

// 需要预缓存的本地资源模式(支持通配符)
const LOCAL_ASSETS = [
  '/assets/**',
  '/*.js',
  '/*.css',
  '/*.woff2'
];

// 需要缓存的外部域名路径(完整前缀匹配)
const EXTERNAL_ASSETS = [
  'https://Your-CDN-Domain/'
];

// 匹配通配符路径
function matchWildcard(pattern, path) {
  const regexPattern = pattern
    .replace(/\*\*/g, '.*') // ** 匹配任意层级目录
    .replace(/\*/g, '[^/]*') // * 匹配单层目录/文件
    .replace(/\//g, '\\/');
  return new RegExp(`^${regexPattern}$`).test(path);
}

// 检查是否匹配任意通配符模式
function shouldCache(url) {
  const path = new URL(url).pathname;
  return LOCAL_ASSETS.some(pattern => matchWildcard(pattern, path));
}

// ====================== 安装阶段 ======================
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(cache => {
        // 预缓存固定资源
        const staticAssets = LOCAL_ASSETS.filter(path => !path.includes('*'));
        return cache.addAll(staticAssets);
      })
      .then(() => self.skipWaiting())
  );
});

// ====================== 激活阶段 ======================
self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys().then(existingCaches => {
      return Promise.all(
        existingCaches.map(cache => {
          if (cache !== CACHE_NAME) return caches.delete(cache);
        })
      );
    }).then(() => self.clients.claim())
  );
});

// ====================== 请求拦截 ======================
self.addEventListener('fetch', (event) => {
  const url = event.request.url;

  // 处理本地资源(缓存优先)
  if (url.startsWith(self.location.origin)) {
    // 检查是否匹配通配符规则
    if (shouldCache(url)) {
      event.respondWith(
        caches.match(event.request)
          .then(cached => cached || fetchAndCache(event.request))
      );
    }
  }
  // 处理目标外部资源(网络优先,失败回退缓存)
  else if (EXTERNAL_ASSETS.some(domain => url.startsWith(domain))) {
    event.respondWith(
      fetchWithFallback(event.request)
    );
  }
});

// ====================== 策略函数 ======================
async function fetchAndCache(request) {
  try {
    const response = await fetch(request);
    if (response.ok) {
      const cache = await caches.open(CACHE_NAME);
      cache.put(request, response.clone());
    }
    return response;
  } catch (err) {
    const cached = await caches.match(request);
    if (cached) return cached;
    throw err;
  }
}

async function fetchWithFallback(request) {
  try {
    const response = await fetch(request);
    if (response.ok) {
      const cache = await caches.open(CACHE_NAME);
      cache.put(request, response.clone());
    }
    return response;
  } catch (err) {
    return caches.match(request);
  }
}

第 2 步

src/components目录下新建SWController.astro

<!-- 使用现代 Toast 提示代替原生 alert -->
<script is:inline>
if ('serviceWorker' in navigator) {
  const showUpdateToast = () => {
    const toast = document.createElement('div');
    toast.style = 'position:fixed; bottom:20px; right:20px; padding:16px; background:#333; color:white; border-radius:8px;';
    toast.innerHTML = `
      新版本可用!<button 
        style="margin-left:12px; padding:4px 8px; background:#666; border:none; color:white;"
        onclick="window.location.reload()"
      >立即刷新</button>
    `;
    document.body.appendChild(toast);
  };

  // 注册并监听更新
  navigator.serviceWorker.register('/sw.js').then(reg => {
    reg.addEventListener('updatefound', () => {
      const newWorker = reg.installing;
      newWorker.addEventListener('statechange', () => {
        if (newWorker.state === 'activated' && !navigator.serviceWorker.controller) {
          showUpdateToast(); // 首次安装不提示
        } else if (newWorker.state === 'activated') {
          showUpdateToast(); // 检测到更新时提示
        }
      });
    });
  });

  // 检查服务器端是否有更新(每 2 小时)
  setInterval(() => navigator.serviceWorker.ready.then(reg => reg.update()), 7200_000);
}
</script>

第 3 步

在布局或页面中(比如Footer.astro)引入组件:

---
import SWController from '../components/SWController.astro';
---

<body>
  <!-- 页面内容 -->
  <slot />
  <SWController />
</body>