pin_drop当前位置:知识文库 ❯ 图文

高级技巧:HTML Service Wo - 详细教程与实战指南

教程简介

Service Worker是现代Web开发中最强大的API之一,它充当浏览器和网络之间的代理服务器,可以拦截网络请求、管理缓存、实现离线体验和推送通知。Service Worker是PWA(渐进式Web应用)的核心技术,使Web应用具备了媲美原生应用的能力。本教程将系统讲解Service Worker的生命周期、注册与安装、Fetch事件拦截、Cache API、离线策略、后台同步和推送通知。

核心概念

Service Worker特点

特点 说明
独立线程 运行在Worker线程,不阻塞主线程
无DOM访问 不能直接操作DOM,通过postMessage通信
生命周期 安装→激活→运行,与页面生命周期独立
HTTPS要求 出于安全考虑,必须运行在HTTPS下(localhost除外)
事件驱动 基于事件模型,不常驻内存
作用域 只能拦截作用域内的请求

Service Worker生命周期

阶段 说明 触发条件
Parsed 解析完成 注册脚本成功解析
Installing 安装中 首次注册或新版本检测到
Installed 安装完成(等待) install事件成功完成
Activating 激活中 旧Service Worker不再控制客户端
Activated 已激活 activate事件成功完成
Redundant 废弃 安装失败或被新版本替代

语法与用法

注册Service Worker

代码示例

// 在主页面中注册
if ('serviceWorker' in navigator) {
    window.addEventListener('load', function() {
        navigator.serviceWorker.register('/sw.js', {
            scope: '/'  // 作用域
        }).then(function(registration) {
            console.log('Service Worker注册成功,作用域:', registration.scope);

            // 检测更新
            registration.addEventListener('updatefound', function() {
                const newWorker = registration.installing;
                console.log('发现新版本:', newWorker.state);

                newWorker.addEventListener('statechange', function() {
                    if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
                        // 新版本已安装,提示用户刷新
                        console.log('新版本可用,请刷新页面');
                    }
                });
            });
        }).catch(function(error) {
            console.log('Service Worker注册失败:', error);
        });
    });
}

安装与激活

代码示例

// sw.js - Service Worker脚本

const CACHE_NAME = 'my-app-v1';
const CACHE_URLS = [
    '/',
    '/index.html',
    '/styles.css',
    '/app.js',
    '/offline.html'
];

// 安装事件
self.addEventListener('install', function(event) {
    console.log('Service Worker安装中...');

    event.waitUntil(
        caches.open(CACHE_NAME)
            .then(function(cache) {
                console.log('缓存文件中...');
                return cache.addAll(CACHE_URLS);
            })
            .then(function() {
                // 跳过等待,立即激活
                return self.skipWaiting();
            })
    );
});

// 激活事件
self.addEventListener('activate', function(event) {
    console.log('Service Worker激活中...');

    event.waitUntil(
        // 清理旧缓存
        caches.keys().then(function(cacheNames) {
            return Promise.all(
                cacheNames.map(function(cacheName) {
                    if (cacheName !== CACHE_NAME) {
                        console.log('删除旧缓存:', cacheName);
                        return caches.delete(cacheName);
                    }
                })
            );
        }).then(function() {
            // 立即控制所有客户端
            return self.clients.claim();
        })
    );
});

Fetch事件拦截

代码示例

// 拦截网络请求
self.addEventListener('fetch', function(event) {
    console.log('拦截请求:', event.request.url);

    event.respondWith(
        // 先尝试网络,失败则使用缓存
        fetch(event.request)
            .then(function(response) {
                // 网络成功,缓存响应
                if (response.ok) {
                    const responseClone = response.clone();
                    caches.open(CACHE_NAME).then(function(cache) {
                        cache.put(event.request, responseClone);
                    });
                }
                return response;
            })
            .catch(function() {
                // 网络失败,使用缓存
                return caches.match(event.request).then(function(cachedResponse) {
                    if (cachedResponse) {
                        return cachedResponse;
                    }
                    // 如果是导航请求,返回离线页面
                    if (event.request.mode === 'navigate') {
                        return caches.match('/offline.html');
                    }
                    return new Response('离线', { status: 503 });
                });
            })
    );
});

