前端 Service Worker

前端 Service Worker 是一种在浏览器后台运行的脚本,它独立于网页,并能拦截和处理网络请求,实现离线缓存、消息推送、后台同步等功能。它充当了浏览器和网络之间的代理服务器,是构建渐进式 Web 应用(PWA)和实现离线体验的核心技术。

Service Worker 的核心能力和优势

  1. 离线支持 (Offline Capabilities) :

    • 缓存控制:Service Worker 可以拦截所有网络请求,并决定如何响应这些请求,例如从缓存中返回资源,或者发起网络请求。这使得 Web 应用即使在没有网络连接的情况下也能正常工作。
    • 提升加载速度:通过缓存常用资源,可以显著减少网络请求,从而加快页面加载速度。
  2. 增强性能 (Performance Enhancement) :

    • 网络代理:作为可编程的网络代理,Service Worker 可以实现各种复杂的缓存策略(如缓存优先、网络优先、Stale-While-Revalidate 等),优化资源加载。
    • 预缓存 (Pre-caching) :在用户访问页面之前,Service Worker 可以预先缓存关键资源,使得首次加载更快。
  3. 后台功能 (Background Features) :

    • 消息推送 (Push Notifications) :即使浏览器关闭,Service Worker 也能接收服务器发送的推送消息,并在用户设备上显示通知。
    • 后台同步 (Background Sync) :允许在网络连接恢复时,将离线操作(如用户提交的表单数据)同步到服务器。
    • 拦截和修改请求/响应:可以修改请求头、响应体等。

Service Worker 的生命周期

Service Worker 的生命周期是一个关键概念,理解它对于正确使用 Service Worker 至关重要:

  1. 注册 (Registration) :

    • 在主线程的 JavaScript 中调用 navigator.serviceWorker.register() 来注册 Service Worker 脚本。
    • 浏览器会下载并解析 Service Worker 脚本。
  2. 安装 (Installation) :

    • 注册成功后,浏览器会尝试安装 Service Worker。
    • install 事件会在 Service Worker 脚本中被触发。
    • 通常在这个阶段进行缓存预加载 (pre-caching),使用 caches.open() 打开缓存,并使用 cache.addAll() 缓存核心静态资源。
    • event.waitUntil() 用于等待缓存操作完成。
  3. 激活 (Activation) :

    • 安装成功后,Service Worker 会进入激活阶段。
    • activate 事件会在 Service Worker 脚本中被触发。
    • 通常在这个阶段清理旧的缓存,确保只使用最新版本的缓存。
    • self.clients.claim() 用于控制未被当前 Service Worker 控制的页面,使其立即生效。
  4. 空闲 (Idle) :

    • 激活后,Service Worker 处于空闲状态,等待事件触发(如 fetch, push, sync)。
  5. 终止 (Termination) :

    • 如果 Service Worker 一段时间内没有活动,浏览器可能会终止它以节省资源。
    • 当需要处理事件时,浏览器会再次启动它。
  6. 更新 (Update) :

    • 当 Service Worker 脚本文件发生变化时,浏览器会重新下载并尝试安装新的版本。
    • 新的 Service Worker 会在后台安装,但不会立即激活,而是等待所有受控页面关闭或刷新后,或者调用 self.skipWaiting() 后才激活,以避免"旧"页面使用"新"Service Worker 导致不一致。

Service Worker 的使用示例:实现离线缓存

这个例子将演示如何使用 Service Worker 缓存静态资源,并使其在离线状态下可用。

项目结构:

bash 复制代码
my-pwa-app/
├── index.html
├── style.css
├── app.js
├── service-worker.js
└── images/
    └── logo.png

1. index.html (主页面)

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Service Worker Offline Demo</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <header>
        <h1>测试 Service Worker </h1>
        <img src="images/logo.png" alt="Logo" width="100">
    </header>
    <main>
        <p> Service Worker's 离线</p>
        <button id="checkOnlineStatus">Check Online Status</button>
        <p id="onlineStatus">当前在线</p>
    </main>
    <script src="app.js"></script>
</body>
</html>

2. style.css (样式文件)

