Skip to content

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 的工作原理
  1. 发起请求 用户在应用中进行某个操作(例如提交表单或发送消息)时,应用会尝试将数据发送到服务器。

  2. 检测请求结果

  • 请求成功: 数据成功发送到服务器,通常没有进一步的操作。
  • 请求失败: 应用需要确定失败的原因。
  1. 判断网络状态
  • 如果请求失败,并且检测到网络不可用(使用 navigator.onLine),可以注册一个 Background Sync 事件。
  • 保存要同步的数据(例如,表单数据)到 LocalStorage 或 IndexedDB,以便在网络恢复时使用。
  1. 注册同步事件 使用 SyncManagerregister 方法请求后台同步。

首先,你需要注册 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 WorkerShared WorkerService Worker
作用域只能在单个页面中使用可以被多个页面共享作用于整个网站,管理请求和缓存
生命周期页面生命周期由连接的页面共享,直到所有页面关闭独立于页面,按需激活和更新
网络请求无法直接处理网络请求无法直接处理网络请求可以拦截和处理网络请求
存储无法直接持久化数据可以通过 IndexedDB 持久化数据可以使用 Cache API 和其他存储方式
注册方式在 JavaScript 中直接创建通过 SharedWorker 构造函数创建通过 navigator.serviceWorker.register 注册
主要用途处理计算密集型任务,避免阻塞主线程跨多个浏览上下文共享数据和状态充当网络请求的代理,实现离线缓存和推送通知
实例数量每个 Worker 一个实例多个浏览上下文共享一个实例每个域名一个实例
消息传递通过 postMessageonmessage通过 port 对象depostMessageonmessage进行消息传递通过 postMessageonmessage
访问 DOM不能直接访问不能直接访问不能直接访问
独立作用域
生命周期与创建它的脚本生命周期相同与创建它的脚本生命周期相同独立于页面生命周期,可以长期存在
跨标签页通信不支持支持支持
网络请求代理不支持不支持支持
离线缓存不支持不支持支持
推送通知不支持不支持支持
示例场景图像处理、数据处理、加密解密跨标签页通信、后台任务管理、资源共享离线缓存、推送通知、网络请求代理