Web Workers 是提升 Web 应用性能的重要工具,通过将计算密集型任务移到后台线程,可以显著改善用户体验。关键要点:
通过合理使用 Web Workers,可以构建更流畅、响应更快的 Web 应用。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>【1】Web Worker 计算演示</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; padding: 20px; background: #f8f9fa; color: #333; }
.demo-container { max-width: 700px; margin: 0 auto; background: white; padding: 24px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
.demo-title { margin-bottom: 16px; font-size: 18px; color: #555; border-bottom: 2px solid #007bff; padding-bottom: 8px; }
.info-box {
background: #e7f3ff; border-left: 4px solid #007bff;
padding: 14px 16px; margin-bottom: 20px; border-radius: 4px;
font-size: 14px; line-height: 1.6;
}
.input-group { display: flex; gap: 12px; align-items: center; margin-bottom: 20px; flex-wrap: wrap; }
label { font-weight: 500; color: #444; }
input[type="number"] {
padding: 10px 14px; border: 2px solid #e0e0e0; border-radius: 6px;
width: 140px; font-size: 15px; transition: border-color 0.3s;
}
input:focus { border-color: #007bff; outline: none; }
.btn {
padding: 10px 24px; border: none; border-radius: 6px;
cursor: pointer; font-size: 14px; font-weight: 500;
transition: all 0.3s;
}
.btn-primary { background: #007bff; color: white; }
.btn-primary:hover { background: #0056b3; transform: translateY(-1px); }
.btn-primary:disabled { background: #ccc; cursor: not-allowed; transform: none; }
.result-panel {
background: #f8f9fa; border-radius: 8px; padding: 20px; margin-top: 20px;
border: 1px solid #e0e0e0;
}
.result-row { display: flex; justify-content: space-between; padding: 10px 0; border-bottom: 1px solid #eee; font-size: 15px; }
.result-row:last-child { border-bottom: none; }
.result-label { color: #666; }
.result-value { font-weight: 600; color: #333; font-family: monospace; font-size: 17px; }
.result-value.success { color: #28a745; }
.progress-bar-container {
height: 8px; background: #e9ecef; border-radius: 4px;
overflow: hidden; margin-top: 16px; display: none;
}
.progress-bar-fill {
height: 100%; background: linear-gradient(90deg, #007bff, #28a745);
border-radius: 4px; transition: width 0.2s ease; width: 0%;
}
.log-area {
margin-top: 16px; background: #1e1e1e; color: #d4d4d4; border-radius: 6px;
padding: 14px; font-family: monospace; font-size: 13px; max-height: 200px; overflow-y: auto;
}
.log-entry { margin-bottom: 4px; }
.log-entry.info { color: #569cd6; }
.log-entry.success { color: #4ec9b0; }
.log-entry.warn { color: #dcdcaa; }
</style>
</head>
<body>
<div class="demo-container">
<div class="demo-title">示例:Web Worker 后台计算(Blob URL 内联)</div>
<div class="info-box">
💡 <strong>原理:</strong>使用 Blob URL 将 Worker 代码内联到单个 HTML 文件中。
Worker 在独立线程执行计算任务,不阻塞主线程 UI。
</div>
<div class="input-group">
<label for="numInput">计算 1 到 N 的累加:</label>
<input type="number" id="numInput" value="100000000" min="1" step="1000000" />
<button class="btn btn-primary" id="calcBtn" onclick="startCalculation()">🚀 开始计算</button>
</div>
<div class="result-panel">
<div class="result-row">
<span class="result-label">输入值 (N)</span>
<span class="result-value" id="inputValue">-</span>
</div>
<div class="result-row">
<span class="result-label">计算结果</span>
<span class="result-value success" id="calcResult">等待计算...</span>
</div>
<div class="result-row">
<span class="result-label">计算耗时</span>
<span class="result-value" id="calcTime">-</span>
</div>
<div class="result-row">
<span class="result-label">Worker 状态</span>
<span class="result-value" id="workerStatus">就绪</span>
</div>
</div>
<div class="progress-bar-container" id="progressContainer">
<div class="progress-bar-fill" id="progressBar"></div>
</div>
<div class="log-area" id="logArea">
<div class="log-entry info">[系统] Web Worker 已准备就绪</div>
</div>
</div>
<script>
const logArea = document.getElementById("logArea")
function log(msg, type = "info") {
const entry = document.createElement("div")
entry.className = `log-entry ${type}`
entry.textContent = msg
logArea.appendChild(entry)
logArea.scrollTop = logArea.scrollHeight
}
// ====== 使用 Blob URL 创建内联 Worker ======
const workerCode = `
self.onmessage = function(event) {
const n = event.data
self.postMessage({ type: 'start', n: n })
// 模拟耗时计算:计算 1 到 N 的累加
let sum = 0
const startTime = performance.now()
for (let i = 1; i <= n; i++) {
sum += i
// 每 5% 发送一次进度
if (i % Math.floor(n / 20) === 0) {
self.postMessage({ type: 'progress', percent: Math.round((i / n) * 100), current: i })
}
}
const elapsed = performance.now() - startTime
self.postMessage({
type: 'done',
result: sum,
time: elapsed.toFixed(2),
n: n
})
self.close()
}
self.onerror = function(error) {
self.postMessage({ type: 'error', message: error.message })
}
`
const blob = new Blob([workerCode], { type: "application/javascript" })
const workerURL = URL.createObjectURL(blob)
let worker = null
document.getElementById("workerStatus").textContent = "已创建 (Blob URL)"
function startCalculation() {
const num = parseInt(document.getElementById("numInput").value)
if (!num || num < 1) {
alert("请输入有效的正整数")
return
}
// 更新UI状态
document.getElementById("inputValue").textContent = num.toLocaleString()
document.getElementById("calcResult").textContent = "计算中..."
document.getElementById("calcTime").textContent = "-"
document.getElementById("workerStatus").textContent = "运行中..."
document.getElementById("calcBtn").disabled = true
document.getElementById("progressContainer").style.display = "block"
document.getElementById("progressBar").style.width = "0%"
log(`[开始] 计算 1 到 ${num.toLocaleString()} 的累加`, "info")
// 创建新的 Worker 实例
worker = new Worker(workerURL)
worker.onmessage = function(event) {
const data = event.data
switch (data.type) {
case "start":
log(`[Worker] 开始计算,N = ${data.n.toLocaleString()}`, "info")
break
case "progress":
document.getElementById("progressBar").style.width = `${data.percent}%`
document.getElementById("calcResult").textContent = `计算中... ${data.percent}%`
break
case "done":
document.getElementById("calcResult").textContent = data.result.toLocaleString()
document.getElementById("calcTime").textContent = `${data.time} ms`
document.getElementById("workerStatus").textContent = "已完成 ✓"
document.getElementById("calcBtn").disabled = false
log(`[完成] 结果: ${data.result.toLocaleString()} | 耗时: ${data.time}ms`, "success")
break
case "error":
document.getElementById("calcResult").textContent = `错误: ${data.message}`
document.getElementById("workerStatus").textContent = "错误 ✗"
document.getElementById("calcBtn").disabled = false
log(`[错误] ${data.message}`, "warn")
break
}
}
worker.onerror = function(error) {
document.getElementById("workerStatus").textContent = "错误 ✗"
document.getElementById("calcBtn").disabled = false
log(`[Worker 错误] ${error.message} (${error.filename}:${error.lineno})`, "warn")
}
// 发送计算任务
worker.postMessage(num)
}
</script>
</body>
</html>```
<h4>003-worker-intensive-compute.html</h4>
```html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>【3】Worker 密集计算 - 斐波那契/素数</title>
<!--
来源: HTML5基础知识/15-WebWorkers处理线程.md
知识点: Worker 中执行密集计算,主线程不阻塞演示
-->
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
padding: 20px; background: #1a1a2e; color: #eee;
min-height: 100vh;
}
.container { max-width: 1000px; margin: 0 auto; }
.header {
text-align: center; padding: 24px; margin-bottom: 24px;
}
.header h1 { font-size: 24px; color: #00d9ff; margin-bottom: 8px; }
.header p { color: #888; font-size: 14px; }
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 20px; }
@media (max-width: 768px) { .grid { grid-template-columns: 1fr; } }
.panel {
background: #16213e; border-radius: 12px; padding: 20px;
border: 1px solid #0f3460;
}
.panel-title {
font-size: 15px; font-weight: 600; color: #00d9ff;
margin-bottom: 16px; padding-bottom: 8px; border-bottom: 1px solid #0f3460;
}
.control-row { display: flex; gap: 10px; align-items: center; margin-bottom: 16px; flex-wrap: wrap; }
label { font-size: 13px; color: #aaa; }
select, input[type="number"] {
padding: 8px 12px; background: #0f3460; border: 1px solid #1a4a7a;
color: #fff; border-radius: 6px; font-size: 13px;
}
select:focus, input:focus { outline: none; border-color: #00d9ff; }
.btn {
padding: 8px 18px; border: none; border-radius: 6px; cursor: pointer;
font-size: 13px; font-weight: 500; transition: all 0.2s;
}
.btn-cyan { background: #00d9ff; color: #1a1a2e; }
.btn-cyan:hover { background: #00b8d9; }
.btn-green { background: #00ff88; color: #1a1a2e; }
.btn-green:hover { background: #00cc6a; }
.btn-red { background: #ff4757; color: white; }
.btn-red:hover { background: #ee3344; }
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
.result-area {
background: #0a0a1a; border-radius: 8px; padding: 14px;
font-family: monospace; font-size: 13px; min-height: 120px;
max-height: 200px; overflow-y: auto; color: #ccc;
}
.result-line { margin-bottom: 4px; line-height: 1.5; }
.result-line.highlight { color: #00ff88; }
.result-line.warn { color: #ffa502; }
.result-line.error { color: #ff4757; }
.stats-row {
display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px;
margin-top: 12px;
}
.stat-box {
background: #0f3460; border-radius: 6px; padding: 10px; text-align: center;
}
.stat-value { font-size: 18px; font-weight: 700; color: #00d9ff; }
.stat-label { font-size: 11px; color: #888; margin-top: 2px; }
/* 动画指示器 */
.busy-indicator {
display: inline-flex; align-items: center; gap: 6px;
padding: 6px 14px; background: rgba(255,167,38,0.15);
border-radius: 20px; font-size: 12px; color: #ffa726; margin-top: 10px;
}
.spinner {
width: 14px; height: 14px; border: 2px solid transparent;
border-top-color: #ffa726; border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.ui-test-area {
background: #16213e; border-radius: 12px; padding: 20px;
border: 1px solid #0f3460; margin-top: 20px;
}
.animation-box {
width: 60px; height: 60px; background: linear-gradient(135deg, #667eea, #764ba2);
border-radius: 8px; animation: bounce 1s ease-in-out infinite;
display: inline-block;
}
@keyframes bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-20px); }
}
.ui-test-text { margin-left: 20px; color: #aaa; font-size: 13px; vertical-align: middle; }
.compat-note {
background: rgba(255,193,7,0.1); border: 1px solid rgba(255,193,7,0.3);
border-radius: 6px; padding: 10px 14px; font-size: 12px; color: #ffc107;
margin-top: 16px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>⚡ Worker 密集计算演示</h1>
<p>斐波那契数列 & 素数计算 — 验证主线程不阻塞</p>
</div>
<div class="grid">
<!-- 斐波那契计算 -->
<div class="panel">
<div class="panel-title">🔢 斐波那契数列计算</div>
<div class="control-row">
<label>第 N 项:</label>
<input type="number" id="fibN" value="45" min="1" max="55" />
<button class="btn btn-cyan" id="fibBtnMain" onclick="calcFibonacci(false)">主线程计算</button>
<button class="btn btn-green" id="fibBtnWorker" onclick="calcFibonacci(true)">Worker 计算</button>
</div>
<div class="result-area" id="fibResult">
<div class="result-line">等待输入参数...</div>
</div>
<div class="stats-row">
<div class="stat-box"><div class="stat-value" id="fibAns">-</div><div class="stat-label">结果</div></div>
<div class="stat-box"><div class="stat-value" id="fibTime">-</div><div class="stat-label">耗时(ms)</div></div>
<div class="stat-box"><div class="stat-value" id="fibMethod">-</div><div class="stat-label">方式</div></div>
</div>
<div class="busy-indicator" id="fibBusy" style="display:none;"><div class="spinner"></div>计算中...</div>
</div>
<!-- 素数计算 -->
<div class="panel">
<div class="panel-title">🔍 素数统计 (范围内)</div>
<div class="control-row">
<label>范围:</label>
<input type="number" id="primeMax" value="1000000" min="1000" step="100000" />
<button class="btn btn-cyan" id="primeBtnMain" onclick="calcPrimes(false)">主线程计算</button>
<button class="btn btn-green" id="primeBtnWorker" onclick="calcPrimes(true)">Worker 计算</button>
</div>
<div class="result-area" id="primeResult">
<div class="result-line">等待输入参数...</div>
</div>
<div class="stats-row">
<div class="stat-box"><div class="stat-value" id="primeCount">-</div><div class="stat-label">素数个数</div></div>
<div class="stat-box"><div class="stat-value" id="primeTime">-</div><div class="stat-label">耗时(ms)</div></div>
<div class="stat-box"><div class="stat-value" id="primeMethod">-</div><div class="stat-label">方式</div></div>
</div>
<div class="busy-indicator" id="primeBusy" style="display:none;"><div class="spinner"></div>计算中...</div>
</div>
</div>
<!-- 主线程 UI 测试区域 -->
<div class="ui-test-area">
<div class="panel-title" style="border:none; margin-bottom:12px;">🎮 主线程响应测试</div>
<p style="color:#888; font-size:13px; margin-bottom:12px;">
上方使用<strong style="color:#ff4757;">主线程计算</strong>时,下方动画会卡顿;使用 <strong style="color:#00ff88;">Worker 计算</strong> 时动画流畅。
</p>
<div style="display:flex; align-items:center;">
<div class="animation-box"></div>
<span class="ui-test-text">✨ 如果这个方块动画流畅 → 主线程未被阻塞</span>
</div>
<div style="margin-top: 16px;">
<input type="text" placeholder="尝试在此输入文字,验证主线程是否响应..."
style="width:100%; padding:10px; background:#0a0a1a; border:1px solid #0f3460; border-radius:6px; color:#fff; font-size:13px;" />
</div>
</div>
<div class="compat-note">
⚠️ <strong>兼容性提示:</strong>Web Workers 在所有现代浏览器均支持。IE 不支持。此示例展示 Worker 处理 CPU 密集型任务的核心优势。
</div>
</div>
<script>
// ====== 辅助函数 ======
function logTo(el, text, cls = '') {
const div = document.createElement('div');
div.className = `result-line ${cls}`;
div.textContent = text;
el.innerHTML = '';
el.appendChild(div);
}
function appendLog(el, text, cls = '') {
const div = document.createElement('div');
div.className = `result-line ${cls}`;
div.textContent = text;
el.appendChild(div);
el.scrollTop = el.scrollHeight;
}
// ====== 斐波那契 ======
// 递归实现(非常慢,用于演示阻塞)
function fibRecursive(n) {
if (n <= 1) return n;
return fibRecursive(n - 1) + fibRecursive(n - 2);
}
function calcFibonacci(useWorker) {
const n = parseInt(document.getElementById('fibN').value);
const resultEl = document.getElementById('fibResult');
const busyEl = document.getElementById('fibBusy');
document.getElementById('fibBtnMain').disabled = true;
document.getElementById('fibBtnWorker').disabled = true;
busyEl.style.display = 'inline-flex';
logTo(resultEl, `[${useWorker ? 'Worker' : '主线程'}] 计算斐波那契(${n})...`, 'warn');
const startTime = performance.now();
if (!useWorker) {
// 主线程计算(会阻塞)
setTimeout(() => {
const result = fibRecursive(n);
const elapsed = performance.now() - startTime;
document.getElementById('fibAns').textContent = result.toLocaleString();
document.getElementById('fibTime').textContent = elapsed.toFixed(1);
document.getElementById('fibMethod').textContent = '主线程';
logTo(resultEl, `fib(${n}) = ${result.toLocaleString()}\n耗时: ${elapsed.toFixed(1)}ms`, 'highlight');
busyEl.style.display = 'none';
document.getElementById('fibBtnMain').disabled = false;
document.getElementById('fibBtnWorker').disabled = false;
}, 10);
} else {
// Worker 计算
const code = `
self.onmessage = function(e) {
const n = e.data;
function fib(n) { if (n <= 1) return n; return fib(n-1) + fib(n-2); }
const result = fib(n);
self.postMessage({ n: n, result: result, time: performance.now() });
};
`;
const blob = new Blob([code], { type: 'application/javascript' });
const worker = new Worker(URL.createObjectURL(blob));
worker.onmessage = function(e) {
const elapsed = performance.now() - startTime;
document.getElementById('fibAns').textContent = e.data.result.toLocaleString();
document.getElementById('fibTime').textContent = elapsed.toFixed(1);
document.getElementById('fibMethod').textContent = 'Worker';
logTo(resultEl, `fib(${e.data.n}) = ${e.data.result.toLocaleString()}\n耗时: ${elapsed.toFixed(1)}ms`, 'highlight');
busyEl.style.display = 'none';
document.getElementById('fibBtnMain').disabled = false;
document.getElementById('fibBtnWorker').disabled = false;
worker.terminate();
};
worker.postMessage(n);
}
}
// ====== 素数计算 ======
function isPrime(num) {
if (num < 2) return false;
for (let i = 2; i <= Math.sqrt(num); i++) {
if (num % i === 0) return false;
}
return true;
}
function countPrimes(max) {
let count = 0;
for (let i = 2; i <= max; i++) {
if (isPrime(i)) count++;
}
return count;
}
function calcPrimes(useWorker) {
const max = parseInt(document.getElementById('primeMax').value);
const resultEl = document.getElementById('primeResult');
const busyEl = document.getElementById('primeBusy');
document.getElementById('primeBtnMain').disabled = true;
document.getElementById('primeBtnWorker').disabled = true;
busyEl.style.display = 'inline-flex';
logTo(resultEl, `[${useWorker ? 'Worker' : '主线程'}] 统计 1-${max.toLocaleString()} 内的素数...`, 'warn');
const startTime = performance.now();
if (!useWorker) {
setTimeout(() => {
const count = countPrimes(max);
const elapsed = performance.now() - startTime;
document.getElementById('primeCount').textContent = count.toLocaleString();
document.getElementById('primeTime').textContent = elapsed.toFixed(1);
document.getElementById('primeMethod').textContent = '主线程';
logTo(resultEl, `1 到 ${max.toLocaleString()} 内有 ${count.toLocaleString()} 个素数\n耗时: ${elapsed.toFixed(1)}ms`, 'highlight');
busyEl.style.display = 'none';
document.getElementById('primeBtnMain').disabled = false;
document.getElementById('primeBtnWorker').disabled = false;
}, 10);
} else {
const code = `
self.onmessage = function(e) {
const max = e.data;
let count = 0;
for (let i = 2; i <= max; i++) {
let prime = true;
for (let j = 2; j <= Math.sqrt(i); j++) {
if (i % j === 0) { prime = false; break; }
}
if (prime) count++;
}
self.postMessage({ max: max, count: count });
};
`;
const blob = new Blob([code], { type: 'application/javascript' });
const worker = new Worker(URL.createObjectURL(blob));
worker.onmessage = function(e) {
const elapsed = performance.now() - startTime;
document.getElementById('primeCount').textContent = e.data.count.toLocaleString();
document.getElementById('primeTime').textContent = elapsed.toFixed(1);
document.getElementById('primeMethod').textContent = 'Worker';
logTo(resultEl, `1 到 ${e.data.max.toLocaleString()} 内有 ${e.data.count.toLocaleString()} 个素数\n耗时: ${elapsed.toFixed(1)}ms`, 'highlight');
busyEl.style.display = 'none';
document.getElementById('primeBtnMain').disabled = false;
document.getElementById('primeBtnWorker').disabled = false;
worker.terminate();
};
worker.postMessage(max);
}
}
</script>
</body>
</html><!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>【5】Worker 终止与超时控制</title>
<!--
来源: HTML5基础知识/15-WebWorkers处理线程.md
知识点: terminate() / setTimeout 自动终止 Worker
-->
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
padding: 20px; background: #0f0f23; color: #e0e0e0;
min-height: 100vh;
}
.container { max-width: 880px; margin: 0 auto; }
.header {
text-align: center; padding: 24px; background: linear-gradient(135deg, #ff6b6b, #ee5a24);
color: white; border-radius: 12px; margin-bottom: 24px;
}
.header h1 { font-size: 22px; margin-bottom: 6px; }
.header p { opacity: 0.9; font-size: 14px; }
.card {
background: #1a1a2e; border-radius: 10px; padding: 24px;
margin-bottom: 20px; border: 1px solid #2d2d4a;
}
.card-title {
font-size: 15px; font-weight: 600; color: #ff6b6b;
border-left: 3px solid #ff6b6b; padding-left: 10px; margin-bottom: 16px;
}
.info-banner {
background: rgba(255,107,107,0.1); border-radius: 6px; padding: 12px 16px;
font-size: 13px; line-height: 1.6; color: #ffa07a; margin-bottom: 16px;
}
.control-row { display: flex; gap: 10px; align-items: center; margin-bottom: 16px; flex-wrap: wrap; }
label { font-size: 13px; color: #aaa; }
input[type="number"], input[type="text"] {
padding: 8px 12px; background: #0d0d1a; border: 1px solid #333;
color: #fff; border-radius: 6px; font-size: 13px; width: 120px;
}
input:focus { outline: none; border-color: #ff6b6b; }
.btn {
padding: 9px 18px; border: none; border-radius: 6px; cursor: pointer;
font-size: 13px; font-weight: 500; transition: all 0.2s;
}
.btn-red { background: #ff6b6b; color: white; }
.btn-red:hover { background: #ee5a24; }
.btn-orange { background: #ffa502; color: #1a1a2e; }
.btn-orange:hover { background: #e69500; }
.btn-blue { background: #3498db; color: white; }
.btn-blue:hover { background: #2980b9; }
.btn-green { background: #2ed573; color: #1a1a2e; }
.btn-green:hover { background: #26af61; }
.btn:disabled { opacity: 0.4; cursor: not-allowed; transform: none !important; }
.timeline {
position: relative; padding: 20px 0; min-height: 80px;
}
.timeline-bar {
height: 40px; background: linear-gradient(90deg, #2ed573, #7bed9f);
border-radius: 20px; transition: width 0.1s linear; width: 0%;
display: flex; align-items: center; justify-content: center;
color: #1a1a2e; font-size: 12px; font-weight: 600; min-width: 30px;
}
.timeline-bar.killed {
background: linear-gradient(90deg, #ff6b6b, #ee5a24);
}
.timeline-label {
display: flex; justify-content: space-between; font-size: 11px;
color: #666; margin-top: 6px;
}
.status-grid {
display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin-top: 16px;
}
.stat-card {
background: #0d0d1a; border-radius: 8px; padding: 14px; text-align: center;
}
.stat-value { font-size: 20px; font-weight: 700; }
.stat-value.running { color: #2ed573; }
.stat-value.stopped { color: #ff6b6b; }
.stat-value.idle { color: #888; }
.stat-label { font-size: 11px; color: #666; margin-top: 4px; }
.log-console {
background: #0a0a15; border-radius: 8px; padding: 14px;
font-family: monospace; font-size: 12px; max-height: 250px;
overflow-y: auto; color: #aaa; line-height: 1.7;
}
.log-line { padding: 2px 0; }
.log-line.ok { color: #2ed573; }
.log-line.warn { color: #ffa502; }
.log-line.err { color: #ff6b6b; }
.log-line.info { color: #70a1ff; }
.method-cards { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-top: 16px; }
@media (max-width: 600px) { .method-cards { grid-template-columns: 1fr; } }
.method-card {
background: #0d0d1a; border-radius: 8px; padding: 16px;
border: 1px solid #2d2d4a;
}
.method-card h4 { color: #ccc; font-size: 14px; margin-bottom: 8px; }
.method-card code {
display: block; background: #1a1a2e; padding: 10px; border-radius: 6px;
font-size: 12px; color: #70a1ff; white-space: pre-wrap; line-height: 1.5;
}
.method-card p { font-size: 12px; color: #777; margin-top: 8px; line-height: 1.5; }
.compat-note {
background: rgba(255,193,7,0.08); border: 1px solid rgba(255,193,7,0.25);
border-radius: 6px; padding: 10px 14px; font-size: 12px; color: #ffc107;
margin-top: 16px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>⏹️ Worker 终止与超时控制</h1>
<p>terminate() 强制终止 / setTimeout 自动超时终止</p>
</div>
<div class="card">
<div class="card-title">🎛️ 终止控制面板</div>
<div class="info-banner">
💡 <strong>两种终止方式:</strong><br>
① <code>worker.terminate()</code> — 主线程立即强制终止 Worker(不可恢复)<br>
② <code>self.close()</code> — Worker 自身主动关闭(优雅退出)<br>
③ 超时自动终止 — 主线程设置定时器,超时后调用 terminate()
</div>
<div class="control-row">
<label>循环次数 (模拟耗时):</label>
<input type="number" id="loopCount" value="100000000" min="1000000" step="10000000" />
<label>超时时间 (ms):</label>
<input type="number" id="timeoutMs" value="3000" min="500" step="500" />
</div>
<div class="control-row">
<button class="btn btn-green" id="startBtn" onclick="startWorker()">▶ 启动 Worker</button>
<button class="btn btn-red" id="terminateBtn" onclick="manualTerminate()" disabled>⛹ 手动 terminate()</button>
<button class="btn btn-orange" id="timeoutBtn" onclick="startWithTimeout()" disabled>⏱ 带超时启动</button>
<button class="btn btn-blue" onclick="clearLogs()">🧹 清空日志</button>
</div>
<!-- 进度时间线 -->
<div style="margin-top: 20px;">
<div style="font-size: 13px; color: #aaa; margin-bottom: 8px;">执行进度</div>
<div class="timeline">
<div class="timeline-bar" id="progressBar">0%</div>
</div>
<div class="timeline-label">
<span>开始</span>
<span id="timeLabel">--</span>
<span>完成</span>
</div>
</div>
<div class="status-grid">
<div class="stat-card">
<div class="stat-value idle" id="statusValue">空闲</div>
<div class="stat-label">Worker 状态</div>
</div>
<div class="stat-card">
<div class="stat-value idle" id="elapsedValue">-</div>
<div class="stat-label">已运行时间</div>
</div>
<div class="stat-card">
<div class="stat-value idle" id="progressValue">0%</div>
<div class="stat-label">计算进度</div>
</div>
</div>
<div class="log-console" id="logConsole"></div>
</div>
<div class="card">
<div class="card-title">📖 终止方法对比</div>
<div class="method-cards">
<div class="method-card">
<h4>🔴 worker.terminate() — 外部强制终止</h4>
<code>// 在主线程中调用
const worker = new Worker('script.js');
worker.terminate(); // 立即终止,不可恢复</code>
<p>特点:主线程调用,立即终止,Worker 内部代码不会继续执行。已发送但未处理的消息会丢失。</p>
</div>
<div class="method-card">
<h4>🟢 self.close() — Worker 自身退出</h4>
<code>// 在 Worker 脚本内部调用
self.onmessage = function(e) {
// 完成工作后
self.close(); // 优雅退出
};</code>
<p>特点:Worker 主动调用,可确保清理工作完成后退出。推荐在正常结束时使用。</p>
</div>
</div>
<div class="compat-note">
⚠️ <strong>兼容性提示:</strong><code>terminate()</code> 和 <code>self.close()</code> 在所有支持 Web Workers 的浏览器中均可用。
</div>
</div>
</div>
<script>
const logEl = document.getElementById('logConsole');
let currentWorker = null;
let timeoutTimer = null;
let elapsedTimer = null;
let startTime = 0;
function log(msg, type = '') {
const div = document.createElement('div');
div.className = `log-line ${type}`;
const t = new Date().toTimeString().substring(0, 8);
div.textContent = `[${t}] ${msg}`;
logEl.appendChild(div);
logEl.scrollTop = logEl.scrollHeight;
}
function clearLogs() { logEl.innerHTML = ''; }
function setStatus(text, cls) {
const el = document.getElementById('statusValue');
el.textContent = text;
el.className = `stat-value ${cls}`;
}
function setButtons(running) {
document.getElementById('startBtn').disabled = running;
document.getElementById('terminateBtn').disabled = !running;
document.getElementById('timeoutBtn').disabled = !running;
}
function resetProgress() {
document.getElementById('progressBar').style.width = '0%';
document.getElementById('progressBar').textContent = '0%';
document.getElementById('progressBar').className = 'timeline-bar';
document.getElementById('elapsedValue').textContent = '-';
document.getElementById('elapsedValue').className = 'stat-value idle';
document.getElementById('progressValue').textContent = '0%';
document.getElementById('timeLabel').textContent = '--';
}
function startElapsedTimer() {
startTime = performance.now();
elapsedTimer = setInterval(() => {
const elapsed = ((performance.now() - startTime) / 1000).toFixed(1);
document.getElementById('elapsedValue').textContent = `${elapsed}s`;
document.getElementById('elapsedValue').className = 'stat-value running';
document.getElementById('timeLabel').textContent = `${elapsed}s`;
}, 100);
}
function stopElapsedTimer() {
if (elapsedTimer) { clearInterval(elapsedTimer); elapsedTimer = null; }
}
// ====== Worker 代码 ======
const workerCode = `
self.onmessage = function(e) {
const total = e.data.total;
self.postMessage({ type: 'start', total: total });
let sum = 0;
for (let i = 0; i <= total; i++) {
sum += i;
// 每 2% 报告一次进度
if (i % Math.floor(total / 50) === 0) {
self.postMessage({
type: 'progress',
percent: Math.round((i / total) * 100),
current: i
});
}
}
self.postMessage({ type: 'done', result: sum });
};
`;
function createWorkerInstance() {
const blob = new Blob([workerCode], { type: 'application/javascript' });
return new Worker(URL.createObjectURL(blob));
}
function startWorker() {
const total = parseInt(document.getElementById('loopCount').value) || 100000000;
resetProgress();
setButtons(true);
setStatus('运行中', 'running');
currentWorker = createWorkerInstance();
startElapsedTimer();
log(`启动 Worker,任务: 循环 ${total.toLocaleString()} 次`, 'ok');
currentWorker.onmessage = function(e) {
const d = e.data;
if (d.type === 'start') {
log(`[Worker] 开始执行,总计 ${d.total.toLocaleString()} 次迭代`, 'info');
} else if (d.type === 'progress') {
const bar = document.getElementById('progressBar');
bar.style.width = `${d.percent}%`;
bar.textContent = `${d.percent}%`;
document.getElementById('progressValue').textContent = `${d.percent}%`;
} else if (d.type === 'done') {
stopElapsedTimer();
const elapsed = ((performance.now() - startTime) / 1000).toFixed(2);
document.getElementById('elapsedValue').textContent = `${elapsed}s`;
setStatus('已完成', 'stopped');
setButtons(false);
log(`[Worker] 完成! 结果 sum=${d.result.toLocaleString()} | 耗时 ${elapsed}s`, 'ok');
currentWorker = null;
}
};
currentWorker.onerror = function(e) {
stopElapsedTimer();
log(`[Worker 错误] ${e.message}`, 'err');
setStatus('错误', 'stopped');
setButtons(false);
e.preventDefault();
currentWorker = null;
};
currentWorker.postMessage({ total: total });
}
function manualTerminate() {
if (!currentWorker) return;
currentWorker.terminate();
stopElapsedTimer();
if (timeoutTimer) { clearTimeout(timeoutTimer); timeoutTimer = null; }
const bar = document.getElementById('progressBar');
bar.classList.add('killed');
bar.textContent = '已终止 ⛹';
const elapsed = ((performance.now() - startTime) / 1000).toFixed(2);
document.getElementById('elapsedValue').textContent = `${elapsed}s`;
document.getElementById('elapsedValue').className = 'stat-value stopped';
setStatus('已终止', 'stopped');
setButtons(false);
log(`⛹ 手动调用 terminate() 终止 Worker (运行 ${elapsed}s)`, 'warn');
currentWorker = null;
}
function startWithTimeout() {
const total = parseInt(document.getElementById('loopCount').value) || 100000000;
const timeout = parseInt(document.getElementById('timeoutMs').value) || 3000;
resetProgress();
setButtons(true);
setStatus('运行中 (有超时)', 'running');
currentWorker = createWorkerInstance();
startElapsedTimer();
log(`启动 Worker (带 ${timeout}ms 超时),任务: ${total.toLocaleString()} 次循环`, 'ok');
// 设置超时定时器
timeoutTimer = setTimeout(() => {
if (!currentWorker) return;
currentWorker.terminate();
stopElapsedTimer();
const bar = document.getElementById('progressBar');
bar.classList.add('killed');
bar.textContent = `⏱ 超时终止 (${timeout}ms)`;
const elapsed = ((performance.now() - startTime) / 1000).toFixed(2);
document.getElementById('elapsedValue').textContent = `${elapsed}s`;
document.getElementById('elapsedValue').className = 'stat-value stopped';
setStatus('超时终止', 'stopped');
setButtons(false);
log(`⏱ 超时! ${timeout}ms 后自动调用 terminate() (已运行 ${elapsed}s)`, 'err');
currentWorker = null;
}, timeout);
currentWorker.onmessage = function(e) {
const d = e.data;
if (d.type === 'start') {
log(`[Worker] 开始执行`, 'info');
} else if (d.type === 'progress') {
const bar = document.getElementById('progressBar');
bar.style.width = `${d.percent}%`;
bar.textContent = `${d.percent}%`;
document.getElementById('progressValue').textContent = `${d.percent}%`;
} else if (d.type === 'done') {
clearTimeout(timeoutTimer);
timeoutTimer = null;
stopElapsedTimer();
const elapsed = ((performance.now() - startTime) / 1000).toFixed(2);
document.getElementById('elapsedValue').textContent = `${elapsed}s`;
setStatus('已完成', 'stopped');
setButtons(false);
log(`[Worker] 在超时前完成! 耗时 ${elapsed}s`, 'ok');
currentWorker = null;
}
};
currentWorker.onerror = function(e) {
clearTimeout(timeoutTimer);
timeoutTimer = null;
stopElapsedTimer();
log(`[Worker 错误] ${e.message}`, 'err');
setStatus('错误', 'stopped');
setButtons(false);
e.preventDefault();
currentWorker = null;
};
currentWorker.postMessage({ total: total });
}
</script>
</body>
</html><!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>【6】Shared Worker 多页面共享状态</title>
<!--
来源: HTML5基础知识/15-WebWorkers处理线程.md
知识点: Shared Worker 多页面/iframe 共享一个 SharedWorker
-->
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white; min-height: 100vh;
}
.container { max-width: 1100px; margin: 0 auto; }
.header {
text-align: center; padding: 28px; background: rgba(255,255,255,0.1);
border-radius: 12px; margin-bottom: 24px; backdrop-filter: blur(10px);
}
.header h1 { font-size: 24px; margin-bottom: 8px; }
.header p { opacity: 0.85; font-size: 14px; }
.warning-banner {
background: rgba(255,193,7,0.2); border: 1px solid rgba(255,193,7,0.4);
border-radius: 8px; padding: 14px 18px; font-size: 13px; line-height: 1.6;
color: #fff3cd; margin-bottom: 20px;
}
.iframes-container {
display: grid; grid-template-columns: 1fr 1fr; gap: 20px;
}
@media (max-width: 800px) { .iframes-container { grid-template-columns: 1fr; } }
.iframe-wrapper {
background: white; border-radius: 12px; overflow: hidden;
box-shadow: 0 8px 32px rgba(0,0,0,0.2);
}
.iframe-header {
background: #f8f9fa; padding: 10px 16px; font-size: 13px; font-weight: 600;
color: #333; border-bottom: 1px solid #eee; display: flex; align-items: center; gap: 8px;
}
.dot { width: 8px; height: 8px; border-radius: 50%; }
.dot.blue { background: #3498db; }
.dot.green { background: #27ae60; }
iframe {
width: 100%; height: 420px; border: none; display: block;
}
.shared-state-panel {
background: rgba(255,255,255,0.1); border-radius: 12px; padding: 20px;
margin-top: 20px; backdrop-filter: blur(10px);
}
.shared-state-title {
font-size: 15px; font-weight: 600; margin-bottom: 12px;
display: flex; align-items: center; gap: 8px;
}
.state-display {
display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 12px;
}
.state-item {
background: rgba(255,255,255,0.1); border-radius: 8px; padding: 12px;
text-align: center;
}
.state-value { font-size: 22px; font-weight: 700; }
.state-label { font-size: 11px; opacity: 0.75; margin-top: 4px; }
.compat-note {
background: rgba(231,76,60,0.15); border: 1px solid rgba(231,76,60,0.35);
border-radius: 6px; padding: 10px 14px; font-size: 12px; color: #ffcdd2;
margin-top: 16px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🔗 Shared Worker 多页面共享</h1>
<p>两个 iframe 共享同一个 SharedWorker,实现跨页面状态同步</p>
</div>
<div class="warning-banner">
⚠️ <strong>Safari 不支持 Shared Worker!</strong>请在 Chrome、Firefox 或 Edge 中打开此页面以获得完整体验。
Shared Worker 允许同一浏览器的多个标签页/iframe 共享同一个 Worker 实例。
</div>
<div class="iframes-container">
<div class="iframe-wrapper">
<div class="iframe-header"><span class="dot blue"></span> 页面 A (Client A)</div>
<iframe id="frameA" src="about:blank"></iframe>
</div>
<div class="iframe-wrapper">
<div class="iframe-header"><span class="dot green"></span> 页面 B (Client B)</div>
<iframe id="frameB" src="about:blank"></iframe>
</div>
</div>
<div class="shared-state-panel">
<div class="shared-state-title">📊 Shared Worker 共享状态</div>
<div class="state-display">
<div class="state-item">
<div class="state-value" id="connCount">0</div>
<div class="state-label">连接数</div>
</div>
<div class="state-item">
<div class="state-value" id="totalCount">0</div>
<div class="state-label">消息总数</div>
</div>
<div class="state-item">
<div class="state-value" id="lastSender">-</div>
<div class="state-label">最后发送者</div>
</div>
<div class="state-item">
<div class="state-value" id="sharedCounter">0</div>
<div class="state-label">共享计数器</div>
</div>
</div>
</div>
<div class="compat-note">
🌐 <strong>兼容性:</strong>Chrome 4+ ✅ | Firefox 29+ ✅ | Edge 79+ ✅ | Safari ❌ 不支持
</div>
</div>
<!-- ========== iframe 子页面模板 ========== -->
<script>
// 创建 iframe 内容 HTML
const iframeHTML = (color, label) => `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; padding: 16px; background: #fafafa; color: #333; }
.client-header { font-size: 15px; font-weight: 700; margin-bottom: 12px; color: ${color}; }
.input-area { display: flex; gap: 8px; margin-bottom: 12px; }
input { flex: 1; padding: 8px 12px; border: 1px solid #ddd; border-radius: 6px; font-size: 13px; }
input:focus { outline: none; border-color: ${color}; }
button { padding: 8px 16px; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 500; background: ${color}; color: white; transition: opacity 0.2s; }
button:hover { opacity: 0.85; }
.message-list { background: #f0f0f0; border-radius: 8px; padding: 12px; max-height: 240px; overflow-y: auto; font-size: 12px; }
.msg-item { padding: 5px 8px; margin-bottom: 4px; border-radius: 4px; animation: fadeIn 0.2s; }
.msg-item.from-me { background: #dbeafe; color: #1e40af; text-align: right; }
.msg-item.from-other { background: #dcfce7; color: #166534; }
.msg-item.system { background: #fef3c7; color: #92400e; font-style: italic; font-size: 11px; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: translateY(0); } }
.status-badge { display: inline-block; padding: 3px 10px; border-radius: 12px; font-size: 11px; background: #d4edda; color: #155724; margin-bottom: 10px; }
.counter-btn { margin-top: 8px; width: 100%; background: ${color} !important; }
</style>
</head>
<body>
<div class="client-header">${label}</div>
<div class="status-badge" id="status">⏳ 连接中...</div>
<div class="input-area">
<input type="text" id="msgInput" placeholder="输入消息广播给所有客户端..." />
<button onclick="sendMessage()">发送</button>
</div>
<button class="counter-btn" onclick="incrementCounter()">➕ 共享计数器 +1</button>
<div class="message-list" id="msgList"></div>
<script>
const msgList = document.getElementById('msgList');
const statusEl = document.getElementById('status');
let myPort = null;
function addMsg(text, type) {
const div = document.createElement('div');
div.className = 'msg-item ' + type;
div.textContent = text;
msgList.appendChild(div);
msgList.scrollTop = msgList.scrollHeight;
}
// 检查是否支持 SharedWorker
if ('SharedWorker' in window) {
try {
// 使用内联 Blob URL 创建 SharedWorker
const sharedCode = \`
let connections = [];
let messageCount = 0;
let sharedCounter = 0;
onconnect = function(e) {
const port = e.ports[0];
connections.push(port);
port.postMessage({
type: 'welcome',
clientName: '${label}',
connectionCount: connections.length,
sharedCounter: sharedCounter,
messageCount: messageCount
});
port.onmessage = function(event) {
const data = event.data;
if (data.type === 'broadcast') {
messageCount++;
sharedCounter = data.counter || sharedCounter;
// 广播给所有连接的客户端
connections.forEach(function(p) {
p.postMessage({
type: 'broadcast',
from: '${label}',
text: data.text,
timestamp: Date.now(),
messageCount: messageCount,
sharedCounter: sharedCounter
});
});
// 通知父窗口更新状态
if (typeof window.parent !== 'undefined') {
window.parent.postMessage({
type: 'stateUpdate',
connCount: connections.length,
totalCount: messageCount,
lastSender: '${label}',
counter: sharedCounter
}, '*');
}
}
if (data.type === 'increment') {
sharedCounter++;
messageCount++;
connections.forEach(function(p) {
p.postMessage({
type: 'counterUpdate',
from: '${label}',
counter: sharedCounter,
messageCount: messageCount
});
});
if (typeof window.parent !== 'undefined') {
window.parent.postMessage({
type: 'stateUpdate',
connCount: connections.length,
totalCount: messageCount,
lastSender: '${label}',
counter: sharedCounter
}, '*');
}
}
};
port.start();
};
// 清理断开的连接
setInterval(function() {
// 简单的心跳检测可以在这里添加
}, 5000);
\`;
const blob = new Blob([sharedCode], { type: 'application/javascript' });
const url = URL.createObjectURL(blob);
const sharedWorker = new SharedWorker(url, 'demo-shared-worker');
myPort = sharedWorker.port;
myPort.start();
myPort.onmessage = function(e) {
const data = e.data;
if (data.type === 'welcome') {
statusEl.textContent = '✅ 已连接 ( #' + data.connectionCount + ' )';
addMsg('欢迎! 当前 ' + data.connectionCount + ' 个客户端在线, 计数器=' + data.sharedCounter, 'system');
notifyParent(data);
} else if (data.type === 'broadcast') {
addMsg('[' + data.from + '] ' + data.text, data.from === '${label}' ? 'from-me' : 'from-other');
updateLocalState(data);
} else if (data.type === 'counterUpdate') {
addMsg('[' + data.from + '] 计数器 → ' + data.counter, 'from-other');
updateLocalState(data);
}
};
function sendMessage() {
const input = document.getElementById('msgInput');
const text = input.value.trim();
if (!text || !myPort) return;
myPort.postMessage({ type: 'broadcast', text: text });
addMsg('我: ' + text, 'from-me');
input.value = '';
}
function incrementCounter() {
if (!myPort) return;
myPort.postMessage({ type: 'increment' });
}
function notifyParent(data) {
window.parent.postMessage({
type: 'stateUpdate',
connCount: data.connectionCount,
totalCount: data.messageCount || 0,
lastSender: '${label}',
counter: data.sharedCounter || 0
}, '*');
}
function updateLocalState(data) {
window.parent.postMessage({
type: 'stateUpdate',
connCount: data.connCount || 0,
totalCount: data.messageCount || 0,
lastSender: data.from || '-',
counter: data.counter || data.sharedCounter || 0
}, '*');
}
document.getElementById('msgInput').addEventListener('keypress', function(e) {
if (e.key === 'Enter') sendMessage();
});
} catch(err) {
statusEl.textContent = '❌ 错误: ' + err.message;
addMsg('SharedWorker 创建失败: ' + err.message, 'system');
}
} else {
statusEl.textContent = '❌ 浏览器不支持 SharedWorker';
addMsg('您的浏览器不支持 SharedWorker (Safari 不支持)', 'system');
}
<\/script>
</body>
</html>`;
// 将 HTML 写入两个 iframe
const frameA = document.getElementById('frameA');
const frameB = document.getElementById('frameB');
frameA.srcdoc = iframeHTML('#3498db', 'Client-A');
frameB.srcdoc = iframeHTML('#27ae60', 'Client-B');
// 监听子页面的状态更新
window.addEventListener('message', function(e) {
if (e.data && e.data.type === 'stateUpdate') {
document.getElementById('connCount').textContent = e.data.connCount;
document.getElementById('totalCount').textContent = e.data.totalCount;
document.getElementById('lastSender').textContent = e.data.lastSender;
document.getElementById('sharedCounter').textContent = e.data.counter;
}
});
</script>
</body>
</html><!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>【7】Web Worker 内联方式对比</title>
<!--
来源: HTML5基础知识/15-WebWorkers处理线程.md
知识点: Blob URL vs 外部 JS 文件 vs importScripts 三种创建方式对比
-->
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
padding: 20px; background: #f5f7fa; color: #333;
}
.container { max-width: 1000px; margin: 0 auto; }
.header {
text-align: center; padding: 24px; background: linear-gradient(135deg, #11998e, #38ef7d);
color: white; border-radius: 12px; margin-bottom: 24px;
}
.header h1 { font-size: 22px; margin-bottom: 6px; }
.header p { opacity: 0.9; font-size: 14px; }
.methods-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-bottom: 24px; }
@media (max-width: 768px) { .methods-grid { grid-template-columns: 1fr; } }
.method-card {
background: white; border-radius: 10px; overflow: hidden;
box-shadow: 0 2px 12px rgba(0,0,0,0.06); transition: transform 0.2s;
}
.method-card:hover { transform: translateY(-3px); }
.method-header {
padding: 14px 16px; font-weight: 600; font-size: 14px; color: white;
display: flex; align-items: center; gap: 8px;
}
.method-header.blob { background: #6c5ce7; }
.method-header.external { background: #0984e3; }
.method-header.import { background: #00b894; }
.method-body { padding: 16px; }
.code-block {
background: #1e1e1e; border-radius: 6px; padding: 12px;
font-family: monospace; font-size: 11px; color: #d4d4d4; overflow-x: auto;
line-height: 1.6; white-space: pre; margin-bottom: 12px;
}
.code-keyword { color: #569cd6; }
.code-string { color: #ce9178; }
.code-comment { color: #6a9955; }
.run-btn {
width: 100%; padding: 10px; border: none; border-radius: 6px;
cursor: pointer; font-size: 13px; font-weight: 600; color: white;
transition: all 0.2s;
}
.run-btn.blob { background: #6c5ce7; }
.run-btn.external { background: #0984e3; }
.run-btn.import { background: #00b894; }
.run-btn:hover { opacity: 0.85; transform: scale(1.02); }
.run-btn:active { transform: scale(0.98); }
.result-mini {
margin-top: 10px; padding: 10px; border-radius: 6px;
font-size: 12px; display: none;
}
.result-mini.success { display: block; background: #d4edda; color: #155724; }
.result-mini.error { display: block; background: #f8d7da; color: #721c24; }
/* 对比表格 */
.compare-section {
background: white; border-radius: 10px; padding: 24px;
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
}
.section-title {
font-size: 16px; font-weight: 600; color: #333;
border-left: 4px solid #38ef7d; padding-left: 12px; margin-bottom: 16px;
}
table { width: 100%; border-collapse: collapse; font-size: 13px; }
th, td { padding: 12px 14px; text-align: left; border-bottom: 1px solid #eee; }
th { background: #f8f9fa; font-weight: 600; color: #555; }
tr:hover td { background: #fafbfd; }
.badge {
display: inline-block; padding: 2px 10px; border-radius: 10px;
font-size: 11px; font-weight: 600;
}
.badge-good { background: #d4edda; color: #155724; }
.badge-mid { background: #fff3cd; color: #856404; }
.badge-bad { background: #f8d7da; color: #721c24; }
.compat-box {
background: #e8f5e9; border-left: 4px solid #4caf50;
padding: 14px 16px; border-radius: 4px; font-size: 13px;
line-height: 1.6; color: #2e7d32; margin-top: 16px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🔄 Web Worker 创建方式对比</h1>
<p>Blob URL 内联 vs 外部 JS 文件 vs importScripts 加载外部库</p>
</div>
<div class="methods-grid">
<!-- 方式一:Blob URL -->
<div class="method-card">
<div class="method-header blob">📦 方式一: Blob URL (内联)</div>
<div class="method-body">
<div class="code-block"><span class="code-comment">// 将 Worker 代码作为字符串</span>
<span class="code-keyword">const</span> code = <span class="code-string">'self.onmessage=e=>...'</span>;
<span class="code-keyword">const</span> blob = <span class="code-keyword">new</span> Blob([code]);
<span class="code-keyword">const</span> url = URL.createObjectURL(blob);
<span class="code-keyword">const</span> w = <span class="code-keyword">new</span> Worker(url);</div>
<button class="run-btn blob" onclick="testBlob()">▶ 运行测试</button>
<div class="result-mini success" id="resultBlob"></div>
</div>
</div>
<!-- 方式二:外部文件 -->
<div class="method-card">
<div class="method-header external">📁 方式二: 外部 JS 文件</div>
<div class="method-body">
<div class="code-block"><span class="code-comment">// 引用外部 .js 文件路径</span>
<span class="code-comment">// worker.js 文件内容:</span>
<span class="code-comment">// self.onmessage=function(e){...}</span>
<span class="code-keyword">const</span> w = <span class="code-keyword">new</span> Worker(<span class="code-string">'./worker.js'</span>);
w.postMessage(data);</div>
<button class="run-btn external" onclick="testExternal()">▶ 运行测试</button>
<div class="result-mini success" id="resultExternal"></div>
</div>
</div>
<!-- 方式三:importScripts -->
<div class="method-card">
<div class="method-header import">📚 方式三: importScripts</div>
<div class="method-body">
<div class="code-block"><span class="code-comment">// Worker 中加载外部脚本</span>
<span class="code-comment">// main-worker.js:</span>
importScripts(<span class="code-string">'utils.js'</span>, <span class="code-string">'math.js'</span>);
self.onmessage = <span class="code-keyword">function</span>(e) {
<span class="code-comment">// 可使用 utils.js/math.js 的函数</span>
self.postMessage(calculate(e.data));
};</div>
<button class="run-btn import" onclick="testImportScripts()">▶ 运行测试</button>
<div class="result-mini success" id="resultImport"></div>
</div>
</div>
</div>
<!-- 对比表格 -->
<div class="compare-section">
<div class="section-title">📊 三种方式详细对比</div>
<table>
<thead>
<tr>
<th>特性</th>
<th>Blob URL (内联)</th>
<th>外部 JS 文件</th>
<th>importScripts</th>
</tr>
</thead>
<tbody>
<tr>
<td>文件数量</td>
<td>单文件 (HTML+JS一体)</td>
<td>需要额外 .js 文件</td>
<td>主文件 + 依赖文件</td>
</tr>
<tr>
<td>部署便利性</td>
<td><span class="badge badge-good">最佳</span></td>
<td><span class="badge badge-mid">一般</span></td>
<td><span class="badge badge-bad">较复杂</span></td>
</tr>
<tr>
<td>调试体验</td>
<td><span class="badge badge-bad">较差</span> (Blob URL 难调试)</td>
<td><span class="badge badge-good">最佳</span> (独立源文件)</td>
<td><span class="badge badge-mid">中等</span></td>
</tr>
<tr>
<td>代码复用</td>
<td><span class="badge badge-bad">无</span></td>
<td><span class="badge badge-good">好</span></td>
<td><span class="badge badge-good">优秀</span></td>
</tr>
<tr>
<td>缓存策略</td>
<td><span class="badge badge-bad">无缓存</span></td>
<td><span class="badge badge-good">浏览器缓存</span></td>
<td><span class="badge badge-good">各文件独立缓存</span></td>
</tr>
<tr>
<td>CORS 限制</td>
<td><span class="badge badge-good">无</span> (同源)</td>
<td><span class="badge badge-mid">需同源或 CORS</span></td>
<td><span class="badge badge-mid">依赖文件需 CORS</span></td>
</tr>
<tr>
<td>适用场景</td>
<td>演示、原型、单文件工具</td>
<td>生产环境、大型项目</td>
<td>需使用第三方库的 Worker</td>
</tr>
<tr>
<td>性能开销</td>
<td><span class="badge badge-mid">每次创建新 Blob</span></td>
<td><span class="badge badge-good">低</span></td>
<td><span class="badge badge-mid">加载依赖有延迟</span></td>
</tr>
</tbody>
</table>
<div class="compat-box">
✅ <strong>兼容性提示:</strong>三种方式在现代浏览器中均完全支持。<br>
• <strong>Blob URL</strong> 最适合示例和演示(如本文件集的所有示例)<br>
• <strong>外部文件</strong> 是生产环境的首选方案<br>
• <strong>importScripts</strong> 适合需要在 Worker 中使用第三方库的场景
</div>
</div>
</div>
<script>
// ====== 测试函数 ======
// 方式一:Blob URL
function testBlob() {
const resultEl = document.getElementById('resultBlob');
resultEl.className = 'result-mini error';
resultEl.textContent = '测试中...';
const code = `
self.onmessage = function(e) {
const n = e.data;
let sum = 0;
for (let i = 1; i <= n; i++) sum += i;
self.postMessage({ method: 'Blob URL', result: sum, n: n });
};
`;
try {
const blob = new Blob([code], { type: 'application/javascript' });
const url = URL.createObjectURL(blob);
const worker = new Worker(url);
worker.onmessage = function(e) {
const d = e.data;
resultEl.className = 'result-mini success';
resultEl.innerHTML = `✅ 成功! 方式: ${d.method}<br>` +
`计算 1~${d.n} 的累加 = <strong>${d.result.toLocaleString()}</strong><br>` +
`耗时: ~<small>即时完成</small>`;
worker.terminate();
URL.revokeObjectURL(url);
};
worker.onerror = function(e) {
resultEl.className = 'result-mini error';
resultEl.textContent = `❌ 失败: ${e.message}`;
e.preventDefault();
};
worker.postMessage(100000);
} catch (err) {
resultEl.className = 'result-mini error';
resultEl.textContent = `❌ 异常: ${err.message}`;
}
}
// 方式二:外部文件 (模拟)
function testExternal() {
const resultEl = document.getElementById('resultExternal');
resultEl.className = 'result-mini error';
resultEl.textContent = '测试中...';
// 模拟外部文件:先创建一个临时的外部 Worker 文件
const externalCode = `
// 这是 "external-worker.js" 的内容
self.onmessage = function(e) {
const n = e.data;
let product = 1;
for (let i = 1; i <= Math.min(n, 20); i++) product *= i;
self.postMessage({
method: 'External File',
result: product,
n: n,
note: '模拟从外部 .js 文件加载'
});
};
`;
// 在实际项目中,这里应该是:
// const worker = new Worker('./external-worker.js');
// 为了让本示例可独立运行,我们用 Blob URL 模拟
try {
const blob = new Blob([externalCode], { type: 'application/javascript' });
const url = URL.createObjectURL(blob);
const worker = new Worker(url);
worker.onmessage = function(e) {
const d = e.data;
resultEl.className = 'result-mini success';
resultEl.innerHTML = `✅ 成功! 方式: ${d.method}<br>` +
`计算 1~${Math.min(d.n,20)} 的阶乘 = <strong>${d.result.toLocaleString()}</strong><br>` +
`<small>${d.note}</small>`;
worker.terminate();
};
worker.onerror = function(e) {
resultEl.className = 'result-mini error';
resultEl.textContent = `❌ 失败: ${e.message}`;
e.preventDefault();
};
worker.postMessage(15);
} catch (err) {
resultEl.className = 'result-mini error';
resultEl.textContent = `❌ 异常: ${err.message}`;
}
}
// 方式三:importScripts
function testImportScripts() {
const resultEl = document.getElementById('resultImport');
resultEl.className = 'result-mini error';
resultEl.textContent = '测试中...';
// 模拟外部依赖库
const utilsLib = `
// utils.js - 工具函数库
var MathUtils = {
fibonacci: function(n) {
if (n <= 1) return n;
var a = 0, b = 1;
for (var i = 2; i <= n; i++) {
var temp = a + b;
a = b;
b = temp;
}
return b;
},
isPrime: function(num) {
if (num < 2) return false;
for (var i = 2; i <= Math.sqrt(num); i++) {
if (num % i === 0) return false;
}
return true;
},
formatNumber: function(n) {
return n.toLocaleString();
}
};
`;
const mainWorker = `
// 通过 importScripts 加载外部库
importScripts('data:text/javascript,' + encodeURIComponent(\`
var MathUtils = {
fibonacci: function(n) {
if (n <= 1) return n;
var a = 0, b = 1;
for (var i = 2; i <= n; i++) { var t = a+b; a = b; b = t; }
return b;
},
isPrime: function(num) {
if (num < 2) return false;
for (var i = 2; i <= Math.sqrt(num); i++) { if (num%i===0) return false; }
return true;
}
};
\`));
self.onmessage = function(e) {
var n = e.data;
var fibResult = MathUtils.fibonacci(n);
var primeCheck = MathUtils.isPrime(fibResult);
self.postMessage({
method: 'importScripts',
fibN: n,
fibResult: fibResult,
isPrime: primeCheck,
note: '通过 importScripts 加载了 MathUtils 库'
});
};
`;
try {
const blob = new Blob([mainWorker], { type: 'application/javascript' });
const url = URL.createObjectURL(blob);
const worker = new Worker(url);
worker.onmessage = function(e) {
const d = e.data;
resultEl.className = 'result-mini success';
resultEl.innerHTML = `✅ 成功! 方式: ${d.method}<br>` +
`fib(${d.fibN}) = <strong>${d.fibResult.toLocaleString()}</strong><br>` +
`结果是否为素数: <strong>${d.isPrime ? '是 ✓' : '否 ✗'}</strong><br>` +
`<small>${d.note}</small>`;
worker.terminate();
};
worker.onerror = function(e) {
resultEl.className = 'result-mini error';
resultEl.textContent = `❌ 失败: ${e.message}`;
e.preventDefault();
};
worker.postMessage(40);
} catch (err) {
resultEl.className = 'result-mini error';
resultEl.textContent = `❌ 异常: ${err.message}`;
}
}
</script>
</body>
</html><!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>【8】Worker importScripts 加载外部库</title>
<!--
来源: HTML5基础知识/15-WebWorkers处理线程.md
知识点: Worker 中使用 importScripts 加载外部脚本/库
-->
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
padding: 20px; background: #1a1a2e; color: #e0e0e0;
min-height: 100vh;
}
.container { max-width: 960px; margin: 0 auto; }
.header {
text-align: center; padding: 24px; background: linear-gradient(135deg, #f093fb, #f5576c);
color: white; border-radius: 12px; margin-bottom: 24px;
}
.header h1 { font-size: 22px; margin-bottom: 6px; }
.header p { opacity: 0.9; font-size: 14px; }
.card {
background: #16213e; border-radius: 10px; padding: 24px;
margin-bottom: 20px; border: 1px solid #0f3460;
}
.card-title {
font-size: 15px; font-weight: 600; color: #f093fb;
border-left: 3px solid #f093fb; padding-left: 10px; margin-bottom: 16px;
}
.info-banner {
background: rgba(240,147,251,0.1); border-radius: 6px; padding: 12px 16px;
font-size: 13px; line-height: 1.6; color: #f5a6ff; margin-bottom: 16px;
}
/* 库展示 */
.libs-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px; margin-bottom: 16px; }
.lib-card {
background: #0d1117; border: 1px solid #30363d; border-radius: 8px; padding: 14px;
transition: border-color 0.2s;
}
.lib-card:hover { border-color: #f093fb; }
.lib-name { font-weight: 600; font-size: 13px; color: #f093fb; margin-bottom: 4px; }
.lib-desc { font-size: 11px; color: #888; line-height: 1.5; }
.lib-status { display: inline-block; padding: 2px 8px; border-radius: 8px; font-size: 10px; margin-top: 6px; }
.lib-status.loaded { background: rgba(46,213,115,0.15); color: #2ed573; }
.lib-status.loading { background: rgba(255,165,2,0.15); color: #ffa502; }
.control-row { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; margin-bottom: 16px; }
label { font-size: 13px; color: #aaa; }
select, input {
padding: 8px 12px; background: #0a0a1a; border: 1px solid #333;
color: #fff; border-radius: 6px; font-size: 13px;
}
input:focus, select:focus { outline: none; border-color: #f093fb; }
.btn {
padding: 9px 18px; border: none; border-radius: 6px; cursor: pointer;
font-size: 13px; font-weight: 500; transition: all 0.2s;
}
.btn-pink { background: linear-gradient(135deg, #f093fb, #f5576c); color: white; }
.btn-pink:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(240,147,251,0.3); }
.btn-pink:disabled { opacity: 0.4; cursor: not-allowed; transform: none !important; }
.result-panel {
background: #0a0a1a; border-radius: 8px; padding: 16px;
font-family: monospace; font-size: 13px; min-height: 120px;
max-height: 300px; overflow-y: auto; color: #ccc; line-height: 1.7;
}
.result-line { padding: 3px 0; }
.result-line.ok { color: #2ed573; }
.result-line.err { color: #ff6b81; }
.result-line.info { color: #70a1ff; }
.result-line.highlight { color: #ffa502; font-weight: 600; }
/* 加载流程 */
.flow-diagram {
display: flex; align-items: center; justify-content: center; gap: 8px;
padding: 20px; flex-wrap: wrap;
}
.flow-node {
background: #0f3460; border: 1px solid #1a4a7a; border-radius: 8px;
padding: 12px 18px; text-align: center; font-size: 12px; color: #aaa;
min-width: 100px;
}
.flow-node.active { border-color: #f093fb; color: #f093fb; background: rgba(240,147,251,0.08); }
.flow-arrow { color: #444; font-size: 18px; }
.code-preview {
background: #0d1117; border-radius: 8px; padding: 14px; margin-top: 12px;
font-family: 'SF Mono', Monaco, Consolas, monospace; font-size: 11px;
color: #c9d1d9; line-height: 1.6; overflow-x: auto;
}
.code-preview .kw { color: #ff7b72; }
.code-preview .str { color: #a5d6ff; }
.code-preview .cm { color: #8b949e; }
.code-preview .fn { color: #d2a8ff; }
.compat-note {
background: rgba(255,193,7,0.08); border: 1px solid rgba(255,193,7,0.25);
border-radius: 6px; padding: 10px 14px; font-size: 12px; color: #ffc107;
margin-top: 16px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📚 importScripts 加载外部库</h1>
<p>在 Web Worker 中通过 importScripts 引入工具函数和第三方库</p>
</div>
<div class="card">
<div class="card-title">🔧 模拟加载的库</div>
<div class="libs-grid">
<div class="lib-card" id="lib-math">
<div class="lib-name">🔢 MathLib</div>
<div class="lib-desc">数学计算库:斐波那契、素数检测、阶乘、GCD、LCM</div>
<span class="lib-status loading" id="status-math">待加载</span>
</div>
<div class="lib-card" id="lib-string">
<div class="lib-name">📝 StringLib</div>
<div class="lib-desc">字符串处理:反转、驼峰转换、字频统计、Base64</div>
<span class="lib-status loading" id="status-string">待加载</span>
</div>
<div class="lib-card" id="lib-data">
<div class="lib-name">📊 DataLib</div>
<div class="lib-desc">数据处理:排序、去重、分组、聚合统计</div>
<span class="lib-status loading" id="status-data">待加载</span>
</div>
</div>
<div class="control-row">
<label>选择功能测试:</label>
<select id="funcSelect">
<option value="fibonacci">MathLib.fibonacci(n)</option>
<option value="isPrime">MathLib.isPrime(n)</option>
<option value="factorial">MathLib.factorial(n)</option>
<option value="reverseString">StringLib.reverse(str)</option>
<option value="wordCount">StringLib.wordCount(str)</option>
<option value="toCamelCase">StringLib.toCamelCase(str)</option>
<option value="unique">DataLib.unique(arr)</option>
<option value="groupBy">DataLib.groupBy(arr, key)</option>
<option value="stats">DataLib.stats(arr)</option>
</select>
<label>参数:</label>
<input type="text" id="funcParam" value="40" placeholder="输入参数..." />
<button class="btn btn-pink" onclick="runTest()" id="runBtn">▶ 执行 (importScripts)</button>
</div>
<div class="result-panel" id="resultPanel">
<div class="result-line info">[系统] 等待执行...</div>
</div>
</div>
<!-- 加载流程 -->
<div class="card">
<div class="card-title">📋 importScripts 加载流程</div>
<div class="flow-diagram">
<div class="flow-node active">主线程<br/>创建 Worker</div>
<div class="flow-arrow">→</div>
<div class="flow-node active">Worker 启动</div>
<div class="flow-arrow">→</div>
<div class="flow-node active">importScripts()<br/>同步加载脚本</div>
<div class="flow-arrow">→</div>
<div class="flow-node active">库函数可用</div>
<div class="flow-arrow">→</div>
<div class="flow-node active">接收消息<br/>调用库函数</div>
</div>
<div class="code-preview"><span class="cm">// Worker 脚本中:</span>
<span class="kw">importScripts</span>(<span class="str">'math-lib.js'</span>, <span class="str">'string-lib.js'</span>, <span class="str">'data-lib.js'</span>);
<span class="cm">// 加载完成后,全局变量 MathLib/StringLib/DataLib 可用</span>
self.<span class="fn">onmessage</span> = <span class="kw">function</span>(e) {
<span class="kw">var</span> result = MathLib.<span class="fn">fibonacci</span>(e.data.n);
self.<span class="fn">postMessage</span>({ result: result });
};</div>
<div class="compat-note">
⚠️ <strong>注意:</strong><code>importScripts()</code> 是<strong>同步阻塞</strong>的,会暂停 Worker 执行直到所有脚本加载完成。
脚本按顺序依次加载,后加载的脚本可使用先加载脚本中的变量和函数。
</div>
</div>
</div>
<script>
const resultPanel = document.getElementById('resultPanel');
function log(msg, type = '') {
const div = document.createElement('div');
div.className = `result-line ${type}`;
const t = new Date().toTimeString().substring(0, 8);
div.textContent = `[${t}] ${msg}`;
resultPanel.appendChild(div);
resultPanel.scrollTop = resultPanel.scrollHeight;
}
function clearLog() { resultPanel.innerHTML = ''; }
// 定义三个模拟库的代码
const mathLibCode = `
var MathLib = {
fibonacci: function(n) {
if (n <= 1) return n;
var a = 0, b = 1;
for (var i = 2; i <= n; i++) { var t = a + b; a = b; b = t; }
return b;
},
isPrime: function(num) {
if (num < 2) return false;
for (var i = 2; i <= Math.sqrt(num); i++) { if (num % i === 0) return false; }
return true;
},
factorial: function(n) {
var r = 1;
for (var i = 2; i <= n; i++) r *= i;
return r;
},
gcd: function(a, b) { return b === 0 ? a : MathLib.gcd(b, a % b); },
lcm: function(a, b) { return Math.abs(a * b) / MathLib.gcd(a, b); }
};
`;
const stringLibCode = `
var StringLib = {
reverse: function(s) { return s.split('').reverse().join(''); },
toCamelCase: function(s) {
return s.toLowerCase().replace(/[-_](01-导读与概述至常见问题(FAQ))/g, function(_, c) { return c.toUpperCase(); });
},
wordCount: function(s) {
var words = s.trim().split(/\\s+/).filter(function(w){return w.length>0;});
var freq = {};
words.forEach(function(w){freq[w]=(freq[w]||0)+1;});
return { totalWords: words.length, frequency: freq };
},
toBase64: function(s) { return btoa(unescape(encodeURIComponent(s))); },
fromBase64: function(s) { return decodeURIComponent(escape(atob(s))); }
};
`;
const dataLibCode = `
var DataLib = {
unique: function(arr) {
var seen = {};
return arr.filter(function(item) {
var k = typeof item + ':' + item;
return seen[k] ? false : (seen[k] = true);
});
},
groupBy: function(arr, keyFn) {
var groups = {};
arr.forEach(function(item) {
var k = typeof keyFn === 'function' ? keyFn(item) : item[keyFn];
(groups[k] = groups[k] || []).push(item);
});
return groups;
},
stats: function(arr) {
if (arr.length === 0) return {};
var sum = arr.reduce(function(a,b){return a+b;},0);
var sorted = arr.slice().sort(function(a,b){return a-b;});
var mid = Math.floor(sorted.length / 2);
return {
count: arr.length,
sum: sum,
avg: sum / arr.length,
min: sorted[0],
max: sorted[sorted.length-1],
median: sorted.length % 2 !== 0 ? sorted[mid] : (sorted[mid-1]+sorted[mid])/2
};
},
chunk: function(arr, size) {
var chunks = [];
for (var i = 0; i < arr.length; i += size) chunks.push(arr.slice(i, i+size));
return chunks;
}
};
`;
function runTest() {
const func = document.getElementById('funcSelect').value;
const param = document.getElementById('funcParam').value.trim();
const runBtn = document.getElementById('runBtn');
clearLog();
runBtn.disabled = true;
log(`开始执行: ${func}(${param})`, 'info');
log(`正在通过 importScripts 加载依赖库...`, 'info');
// 更新库状态显示
document.getElementById('status-math').textContent = '加载中...';
document.getElementById('status-string').textContent = '加载中...';
document.getElementById('status-data').textContent = '加载中...';
// 构建完整的 Worker 代码(包含三个库 + 主逻辑)
const workerCode = `
// ====== 通过 importScripts 加载三个模拟库 ======
` +
`importScripts(` +
`'data:text/javascript;base64,` + btoa(unescape(encodeURIComponent(mathLibCode))) + `',` +
`'data:text/javascript;base64,` + btoa(unescape(encodeURIComponent(stringLibCode))) + `',` +
`'data:text/javascript;base64,` + btoa(unescape(encodeURIComponent(dataLibCode))) + `'` +
`);
// 库加载完成,通知主线程
self.postMessage({ type: 'loaded', libs: ['MathLib', 'StringLib', 'DataLib'] });
self.onmessage = function(e) {
var data = e.data;
var funcName = data.func;
var param = data.param;
var result;
try {
switch (funcName) {
case 'fibonacci':
result = MathLib.fibonacci(parseInt(param));
break;
case 'isPrime':
result = MathLib.isPrime(parseInt(param));
break;
case 'factorial':
result = MathLib.factorial(parseInt(param));
break;
case 'reverseString':
result = StringLib.reverse(param);
break;
case 'wordCount':
result = StringLib.wordCount(param);
break;
case 'toCamelCase':
result = StringLib.toCamelCase(param);
break;
case 'unique':
result = DataLib.unique(JSON.parse(param));
break;
case 'groupBy':
result = DataLib.groupBy(JSON.parse(param), data.key || 'type');
break;
case 'stats':
result = DataLib.stats(JSON.parse(param));
break;
default:
throw new Error('未知函数: ' + funcName);
}
self.postMessage({
type: 'result',
func: funcName,
param: param,
result: result,
resultType: typeof result
});
} catch(err) {
self.postMessage({ type: 'error', message: err.message, func: funcName });
}
};
self.onerror = function(e) {
self.postMessage({ type: 'error', message: e.message });
};
`;
try {
const blob = new Blob([workerCode], { type: 'application/javascript' });
const url = URL.createObjectURL(blob);
const worker = new Worker(url);
worker.onmessage = function(e) {
const d = e.data;
if (d.type === 'loaded') {
log(`✅ 库加载完成: ${d.libs.join(', ')}`, 'ok');
document.getElementById('status-math').className = 'lib-status loaded';
document.getElementById('status-math').textContent = '已加载 ✓';
document.getElementById('status-string').className = 'lib-status loaded';
document.getElementById('status-string').textContent = '已加载 ✓';
document.getElementById('status-data').className = 'lib-status loaded';
document.getElementById('status-data').textContent = '已加载 ✓';
// 发送任务
let parsedParam = param;
if (['unique', 'groupBy', 'stats'].includes(func)) {
try { parsedParam = param; } catch(e) {}
}
worker.postMessage({ func: func, param: param });
log(`发送任务: ${func}(${param})`, 'info');
} else if (d.type === 'result') {
const resultStr = typeof d.result === 'object'
? JSON.stringify(d.result, null, 2).substring(0, 500)
: d.result.toLocaleString ? d.result.toLocaleString() : d.result;
log(`━━━ 执行结果 ━━━`, 'highlight');
log(`函数: ${d.func}(${d.param})`, 'info');
log(`返回类型: ${d.resultType}`, 'info');
log(`结果: ${resultStr}`, 'ok');
log(`━━━━━━━━━━━━━━━`, 'highlight');
runBtn.disabled = false;
worker.terminate();
} else if (d.type === 'error') {
log(`❌ 错误 [${d.func}]: ${d.message}`, 'err');
runBtn.disabled = false;
worker.terminate();
}
};
worker.onerror = function(e) {
log(`❌ Worker 错误: ${e.message} (${e.filename}:${e.lineno})`, 'err');
runBtn.disabled = false;
e.preventDefault();
};
} catch (err) {
log(`❌ 创建 Worker 失败: ${err.message}`, 'err');
runBtn.disabled = false;
}
}
</script>
</body>
</html><!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>【10】Worker 消息传递性能对比</title>
<!--
来源: HTML5基础知识/15-WebWorkers处理线程.md
知识点: Structured Clone vs Transferable vs SharedArrayBuffer 性能对比
-->
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
padding: 20px; background: #121212; color: #e0e0e0;
min-height: 100vh;
}
.container { max-width: 1080px; margin: 0 auto; }
.header {
text-align: center; padding: 24px; background: linear-gradient(135deg, #e040fb, #7c4dff);
color: white; border-radius: 12px; margin-bottom: 24px;
}
.header h1 { font-size: 22px; margin-bottom: 6px; }
.header p { opacity: 0.9; font-size: 14px; }
.card {
background: #1e1e1e; border: 1px solid #333; border-radius: 10px;
padding: 24px; margin-bottom: 20px;
}
.card-title {
font-size: 15px; font-weight: 600; color: #e040fb;
border-left: 3px solid #e040fb; padding-left: 10px; margin-bottom: 16px;
}
.info-banner {
background: rgba(224,64,251,0.1); border: 1px solid rgba(224,64,251,0.25);
border-radius: 6px; padding: 12px 16px; font-size: 13px; line-height: 1.6;
color: #ce93d8; margin-bottom: 16px;
}
.method-cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-bottom: 20px; }
@media (max-width: 800px) { .method-cards { grid-template-columns: 1fr; } }
.method-card {
background: #121212; border: 2px solid #333; border-radius: 10px;
padding: 18px; text-align: center; transition: all 0.3s;
}
.method-card.active { border-color: #e040fb; box-shadow: 0 0 20px rgba(224,64,251,0.15); }
.method-icon { font-size: 28px; margin-bottom: 8px; }
.method-name { font-size: 14px; font-weight: 700; margin-bottom: 4px; }
.method-desc { font-size: 11px; color: #888; line-height: 1.5; margin-bottom: 12px; }
.method-result { font-family: monospace; font-size: 22px; font-weight: 700; color: #e040fb; }
.method-unit { font-size: 11px; color: #666; }
.method-bar-container {
height: 6px; background: #333; border-radius: 3px; margin-top: 10px; overflow: hidden;
}
.method-bar { height: 100%; border-radius: 3px; transition: width 0.5s ease; width: 0%; }
.controls { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 20px; align-items: center; }
label { font-size: 13px; color: #aaa; }
select, input[type="number"] {
padding: 8px 12px; background: #121212; border: 1px solid #444;
color: #fff; border-radius: 6px; font-size: 13px;
}
select:focus, input:focus { outline: none; border-color: #e040fb; }
.btn {
padding: 10px 22px; border: none; border-radius: 6px; cursor: pointer;
font-size: 13px; font-weight: 600; transition: all 0.2s;
}
.btn-primary { background: linear-gradient(135deg, #e040fb, #7c4dff); color: white; }
.btn-primary:hover { transform: translateY(-1px); box-shadow: 0 4px 16px rgba(224,64,251,0.3); }
.btn-primary:disabled { opacity: 0.4; cursor: not-allowed; transform: none !important; }
/* 对比表格 */
.compare-table { width: 100%; border-collapse: collapse; font-size: 13px; margin-top: 16px; }
.compare-table th, .compare-table td {
padding: 12px 14px; text-align: left; border-bottom: 1px solid #333;
}
.compare-table th { background: #1a1a2e; color: #aaa; font-weight: 600; }
.compare-table tr:hover td { background: #1a1a2e; }
.chart-container {
background: #121212; border-radius: 8px; padding: 20px; margin-top: 16px;
}
.bar-chart { display: flex; align-items: flex-end; gap: 30px; height: 220px; padding: 0 20px; }
.chart-col { display: flex; flex-direction: column; align-items: center; flex: 1; }
.chart-bar-wrapper {
width: 60px; height: 180px; background: #222; border-radius: 6px 6px 0 0;
display: flex; flex-direction: column-reverse; align-items: center;
position: relative; overflow: hidden;
}
.chart-bar {
width: 100%; border-radius: 4px 4px 0 0; transition: height 0.6s ease;
min-height: 2px;
}
.chart-bar.clone { background: linear-gradient(to top, #ef5350, #e53935); }
.chart-bar.transfer { background: linear-gradient(to top, #42a5f5, #1e88e5); }
.chart-bar.shared { background: linear-gradient(to top, #66bb6a, #43a047); }
.chart-value { font-size: 12px; font-weight: 700; color: #fff; margin-bottom: 4px; z-index: 1; }
.chart-label { font-size: 12px; color: #888; margin-top: 8px; font-weight: 600; }
.detail-log {
background: #0a0a0a; border-radius: 6px; padding: 14px;
font-family: monospace; font-size: 11px; max-height: 200px;
overflow-y: auto; color: #999; line-height: 1.7; margin-top: 16px;
}
.log-ok { color: #66bb6a; }
.log-info { color: #42a5f5; }
.log-warn { color: #ffa726; }
.log-err { color: #ef5350; }
.compat-note {
background: rgba(255,167,38,0.1); border: 1px solid rgba(255,167,38,0.25);
border-radius: 6px; padding: 10px 14px; font-size: 12px; color: #ffa726;
margin-top: 16px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📊 Worker 消息传递性能对比</h1>
<p>Structured Clone vs Transferable vs SharedArrayBuffer — 数据传输方式基准测试</p>
</div>
<div class="card">
<div class="card-title">🧪 基准测试</div>
<div class="info-banner">
💡 <strong>三种数据传递方式:</strong><br>
• <strong>Structured Clone</strong> — 默认方式,数据被复制(大对象开销大)<br>
• <strong>Transferable Objects</strong> — 所有权转移,零拷贝(原变量不可再用)<br>
• <strong>SharedArrayBuffer</strong> — 共享内存,零拷贝且双方可读写(需要特殊头)
</div>
<div class="controls">
<label>数据大小 (KB):</label>
<select id="dataSize">
<option value="1">1 KB</option>
<option value="10">10 KB</option>
<option value="100" selected>100 KB</option>
<option value="1024">1 MB</option>
<option value="10240">10 MB</option>
</select>
<label>往返次数:</label>
<input type="number" id="roundTrips" value="100" min="10" max="1000" step="10" />
<button class="btn btn-primary" id="benchBtn" onclick="runBenchmark()">🚀 运行基准测试</button>
</div>
<div class="method-cards">
<div class="method-card" id="cardClone">
<div class="method-icon">📋</div>
<div class="method-name">Structured Clone</div>
<div class="method-desc">默认方式,完整复制数据。安全但大对象慢。</div>
<div class="method-result" id="resClone">--</div>
<div class="method-unit">ms (总耗时)</div>
<div class="method-bar-container"><div class="method-bar" id="barClone" style="background:#ef5350;"></div></div>
</div>
<div class="method-card" id="cardTransfer">
<div class="method-icon">🚀</div>
<div class="method-name">Transferable</div>
<div class="method-desc">所有权转移,零拷贝。最快但原数据失效。</div>
<div class="method-result" id="resTransfer">--</div>
<div class="method-unit">ms (总耗时)</div>
<div class="method-bar-container"><div class="method-bar" id="barTransfer" style="background:#42a5f5;"></div></div>
</div>
<div class="method-card" id="cardShared">
<div class="method-icon">🔗</div>
<div class="method-name">SharedArrayBuffer</div>
<div class="method-desc">共享内存区域,零拷贝可读写。需 COOP/COEP 头。</div>
<div class="method-result" id="resShared">--</div>
<div class="method-unit">ms (总耗时)</div>
<div class="method-bar-container"><div class="method-bar" id="barShared" style="background:#66bb6a;"></div></div>
</div>
</div>
<!-- 柱状图 -->
<div class="chart-container">
<div class="bar-chart" id="barChart">
<div class="chart-col">
<div class="chart-bar-wrapper">
<div class="chart-bar clone" id="chartBarClone" style="height:0%"></div>
<div class="chart-value" id="chartValClone">--</div>
</div>
<div class="chart-label">Clone</div>
</div>
<div class="chart-col">
<div class="chart-bar-wrapper">
<div class="chart-bar transfer" id="chartBarTransfer" style="height:0%"></div>
<div class="chart-value" id="chartValTransfer">--</div>
</div>
<div class="chart-label">Transferable</div>
</div>
<div class="chart-col">
<div class="chart-bar-wrapper">
<div class="chart-bar shared" id="chartBarShared" style="height:0%"></div>
<div class="chart-value" id="chartValShared">--</div>
</div>
<div class="chart-label">SharedAB</div>
</div>
</div>
</div>
<div class="detail-log" id="detailLog">
<div class="log-info">[系统] 点击 "运行基准测试" 开始性能对比...</div>
</div>
</div>
<!-- 对比表格 -->
<div class="card">
<div class="card-title">📖 详细对比</div>
<table class="compare-table">
<thead>
<tr>
<th>特性</th>
<th>Structured Clone</th>
<th>Transferable</th>
<th>SharedArrayBuffer</th>
</tr>
</thead>
<tbody>
<tr>
<td>拷贝开销</td>
<td>完整深拷贝(高开销)</td>
<td>零拷贝(转移所有权)</td>
<td>零拷贝(共享内存)</td>
</tr>
<tr>
<td>原数据可用性</td>
<td>✅ 可用(副本独立)</td>
<td>❌ 不可用(已被转移)</td>
<td>✅ 双方均可访问</td>
</tr>
<tr>
<td>数据大小限制</td>
<td>受内存限制</td>
<td>受内存限制</td>
<td>受内存限制</td>
</tr>
<tr>
<td>支持的类型</td>
<td>大部分 JS 对象</td>
<td>ArrayBuffer, MessagePort, ImageBitmap, OffscreenCanvas</td>
<td>仅 ArrayBuffer</td>
</tr>
<tr>
<td>安全性</td>
<td>✅ 高(隔离)</td>
<td>✅ 高(唯一所有权)</td>
<td>⚠️ 需防止竞态条件</td>
</tr>
<tr>
<td>特殊要求</td>
<td>无</td>
<td>无</td>
<td>COOP/COEP 安全头</td>
</tr>
<tr>
<td>典型场景</td>
<td>普通消息传递</td>
<td>大数据量传输</td>
<td>高频实时数据共享</td>
</tr>
</tbody>
</table>
<div class="compat-note">
⚠️ <strong>SharedArrayBuffer 兼容性:</strong>由于 Spectre 漏洞安全问题,现代浏览器要求页面设置
<code>Cross-Origin-Opener-Policy: same-origin</code> 和 <code>Cross-Origin-Embedder-Policy: require-corp</code>
安全头才能启用 SharedArrayBuffer。本地 file:// 协议通常不可用。
</div>
</div>
</div>
<script>
const detailLog = document.getElementById('detailLog');
function log(msg, cls = '') {
const div = document.createElement('div');
div.className = cls || '';
const t = new Date().toTimeString().substring(0, 8);
div.textContent = `[${t}] ${msg}`;
detailLog.appendChild(div);
detailLog.scrollTop = detailLog.scrollHeight;
}
async function runBenchmark() {
const btn = document.getElementById('benchBtn');
btn.disabled = true;
const sizeKB = parseInt(document.getElementById('dataSize').value);
const rounds = parseInt(document.getElementById('roundTrips').value) || 100;
const sizeBytes = sizeKB * 1024;
detailLog.innerHTML = '';
log(`=== 基准测试开始 ===`);
log(`数据大小: ${sizeKB} KB (${sizeBytes} bytes) | 往返次数: ${rounds}`);
// 重置卡片
['Clone', 'Transfer', 'Shared'].forEach(m => {
document.getElementById(`res${m}`).textContent = '--';
document.getElementById(`bar${m}`).style.width = '0%';
document.getElementById(`chartBar${m}`).style.height = '0%';
document.getElementById(`chartVal${m}`).textContent = '--';
document.getElementById(`card${m.replace('Transfer','Transfer').replace('Shared','Shared')}`).classList.remove('active');
});
// 创建测试数据
const testData = new Uint8Array(sizeBytes);
for (let i = 0; i < sizeBytes; i++) testData[i] = i & 0xff;
const results = {};
// ====== 测试 1: Structured Clone ======
log('', '');
log('--- 测试 1: Structured Clone ---', 'info');
document.getElementById('cardClone').classList.add('active');
results.clone = await benchmarkMethod('clone', testData, sizeBytes, rounds, () => {
// 每次 copy 数据
const arr = new Uint8Array(testData);
return { buffer: arr.buffer, method: 'clone' };
}, (w, data) => {
w.postMessage(data);
});
// ====== 测试 2: Transferable ======
log('', '');
log('--- 测试 2: Transferable ---', 'info');
document.getElementById('cardTransfer').classList.add('active');
results.transfer = await benchmarkMethod('transfer', testData, sizeBytes, rounds, () => {
const ab = new ArrayBuffer(sizeBytes);
new Uint8Array(ab).set(testData);
return { buffer: ab };
}, (w, data) => {
w.postMessage(data, [data.buffer]); // Transfer!
});
// ====== 测试 3: SharedArrayBuffer ======
log('', '');
log('--- 测试 3: SharedArrayBuffer ---', 'info');
document.getElementById('cardShared').classList.add('active');
if (typeof SharedArrayBuffer !== 'undefined') {
results.shared = await benchmarkMethod('shared', testData, sizeBytes, rounds, () => {
try {
const sab = new SharedArrayBuffer(sizeBytes);
new Uint8Array(sab).set(testData);
return { buffer: sab };
} catch(e) {
return null; // 可能因为 COOP/COEP 不可用
}
}, (w, data) => {
if (!data || !data.buffer) throw new Error('SAB unavailable');
w.postMessage(data); // SAB 自动共享,不需要 transfer list
});
} else {
log('⚠️ SharedArrayBuffer 不可用(可能缺少 COOP/COEP 安全头)', 'warn');
results.shared = null;
}
// ====== 显示结果 ======
displayResults(results);
log('', '');
log(`=== 测试完成 ===`, 'log-ok');
btn.disabled = false;
}
function benchmarkMethod(name, originalData, sizeBytes, rounds, prepareFn, sendFn) {
return new Promise((resolve) => {
const code = `
self.onmessage = function(e) {
if (e.data.method === 'shared') {
// SharedArrayBuffer: 直接读取并返回确认
self.postMessage({ received: true, firstByte: new Uint8Array(e.data.buffer)[0] });
} else {
// Clone/Transfer: 返回数据信息
self.postMessage({
received: true,
byteLength: e.data.buffer.byteLength,
firstByte: new Uint8Array(e.data.buffer)[0]
});
}
};
`;
const blob = new Blob([code], { type: 'application/javascript' });
const worker = new Worker(URL.createObjectURL(blob));
let completed = 0;
const startTime = performance.now();
worker.onmessage = function() {
completed++;
if (completed >= rounds) {
const elapsed = performance.now() - startTime;
worker.terminate();
resolve(elapsed);
} else {
try {
const data = prepareFn();
if (!data) throw new Error('prepare failed');
sendFn(worker, data);
} catch(err) {
// 出错时提前结束
const elapsed = performance.now() - startTime;
worker.terminate();
resolve(elapsed);
}
}
};
worker.onerror = function(e) {
log(`[${name}] Worker 错误: ${e.message}`, 'log-err');
resolve(performance.now() - startTime);
e.preventDefault();
};
// 发送第一个任务
try {
const data = prepareFn();
if (!data) throw new Error('prepare failed');
sendFn(worker, data);
} catch(err) {
log(`[${name}] 准备失败: ${err.message},跳过`, 'log-warn');
resolve(null);
}
});
}
function displayResults(results) {
const times = [
{ key: 'clone', val: results.clone },
{ key: 'transfer', val: results.transfer },
{ key: 'shared', val: results.shared }
];
const validTimes = times.filter(t => t.val !== null && t.val !== undefined);
if (validTimes.length === 0) return;
const maxTime = Math.max(...validTimes.map(t => t.val));
times.forEach(t => {
const el = t.key.charAt(0).toUpperCase() + t.key.slice(1);
const capKey = el === 'Clone' ? 'Clone' : el === 'Transfer' ? 'Transfer' : 'Shared';
if (t.val === null || t.val === undefined) {
document.getElementById(`res${capKey}`).textContent = 'N/A';
log(`[${capKey}] 不可用`, 'log-warn');
return;
}
const timeMs = t.val.toFixed(1);
const pct = maxTime > 0 ? (t.val / maxTime * 100) : 0;
document.getElementById(`res${capKey}`).textContent = `${timeMs}`;
document.getElementById(`bar${capKey}`).style.width = `${pct}%`;
document.getElementById(`chartBar${capKey}`).style.height = `${Math.max(pct, 2)}%`;
document.getElementById(`chartVal${capKey}`).textContent = `${timeMs}`;
log(`[${capKey}] 总耗时: ${timeMs} ms | 平均: ${(t.val / (parseInt(document.getElementById('roundTrips').value)||100)).toFixed(2)} ms/次`,
'log-ok');
});
// 计算加速比
if (results.clone && results.transfer) {
const speedup = (results.clone / results.transfer).toFixed(1);
log(``, '');
log(`🚀 Transferable 比 Structured Clone 快 ${speedup}x`, 'log-info');
}
if (results.clone && results.shared) {
const speedup2 = (results.clone / results.shared).toFixed(1);
log(`🔗 SharedArrayBuffer 比 Structured Clone 快 ${speedup2}x`, 'log-info');
}
}
</script>
</body>
</html>