PWA 开发:Service Worker、离线缓存与安装

FreeGuideOnline 最新 2026-06-12

PWA 渐进式 Web 应用开发

PWA(Progressive Web App)让网页同时拥有网站和原生应用的优势:快速加载、离线访问、可安装到设备主屏。本教程将手把手带你掌握 PWA 的核心技术——Service Worker、离线缓存与应用安装。

1. 什么是 PWA

PWA 不是某种特定的框架或语言,而是一套现代化的 Web 能力组合,主要包括:

  • 渐进式增强:在任何浏览器都能运行,现代化浏览器体验更佳。
  • 响应式设计:适配各种屏幕。
  • 离线或弱网可用:通过 Service Worker 缓存关键资源。
  • 像原生应用一样交互:通过 Web App Manifest 实现全屏、图标及安装。
  • 始终更新:Service Worker 更新机制确保内容新鲜。
  • 安全:必须使用 HTTPS。

本教程聚焦于后三类:Manifest、Service Worker 与离线缓存。

2. 准备工作:一个简单的页面

首先创建一个最基本的网页项目。目录结构如下:

pwa-demo/
├── index.html
├── style.css
├── app.js
├── manifest.json
├── sw.js
└── images/
    └── icon-192.png
    └── icon-512.png

index.html 中引入必要文件:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="stylesheet" href="style.css">
  <link rel="manifest" href="manifest.json">
  <meta name="theme-color" content="#2196F3">
  <title>我的 PWA 应用</title>
</head>
<body>
  <h1>欢迎使用 PWA</h1>
  <p>这是一个渐进式 Web 应用示例。</p>
  <script src="app.js"></script>
</body>
</html>

注意:PWA 要求 HTTPS 环境,本地开发可使用 localhost 或配合工具如 ngrok

3. Web App Manifest:让应用可安装

Manifest 是一个 JSON 文件,告诉浏览器你的应用应该以何种方式呈现给用户,并支持添加到主屏幕。

基础 manifest.json

{
  "name": "我的 PWA 应用",
  "short_name": "PWA Demo",
  "start_url": "/index.html",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#2196F3",
  "icons": [
    {
      "src": "images/icon-192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "images/icon-512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}

关键字段说明:

  • name / short_name:应用全称与短名称(用于主屏标题)。
  • start_url:启动时加载的页面,通常为根路径。
  • display:显示模式,standalone 看起来更像独立应用(无浏览器 UI)。
  • icons:至少提供 192px 和 512px 的图标,浏览器会按需缩放。
  • theme_color:工具栏颜色。
  • background_color:启动画面背景色。

在 HTML 中通过 <link rel="manifest" href="manifest.json"> 引入。此时用 Chrome 打开页面,DevTools 的 Application -> Manifest 面板可检查 Manifest 是否正确,符合条件时浏览器会弹出安装提示。

4. Service Worker:离线能力的基石

Service Worker 是运行在浏览器背后的独立脚本,拦截和处理网络请求,使丰富的离线体验成为可能。

4.1 注册 Service Worker

app.js 中添加注册逻辑:

if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/sw.js')
      .then(registration => {
        console.log('SW 注册成功: ', registration.scope);
      })
      .catch(err => {
        console.log('SW 注册失败: ', err);
      });
  });
}

注册在页面 load 后执行,避免与首屏资源加载竞争。

4.2 Service Worker 生命周期

  • 安装 (install):适合预缓存关键文件。
  • 激活 (activate):清理旧缓存。
  • 激活后控制页面 (fetch):拦截请求,从缓存或网络响应。

4.3 编写 sw.js:离线缓存

下面创建一个完整的 sw.js,实现预缓存与运行时缓存。

// 缓存版本和资源清单
const CACHE_NAME = 'pwa-cache-v1';
const urlsToCache = [
  '/',
  '/index.html',
  '/style.css',
  '/app.js',
  '/images/icon-192.png',
  '/images/icon-512.png'
];

// 安装事件:预缓存关键资源
self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(cache => {
        console.log('缓存已打开');
        return cache.addAll(urlsToCache);
      })
  );
});

// 激活事件:清理旧版本缓存
self.addEventListener('activate', event => {
  event.waitUntil(
    caches.keys().then(cacheNames => {
      return Promise.all(
        cacheNames.filter(name => name !== CACHE_NAME)
          .map(name => caches.delete(name))
      );
    })
  );
});