css 复制代码
body {
    font-family: Arial, sans-serif;
    margin: 20px;
    background-color: #f4f4f4;
    color: #333;
}
header {
    background-color: #007bff;
    color: white;
    padding: 15px;
    text-align: center;
    border-radius: 8px;
    margin-bottom: 20px;
}
main {
    background-color: white;
    padding: 20px;
    border-radius: 8px;
    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
img {
    display: block;
    margin: 10px auto;
}
button {
    padding: 10px 15px;
    background-color: #28a745;
    color: white;
    border: none;
    border-radius: 5px;
    cursor: pointer;
    margin-top: 15px;
}
button:hover {
    background-color: #218838;
}
#onlineStatus {
    margin-top: 10px;
    font-weight: bold;
}

3. app.js (主线程脚本,注册 Service Worker)

js 复制代码
// app.js
if ('serviceWorker' in navigator) {
    window.addEventListener('load', () => {
        navigator.serviceWorker.register('/service-worker.js')
            .then(registration => {
                console.log('Service Worker registered with scope:', registration.scope);
            })
            .catch(error => {
                console.error('Service Worker registration failed:', error);
            });
    });
} else {
    console.warn('Service Workers are not supported in this browser.');
}

// 检查在线状态的按钮逻辑
const checkOnlineStatusBtn = document.getElementById('checkOnlineStatus');
const onlineStatusDiv = document.getElementById('onlineStatus');

function updateOnlineStatus() {
    onlineStatusDiv.textContent = `You are: ${navigator.onLine ? 'Online' : 'Offline'}`;
    onlineStatusDiv.style.color = navigator.onLine ? 'green' : 'red';
}

checkOnlineStatusBtn.addEventListener('click', updateOnlineStatus);
window.addEventListener('online', updateOnlineStatus);
window.addEventListener('offline', updateOnlineStatus);

// 初始更新状态
updateOnlineStatus();

4. service-worker.js (Service Worker 脚本)

js 复制代码
// service-worker.js
const CACHE_NAME = 'my-pwa-cache-v1'; // 缓存版本号,更新时修改
const urlsToCache = [
    '/', // 根路径,通常指 index.html
    '/index.html',
    '/style.css',
    '/app.js',
    '/images/logo.png'
];

// 安装事件:缓存应用外壳 (App Shell)
self.addEventListener('install', (event) => {
    console.log('[Service Worker] Installing...');
    event.waitUntil(
        caches.open(CACHE_NAME)
            .then((cache) => {
                console.log('[Service Worker] Caching app shell');
                return cache.addAll(urlsToCache); // 缓存所有预设资源
            })
            .then(() => self.skipWaiting()) // 强制新 Service Worker 立即激活
            .catch((error) => {
                console.error('[Service Worker] Caching failed:', error);
            })
    );
});

// 激活事件:清理旧缓存
self.addEventListener('activate', (event) => {
    console.log('[Service Worker] Activating...');
    event.waitUntil(
        caches.keys().then((cacheNames) => {
            return Promise.all(
                cacheNames.map((cacheName) => {
                    if (cacheName !== CACHE_NAME) {
                        console.log('[Service Worker] Deleting old cache:', cacheName);
                        return caches.delete(cacheName); // 删除旧版本缓存
                    }
                })
            );
        }).then(() => self.clients.claim()) // 立即控制所有客户端
    );
});

// Fetch 事件:拦截网络请求并提供缓存策略
self.addEventListener('fetch', (event) => {
    // 检查请求是否来自同源,并且是 GET 请求
    // 对于跨域请求或 POST 请求,通常不进行缓存,直接走网络
    if (event.request.url.startsWith(self.location.origin) && event.request.method === 'GET') {
        event.respondWith(
            caches.match(event.request) // 尝试从缓存中匹配请求
                .then((response) => {
                    // 如果缓存中有匹配的响应,则返回缓存的响应
                    if (response) {
                        console.log('[Service Worker] Serving from cache:', event.request.url);
                        return response;
                    }
                    // 如果缓存中没有,则发起网络请求
                    console.log('[Service Worker] Fetching from network:', event.request.url);
                    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;
                        })
                        .catch(() => {
                            // 网络请求失败(离线或网络问题),可以返回一个离线页面或默认图片
                            console.log('[Service Worker] Network request failed, serving offline page/fallback if available.');
                            // 可以在这里返回一个预设的离线页面
                            // return caches.match('/offline.html');
                            // 或者返回一个默认的图片等
                            return new Response('<h1>You are offline!</h1>', {
                                headers: { 'Content-Type': 'text/html' }
                            });
                        });
                })
        );
    } else {
        // 对于非同源或非 GET 请求,直接通过网络请求
        console.log('[Service Worker] Bypassing cache for:', event.request.url);
        return fetch(event.request);
    }
});

