works
Service Worker
基本使用
主程序
js
navigator.serviceWorker.register('./work.js')
navigator.serviceWorker.addEventListener('message', function (e) {
console.log('收到 Service Worker 消息:', e.data)
})
ServiceWorke.onclick = () => {
if (navigator.serviceWorker.controller) {
console.log('发送消息到 Service Worker')
navigator.serviceWorker.controller.postMessage('1111')
} else {
console.log('Service Worker 未控制当前页面')
}
}
work.js
js
self.addEventListener('message', function (e) {
console.log('service worker receive message', e.data);
e.waitUntil(
self.clients.matchAll().then(function (clients) {
if (!clients || clients.length === 0) {
return;
}
clients.forEach(function (client) {
client.postMessage(e.data + 3435);
});
})
);
});
应用场景
1.离线支持
js
// sw.js (Service Worker)
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open('my-cache').then((cache) => {
return cache.addAll([
'/',
'/index.html',
'/styles.css',
'/script.js',
'/offline.html' // 离线时显示的页面
]);
})
);
});
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((response) => {
return response || fetch(event.request).catch(() => {
return caches.match('/offline.html'); // 返回离线页面
});
})
);
});
2. 资源缓存和版本管理
js
// sw.js (Service Worker)
const CACHE_NAME = 'my-cache-v2';
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll([
'/',
'/index.html',
'/styles.css',
'/script.js'
]);
})
);
});
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) {
return caches.delete(cacheName);
}
})
);
})
);
});
3. 背景同步(Background Sync)是一种 Web API,允许在用户的设备恢复网络连接后,自动将离线期间的操作(如表单提交、消息发送等)进行同步。它的主要目的是提高用户体验,确保用户在离线时进行的操作不会丢失。
- Background Sync 的工作原理
发起请求 用户在应用中进行某个操作(例如提交表单或发送消息)时,应用会尝试将数据发送到服务器。
检测请求结果
- 请求成功: 数据成功发送到服务器,通常没有进一步的操作。
- 请求失败: 应用需要确定失败的原因。
- 判断网络状态
- 如果请求失败,并且检测到网络不可用(使用
navigator.onLine
),可以注册一个 Background Sync 事件。 - 保存要同步的数据(例如,表单数据)到 LocalStorage 或 IndexedDB,以便在网络恢复时使用。
- 注册同步事件 使用
SyncManager
的register
方法请求后台同步。
首先,你需要注册 Service Worker,这通常在网页加载时完成
javascript
if ('serviceWorker' in navigator && 'SyncManager' in window) {
navigator.serviceWorker.register('/sw.js').then((registration) => {
console.log('Service Worker registered with scope:', registration.scope);
}).catch((error) => {
console.error('Service Worker registration failed:', error);
});
}
在适当的时机(例如,用户提交表单时),请求后台同步
js
function requestSync() {
navigator.serviceWorker.ready.then((registration) => {
return registration.sync.register('sync-data');
}).then(() => {
console.log('Background sync registered!');
}).catch((error) => {
console.error('Background sync registration failed:', error);
});
}
在 Service Worker 中监听 sync 事件并定义同步逻辑
js
// sw.js (Service Worker)
self.addEventListener('sync', (event) => {
if (event.tag === 'sync-data') {
event.waitUntil(
// 执行数据同步操作
fetch('/api/sync', {
method: 'POST',
body: JSON.stringify({ /* 数据 */ }),
headers: { 'Content-Type': 'application/json' }
}).then(response => {
return response.json();
}).then(data => {
console.log('Data synced successfully:', data);
}).catch(error => {
console.error('Sync failed:', error);
})
);
}
});
4.自定义网络请求处理
js
// sw.js (Service Worker)
self.addEventListener('fetch', (event) => {
if (event.request.url.includes('/api/data')) {
event.respondWith(
fetch(event.request).then((response) => {
return response; // 可以在这里处理响应
})
);
}
});
5. 推送通知
首先,创建一个简单的 HTML 文件,用于注册 Service Worker 和处理推送功能。
js
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Push Notification Demo</title>
</head>
<body>
<h1>Push Notification Demo</h1>
<button id="subscribe">Subscribe to Push Notifications</button>
<script src="app.js"></script>
</body>
</html>
接下来,创建一个 JavaScript 文件用于注册 Service Worker 和处理推送订阅。
js
// app.js
const publicVapidKey = 'YOUR_PUBLIC_VAPID_KEY'; // 替换为你的公钥
// 注册 Service Worker
if ('serviceWorker' in navigator && 'PushManager' in window) {
navigator.serviceWorker.register('sw.js')
.then(registration => {
console.log('Service Worker registered with scope:', registration.scope);
initializePushNotifications(registration);
})
.catch(err => console.error('Service Worker registration failed:', err));
}
// 初始化推送通知
function initializePushNotifications(registration) {
const subscribeButton = document.getElementById('subscribe');
subscribeButton.addEventListener('click', () => {
const options = {
userVisibleOnly: true,
applicationServerKey: urlB64ToUint8Array(publicVapidKey)
};
registration.pushManager.subscribe(options)
.then(subscription => {
console.log('User is subscribed:', subscription);
// 将订阅发送到服务器
return fetch('/api/subscribe', {
method: 'POST',
body: JSON.stringify(subscription),
headers: {
'Content-Type': 'application/json'
}
});
})
.catch(err => {
console.log('Failed to subscribe the user: ', err);
});
});
}
// 将 VAPID 公钥转换为 Uint8Array
function urlB64ToUint8Array(base64String) {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
const rawData = window.atob(base64);
return new Uint8Array([...rawData].map(char => char.charCodeAt(0)));
}
然后,创建 Service Worker 文件来处理推送事件
js
// sw.js
self.addEventListener('push', event => {
const data = event.data ? event.data.json() : {};
const title = data.title || 'Default Title';
const options = {
body: data.body || 'Default body text.',
icon: 'icon.png', // 替换为你的图标路径
badge: 'badge.png' // 替换为你的徽章路径
};
event.waitUntil(
self.registration.showNotification(title, options)
);
});
// 处理通知点击事件
self.addEventListener('notificationclick', event => {
event.notification.close(); // 关闭通知
event.waitUntil(
clients.openWindow('https://yourwebsite.com') // 替换为你希望打开的 URL
);
});
你需要一个服务器端应用来处理订阅请求并发送推送通知。以下是一个简单的 Node.js 示例,使用 web-push 库
js
// server.js
const express = require('express');
const webPush = require('web-push');
const bodyParser = require('body-parser');
const app = express();
const port = 3000;
app.use(bodyParser.json());
// VAPID keys
const vapidKeys = webPush.generateVAPIDKeys();
webPush.setVapidDetails('mailto:your-email@example.com', vapidKeys.publicKey, vapidKeys.privateKey);
console.log('Public VAPID Key:', vapidKeys.publicKey);
let subscriptions = [];
// 处理订阅请求
app.post('/api/subscribe', (req, res) => {
const subscription = req.body;
subscriptions.push(subscription);
res.status(201).json({});
});
// 发送推送通知
app.post('/send-notification', (req, res) => {
const notificationPayload = {
title: 'Hello!',
body: 'This is a push notification!',
};
const promises = subscriptions.map(subscription => {
return webPush.sendNotification(subscription, JSON.stringify(notificationPayload))
.catch(error => console.error('Error sending notification:', error));
});
Promise.all(promises)
.then(() => res.sendStatus(200))
.catch(error => res.sendStatus(500));
});
app.listen(port, () => {
console.log(`Server running on http://localhost:${port}`);
});
WebWorker
基本使用
创建一个 WebWorker:
js
// main.js (主线程)
const worker = new Worker('worker.js');
worker.onmessage = function(event) {
console.log('Received message from worker:', event.data);
};
worker.postMessage('Hello from main thread!');
WebWorker 脚本:
js
// worker.js (WebWorker)
self.onmessage = function(event) {
console.log('Received message from main thread:', event.data);
const result = event.data.toUpperCase();
self.postMessage(result);
};
应用场景
1. 图像处理
主线程 (main.js):
js
const worker = new Worker('image-worker.js');
const image = new Image();
image.src = 'image.jpg';
image.onload = function() {
const canvas = document.createElement('canvas');
canvas.width = image.width;
canvas.height = image.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(image, 0, 0);
const imageData = ctx.getImageData(0, 0, image.width, image.height);
worker.postMessage(imageData, [imageData.data.buffer]);
};
worker.onmessage = function(event) {
const processedImageData = event.data;
const canvas = document.createElement('canvas');
canvas.width = processedImageData.width;
canvas.height = processedImageData.height;
const ctx = canvas.getContext('2d');
ctx.putImageData(processedImageData, 0, 0);
document.body.appendChild(canvas);
};
WebWorker (image-worker.js)
js
self.onmessage = function(event) {
const imageData = event.data;
const data = imageData.data;
// 简单的灰度处理
for (let i = 0; i < data.length; i += 4) {
const avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
data[i] = avg; // R
data[i + 1] = avg; // G
data[i + 2] = avg; // B
}
self.postMessage(imageData, [imageData.data.buffer]);
};
2. 数据处理
主线程 (main.js):
js
const worker = new Worker('data-worker.js');
const data = [5, 3, 8, 4, 2, 9, 1, 7, 6];
worker.postMessage(data);
worker.onmessage = function(event) {
const sortedData = event.data;
console.log('Sorted data:', sortedData);
};
WebWorker (data-worker.js)
js
self.onmessage = function(event) {
const data = event.data;
// 简单的排序算法
const sortedData = data.sort((a, b) => a - b);
self.postMessage(sortedData);
};
3. 加密解密
主线程 (main.js):
js
const worker = new Worker('crypto-worker.js');
const message = 'Hello, World!';
worker.postMessage(message);
worker.onmessage = function(event) {
const encryptedMessage = event.data;
console.log('Encrypted message:', encryptedMessage);
};
WebWorker (crypto-worker.js):
js
self.onmessage = function(event) {
const message = event.data;
// 简单的加密(例如 Base64 编码)
const encryptedMessage = btoa(message);
self.postMessage(encryptedMessage);
};
SharedWorker
主要特点:
- 共享实例: 多个浏览上下文可以共享同一个 SharedWorker 实例,这使得它们可以共享数据和状态。
- 消息传递: SharedWorker 通过 port 对象进行消息传递。每个浏览上下文都有一个 port 对象,用于与 SharedWorker 通信。
- 独立作用域: 与 WebWorker 类似,SharedWorker 运行在一个独立的全局上下文中,不能直接访问 DOM 或主线程的全局变量。
主要应用场景:
1.跨标签页通信:在多个标签页之间共享数据和状态,例如实时聊天应用、多标签页协作编辑等。
SharedWorker (shared-worker.js):
js
const connections = [];
self.onconnect = function(event) {
const port = event.ports[0];
connections.push(port);
port.onmessage = function(event) {
const message = event.data;
broadcast(message);
};
port.start();
};
function broadcast(message) {
connections.forEach(connection => {
connection.postMessage(message);
});
}
主线程 (main.js):
js
const worker = new SharedWorker('shared-worker.js');
worker.port.onmessage = function(event) {
const message = event.data;
const messageElement = document.createElement('div');
messageElement.textContent = message;
document.getElementById('messages').appendChild(messageElement);
};
worker.port.start();
document.getElementById('send-button').addEventListener('click', function() {
const message = document.getElementById('message-input').value;
worker.port.postMessage(message);
});
HTML (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>SharedWorker Demo - Cross-Tab Communication</title>
</head>
<body>
<input type="text" id="message-input" placeholder="Enter message">
<button id="send-button">Send</button>
<div id="messages"></div>
<script src="main.js"></script>
</body>
</html>
2.后台任务管理:管理后台任务,如定时任务、数据同步等,多个标签页可以共享同一个任务管理器。
SharedWorker (shared-worker.js):
js
const connections = [];
let timerId;
self.onconnect = function(event) {
const port = event.ports[0];
connections.push(port);
port.onmessage = function(event) {
const command = event.data;
if (command === 'start') {
startTimer();
} else if (command === 'stop') {
stopTimer();
}
};
port.start();
};
function startTimer() {
if (!timerId) {
timerId = setInterval(() => {
broadcast('Timer tick');
}, 1000);
}
}
function stopTimer() {
if (timerId) {
clearInterval(timerId);
timerId = null;
}
}
function broadcast(message) {
connections.forEach(connection => {
connection.postMessage(message);
});
}
主线程 (main.js):
js
const worker = new SharedWorker('shared-worker.js');
worker.port.onmessage = function(event) {
const message = event.data;
const messageElement = document.createElement('div');
messageElement.textContent = message;
document.getElementById('messages').appendChild(messageElement);
};
worker.port.start();
document.getElementById('start-button').addEventListener('click', function() {
worker.port.postMessage('start');
});
document.getElementById('stop-button').addEventListener('click', function() {
worker.port.postMessage('stop');
});
HTML (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>SharedWorker Demo - Background Task Management</title>
</head>
<body>
<button id="start-button">Start Timer</button>
<button id="stop-button">Stop Timer</button>
<div id="messages"></div>
<script src="main.js"></script>
</body>
</html>
3.资源共享:共享资源,如数据库连接、缓存数据等,减少资源重复创建和销毁的开销。
SharedWorker (shared-worker.js):
js
const cache = {};
self.onconnect = function(event) {
const port = event.ports[0];
port.onmessage = function(event) {
const { command, key, value } = event.data;
if (command === 'set') {
cache[key] = value;
port.postMessage({ key, value: cache[key] });
} else if (command === 'get') {
port.postMessage({ key, value: cache[key] });
}
};
port.start();
};
主线程 (main.js):
js
const worker = new SharedWorker('shared-worker.js');
worker.port.onmessage = function(event) {
const { key, value } = event.data;
document.getElementById('cache-display').textContent = `Cache[${key}]: ${value}`;
};
worker.port.start();
document.getElementById('set-button').addEventListener('click', function() {
const key = document.getElementById('key-input').value;
const value = document.getElementById('value-input').value;
worker.port.postMessage({ command: 'set', key, value });
});
document.getElementById('get-button').addEventListener('click', function() {
const key = document.getElementById('key-input').value;
worker.port.postMessage({ command: 'get', key });
});
HTML (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>SharedWorker Demo - Resource Sharing</title>
</head>
<body>
<input type="text" id="key-input" placeholder="Enter key">
<input type="text" id="value-input" placeholder="Enter value">
<button id="set-button">Set Cache</button>
<button id="get-button">Get Cache</button>
<div id="cache-display"></div>
<script src="main.js"></script>
</body>
</html>
对比
特性 | Web Worker | Shared Worker | Service Worker |
---|---|---|---|
作用域 | 只能在单个页面中使用 | 可以被多个页面共享 | 作用于整个网站,管理请求和缓存 |
生命周期 | 页面生命周期 | 由连接的页面共享,直到所有页面关闭 | 独立于页面,按需激活和更新 |
网络请求 | 无法直接处理网络请求 | 无法直接处理网络请求 | 可以拦截和处理网络请求 |
存储 | 无法直接持久化数据 | 可以通过 IndexedDB 持久化数据 | 可以使用 Cache API 和其他存储方式 |
注册方式 | 在 JavaScript 中直接创建 | 通过 SharedWorker 构造函数创建 | 通过 navigator.serviceWorker.register 注册 |
主要用途 | 处理计算密集型任务,避免阻塞主线程 | 跨多个浏览上下文共享数据和状态 | 充当网络请求的代理,实现离线缓存和推送通知 |
实例数量 | 每个 Worker 一个实例 | 多个浏览上下文共享一个实例 | 每个域名一个实例 |
消息传递 | 通过 postMessage 和 onmessage | 通过 port 对象depostMessage 和 onmessage 进行消息传递 | 通过 postMessage 和 onmessage |
访问 DOM | 不能直接访问 | 不能直接访问 | 不能直接访问 |
独立作用域 | 是 | 是 | 是 |
生命周期 | 与创建它的脚本生命周期相同 | 与创建它的脚本生命周期相同 | 独立于页面生命周期,可以长期存在 |
跨标签页通信 | 不支持 | 支持 | 支持 |
网络请求代理 | 不支持 | 不支持 | 支持 |
离线缓存 | 不支持 | 不支持 | 支持 |
推送通知 | 不支持 | 不支持 | 支持 |
示例场景 | 图像处理、数据处理、加密解密 | 跨标签页通信、后台任务管理、资源共享 | 离线缓存、推送通知、网络请求代理 |