WebWorker

WebWorker(HTML5新引入技术)

image-20250206211458551

一、基本概念

JS 在浏览器中默认运行在单线程环境中,所有任务(渲染、事件处理等)都在同一线程上执行。如果某个任务耗时过长,就可能导致页面冻结、响应迟缓。Web Worker允许我们创建新的线程,专门用来处理耗时任务,从而避免主进程被阻塞。

二、工作原理

Web Worker 与主线程之间采用消息传递进行通信。主线程与 Worker 线程之间不能共享同一上下文(例如不能直接访问 Dom)。只能通过postMessage方法传递消息,Worker 接收到消息后,通过onMessage事件处理消息,返回结果。

什么叫做不能共享同一上下文?

上下文是指在特定环境中执行的代码和能够访问的资源。在浏览器中,主线程的上下文包括对DOM的访问、事件处理、UI渲染等。Worker 线程的上下文是独立的,没有访问DOM的能力,也无法直接和主线程共享变量、对象等。Worker 线程主要用于 计算密集型任务,例如数据处理、文件读取等,执行这些任务时并不需要与 DOM 交互,因此将它们隔离开有助于性能优化。

三、多种类型

Dedicated Worker(专用工作者)
最常见的Worker类型,专门为主线程(调用它的脚本)服务,每个Worker只能由一个主线程使用。主线程和Worker通过postMessage()和onmessage进行通信。

  • 1、创建专用Worker脚本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// worker.js 后台执行的代码
// 监听来自主线程的消息
self.onmessage = function(event) {
console.log('Worker received:', event.data);
// 执行一些耗时操作,比如计算斐波那契数列
const result = fibonacci(event.data);
// 将结果发送回主线程
self.postMessage(result);
};

// 一个简单的递归函数来计算斐波那契数列(注意:实际项目中应避免递归带来的性能问题)
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
  • 2、在主线程中创建Worker实例
1
2
3
4
5
6
7
8
9
10
11
12
13
// main.js
// 创建 Worker 对象,指定 worker.js 为执行脚本
const worker = new Worker('worker.js');
// 发送消息给 Worker,开始计算
worker.postMessage(10); // 比如计算 fibonacci(10)
// 接收 Worker 发回的消息
worker.onmessage = function(event) {
console.log('Result from worker:', event.data);
};
// 错误处理
worker.onerror = function(error) {
console.error('Worker error:', error);
};
  • 3、终止 Worker
1
worker.terminate();

Shared Worker(共享工作者)
允许多个脚本(甚至来自不同窗口、iframe 或 web worker)共享一个 Worker 实例,通过共享的通信端口(MessagePort)进行交互。允许多个 JavaScript 线程(例如多个网页窗口)与一个 Worker 进行通信,适合多个页面或多个上下文间共享资源和数据的情况。共享 Worker 的生命周期由浏览器管理,当所有连接的页面关闭时,Worker 才会结束运行。

  • 创建共享Worker脚本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// sharedWorker.js
// 用于保存所有连接的 MessagePort 对象
var connections = [];
// 当有新的连接建立时,会触发 onconnect 事件
onconnect = function(e) {
// e.ports 是一个数组,通常只有一个端口
var port = e.ports[0];
connections.push(port);

// 监听来自页面的消息
port.onmessage = function(event) {
console.log('SharedWorker 收到消息:', event.data);

// 示例:将收到的消息广播给所有连接的页面
connections.forEach(function(p) {
p.postMessage('Worker 广播: ' + event.data);
});
};

// 可选:监听 port 的关闭等事件
port.onclose = function() {
// 这里可以实现移除断开的 port
console.log('Port 关闭');
};
};
  • 使用共享 Worker
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>共享 Worker 示例</title>
</head>
<body>
<h1>共享 Worker 示例</h1>
<script>
// 创建共享 Worker 实例,注意路径与文件名要正确
var mySharedWorker = new SharedWorker('sharedWorker.js');

// 获取通信端口并启动通信通道
mySharedWorker.port.start(); // 某些浏览器可能自动启动,也可以不调用,但为了保险建议调用

// 发送消息到共享 Worker
mySharedWorker.port.postMessage('Hello from 页面1');

// 监听来自共享 Worker 的消息
mySharedWorker.port.onmessage = function(event) {
console.log('页面1收到消息:', event.data);
};

// 示例:可以添加按钮,点击后向 Worker 发送消息
// document.getElementById('sendBtn').onclick = function() {
// mySharedWorker.port.postMessage('点击按钮发送的消息');
// };
</script>
</body>
</html>