// 监听消息事件 (可选,用于主线程与 Service Worker 通信)
self.addEventListener('message', (event) => {
    if (event.data && event.data.type === 'SKIP_WAITING') {
        self.skipWaiting();
    }
});

如何测试

  1. 启动一个本地服务器 :Service Worker 只能在 localhost 或 HTTPS 环境下运行。你可以使用 http-server (npm 包) 或 VS Code 的 Live Server 插件。

    • 安装 http-server: npm install -g http-server
    • 在 my-pwa-app 目录下运行: http-server -c-1 ( -c-1 禁用缓存,方便测试)
  2. 打开浏览器 :访问 http://localhost:8080 (或你服务器的端口)。

  3. 检查 Service Worker 状态:

    • Chrome 开发者工具: Application -> Service Workers。你应该能看到 service-worker.js 处于 activated and running 状态。
    • Cache Storage: 在 Application -> Cache Storage 中,你应该能看到 my-pwa-cache-v1 缓存,里面包含了 index.html, style.css, app.js, images/logo.png 等资源。
  4. 模拟离线:

    • 在 Chrome 开发者工具的 Network 面板中勾选 Offline。
    • 或者断开你的网络连接。
  5. 刷新页面:即使在离线状态下,页面也应该能正常加载,因为它从 Service Worker 缓存中获取了所有资源。

Service Worker 的注意事项和限制

  • 仅限 HTTPS 或 localhost :出于安全考虑,Service Worker 只能在安全上下文(HTTPS)中注册和运行,或者在 localhost 上进行开发测试。
  • 同源策略:Service Worker 脚本必须与它所控制的页面同源。
  • 生命周期复杂 :理解 install, activate, skipWaiting(), clients.claim() 的作用对于正确更新和管理 Service Worker 至关重要。
  • 无法直接访问 DOM :Service Worker 运行在独立的线程中,无法直接访问 window、document 等 DOM 对象。与主线程的通信必须通过 postMessage()。
  • 调试复杂 :Service Worker 的调试需要借助浏览器开发者工具的 Application 面板。
  • 资源消耗:Service Worker 会占用一定的内存和 CPU,浏览器可能会在空闲时终止它。

进阶使用场景

  • 网络优先 (Network First) :尝试从网络获取资源,如果网络不可用或请求失败,则从缓存中获取。
  • 缓存优先 (Cache First) :尝试从缓存获取资源,如果缓存中没有,则从网络获取。
  • Stale-While-Revalidate:立即从缓存中返回资源,同时在后台发起网络请求并更新缓存。
  • 路由策略:根据请求的 URL 或类型,应用不同的缓存策略(例如,图片使用缓存优先,API 请求使用网络优先)。
  • 后台同步 :使用 SyncManager API 在网络恢复时发送离线数据。
  • 推送通知:结合 Push API 实现消息推送。

Service Worker 是现代 Web 开发中实现高性能和离线体验的强大工具,但其生命周期和调试过程相对复杂,需要开发者仔细理解和实践。

相关推荐
zach20 分钟前
前端落地实战:Next.js 项目 Nginx + PM2 生产部署全流程(附踩坑总结)
前端·pm2·next.js
overmind29 分钟前
oeasy教h5前端001 修改的乐趣_直接修改在线网页
前端
只睡四小时34 分钟前
JS 手写 D-pad 空间导航:电视端焦点引擎实战
android·开发语言·前端·javascript·ecmascript·hls·空间导航
恋猫de小郭37 分钟前
Android 原生的 Compose A2UI 也来了,你还抱着 XML 养老吗?
android·前端·flutter
庄园特聘拆椅狂魔37 分钟前
从连连看到粒子消散——前端小游戏与视觉特效的技术选型
前端
JudithHuang1 小时前
React 常用 Hooks
前端·react.js·前端框架
sycmancia2 小时前
Qml——Window元素使用
前端
每天都好困2 小时前
03 AR 投影与误差修正:把地图上的点贴到视频画面上
前端
每天都好困2 小时前
04 地图图层封装:像管理 Photoshop 图层一样管理地图元素
前端
去伪存真2 小时前
构建你的第一个 DevOps AI Agent:自动化捕获、分析并修复 CI 故障
前端·agent