前端 Service Worker 是一种在浏览器后台运行的脚本,它独立于网页,并能拦截和处理网络请求,实现离线缓存、消息推送、后台同步等功能。它充当了浏览器和网络之间的代理服务器,是构建渐进式 Web 应用(PWA)和实现离线体验的核心技术。
Service Worker 的核心能力和优势
-
离线支持 (Offline Capabilities) :
- 缓存控制:Service Worker 可以拦截所有网络请求,并决定如何响应这些请求,例如从缓存中返回资源,或者发起网络请求。这使得 Web 应用即使在没有网络连接的情况下也能正常工作。
- 提升加载速度:通过缓存常用资源,可以显著减少网络请求,从而加快页面加载速度。
-
增强性能 (Performance Enhancement) :
- 网络代理:作为可编程的网络代理,Service Worker 可以实现各种复杂的缓存策略(如缓存优先、网络优先、Stale-While-Revalidate 等),优化资源加载。
- 预缓存 (Pre-caching) :在用户访问页面之前,Service Worker 可以预先缓存关键资源,使得首次加载更快。
-
后台功能 (Background Features) :
- 消息推送 (Push Notifications) :即使浏览器关闭,Service Worker 也能接收服务器发送的推送消息,并在用户设备上显示通知。
- 后台同步 (Background Sync) :允许在网络连接恢复时,将离线操作(如用户提交的表单数据)同步到服务器。
- 拦截和修改请求/响应:可以修改请求头、响应体等。
Service Worker 的生命周期
Service Worker 的生命周期是一个关键概念,理解它对于正确使用 Service Worker 至关重要:
-
注册 (Registration) :
- 在主线程的 JavaScript 中调用
navigator.serviceWorker.register()
来注册 Service Worker 脚本。 - 浏览器会下载并解析 Service Worker 脚本。
- 在主线程的 JavaScript 中调用
-
安装 (Installation) :
- 注册成功后,浏览器会尝试安装 Service Worker。
install
事件会在 Service Worker 脚本中被触发。- 通常在这个阶段进行缓存预加载 (pre-caching),使用
caches.open()
打开缓存,并使用cache.addAll()
缓存核心静态资源。 event.waitUntil()
用于等待缓存操作完成。
-
激活 (Activation) :
- 安装成功后,Service Worker 会进入激活阶段。
activate
事件会在 Service Worker 脚本中被触发。- 通常在这个阶段清理旧的缓存,确保只使用最新版本的缓存。
self.clients.claim()
用于控制未被当前 Service Worker 控制的页面,使其立即生效。
-
空闲 (Idle) :
- 激活后,Service Worker 处于空闲状态,等待事件触发(如
fetch
,push
,sync
)。
- 激活后,Service Worker 处于空闲状态,等待事件触发(如
-
终止 (Termination) :
- 如果 Service Worker 一段时间内没有活动,浏览器可能会终止它以节省资源。
- 当需要处理事件时,浏览器会再次启动它。
-
更新 (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();
}
});
如何测试
-
启动一个本地服务器 :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
禁用缓存,方便测试)
- 安装
-
打开浏览器 :访问
http://localhost:8080
(或你服务器的端口)。 -
检查 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
等资源。
- Chrome 开发者工具:
-
模拟离线:
- 在 Chrome 开发者工具的
Network
面板中勾选Offline
。 - 或者断开你的网络连接。
- 在 Chrome 开发者工具的
-
刷新页面:即使在离线状态下,页面也应该能正常加载,因为它从 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 开发中实现高性能和离线体验的强大工具,但其生命周期和调试过程相对复杂,需要开发者仔细理解和实践。