Service Worker 与 PWA 完整指南:从零构建离线优先的现代 Web 应用

By | 2026年6月17日

前言

Progressive Web App(PWA)已成为提升 Web 应用体验的关键技术,而 Service Worker 是其核心引擎。它能拦截网络请求、管理缓存、实现离线访问,甚至支持后台同步与推送通知。然而,Service Worker 的生命周期复杂,缓存策略选择不当容易导致资源过期或版本混乱。本文将从零开始,带你构建一个完整的 PWA,涵盖注册、安装、激活、拦截请求、缓存更新等全流程,并分享生产环境下的最佳实践。

1. 项目初始化

创建一个简单的 HTML 页面,包含一个图标和清单文件。


mkdir pwa-demo && cd pwa-demo

创建 index.html


<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>PWA 演示</title>
    <link rel="manifest" href="manifest.json">
</head>
<body>
    <h1>Hello, PWA!</h1>
    <img src="icon.png" alt="icon" width="100">
    <script src="app.js"></script>
</body>
</html>

创建 manifest.json


{
    "name": "PWA Demo",
    "short_name": "PWADemo",
    "start_url": "/",
    "display": "standalone",
    "background_color": "#ffffff",
    "theme_color": "#000000",
    "icons": [
        {
            "src": "icon-192.png",
            "sizes": "192x192",
            "type": "image/png"
        },
        {
            "src": "icon-512.png",
            "sizes": "512x512",
            "type": "image/png"
        }
    ]
}

> 注意:图标文件需准备 192×192 和 512×512 的 PNG 图片,可临时用占位图代替。

2. 注册 Service Worker

创建 sw.js 文件,这是 Service Worker 的脚本。在 app.js 中注册它。


// app.js
if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('/sw.js')
        .then(registration => {
            console.log('SW 注册成功:', registration.scope);
        })
        .catch(error => {
            console.log('SW 注册失败:', error);
        });
} else {
    console.log('当前浏览器不支持 Service Worker');
}

> 注意sw.js 必须放在站点根目录(或同域下),且 scope 默认为脚本所在目录。建议放在根目录以控制整个站点。

3. 生命周期与核心事件

Service Worker 有三个关键事件:installactivatefetch

3.1 install 事件:预缓存静态资源

sw.js 中监听 install 事件,通常在此处预缓存应用外壳(App Shell)所需的静态资源。


// sw.js
const CACHE_NAME = 'pwa-demo-v1';
const urlsToCache = [
    '/',
    '/index.html',
    '/icon.png',
    '/app.js'
];

self.addEventListener('install', event => {
    event.waitUntil(
        caches.open(CACHE_NAME)
            .then(cache => {
                console.log('缓存已打开');
                return cache.addAll(urlsToCache);
            })
            .catch(err => console.error('缓存失败:', err))
    );
});

> 注意cache.addAll() 是原子操作,任一请求失败则整个缓存失败。建议先单独缓存关键资源,或使用 try-catch。

3.2 activate 事件:清理旧缓存

当新 Service Worker 激活时,应删除旧版本的缓存,避免磁盘空间浪费。


self.addEventListener('activate', event => {
    const cacheWhitelist = [CACHE_NAME];
    event.waitUntil(
        caches.keys().then(cacheNames => {
            return Promise.all(
                cacheNames.map(cacheName => {
                    if (cacheWhitelist.indexOf(cacheName) === -1) {
                        console.log('删除旧缓存:', cacheName);
                        return caches.delete(cacheName);
                    }
                })
            );
        })
    );
});

3.3 fetch 事件:拦截请求并返回缓存

这是核心部分,决定缓存策略。常用策略有:

  • Cache First:优先从缓存获取,失败则请求网络。
  • Network First:优先网络,失败则回退缓存。
  • Stale While Revalidate:同时返回缓存和网络更新。

以下实现 Cache First 策略:


self.addEventListener('fetch', event => {
    event.respondWith(
        caches.match(event.request)
            .then(response => {
                if (response) {
                    return response; // 缓存命中
                }
                return fetch(event.request); // 请求网络
            })
            .catch(() => {
                // 离线且无缓存时返回默认页
                return caches.match('/offline.html');
            })
    );
});

