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. 练习项目

  1. 创建一个HTML文件,包含以下内容:

    • 页面标题为"HTML Web Workers练习"
    • 页面头部包含必要的元标签(字符集、视口等)
    • 创建一个复杂计算应用,包含:
      • 输入框用于输入大数字
      • 开始计算和停止计算按钮
      • 显示计算进度和结果
      • 使用Dedicated Worker执行计算
    • 创建一个图像处理应用,包含:
      • 上传图像功能
      • 图像滤镜效果(灰度、反转、模糊等)
      • 使用Dedicated Worker处理图像
      • 显示原始图像和处理后的图像
    • 创建一个实时数据处理应用,包含:
      • 模拟实时数据流
      • 计算统计信息(平均值、最大值、最小值等)
      • 使用Dedicated Worker处理数据
      • 实时更新UI显示统计结果
    • 创建一个Service Worker示例,包含:
      • 注册/注销Service Worker按钮
      • 缓存静态资源
      • 支持离线访问
      • 显示缓存状态
    • 确保页面在不同设备上都能正常显示
    • 添加响应式设计,适应不同屏幕尺寸
  2. 创建必要的Worker脚本文件

  3. 在浏览器中打开文件,验证功能

  4. 测试复杂计算功能

  5. 测试图像处理功能

  6. 测试实时数据处理功能

  7. 测试Service Worker功能

  8. 优化性能,减少通信开销

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无障碍访问,了解如何创建可访问性良好的网页。

« 上一篇 HTML Web存储 下一篇 » HTML无障碍访问