Cache API

代码示例

// 打开缓存
caches.open('my-cache').then(function(cache) { ... });

// 添加缓存
cache.add('/page.html');                          // 请求并缓存
cache.addAll(['/page1.html', '/page2.html']);     // 批量请求并缓存
cache.put(request, response);                     // 缓存指定的请求/响应对

// 查找缓存
cache.match(request);                             // 查找匹配的缓存
caches.match(request);                            // 在所有缓存中查找

// 删除缓存
cache.delete(request);                            // 删除指定缓存项
caches.delete('my-cache');                        // 删除整个缓存

// 获取缓存键
cache.keys();                                     // 获取所有缓存请求
caches.keys();                                    // 获取所有缓存名称

离线策略

策略 说明 适用场景
Cache First 优先缓存,缓存不存在时请求网络 静态资源、字体
Network First 优先网络,网络失败时使用缓存 API请求、频繁更新的内容
Stale While Revalidate 先返回缓存,同时更新缓存 非关键的动态内容
Cache Only 只使用缓存 离线页面
Network Only 只使用网络 非GET请求

代码示例

// Cache First策略
function cacheFirst(event) {
    event.respondWith(
        caches.match(event.request).then(function(cachedResponse) {
            return cachedResponse || fetch(event.request).then(function(response) {
                const responseClone = response.clone();
                caches.open(CACHE_NAME).then(function(cache) {
                    cache.put(event.request, responseClone);
                });
                return response;
            });
        })
    );
}

// Network First策略
function networkFirst(event) {
    event.respondWith(
        fetch(event.request).then(function(response) {
            const responseClone = response.clone();
            caches.open(CACHE_NAME).then(function(cache) {
                cache.put(event.request, responseClone);
            });
            return response;
        }).catch(function() {
            return caches.match(event.request);
        })
    );
}

// Stale While Revalidate策略
function staleWhileRevalidate(event) {
    event.respondWith(
        caches.match(event.request).then(function(cachedResponse) {
            const fetchPromise = fetch(event.request).then(function(response) {
                caches.open(CACHE_NAME).then(function(cache) {
                    cache.put(event.request, response);
                });
                return response;
            });
            return cachedResponse || fetchPromise;
        })
    );
}

后台同步

代码示例

// 主页面:注册同步事件
navigator.serviceWorker.ready.then(function(registration) {
    return registration.sync.register('sync-data');
});

// Service Worker:处理同步
self.addEventListener('sync', function(event) {
    if (event.tag === 'sync-data') {
        event.waitUntil(
            // 执行同步操作
            fetch('/api/sync', {
                method: 'POST',
                body: JSON.stringify(getPendingData())
            })
        );
    }
});

推送通知

代码示例

// 主页面:请求通知权限并订阅
async function subscribePush() {
    const permission = await Notification.requestPermission();
    if (permission !== 'granted') return;

    const registration = await navigator.serviceWorker.ready;
    const subscription = await registration.pushManager.subscribe({
        userVisibleOnly: true,
        applicationServerKey: 'YOUR_PUBLIC_KEY'
    });

    // 将subscription发送到服务器
    await fetch('/api/push/subscribe', {
        method: 'POST',
        body: JSON.stringify(subscription)
    });
}

// Service Worker:处理推送
self.addEventListener('push', function(event) {
    const data = event.data ? event.data.json() : {};
    const title = data.title || '新消息';
    const options = {
        body: data.body || '',
        icon: '/icon-192.png',
        badge: '/badge-72.png',
        data: data.url || '/'
    };

    event.waitUntil(
        self.registration.showNotification(title, options)
    );
});

// Service Worker:通知点击
self.addEventListener('notificationclick', function(event) {
    event.notification.close();
    event.waitUntil(
        clients.openWindow(event.notification.data)
    );
});

代码示例

