前端 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 运行在独立的线程中,无法直接访问 windowdocument 等 DOM 对象。与主线程的通信必须通过 postMessage()
  • 调试复杂 :Service Worker 的调试需要借助浏览器开发者工具的 Application 面板。
  • 资源消耗:Service Worker 会占用一定的内存和 CPU,浏览器可能会在空闲时终止它。

进阶使用场景

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

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

相关推荐
10年前端老司机2 小时前
什么!纯前端也能识别图片中的文案、还支持100多个国家的语言
前端·javascript·vue.js
摸鱼仙人~2 小时前
React 性能优化实战指南:从理论到实践的完整攻略
前端·react.js·性能优化
程序员阿超的博客2 小时前
React动态渲染:如何用map循环渲染一个列表(List)
前端·react.js·前端框架
magic 2453 小时前
模拟 AJAX 提交 form 表单及请求头设置详解
前端·javascript·ajax
只喜欢赚钱的棉花没有糖8 小时前
http的缓存问题
前端·javascript·http
小小小小宇8 小时前
请求竞态问题统一封装
前端
loriloy8 小时前
前端资源帖
前端
源码超级联盟8 小时前
display的block和inline-block有什么区别
前端
GISer_Jing8 小时前
前端构建工具(Webpack\Vite\esbuild\Rspack)拆包能力深度解析
前端·webpack·node.js