// 请求拦截:先查缓存,后走网络
self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request)
      .then(response => {
        // 缓存命中,返回缓存
        if (response) {
          return response;
        }
        // 否则发起网络请求并动态缓存
        return fetch(event.request).then(networkResponse => {
          // 确保响应有效
          if (!networkResponse || networkResponse.status !== 200 || networkResponse.type !== 'basic') {
            return networkResponse;
          }
          const responseToCache = networkResponse.clone();
          caches.open(CACHE_NAME).then(cache => {
            cache.put(event.request, responseToCache);
          });
          return networkResponse;
        });
      })
  );
});

这段代码实现了 Cache First (缓存优先) 策略,适合稳定的静态资源。动态请求会被网络获取后自动缓存,下次离线时可用。

4.4 缓存策略进阶

不同的资源需要不同的缓存策略:

策略名称 描述 适用场景
Cache First 优先从缓存获取,缓存不存在才去网络 静态资源、应用外壳
Network First 优先网络请求,失败时使用缓存 API 数据、需常更新的内容
Stale-While-Revalidate 立即返回缓存,同时后台更新缓存 平衡性能与新鲜度
Network Only 始终从网络获取 敏感信息或实时数据
Cache Only 仅从缓存获取,网络都不尝试 必须完全离线运行的资源

示例:为 API 请求使用 Network First 策略

self.addEventListener('fetch', event => {
  if (event.request.url.includes('/api/')) {
    event.respondWith(
      fetch(event.request)
        .then(response => {
          return caches.open(CACHE_NAME).then(cache => {
            cache.put(event.request, response.clone());
            return response;
          });
        })
        .catch(() => {
          return caches.match(event.request);
        })
    );
  }
  // 其他请求沿用默认逻辑
});

5. 触发安装提示

当应用满足以下条件时,浏览器会自动触发安装横幅:

  1. 拥有有效的 Web App Manifest。
  2. 已注册 Service Worker。
  3. 网站通过 HTTPS 提供(或 localhost)。
  4. 用户与网站有一定交互(如停留一段时间)。

你还可以主动捕获 beforeinstallprompt 事件,自定义安装按钮:

let deferredPrompt;
window.addEventListener('beforeinstallprompt', (e) => {
  // 阻止默认的横幅
  e.preventDefault();
  deferredPrompt = e;
  // 显示自定义安装按钮
  const installBtn = document.createElement('button');
  installBtn.textContent = '安装应用';
  installBtn.style.cssText = 'position:fixed;bottom:20px;right:20px;';
  document.body.appendChild(installBtn);
  
  installBtn.addEventListener('click', () => {
    deferredPrompt.prompt();
    deferredPrompt.userChoice.then(choiceResult => {
      if (choiceResult.outcome === 'accepted') {
        console.log('用户同意安装');
      }
      deferredPrompt = null;
    });
  });
});

6. 调试与测试

Chrome DevTools 是调试 PWA 的利器:

  • Application 面板
    • Manifest:查看 manifest 解析结果和安装条件。
    • Service Workers:查看注册状态、更新、停止、模拟离线。
    • Cache Storage:查看缓存资源列表,删除指定缓存。
  • Lighthouse 审计:生成 PWA 报告,指导 PWA 最佳实践。
  • 离线模拟:在 Service Workers 面板勾选 “Offline”,然后刷新页面,验证离线可用性。

7. 更新你的 PWA

sw.js 文件本身发生变化时,浏览器会检测到新的 Service Worker。但默认旧版本仍在运行,直到所有使用它的页面关闭。为了更及时的更新,可在 install 事件中调用 self.skipWaiting(),并在 activate 中调用 self.clients.claim()

self.addEventListener('install', event => {
  self.skipWaiting(); // 立即生效,不等待页面关闭
  // 预缓存...
});

self.addEventListener('activate', event => {
  event.waitUntil(
    clients.claim() // 让新 SW 立即控制所有客户端
      .then(() => caches.keys().then(/*清理旧缓存*/))
  );
});

8. 最佳实践总结

  • 合理拆分缓存版本:用不同版本号或名称管理静态资源、图片、API 缓存,便于独立更新。
  • 谨慎选择缓存策略:避免将 HTML 入口文件缓存过久导致更新困难,通常对首页用 Network First 或 Stale-While-Revalidate。
  • 控制缓存大小:限制 Cache Storage 的大小,定期清理旧数据。
  • 确保 key 资源及时更新:利用 Service Worker 的 message 事件通知用户有新内容可用。
  • 渐进增强:始终保证基础功能在不支持 Service Worker 的浏览器下也能访问。

9. 完整代码获取

你可以在 GitHub 上找到本教程的示例项目(示例地址),直接克隆并替换自己的资源即可体验。

现在你已经掌握了如何将一个普通网页转变为可安装、可离线访问的 PWA。进一步的探索方向包括推送通知、后台同步、IndexedDB 离线存储等。PWA 的世界刚刚开始。