Service Worker(服务工作者)
与传统的 Worker 不同,Service Worker 是一种特殊类型的 Web Worker,通常用于拦截网络请求、实现离线缓存、消息推送等功能。与传统的 Worker 不同,它的生命周期并不与浏览器页面绑定,而是与整个网站相关联。主要与浏览器的主线程和 Web 页面进行交互。它还可以处理浏览器的网络请求,拦截和缓存请求。运行在浏览器的独立线程中,不依赖于网页的生命周期,即使网页关闭,也可以继续运行(直至被浏览器回收)。

  • 生命周期

    • 安装(install):初次注册或更新时触发,可用于缓存静态资源。
    • 激活(activate):安装完成后触发,通常用于清理旧缓存或旧版本数据。
    • 闲置(idle):在不拦截请求时进入闲置状态。
  • 注册Service Worker

    在网页的主线程中,通过 navigator.serviceWorker.register() 方法注册 Service Worker 脚本。注册后,浏览器会下载、安装并激活 Service Worker。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 主线程
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
navigator.serviceWorker.register('/serviceWorker.js')
.then(function(registration) {
console.log('Service Worker 注册成功,作用域:', registration.scope);
})
.catch(function(error) {
console.error('Service Worker 注册失败:', error);
});
});
} else {
console.log('当前浏览器不支持 Service Worker');
}

  • Service Worker 脚本

    在安装阶段,可以预缓存一些必要的静态资源,这样即使在离线时也可以快速加载页面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 定义缓存名称和要缓存的资源列表
const CACHE_NAME = 'my-site-cache-v1';
const urlsToCache = [
'/',
'/styles/main.css',
'/script/main.js',
'/images/logo.png'
];
// self 指向 Service Worker 的全局作用域
self.addEventListener('install', function(event) {
console.log('Service Worker 正在安装...');
// 预缓存资源
event.waitUntil( // 让浏览器等待缓存操作完成后再认为安装成功。
caches.open(CACHE_NAME) // 打开一个缓存容器
.then(function(cache) {
console.log('打开缓存');
return cache.addAll(urlsToCache);// 一次性添加多个资源
})
);
});
  • 激活阶段

    激活阶段通常用于清理旧的缓存或执行其他更新任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
self.addEventListener('activate', function(event) {
console.log('Service Worker 激活中...');
// 定义白名单,保留当前缓存的名称
const cacheWhitelist = [CACHE_NAME];
event.waitUntil(
caches.keys().then(function(cacheNames) {
return Promise.all(
cacheNames.map(function(cacheName) {
if (cacheWhitelist.indexOf(cacheName) === -1) {
// 删除不在白名单中的旧缓存
console.log('删除缓存:', cacheName);
return caches.delete(cacheName);
}
})
);
})
);
});

  • 拦截网络请求(fetch 事件)

    Service Worker 最重要的功能之一就是拦截网络请求,并决定返回缓存数据或网络数据。监听 fetch 事件后,使用 event.respondWith() 直接提供一个响应。先查找缓存中是否存在请求的资源,如果存在则返回缓存内容;否则,执行网络请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
self.addEventListener('fetch', function(event) {
console.log('拦截请求:', event.request.url);
event.respondWith(
caches.match(event.request)
.then(function(response) {
// 如果缓存中有匹配的资源,直接返回缓存
if (response) {
return response;
}
// 否则,通过网络请求获取资源
return fetch(event.request);
})
);
});

  • 进阶用法

1 离线体验

通过上述缓存策略,用户在离线时依然可以加载部分预缓存的页面和资源。开发者还可以扩展逻辑,当网络请求失败时返回离线页面或自定义错误页面。

2 后台同步和推送通知

  • 后台同步(Background Sync)
    Service Worker 可以在网络恢复后自动同步数据。需要借助 SyncManager API 实现。
  • 推送通知(Push Notifications)
    结合 Push API 与 Service Worker,可以实现消息推送,即使网页未打开也能收到通知。

3 与页面通信

通过 postMessage 方法,可以在 Service Worker 和网页之间进行双向通信。例如,当缓存更新时通知页面刷新数据。

1
2
3
4
5
self.clients.matchAll().then(function(clients) {
clients.forEach(function(client) {
client.postMessage('缓存已更新,请刷新页面');
});
});

四、注意事项

  • 不支持 DOM 操作
    由于 Worker 运行在独立的线程中,它没有访问 DOM、window 对象或 document 对象的权限。如果需要更新 UI,必须通过消息将结果传回主线程,然后在主线程中进行 DOM 操作。
  • 同源策略
    Worker 脚本必须遵循同源策略(同一协议、域名、端口)。跨域加载 Worker 脚本需要额外配置 CORS。
  • 调试困难
    由于 Worker 在单独的线程中运行,调试时可能会比主线程稍微复杂一些,但现代浏览器大多已经支持 Worker 的调试功能。
  • 数据传输
    消息传递过程中的数据会被序列化(通过结构化克隆算法),因此有些数据类型(如函数、DOM 节点等)无法传递。如果数据量特别大,可以考虑使用 Transferable Objects 来避免不必要的复制,提高效率。
  • 错误处理
    Worker 中发生的错误不会直接抛出到主线程,需要通过 onerror 事件监听进行捕获和处理。

五、使用场景

  • 计算密集型任务
    如复杂数学计算、大量数据处理、图像处理、音视频编解码等,可以将这些任务放在 Worker 中执行,避免页面卡顿。
  • 数据处理和预处理
    例如在接收到网络数据后进行预处理,然后再传回主线程进行显示。
  • 离线缓存和网络拦截
    通过 Service Worker 实现离线访问、网络请求缓存、后台数据同步等功能。
  • 多窗口/标签页共享数据
    共享 Worker 可以在多个页面之间共享数据和状态,实现进程间通信。