HTML Web Workers
在本章节中,我们将学习HTML Web Workers的基本概念、语法和用法。HTML5引入了Web Workers API,使得在浏览器中运行后台脚本变得可能,无需阻塞主线程。
1. 什么是HTML Web Workers?
HTML Web Workers是一种在浏览器后台运行JavaScript代码的机制,允许在独立于主线程的另一个线程中执行脚本。这样可以避免长时间运行的脚本阻塞主线程,从而提高网页的响应性和性能。
1.1 Web Workers的优势
- 提高页面响应性:将耗时的任务放在后台执行,主线程可以继续处理用户交互
- 充分利用多核CPU:可以并行执行多个任务,提高CPU利用率
- 避免UI冻结:长时间运行的脚本不会导致浏览器界面冻结
- 支持大量计算:适合处理复杂的计算任务、数据分析、图像处理等
1.2 Web Workers的类型
HTML5提供了三种类型的Web Workers:
- Dedicated Workers:专用Worker,只能被创建它的脚本使用
- Shared Workers:共享Worker,可以被同一域名下的多个脚本使用
- Service Workers:服务Worker,用于处理网络请求、缓存资源等,支持离线功能
| 特性 | Dedicated Workers | Shared Workers | Service Workers |
|---|---|---|---|
| 作用域 | 只能被创建它的脚本使用 | 同一域名下的多个脚本共享 | 整个域名,支持离线功能 |
| 通信方式 | postMessage() | postMessage(),通过端口通信 | postMessage(),事件监听 |
| 生命周期 | 与创建它的脚本绑定 | 独立于创建它的脚本,需要显式关闭 | 长期运行,支持后台同步 |
| 主要用途 | 复杂计算、数据分析 | 多页面共享数据、协作处理 | 离线缓存、推送通知、网络代理 |
2. Dedicated Workers的使用
2.1 基本操作
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Dedicated Workers示例</title>
</head>
<body>
<h1>Dedicated Workers示例</h1>
<div>
<label for="number">输入一个大数字:</label>
<input type="number" id="number" value="1000000">
<button onclick="startCalculation()">开始计算</button>
<button onclick="stopCalculation()">停止计算</button>
</div>
<div id="result"></div>
<div id="status"></div>
<script>
let worker;
function startCalculation() {
const number = document.getElementById('number').value;
const status = document.getElementById('status');
const result = document.getElementById('result');
// 创建Worker
worker = new Worker('worker.js');
// 监听Worker消息
worker.onmessage = function(e) {
if (e.data.type === 'result') {
result.textContent = `计算结果:${e.data.value}`;
status.textContent = '计算完成';
} else if (e.data.type === 'progress') {
status.textContent = `计算进度:${e.data.value}%`;
}
};
// 监听Worker错误
worker.onerror = function(e) {
status.textContent = `错误:${e.message} (行号:${e.lineno})`;
result.textContent = '';
};
// 向Worker发送消息
worker.postMessage({ number: parseInt(number) });
status.textContent = '计算中...';
result.textContent = '';
}
function stopCalculation() {
if (worker) {
worker.terminate();
worker = null;
document.getElementById('status').textContent = '计算已停止';
}
}
</script>
</body>
</html>2.2 Worker脚本(worker.js)
// worker.js
// 监听主线程消息
self.onmessage = function(e) {
const number = e.data.number;
let sum = 0;
// 执行耗时计算
for (let i = 0; i <= number; i++) {
sum += i;
// 发送进度更新(每10000次迭代)
if (i % 10000 === 0) {
const progress = Math.round((i / number) * 100);
self.postMessage({ type: 'progress', value: progress });
}
}
// 发送计算结果
self.postMessage({ type: 'result', value: sum });
};
// 监听错误
self.onerror = function(e) {
self.postMessage({ type: 'error', value: e.message });
};2.3 常用方法和事件
| 主线程方法/事件 | 描述 |
|---|---|
new Worker(url) |
创建一个新的Worker |
worker.postMessage(data) |
向Worker发送消息 |
worker.onmessage |
监听Worker发送的消息 |
worker.onerror |
监听Worker中的错误 |
worker.terminate() |
终止Worker |
| Worker线程方法/事件 | 描述 |
|---|---|
self.onmessage |
监听主线程发送的消息 |
self.postMessage(data) |
向主线程发送消息 |
self.onerror |
监听Worker中的错误 |
self.close() |
关闭Worker |
3. Shared Workers的使用
3.1 基本操作
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Shared Workers示例</title>
</head>
<body>
<h1>Shared Workers示例</h1>
<div>
<label for="message">发送消息:</label>
<input type="text" id="message">
<button onclick="sendMessage()">发送</button>
<button onclick="getCount()">获取连接数</button>
</div>
<div id="messages"></div>
<div id="status"></div>
<script>
let worker;
let port;
// 创建Shared Worker
worker = new SharedWorker('shared-worker.js');
port = worker.port;
// 监听Worker消息
port.onmessage = function(e) {
const messages = document.getElementById('messages');
const status = document.getElementById('status');
if (e.data.type === 'message') {
messages.innerHTML += `<p>${e.data.value}</p>`;
} else if (e.data.type === 'count') {
status.textContent = `当前连接数:${e.data.value}`;
}
};
// 启动端口通信
port.start();
function sendMessage() {
const message = document.getElementById('message').value;
port.postMessage({ type: 'message', value: message });
}
function getCount() {
port.postMessage({ type: 'getCount' });
}
</script>
</body>
</html>3.2 Shared Worker脚本(shared-worker.js)
// shared-worker.js
let connections = 0;
let messages = [];
// 监听连接
self.onconnect = function(e) {
const port = e.ports[0];
connections++;
// 发送连接数
port.postMessage({ type: 'count', value: connections });
// 发送历史消息
messages.forEach(message => {
port.postMessage({ type: 'message', value: message });
});
// 监听端口消息
port.onmessage = function(e) {
if (e.data.type === 'message') {
const message = e.data.value;
messages.push(message);
// 向所有连接的端口广播消息
// 注意:在实际应用中,需要维护所有连接的端口列表
port.postMessage({ type: 'message', value: `收到:${message}` });
} else if (e.data.type === 'getCount') {
port.postMessage({ type: 'count', value: connections });
}
};
// 监听端口关闭
port.onmessageerror = function() {
connections--;
};
};3.3 Shared Workers与Dedicated Workers的区别
- 作用域:Shared Workers可以被同一域名下的多个脚本共享;Dedicated Workers只能被创建它的脚本使用
- 通信方式:Shared Workers通过端口通信;Dedicated Workers直接通信
- 生命周期:Shared Workers独立于创建它的脚本,需要显式关闭;Dedicated Workers与创建它的脚本绑定
- 使用场景:Shared Workers适合多页面共享数据、协作处理;Dedicated Workers适合单个页面的后台任务
4. Service Workers的使用
4.1 基本操作
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Service Workers示例</title>
</head>
<body>
<h1>Service Workers示例</h1>
<div id="status"></div>
<button onclick="registerServiceWorker()">注册Service Worker</button>
<button onclick="unregisterServiceWorker()">注销Service Worker</button>
<button onclick="clearCache()">清除缓存</button>
<script>
function registerServiceWorker() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('service-worker.js')
.then(registration => {
document.getElementById('status').textContent = `Service Worker注册成功:${registration.scope}`;
})
.catch(error => {
document.getElementById('status').textContent = `Service Worker注册失败:${error}`;
});
} else {
document.getElementById('status').textContent = '浏览器不支持Service Worker';
}
}
function unregisterServiceWorker() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready
.then(registration => {
registration.unregister();
document.getElementById('status').textContent = 'Service Worker已注销';
})
.catch(error => {
document.getElementById('status').textContent = `Service Worker注销失败:${error}`;
});
}
}
function clearCache() {
if ('caches' in window) {
caches.keys().then(cacheNames => {
cacheNames.forEach(cacheName => {
caches.delete(cacheName);
});
document.getElementById('status').textContent = '缓存已清除';
});
}
}
// 监听Service Worker消息
if ('serviceWorker' in navigator) {
navigator.serviceWorker.addEventListener('message', event => {
document.getElementById('status').textContent += `\n收到Service Worker消息:${event.data}`;
});
}
</script>
</body>
</html>4.2 Service Worker脚本(service-worker.js)
// service-worker.js
const CACHE_NAME = 'my-cache-v1';
const urlsToCache = [
'/',
'/index.html',
'/style.css',
'/script.js',
'/image.jpg'
];
// 安装事件:缓存资源
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => {
console.log('缓存已打开');
return cache.addAll(urlsToCache);
})
);
});
// 激活事件:清理旧缓存
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);
}
})
);
})
);
});
// fetch事件:拦截网络请求
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => {
// 如果缓存中有响应,则返回缓存的响应
if (response) {
return response;
}
// 否则,发起网络请求
return fetch(event.request).then(
response => {
// 检查响应是否有效
if (!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
// 克隆响应(因为响应流只能使用一次)
const responseToCache = response.clone();
// 将响应添加到缓存
caches.open(CACHE_NAME)
.then(cache => {
cache.put(event.request, responseToCache);
});
return response;
}
);
})
);
});
// 推送事件:处理推送通知
self.addEventListener('push', event => {
const options = {
body: '这是一条推送通知',
icon: '/icon.png',
badge: '/badge.png',
data: {
url: '/notifications'
}
};
event.waitUntil(
self.registration.showNotification('推送通知标题', options)
);
});
// 通知点击事件:处理通知点击
self.addEventListener('notificationclick', event => {
event.notification.close();
event.waitUntil(
clients.openWindow(event.notification.data.url)
);
});5. Web Workers的限制
5.1 运行环境限制
- Web Workers无法访问DOM
- 无法使用window、document、parent等全局对象
- 无法访问主线程的变量和函数
- 只能通过postMessage()方法与主线程通信
5.2 同源策略限制
- Web Workers脚本必须与主线程脚本同源
- 无法从不同域名加载Worker脚本
- 可以使用blob URL或data URL创建内联Worker
5.3 浏览器支持
| 浏览器 | Dedicated Workers | Shared Workers | Service Workers |
|---|---|---|---|
| Chrome | 4+ | 4+ | 40+ |
| Firefox | 3.5+ | 2+ | 44+ |
| Safari | 4+ | 5+ | 11.1+ |
| Edge | 12+ | 12+ | 17+ |
| Opera | 10.5+ | 11.5+ | 27+ |
| IE | 10+ | 不支持 | 不支持 |
6. Web Workers的最佳实践
6.1 使用场景
- 复杂计算:如数学计算、数据分析、模拟等
- 图像处理:如图像滤镜、压缩、转换等
- 数据处理:如大规模数据排序、搜索等
- 网络请求:如长时间的轮询、WebSocket连接等
- 实时数据处理:如音频/视频处理、实时通信等
6.2 性能优化
// 最佳实践:批量发送消息,减少通信开销
function processLargeData(data) {
const worker = new Worker('worker.js');
// 分块处理数据
const chunkSize = 1000;
for (let i = 0; i < data.length; i += chunkSize) {
const chunk = data.slice(i, i + chunkSize);
worker.postMessage({ type: 'chunk', data: chunk });
}
worker.postMessage({ type: 'complete' });
}
// 最佳实践:使用Transferable Objects传递大量数据
const arrayBuffer = new ArrayBuffer(1024 * 1024); // 1MB
worker.postMessage(arrayBuffer, [arrayBuffer]); // 转移所有权,避免复制6.3 错误处理
// 主线程错误处理
worker.onerror = function(e) {
console.error(`Worker错误:${e.message} (行号:${e.lineno},文件名:${e.filename})`);
// 可以在这里添加错误恢复逻辑
};
// Worker线程错误处理
self.onerror = function(e) {
console.error(`Worker内部错误:${e.message}`);
self.postMessage({ type: 'error', message: e.message });
};6.4 资源管理
// 最佳实践:在不需要时终止Worker
function cleanup() {
if (worker) {
worker.terminate();
worker = null;
}
}
// 监听页面卸载事件,清理Worker
window.addEventListener('beforeunload', cleanup);
// 或在任务完成后清理
worker.onmessage = function(e) {
if (e.data.type === 'complete') {
worker.terminate();
worker = null;
}
};7. Web Workers的应用场景
7.1 复杂计算
// 主线程
const worker = new Worker('prime-calculator.js');
worker.postMessage({ number: 100000000 });
worker.onmessage = function(e) {
console.log(`最大质数:${e.data}`);
worker.terminate();
};
// Worker线程(prime-calculator.js)
self.onmessage = function(e) {
const number = e.data.number;
let maxPrime = 2;
for (let i = 3; i <= number; i++) {
let isPrime = true;
for (let j = 2; j <= Math.sqrt(i); j++) {
if (i % j === 0) {
isPrime = false;
break;
}
}
if (isPrime) {
maxPrime = i;
}
}
self.postMessage(maxPrime);
};7.2 图像处理
// 主线程
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const image = new Image();
image.onload = function() {
canvas.width = image.width;
canvas.height = image.height;
ctx.drawImage(image, 0, 0);
// 获取图像数据
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
// 创建Worker处理图像
const worker = new Worker('image-processor.js');
// 传递图像数据(使用Transferable Objects)
worker.postMessage({ data: data.buffer, width: canvas.width, height: canvas.height }, [data.buffer]);
// 接收处理后的图像数据
worker.onmessage = function(e) {
// 更新画布
const processedData = new ImageData(new Uint8ClampedArray(e.data), canvas.width, canvas.height);
ctx.putImageData(processedData, 0, 0);
worker.terminate();
};
};
image.src = 'image.jpg';
// Worker线程(image-processor.js)
self.onmessage = function(e) {
const data = new Uint8ClampedArray(e.data.data);
const width = e.data.width;
const height = e.data.height;
// 图像处理:灰度转换
for (let i = 0; i < data.length; i += 4) {
const gray = (data[i] + data[i + 1] + data[i + 2]) / 3;
data[i] = gray; // 红色
data[i + 1] = gray; // 绿色
data[i + 2] = gray; // 蓝色
// 透明度不变
}
// 返回处理后的图像数据
self.postMessage(data.buffer, [data.buffer]);
};7.3 实时数据处理
// 主线程
const worker = new Worker('data-processor.js');
// 模拟实时数据流
setInterval(() => {
const data = generateRandomData();
worker.postMessage({ data: data });
}, 1000);
worker.onmessage = function(e) {
const processedData = e.data;
updateUI(processedData);
};
// Worker线程(data-processor.js)
self.onmessage = function(e) {
const data = e.data.data;
// 实时数据处理:计算统计信息
const result = {
average: calculateAverage(data),
max: Math.max(...data),
min: Math.min(...data),
count: data.length
};
self.postMessage(result);
};
function calculateAverage(data) {
const sum = data.reduce((acc, val) => acc + val, 0);
return sum / data.length;
}8. 常见问题解答
Q: Web Workers可以访问DOM吗?
A: 不可以,Web Workers无法访问DOM,因为DOM操作必须在主线程中进行。如果需要更新DOM,可以通过postMessage()方法将结果发送给主线程,由主线程更新DOM。
Q: Web Workers可以使用哪些API?
A: Web Workers可以使用以下API:
- 基本的JavaScript全局对象:Object, Array, Date, Math, String等
- 网络请求:XMLHttpRequest, fetch
- 定时器:setTimeout, setInterval
- 存储:localStorage, sessionStorage, IndexedDB
- 文件操作:FileReader
- 其他:navigator, location(只读)
Q: 如何调试Web Workers?
A: 可以使用浏览器的开发者工具调试Web Workers:
- Chrome:在"Sources"面板中,展开"Workers"部分,可以查看和调试Worker脚本
- Firefox:在"调试器"面板中,选择"Worker"上下文
- Safari:在"Develop"菜单中,选择"Web Inspector",然后在"Sources"面板中查看Worker脚本
Q: Web Workers会影响页面性能吗?
A: Web Workers可以提高页面性能,因为它们将耗时的任务放在后台执行,避免阻塞主线程。但是,创建过多的Worker或频繁的通信可能会影响性能,应该合理使用。
Q: 如何在Web Workers中加载外部脚本?
A: 可以使用importScripts()方法在Worker中加载外部脚本:
// 在Worker中加载外部脚本
importScripts('script1.js', 'script2.js');
// 现在可以使用加载的脚本中的函数
const result = externalFunction();Q: Web Workers支持ES模块吗?
A: 现代浏览器支持在Web Workers中使用ES模块,可以通过以下方式创建使用ES模块的Worker:
// 创建使用ES模块的Worker
const worker = new Worker('worker.js', { type: 'module' });
// Worker脚本(worker.js)
import { myFunction } from './module.js';
self.onmessage = function(e) {
const result = myFunction(e.data);
self.postMessage(result);
};9. 练习项目
创建一个HTML文件,包含以下内容:
- 页面标题为"HTML Web Workers练习"
- 页面头部包含必要的元标签(字符集、视口等)
- 创建一个复杂计算应用,包含:
- 输入框用于输入大数字
- 开始计算和停止计算按钮
- 显示计算进度和结果
- 使用Dedicated Worker执行计算
- 创建一个图像处理应用,包含:
- 上传图像功能
- 图像滤镜效果(灰度、反转、模糊等)
- 使用Dedicated Worker处理图像
- 显示原始图像和处理后的图像
- 创建一个实时数据处理应用,包含:
- 模拟实时数据流
- 计算统计信息(平均值、最大值、最小值等)
- 使用Dedicated Worker处理数据
- 实时更新UI显示统计结果
- 创建一个Service Worker示例,包含:
- 注册/注销Service Worker按钮
- 缓存静态资源
- 支持离线访问
- 显示缓存状态
- 确保页面在不同设备上都能正常显示
- 添加响应式设计,适应不同屏幕尺寸
创建必要的Worker脚本文件
在浏览器中打开文件,验证功能
测试复杂计算功能
测试图像处理功能
测试实时数据处理功能
测试Service Worker功能
优化性能,减少通信开销
10. 小结
- HTML Web Workers允许在浏览器后台运行JavaScript代码,避免阻塞主线程
- 有三种类型的Web Workers:Dedicated Workers、Shared Workers和Service Workers
- Dedicated Workers只能被创建它的脚本使用,适合处理单个页面的后台任务
- Shared Workers可以被同一域名下的多个脚本使用,适合多页面共享数据
- Service Workers用于处理网络请求、缓存资源等,支持离线功能
- Web Workers无法访问DOM,只能通过postMessage()方法与主线程通信
- Web Workers适合处理复杂计算、图像处理、实时数据处理等耗时任务
- 合理使用Web Workers可以提高页面响应性和性能
- 现代浏览器对Web Workers有良好的支持
在下一章节中,我们将学习HTML无障碍访问,了解如何创建可访问性良好的网页。