> 注意:对于 API 请求(如 /api/data),通常使用 Network First 或 Stale While Revalidate 以保证数据新鲜。可针对不同 URL 模式使用不同策略。

4. 更新 Service Worker

sw.js 内容变化(字节级别),浏览器会检测到并触发更新。但新 SW 不会立即接管,需等待旧 SW 控制的所有页面关闭。可手动调用 self.skipWaiting()clients.claim() 实现立即更新。


// 在 install 事件中
self.addEventListener('install', event => {
    self.skipWaiting(); // 立即激活新 SW
    // ... 缓存逻辑
});

// 在 activate 事件中
self.addEventListener('activate', event => {
    clients.claim(); // 立即控制所有客户端
    // ... 清理缓存
});

> 最佳实践:通常不推荐立即激活,而是通知用户有新版本,让用户手动刷新。可通过监听 controllerchange 事件实现。

5. 后台同步与推送通知

5.1 后台同步

当用户离线时提交的表单,可在网络恢复后自动同步。


// 注册同步事件
navigator.serviceWorker.ready.then(registration => {
    registration.sync.register('sync-form-data');
});

// sw.js 中监听
self.addEventListener('sync', event => {
    if (event.tag === 'sync-form-data') {
        event.waitUntil(syncFormData());
    }
});

async function syncFormData() {
    // 从 IndexedDB 读取待同步数据并发送请求
    const data = await getPendingData();
    await fetch('/api/submit', { method: 'POST', body: data });
}

5.2 推送通知

需要服务端配合,这里只展示客户端接收。


// 请求权限
Notification.requestPermission().then(permission => {
    if (permission === 'granted') {
        // 订阅推送
    }
});

// sw.js 中监听推送事件
self.addEventListener('push', event => {
    const data = event.data.json();
    const options = {
        body: data.body,
        icon: '/icon.png'
    };
    event.waitUntil(
        self.registration.showNotification(data.title, options)
    );
});

6. 常见陷阱与调试

  • HTTPS 要求:Service Worker 只能在 HTTPS 或 localhost 下工作。
  • 作用域限制sw.js 只能控制其所在目录及子目录下的页面。
  • 缓存更新:修改 sw.js 后浏览器可能不更新,需在开发者工具中勾选“Update on reload”。
  • 调试工具:Chrome DevTools 的 Application 面板可查看 SW 状态、缓存内容及清除缓存。

7. 完整示例代码

以下是整合后的 sw.js


const CACHE_NAME = 'pwa-demo-v2';
const urlsToCache = [
    '/',
    '/index.html',
    '/icon.png',
    '/app.js',
    '/offline.html'
];

self.addEventListener('install', event => {
    self.skipWaiting();
    event.waitUntil(
        caches.open(CACHE_NAME)
            .then(cache => cache.addAll(urlsToCache))
    );
});

self.addEventListener('activate', event => {
    clients.claim();
    event.waitUntil(
        caches.keys().then(cacheNames => {
            return Promise.all(
                cacheNames.map(cacheName => {
                    if (cacheName !== CACHE_NAME) {
                        return caches.delete(cacheName);
                    }
                })
            );
        })
    );
});

self.addEventListener('fetch', event => {
    event.respondWith(
        caches.match(event.request)
            .then(response => response || fetch(event.request))
            .catch(() => caches.match('/offline.html'))
    );
});

总结

通过本文,你已掌握 Service Worker 的核心概念与实现:

  • 注册与生命周期管理
  • 缓存策略(Cache First、Network First 等)
  • 更新机制与立即激活技巧
  • 后台同步与推送通知入门
  • 常见陷阱与调试方法

下一步,可以探索 Workbox 库简化 Service Worker 编写,或结合 IndexedDB 实现更复杂的离线数据管理。PWA 的未来还包括 Web 包、流式安装等新特性,值得持续关注。

希望本文能为你的 PWA 实践提供清晰指引。如有疑问,欢迎在评论区交流。