示例1:完整的离线应用

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>离线应用示例</title>
    <link rel="manifest" href="manifest.json">
    <style>
        body {
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
            line-height: 1.6;
            color: #333;
            max-width: 800px;
            margin: 0 auto;
            padding: 2rem;
        }
        .status-bar {
            padding: 0.75rem 1rem;
            border-radius: 4px;
            margin-bottom: 1rem;
            font-weight: 600;
        }
        .online { background: #d4edda; color: #155724; }
        .offline { background: #f8d7da; color: #721c24; }
        .card {
            border: 1px solid #ddd;
            border-radius: 8px;
            padding: 1.5rem;
            margin: 1rem 0;
        }
        button {
            padding: 0.5rem 1rem;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            background: #4A90D9;
            color: white;
            margin: 0.25rem;
        }
    </style>
</head>
<body>
    <div id="statusBar" class="status-bar online">在线</div>

    <h1>离线应用示例</h1>

    <div class="card">
        <h2>Service Worker状态</h2>
        <p id="swStatus">检测中...</p>
        <button onclick="registerSW()">注册Service Worker</button>
        <button onclick="unregisterSW()">注销Service Worker</button>
    </div>

    <div class="card">
        <h2>缓存管理</h2>
        <div id="cacheInfo">加载中...</div>
        <button onclick="updateCache()">更新缓存</button>
        <button onclick="clearCache()">清除缓存</button>
    </div>

    <script>
        // 网络状态监听
        function updateOnlineStatus() {
            const bar = document.getElementById('statusBar');
            if (navigator.onLine) {
                bar.className = 'status-bar online';
                bar.textContent = '在线';
            } else {
                bar.className = 'status-bar offline';
                bar.textContent = '离线 - 使用缓存数据';
            }
        }

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

        // 注册Service Worker
        async function registerSW() {
            if (!('serviceWorker' in navigator)) {
                document.getElementById('swStatus').textContent = '浏览器不支持Service Worker';
                return;
            }

            try {
                // 注意:实际使用时需要创建sw.js文件
                // 此处仅演示注册逻辑
                document.getElementById('swStatus').textContent = 'Service Worker需要HTTPS环境和sw.js文件才能运行';
            } catch (error) {
                document.getElementById('swStatus').textContent = '注册失败:' + error.message;
            }
        }

        async function unregisterSW() {
            const registration = await navigator.serviceWorker.getRegistration();
            if (registration) {
                await registration.unregister();
                document.getElementById('swStatus').textContent = 'Service Worker已注销';
            }
        }

        // 缓存管理
        async function showCacheInfo() {
            if (!('caches' in window)) {
                document.getElementById('cacheInfo').textContent = '浏览器不支持Cache API';
                return;
            }
            const names = await caches.keys();
            if (names.length === 0) {
                document.getElementById('cacheInfo').textContent = '暂无缓存';
                return;
            }
            let info = '缓存列表:';
            for (const name of names) {
                const cache = await caches.open(name);
                const keys = await cache.keys();
                info += `\n${name} (${keys.length}个文件)`;
            }
            document.getElementById('cacheInfo').textContent = info;
        }

        async function clearCache() {
            const names = await caches.keys();
            await Promise.all(names.map(name => caches.delete(name)));
            showCacheInfo();
        }

        async function updateCache() {
            showCacheInfo();
        }

        showCacheInfo();
    </script>
</body>
</html>

浏览器兼容性

特性 Chrome Firefox Safari Edge 移动端
Service Worker 40+ 44+ 11.1+ 17+ 完全支持
Cache API 40+ 39+ 11.1+ 17+ 完全支持
Fetch事件 40+ 44+ 11.1+ 17+ 完全支持
Push API 50+ 44+ 16+ 17+ 部分支持
Background Sync 49+ 不支持 不支持 79+ 部分支持
Notification API 50+ 46+ 16+ 17+ 部分支持
skipWaiting 41+ 44+ 11.1+ 17+ 完全支持
clients.claim 42+ 44+ 11.1+ 17+ 完全支持

注意事项与最佳实践

  1. 必须使用HTTPS:Service Worker只能运行在HTTPS环境下(localhost除外)

  2. 注意作用域:Service Worker默认的作用域是其所在目录及子目录

  3. 合理使用缓存策略:静态资源用Cache First,动态数据用Network First

  4. 版本化缓存名称:使用版本号命名缓存(如my-app-v1),方便更新

  5. 清理旧缓存:在activate事件中删除旧版本缓存

  6. 优雅降级:检测Service Worker支持后再注册,不支持时正常工作

  7. 避免缓存POST请求:只缓存GET请求,POST请求不应缓存

  8. 处理安装失败:install事件中缓存失败会导致Service Worker安装失败

  9. 更新策略:浏览器会自动检测SW文件变化,但需要关闭所有标签页才能激活新版本

  10. 调试工具:使用Chrome DevTools的Application面板调试Service Worker

代码规范示例

代码示例

// 规范:Service Worker模板
const CACHE_NAME = 'my-app-v1';
const STATIC_URLS = ['/', '/styles.css', '/app.js'];
const DYNAMIC_CACHE = 'my-app-dynamic-v1';

// 安装:预缓存静态资源
self.addEventListener('install', event => {
    event.waitUntil(
        caches.open(CACHE_NAME)
            .then(cache => cache.addAll(STATIC_URLS))
            .then(() => self.skipWaiting())
    );
});

// 激活:清理旧缓存
self.addEventListener('activate', event => {
    event.waitUntil(
        caches.keys().then(keys =>
            Promise.all(keys.filter(k => k !== CACHE_NAME && k !== DYNAMIC_CACHE)
                .map(k => caches.delete(k)))
        ).then(() => self.clients.claim())
    );
});

// Fetch:策略路由
self.addEventListener('fetch', event => {
    const url = new URL(event.request.url);

    // 静态资源:Cache First
    if (STATIC_URLS.includes(url.pathname)) {
        event.respondWith(cacheFirst(event.request));
        return;
    }

    // API请求:Network First
    if (url.pathname.startsWith('/api/')) {
        event.respondWith(networkFirst(event.request));
        return;
    }

    // 其他:Stale While Revalidate
    event.respondWith(staleWhileRevalidate(event.request));
});

常见问题与解决方案

问题1:Service Worker更新不生效

解决方案:使用skipWaiting和clients.claim强制更新。

代码示例

self.addEventListener('install', event => {
    event.waitUntil(
        cacheResources().then(() => self.skipWaiting())
    );
});

self.addEventListener('activate', event => {
    event.waitUntil(
        cleanOldCaches().then(() => self.clients.claim())
    );
});

问题2:缓存了错误的响应

解决方案:只缓存成功的响应。

代码示例

fetch(request).then(response => {
    if (response.ok) {
        cache.put(request, response.clone());
    }
    return response;
});

问题3:离线时导航请求返回空白

解决方案:为导航请求提供离线回退页面。

代码示例

if (event.request.mode === 'navigate') {
    event.respondWith(
        fetch(event.request).catch(() => caches.match('/offline.html'))
    );
}

总结

Service Worker是现代Web应用的核心技术,赋予了Web应用离线能力和后台处理能力。通过本教程的学习,你应该掌握了:

  1. 生命周期:install、activate、fetch等事件的正确处理

  2. Cache API:缓存的增删改查操作

  3. 离线策略:Cache First、Network First、Stale While Revalidate

  4. 后台同步:使用sync事件处理离线数据同步

  5. 推送通知:Push API和Notification API的使用

Service Worker使Web应用从"必须在线"进化到"离线可用",是PWA的核心基础。

常见问题

问题1:Service Worker更新不生效?

使用skipWaiting和clients.claim强制更新。

问题2:缓存了错误的响应?

只缓存成功的响应。

问题3:离线时导航请求返回空白?

为导航请求提供离线回退页面。

标签: HTML Service Worker PWA 缓存

本文由小确幸生活整理发布,转载请注明出处

本文涉及AI创作

内容由AI创作,请仔细甄别

list快速访问

上一篇: 高级技巧:HTML Web Compon - 详细教程与实战指南 下一篇: 高级技巧:HTML PWA开发 - 详细教程与实战指南

poll相关推荐