如何做贴吧类网站多钱,传奇类网页游戏,注册公司需要哪些资料,wordpress 繁体语言包尊敬的各位技术同仁#xff0c;大家好#xff01;在现代复杂的前端应用开发中#xff0c;我们经常面临一个挑战#xff1a;如何在用户同时打开的多个浏览器 Tab 页之间#xff0c;保持数据的强一致性。想象一下#xff0c;一个用户在一个 Tab 页修改了某个设置#xff0…尊敬的各位技术同仁大家好在现代复杂的前端应用开发中我们经常面临一个挑战如何在用户同时打开的多个浏览器 Tab 页之间保持数据的强一致性。想象一下一个用户在一个 Tab 页修改了某个设置而另一个 Tab 页却依然显示着旧的数据或者多个 Tab 页同时尝试更新同一个资源导致数据冲突或丢失。这些场景轻则影响用户体验重则引发严重的业务逻辑错误。今天我们将深入探讨如何利用 Web 平台提供的两大强大工具——SharedWorker和Lock API——来构建一个跨 Tab 页的强一致性通信机制从而有效解决这些并发与同步问题。我们将从问题的根源出发逐步剖析这两种技术的原理最终通过具体的代码示例展示如何将它们巧妙结合实现我们所需的高可靠性系统。跨 Tab 页通信的挑战与强一致性需求浏览器天然的设计哲学是隔离。每个 Tab 页通常运行在独立的进程或线程中拥有独立的 JavaScript 运行时、DOM 树和内存空间。这种隔离性保障了安全性与稳定性但也为跨 Tab 页的数据共享与同步带来了挑战。传统跨 Tab 页通信手段及其局限在深入探讨解决方案之前我们先回顾一下常见的跨 Tab 页通信手段并分析它们在实现“强一致性”方面的不足localStorage/sessionStorage:优点:简单易用数据持久化localStorage跨 Tab 页共享。缺点:非原子性:对localStorage的写入操作不是原子的。如果两个 Tab 页几乎同时读取、修改、写入同一个键值很容易发生竞态条件导致后写入的数据覆盖前写入的数据或者基于旧数据进行的计算结果被覆盖。无通知机制:localStorage的storage事件只能通知到非当前写入的 Tab 页无法通知到当前写入的 Tab 页。这使得同步逻辑变得复杂。容量限制:通常为 5-10MB。强一致性挑战:缺乏原生的锁定机制无法保证对共享数据的并发访问是安全的。BroadcastChannelAPI:优点:专门为跨 Tab 页同源广播消息设计API 简洁。缺点:纯消息广播:BroadcastChannel只是一个消息通道它本身不提供任何状态管理或同步机制。它只能通知其他 Tab 页“某个事件发生了”或“某个数据可能已更新”但不能保证这些操作的原子性或顺序性。无原生锁定:同样缺乏对共享资源的锁定能力。如果多个 Tab 页都监听并尝试响应同一消息仍然可能出现竞态条件。强一致性挑战:适用于事件通知或非关键数据的同步但无法独立保证对共享状态的原子性更新。window.postMessage(配合window.opener或iframe):优点:允许跨窗口/框架通信。缺点:限定通信目标:只能与opener窗口或iframe中的内容通信不适用于任意 Tab 页之间的广播。复杂性:需要维护窗口引用处理消息来源。强一致性挑战:无法提供全局的协调和锁定机制。IndexedDB:优点:客户端结构化存储容量大支持事务。缺点:事务粒度:IndexedDB事务仅在其自身范围内提供原子性。跨 Tab 页的多个IndexedDB事务如果操作同一数据仍然可能需要额外的协调。复杂性:API 相对复杂直接用于通信不如专门的通信 API 方便。强一致性挑战:虽然其事务机制有助于数据完整性但要实现跨 Tab 页的逻辑操作的强一致性仍需额外的同步原语。例如两个 Tab 页各自在一个事务中读取、修改、写入同一个计数器没有外部协调仍可能导致错误。综上所述传统的通信手段在实现“强一致性”时力不从心主要症结在于缺乏一个统一的协调中心和原子的锁定机制。而这正是SharedWorker和Lock API能够大放异彩的地方。SharedWorker跨 Tab 页的中央协调者SharedWorker是 Web Worker 的一种特殊形式它可以在同源的所有浏览器 Tab 页、窗口、iframe甚至其他SharedWorker之间共享。与普通的WebWorker也称为DedicatedWorker不同DedicatedWorker每次加载页面都会创建一个新的实例而SharedWorker在同一源下只会被实例化一次所有连接到它的上下文比如多个 Tab 页都会共享这同一个实例。SharedWorker 的核心特性单例模式:同一源下的所有页面共享同一个SharedWorker实例。这使得它天然成为一个理想的中央协调者可以管理共享状态、处理并发请求并广播结果。独立线程:SharedWorker运行在独立的线程中不会阻塞主线程保持页面响应性。端口通信:主线程或任何其他上下文通过MessagePort对象与SharedWorker进行通信。每个连接到SharedWorker的上下文都会获得一个独立的MessagePort。持久性:只要有至少一个 Tab 页或窗口连接着SharedWorker它就会一直运行。当所有连接都关闭后SharedWorker也会被终止。SharedWorker 如何解决一致性问题作为中央协调者SharedWorker可以统一管理共享状态:所有跨 Tab 页共享的数据都存储在SharedWorker内部。序列化操作:所有对共享状态的修改请求都发送到SharedWorker。由于SharedWorker是单线程的它会按照接收到的顺序或内部调度策略依次处理这些请求从而避免内部的竞态条件。广播更新:当共享状态发生变化时SharedWorker可以通过其维护的MessagePort列表将最新的状态广播给所有连接的 Tab 页确保所有 Tab 页都及时获取到一致的数据。然而SharedWorker自身并不能解决所有并发问题。例如如果多个 Tab 页同时向SharedWorker发送“请求修改”消息SharedWorker会按顺序处理它们这解决了SharedWorker内部的竞态。但如果在 Tab 页发送请求之前需要进行一些基于当前共享状态的预判断或计算而这个判断或计算的结果又可能被其他 Tab 页在发送消息的间隙所改变那么仍然存在“读-改-写”的竞态条件。例如Tab A 和 Tab B 都想将一个计数器从 10 增加到 11。Tab A 读取到计数器是 10。Tab B 读取到计数器是 10。Tab A 计算出新值 11发送给SharedWorker。Tab B 计算出新值 11发送给SharedWorker。SharedWorker收到 Tab A 的请求将计数器设置为 11。SharedWorker收到 Tab B 的请求将计数器设置为 11。最终结果是 11而不是预期的 12。这就是我们需要Lock API来协调客户端行为的原因。SharedWorker 基本代码示例1.shared-worker.js(SharedWorker 脚本)// 定义一个用于存储所有连接端口的数组 const ports []; // 示例共享状态 let sharedCounter 0; let sharedMessage Hello from SharedWorker!; /** * 广播最新状态给所有连接的客户端 */ function broadcastState() { const state { counter: sharedCounter, message: sharedMessage }; ports.forEach(port { port.postMessage({ type: STATE_UPDATE, payload: state }); }); console.log([SharedWorker] State broadcasted:, state); } // 当新的连接被建立时触发 self.onconnect (event) { const port event.ports[0]; // 获取连接端口 ports.push(port); // 将端口添加到列表中 console.log([SharedWorker] New client connected. Total connections: ${ports.length}); // 向新连接的客户端发送当前状态 port.postMessage({ type: INITIAL_STATE, payload: { counter: sharedCounter, message: sharedMessage } }); // 监听来自客户端的消息 port.onmessage (msgEvent) { const message msgEvent.data; console.log([SharedWorker] Received message from client:, message); switch (message.type) { case INCREMENT_COUNTER: sharedCounter; console.log([SharedWorker] Counter incremented to: ${sharedCounter}); broadcastState(); // 状态更新后广播 // 可以选择向发送方回复确认消息 port.postMessage({ type: INCREMENT_ACK, success: true, newCounter: sharedCounter }); break; case SET_MESSAGE: if (message.payload typeof message.payload string) { sharedMessage message.payload; console.log([SharedWorker] Message updated to: ${sharedMessage}); broadcastState(); // 状态更新后广播 port.postMessage({ type: SET_MESSAGE_ACK, success: true, newMessage: sharedMessage }); } else { port.postMessage({ type: SET_MESSAGE_ACK, success: false, error: Invalid message payload }); } break; case GET_CURRENT_STATE: port.postMessage({ type: CURRENT_STATE, payload: { counter: sharedCounter, message: sharedMessage } }); break; default: console.warn([SharedWorker] Unknown message type: ${message.type}); } }; // 监听端口断开事件例如 Tab 页关闭 port.onmessageerror (error) { console.error([SharedWorker] Message error on port:, error); // 通常不需要手动移除因为 onclose 会处理 }; // 当端口关闭时触发例如 Tab 页关闭 // 注意onclose 事件在某些浏览器中可能不会立即触发或行为不一致 // 更可靠的断开连接检测通常依赖于主线程的错误处理或心跳机制 // 不过对于 SharedWorker当所有连接都断开时worker 实例会被终止 // 因此这里无需显式移除 port因为它会自动清理 // 实际应用中如果需要精确管理连接数可以考虑更复杂的生命周期管理 // 例如port.onclose () { const index ports.indexOf(port); if (index -1) ports.splice(index, 1); console.log([SharedWorker] Client disconnected. Total connections: ${ports.length}); }; }; console.log([SharedWorker] Script loaded.);2.index.html(客户端页面)!DOCTYPE html html langen head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 titleSharedWorker Client/title style body { font-family: sans-serif; margin: 20px; } .container { border: 1px solid #ccc; padding: 15px; margin-bottom: 15px; border-radius: 5px; } button { padding: 8px 15px; margin-right: 10px; cursor: pointer; } input[typetext] { padding: 8px; width: 200px; margin-right: 10px; } #status { margin-top: 15px; padding: 10px; background-color: #f0f0f0; border-radius: 3px; } /style /head body h1SharedWorker 客户端示例/h1 p打开多个 Tab 页观察计数器和消息的同步。/p div classcontainer h2共享计数器/h2 p当前计数器值: span idcounterValue0/span/p button idincrementButton增加计数器 (非强一致性)/button /div div classcontainer h2共享消息/h2 p当前共享消息: span idmessageValueHello from SharedWorker!/span/p input typetext idmessageInput placeholder输入新消息 button idsetMessageButton设置消息 (非强一致性)/button /div div idstatus h3事件日志:/h3 ul ideventLog/ul /div script const counterValueSpan document.getElementById(counterValue); const messageValueSpan document.getElementById(messageValue); const incrementButton document.getElementById(incrementButton); const messageInput document.getElementById(messageInput); const setMessageButton document.getElementById(setMessageButton); const eventLog document.getElementById(eventLog); let sharedWorker; let workerPort; function logEvent(message, type info) { const li document.createElement(li); li.textContent [${new Date().toLocaleTimeString()}] ${message}; if (type error) li.style.color red; eventLog.prepend(li); if (eventLog.children.length 20) { eventLog.removeChild(eventLog.lastChild); } } if (window.SharedWorker) { // 尝试连接到 SharedWorker // URL 必须是同源的 sharedWorker new SharedWorker(shared-worker.js); workerPort sharedWorker.port; // 获取端口对象 logEvent(尝试连接到 SharedWorker...); // 监听来自 SharedWorker 的消息 workerPort.onmessage (event) { const message event.data; logEvent(收到 SharedWorker 消息: ${JSON.stringify(message)}); switch (message.type) { case INITIAL_STATE: case STATE_UPDATE: case CURRENT_STATE: counterValueSpan.textContent message.payload.counter; messageValueSpan.textContent message.payload.message; logEvent(更新页面状态: 计数器${message.payload.counter}, 消息${message.payload.message}); break; case INCREMENT_ACK: if (message.success) { logEvent(计数器增加成功新值: ${message.newCounter}); } else { logEvent(计数器增加失败: ${message.error}, error); } break; case SET_MESSAGE_ACK: if (message.success) { logEvent(消息设置成功新消息: ${message.newMessage}); } else { logEvent(消息设置失败: ${message.error}, error); } break; default: logEvent(未知消息类型: ${message.type}); } }; workerPort.onerror (error) { logEvent(SharedWorker 错误: ${error.message}, error); console.error(SharedWorker error:, error); }; // 启动端口通信 workerPort.start(); logEvent(SharedWorker 端口已启动。); } else { logEvent(您的浏览器不支持 SharedWorker。, error); alert(您的浏览器不支持 SharedWorker。); } incrementButton.addEventListener(click, () { if (workerPort) { logEvent(发送 INCREMENT_COUNTER 请求...); workerPort.postMessage({ type: INCREMENT_COUNTER }); } }); setMessageButton.addEventListener(click, () { if (workerPort) { const newMessage messageInput.value.trim(); if (newMessage) { logEvent(发送 SET_MESSAGE 请求: ${newMessage}...); workerPort.postMessage({ type: SET_MESSAGE, payload: newMessage }); } else { logEvent(请填写要设置的消息。, warn); } } }); // 首次加载时请求当前状态以防 SharedWorker 已经运行 window.addEventListener(load, () { if (workerPort) { workerPort.postMessage({ type: GET_CURRENT_STATE }); } }); /script /body /html在上述示例中我们创建了一个SharedWorker来管理sharedCounter和sharedMessage。所有连接的 Tab 页都可以发送请求来修改这些状态SharedWorker会处理这些请求并广播最新的状态给所有 Tab 页。然而正如前面提到的如果 Tab 页在发送INCREMENT_COUNTER之前需要基于sharedCounter的值进行一些复杂判断并且这个判断和发送消息之间存在时间差那么仍然可能出现问题。这就是Lock API发挥作用的地方。Web Locks API (Lock API)浏览器原生的原子锁Web Locks API通常简称为Lock API提供了一种在同源内所有上下文包括 Tab 页、窗口、iframe和 Web Worker之间协调对共享资源访问的机制。它允许开发者请求一个带名称的锁并保证在锁被持有期间没有其他上下文可以获取到同名的独占锁。Lock API 的核心特性原子性保证:Lock API是浏览器原生的它确保锁的获取是原子的。一旦一个上下文成功获取到独占锁其他上下文就无法获取同名独占锁直到锁被释放。命名锁:锁通过字符串名称进行标识。只要名称相同不同上下文之间就能竞争同一个锁。作用域:锁的作用域限定在同一个源 (origin) 内。两种模式:exclusive(独占模式):这是默认模式。一旦一个上下文获得了独占锁其他上下文就不能获得同名的任何锁无论是独占还是共享直到该独占锁被释放。适用于写操作。shared(共享模式):多个上下文可以同时获取同名的共享锁。但如果有一个上下文尝试获取同名的独占锁它必须等待所有共享锁都被释放。适用于读操作。request()方法:navigator.locks.request(name, [options,] callback)是核心方法。它返回一个 Promise该 Promise 在锁被成功获取后解决并在callback函数执行完毕或 Promise 解决/拒绝后自动释放锁。自动释放:最强大的特性之一是当获取锁的上下文Tab 页或 Worker关闭或发生导航时浏览器会自动释放该上下文持有的所有锁。这大大降低了死锁的风险。Lock API 如何解决一致性问题Lock API提供了我们梦寐以求的“互斥访问”能力。通过在关键代码块即所谓的“临界区”前后请求和释放锁我们可以确保在任何给定时间只有一个上下文能够执行该临界区的代码。结合SharedWorkerLock API的作用是协调多个客户端对SharedWorker内部状态修改操作的请求。即在客户端发送可能导致竞态的消息之前先通过Lock API获得一个独占锁。场景示例:Tab A想要执行一个需要强一致性的操作例如基于当前计数器的值进行复杂计算后更新计数器。Tab A调用navigator.locks.request(my_resource_lock, ...)。如果锁可用Tab A成功获取锁然后执行其临界区代码向SharedWorker发送消息请求获取当前状态如果需要。基于获取到的状态进行计算。向SharedWorker发送消息请求更新状态。等待SharedWorker的确认或状态更新广播。在Tab A持有锁期间如果Tab B也尝试获取my_resource_lock它将必须等待。Tab A的操作完成后request回调函数执行完毕或其内部 Promise 解决锁会自动释放。Tab B随后可以获取锁并执行其操作。通过这种方式即使SharedWorker内部是单线程处理请求外部客户端在发起请求之前也通过Lock API进行了协调从而保证了整个“读-改-写”或“操作-更新”流程的原子性和强一致性。Lock API 基本代码示例async function doSomethingWithLock(resourceName, operation) { console.log([Tab] 尝试获取独占锁 ${resourceName}...); try { await navigator.locks.request(resourceName, { mode: exclusive, ifAvailable: false, steal: false, signal: AbortSignal.timeout(5000) }, async (lock) { // lock 对象本身没有太多直接用途它的存在表示你持有了锁 if (lock) { console.log([Tab] 成功获取独占锁 ${resourceName}。); try { // 执行临界区操作 await operation(); } catch (opError) { console.error([Tab] 临界区操作失败:, opError); throw opError; // 重新抛出让外层捕获 } finally { console.log([Tab] 独占锁 ${resourceName} 释放中...); // 锁会在 callback 结束时自动释放无需手动调用 release } } else { // ifAvailable: true 时可能发生但我们设置为 false console.warn([Tab] 未能获取独占锁 ${resourceName} (意外情况因为 ifAvailablefalse)); } }); console.log([Tab] 独占锁 ${resourceName} 已释放。); } catch (error) { if (error.name AbortError) { console.warn([Tab] 获取锁 ${resourceName} 超时或被中止。); } else { console.error([Tab] 获取锁或执行操作时发生错误:, error); } throw error; // 重新抛出错误以便调用者处理 } } // 示例使用 async function exampleUsage() { await doSomethingWithLock(my_shared_resource, async () { // 模拟一个耗时且需要独占访问的操作 console.log([Tab] 独占操作开始...); await new Promise(resolve setTimeout(resolve, 2000)); console.log([Tab] 独占操作完成。); }); } // exampleUsage(); // 调用此函数来测试navigator.locks.request参数说明:参数名类型描述namestring锁的名称。这是识别和竞争锁的关键。optionsobject可选对象用于配置锁的行为。modestring锁的模式。exclusive(默认) 或shared。ifAvailableboolean如果设置为true则在锁不可用时Promise 会立即以undefined解决而不是等待。默认false。stealboolean如果设置为true且锁不可用则会尝试“窃取”当前持有的锁。只有当当前锁持有者是一个非活动的 Tab 页或 Worker 时窃取才可能成功。使用时需谨慎可能导致数据不一致。默认false。signalAbortSignal允许你通过一个AbortController实例来取消锁的获取请求。如果signal被触发请求锁的 Promise 将会以AbortError拒绝。常用于设置超时。callbackFunction一个异步回调函数当锁被成功获取后执行。它接收一个lock对象作为参数通常不需要直接使用。该函数返回的 Promise 解决后锁会自动释放。基于 SharedWorker 与 Lock API 的锁竞争实现强一致性通信现在我们有了SharedWorker作为统一的状态管理者和消息分发中心以及Lock API作为客户端协调并发请求的原子锁。是时候将它们结合起来构建一个真正的强一致性通信方案了。核心思想与工作流程SharedWorker 作为唯一数据源:所有的共享状态都只在SharedWorker内部维护。客户端通过 Lock API 协调请求:当任何客户端Tab 页需要执行一个涉及共享状态修改的“临界操作”时它必须首先请求一个独占锁。锁内操作与 SharedWorker 交互:只有在成功获取锁之后客户端才能向SharedWorker发送修改请求。这个请求可能包含读取当前状态、基于当前状态计算新值、然后提交新值的整个流程。SharedWorker 处理并广播:SharedWorker接收到请求后更新其内部状态然后将最新的状态广播给所有连接的客户端。客户端释放锁:客户端在收到SharedWorker的确认或状态更新广播后或者在临界操作完成后锁会自动释放。通过这种模式我们确保了在任何时刻只有一个客户端能够发起对共享状态的原子性修改请求。SharedWorker作为单线程实体其内部状态的修改总是顺序执行的。所有客户端都能通过SharedWorker实时获取到最新的、一致的共享状态。代码示例强一致性计数器与消息管理我们将修改之前的客户端代码为计数器增加一个强一致性的操作。1.shared-worker.js(保持不变它只负责处理收到的请求和广播状态)// shared-worker.js 与之前相同 const ports []; let sharedCounter 0; let sharedMessage Hello from SharedWorker!; function broadcastState() { const state { counter: sharedCounter, message: sharedMessage }; ports.forEach(port { port.postMessage({ type: STATE_UPDATE, payload: state }); }); console.log([SharedWorker] State broadcasted:, state); } self.onconnect (event) { const port event.ports[0]; ports.push(port); console.log([SharedWorker] New client connected. Total connections: ${ports.length}); port.postMessage({ type: INITIAL_STATE, payload: { counter: sharedCounter, message: sharedMessage } }); port.onmessage async (msgEvent) { // 注意这里改为 async以便等待处理 const message msgEvent.data; console.log([SharedWorker] Received message from client:, message); switch (message.type) { case INCREMENT_COUNTER: // SharedWorker 内部是单线程的所以这里不需要 Lock API // 收到请求就直接处理确保内部状态更新的原子性 sharedCounter; console.log([SharedWorker] Counter incremented to: ${sharedCounter}); broadcastState(); port.postMessage({ type: INCREMENT_ACK, success: true, newCounter: sharedCounter }); break; case INCREMENT_COUNTER_COMPLEX: // 对于更复杂的更新SharedWorker 也可以提供一个确认机制 // 这里模拟一个带条件的复杂递增 if (sharedCounter message.payload.maxLimit) { sharedCounter message.payload.value; console.log([SharedWorker] Complex counter incremented by ${message.payload.value} to: ${sharedCounter}); broadcastState(); port.postMessage({ type: INCREMENT_COMPLEX_ACK, success: true, newCounter: sharedCounter }); } else { console.warn([SharedWorker] Complex increment failed: max limit reached (${message.payload.maxLimit})); port.postMessage({ type: INCREMENT_COMPLEX_ACK, success: false, error: Max limit reached, currentCounter: sharedCounter }); } break; case SET_MESSAGE: if (message.payload typeof message.payload string) { sharedMessage message.payload; console.log([SharedWorker] Message updated to: ${sharedMessage}); broadcastState(); port.postMessage({ type: SET_MESSAGE_ACK, success: true, newMessage: sharedMessage }); } else { port.postMessage({ type: SET_MESSAGE_ACK, success: false, error: Invalid message payload }); } break; case GET_CURRENT_STATE: port.postMessage({ type: CURRENT_STATE, payload: { counter: sharedCounter, message: sharedMessage } }); break; default: console.warn([SharedWorker] Unknown message type: ${message.type}); } }; }; console.log([SharedWorker] Script loaded.);2.index.html(客户端页面增加强一致性操作按钮)!DOCTYPE html html langen head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 titleSharedWorker Lock API Client/title style body { font-family: sans-serif; margin: 20px; } .container { border: 1px solid #ccc; padding: 15px; margin-bottom: 15px; border-radius: 5px; } button { padding: 8px 15px; margin-right: 10px; cursor: pointer; } input[typetext] { padding: 8px; width: 200px; margin-right: 10px; } input[typenumber] { padding: 8px; width: 80px; margin-right: 10px; } #status { margin-top: 15px; padding: 10px; background-color: #f0f0f0; border-radius: 3px; } .error { color: red; } /style /head body h1SharedWorker Lock API 客户端示例/h1 p打开多个 Tab 页通过锁定机制协调对共享数据的更新。/p div classcontainer h2共享计数器/h2 p当前计数器值: span idcounterValue0/span/p button idincrementButton增加计数器 (非强一致性)/button hr h3强一致性递增 (使用 Lock API)/h3 p递增值: input typenumber idincrementAmount value1 min1/p p最大限制: input typenumber idmaxLimit value10 min1/p button idincrementStronglyButton强一致性递增/button span idstrongIncrementStatus stylemargin-left: 10px;/span /div div classcontainer h2共享消息/h2 p当前共享消息: span idmessageValueHello from SharedWorker!/span/p input typetext idmessageInput placeholder输入新消息 button idsetMessageButton设置消息 (非强一致性)/button /div div idstatus h3事件日志:/h3 ul ideventLog/ul /div script const counterValueSpan document.getElementById(counterValue); const messageValueSpan document.getElementById(messageValue); const incrementButton document.getElementById(incrementButton); const incrementAmountInput document.getElementById(incrementAmount); const maxLimitInput document.getElementById(maxLimit); const incrementStronglyButton document.getElementById(incrementStronglyButton); const strongIncrementStatus document.getElementById(strongIncrementStatus); const messageInput document.getElementById(messageInput); const setMessageButton document.getElementById(setMessageButton); const eventLog document.getElementById(eventLog); let sharedWorker; let workerPort; let currentCounterState 0; // 客户端维护的最新计数器状态 function logEvent(message, type info) { const li document.createElement(li); li.textContent [${new Date().toLocaleTimeString()}] ${message}; if (type error) li.classList.add(error); if (type warn) li.style.color orange; eventLog.prepend(li); if (eventLog.children.length 20) { eventLog.removeChild(eventLog.lastChild); } } if (window.SharedWorker navigator.locks) { sharedWorker new SharedWorker(shared-worker.js); workerPort sharedWorker.port; logEvent(尝试连接到 SharedWorker...); workerPort.onmessage (event) { const message event.data; // logEvent(收到 SharedWorker 消息: ${JSON.stringify(message)}); // 调试时开启 switch (message.type) { case INITIAL_STATE: case STATE_UPDATE: case CURRENT_STATE: currentCounterState message.payload.counter; // 更新客户端维护的最新状态 counterValueSpan.textContent message.payload.counter; messageValueSpan.textContent message.payload.message; logEvent(更新页面状态: 计数器${message.payload.counter}, 消息${message.payload.message}); break; case INCREMENT_ACK: if (message.success) { logEvent(计数器非强一致性增加成功新值: ${message.newCounter}); } else { logEvent(计数器非强一致性增加失败: ${message.error}, error); } break; case INCREMENT_COMPLEX_ACK: // 此 ACK 消息通常是在 Lock API 内部等待的这里只是为了演示 // 实际逻辑中Lock API 的 Promise 解决就代表操作完成 if (message.success) { logEvent(计数器强一致性增加成功新值: ${message.newCounter}); strongIncrementStatus.textContent 成功! 新值: ${message.newCounter}; strongIncrementStatus.style.color green; } else { logEvent(计数器强一致性增加失败: ${message.error} (当前值: ${message.currentCounter}), error); strongIncrementStatus.textContent 失败: ${message.error}; strongIncrementStatus.style.color red; } break; case SET_MESSAGE_ACK: if (message.success) { logEvent(消息设置成功新消息: ${message.newMessage}); } else { logEvent(消息设置失败: ${message.error}, error); } break; default: logEvent(未知消息类型: ${message.type}); } }; workerPort.onerror (error) { logEvent(SharedWorker 错误: ${error.message}, error); console.error(SharedWorker error:, error); }; workerPort.start(); logEvent(SharedWorker 端口已启动。); } else { logEvent(您的浏览器不支持 SharedWorker 或 Lock API。, error); alert(您的浏览器不支持 SharedWorker 或 Lock API。); } // --- 非强一致性操作 --- incrementButton.addEventListener(click, () { if (workerPort) { logEvent(发送 INCREMENT_COUNTER 请求 (非强一致性)...); workerPort.postMessage({ type: INCREMENT_COUNTER }); } }); setMessageButton.addEventListener(click, () { if (workerPort) { const newMessage messageInput.value.trim(); if (newMessage) { logEvent(发送 SET_MESSAGE 请求 (非强一致性): ${newMessage}...); workerPort.postMessage({ type: SET_MESSAGE, payload: newMessage }); } else { logEvent(请填写要设置的消息。, warn); } } }); // --- 强一致性操作 --- incrementStronglyButton.addEventListener(click, async () { if (!workerPort) { logEvent(SharedWorker 未连接。, error); return; } const incrementAmount parseInt(incrementAmountInput.value, 10); const maxLimit parseInt(maxLimitInput.value, 10); if (isNaN(incrementAmount) || incrementAmount 0) { logEvent(递增值必须是大于0的数字。, warn); return; } if (isNaN(maxLimit) || maxLimit 0) { logEvent(最大限制必须是大于0的数字。, warn); return; } strongIncrementStatus.textContent 尝试获取锁...; strongIncrementStatus.style.color gray; logEvent([Tab] 尝试获取独占锁 counter_update_lock 来执行强一致性递增...); try { // 使用 Lock API 请求独占锁 // signal: AbortSignal.timeout(5000) 设定5秒超时防止无限等待 await navigator.locks.request(counter_update_lock, { mode: exclusive, signal: AbortSignal.timeout(5000) }, async (lock) { // lock 参数的存在表示我们成功获得了锁 if (!lock) { // 理论上不会发生因为我们没有设置 ifAvailable: true logEvent([Tab] 意外未能获取独占锁 counter_update_lock。, error); strongIncrementStatus.textContent 获取锁失败 (意外); strongIncrementStatus.style.color red; return; } logEvent([Tab] 成功获取独占锁 counter_update_lock。开始执行临界区操作...); strongIncrementStatus.textContent 获取锁成功正在操作...; strongIncrementStatus.style.color blue; // 临界区开始在这里执行需要强一致性的操作 // 1. 发送消息给 SharedWorker 请求更新 // 2. 等待 SharedWorker 的确认消息 await new Promise((resolve, reject) { const messageHandler (event) { const response event.data; if (response.type INCREMENT_COMPLEX_ACK) { workerPort.removeEventListener(message, messageHandler); // 移除监听器 if (response.success) { resolve(response); } else { reject(new Error(response.error || 强一致性递增失败)); } } }; workerPort.addEventListener(message, messageHandler); workerPort.postMessage({ type: INCREMENT_COUNTER_COMPLEX, payload: { value: incrementAmount, maxLimit: maxLimit } }); logEvent([Tab] 发送强一致性递增请求 (递增值: ${incrementAmount}, 最大限制: ${maxLimit})...); }); logEvent([Tab] 强一致性递增操作完成。锁即将自动释放。); }); // Lock API 的 Promise 解决时表示锁已释放且临界区操作成功 logEvent([Tab] 独占锁 counter_update_lock 已释放。, info); } catch (error) { if (error.name AbortError) { logEvent([Tab] 获取锁 counter_update_lock 超时或被中止。, warn); strongIncrementStatus.textContent 获取锁超时或被取消; strongIncrementStatus.style.color orange; } else { logEvent([Tab] 强一致性递增操作失败: ${error.message}, error); strongIncrementStatus.textContent 操作失败: ${error.message}; strongIncrementStatus.style.color red; } console.error([Tab] 强一致性递增错误:, error); } finally { // 无论成功失败确保状态显示正确 // 最终的 counterValueSpan 会通过 SharedWorker 的 STATE_UPDATE 消息更新 incrementStronglyButton.disabled false; // 重新启用按钮 } }); // 首次加载时请求当前状态以防 SharedWorker 已经运行 window.addEventListener(load, () { if (workerPort) { workerPort.postMessage({ type: GET_CURRENT_STATE }); } }); /script /body /html在上述index.html示例中我们新增了一个“强一致性递增”按钮。当用户点击此按钮时客户端首先使用navigator.locks.request(counter_update_lock, ...)尝试获取一个名为counter_update_lock的独占锁。AbortSignal.timeout(5000)用于设置锁获取的超时时间防止因其他 Tab 页长时间持有锁而导致当前 Tab 页无限等待。一旦成功获取锁lock参数存在客户端进入临界区。在临界区内客户端向SharedWorker发送一个INCREMENT_COUNTER_COMPLEX消息。这个消息包含了递增值和最大限制让SharedWorker执行一个带条件的递增操作。客户端通过监听SharedWorker的消息等待INCREMENT_COMPLEX_ACK响应。这是一个Promise包装的等待过程确保客户端在锁释放前已经知道SharedWorker的操作结果。SharedWorker处理请求更新sharedCounter并广播STATE_UPDATE给所有客户端。客户端收到INCREMENT_COMPLEX_ACK消息后Promise解决。navigator.locks.request的回调函数执行完毕浏览器自动释放counter_update_lock。其他等待该锁的 Tab 页现在可以尝试获取它。通过这种机制即使多个 Tab 页同时点击“强一致性递增”按钮它们也会按照顺序一个接一个地获取锁向SharedWorker发送请求从而确保每次递增操作都是原子且基于最新的共享状态进行的。例如如果计数器最大限制是10当前是9两个Tab页同时请求递增2。只有一个Tab页能获取锁它发送请求SharedWorker处理后发现9211 10则拒绝并广播当前状态9。另一个Tab页获取锁后发现当前是9同样拒绝。这样就避免了计数器超出限制或产生错误计算。进阶场景与考量共享锁 (Shared Lock) 用于读操作在某些情况下我们可能希望多个 Tab 页可以同时读取共享资源但只有在写入时才需要独占访问。这时可以使用shared模式的锁。读操作:navigator.locks.request(my_resource, { mode: shared }, async (lock) { /* 读取操作 */ });写操作:navigator.locks.request(my_resource, { mode: exclusive }, async (lock) { /* 写入操作 */ });当有共享锁被持有独占锁的请求会等待。当独占锁被持有任何共享锁或独占锁的请求都会等待。这提供了一种经典的读写锁模型。新 Tab 页加载时的状态同步当一个新的 Tab 页打开并连接到SharedWorker时它需要立即获取当前的最新状态。在我们的示例中SharedWorker在onconnect事件中立即发送INITIAL_STATE消息或者客户端在window.load时发送GET_CURRENT_STATE请求都能实现这一点。错误处理与鲁棒性AbortSignal:在navigator.locks.request中使用AbortSignal.timeout()是非常重要的可以防止锁请求无限等待提升用户体验。try...catch:始终在异步操作包括锁的获取和临界区内的操作中使用try...catch块来捕获和处理错误。SharedWorker 崩溃:如果SharedWorker脚本本身出现未捕获的错误导致崩溃所有连接到它的port都会收到错误事件。客户端需要有重连或降级策略。幸运的是SharedWorker崩溃时浏览器会自动释放所有由其客户端持有的锁避免死锁。Tab 页崩溃/关闭:Lock API的一个强大之处在于如果持有锁的 Tab 页意外关闭或导航离开浏览器会自动释放该 Tab 页持有的所有锁这有效防止了死锁。性能考量虽然SharedWorker和Lock API提供了强大的同步能力但它们并非没有开销。消息传递开销:postMessage涉及数据的序列化和反序列化。对于大量或复杂的数据这可能产生性能负担。锁竞争开销:频繁的锁竞争会导致一些 Tab 页等待这在高并发场景下可能影响响应速度。权衡:这种机制最适用于需要严格一致性的关键业务操作。对于非核心、允许最终一致性的数据同步BroadcastChannel或localStorage配合事件监听可能更简单高效。替代方案与混合模式IndexedDBLock API:如果共享状态需要持久化并且SharedWorker的生命周期不够例如用户关闭所有 Tab 页后状态丢失可以将IndexedDB作为后端存储然后使用Lock API来协调对IndexedDB事务的访问。SharedWorker依然可以作为协调者但数据源变为IndexedDB。服务器端同步:对于需要跨浏览器、跨设备甚至跨用户的数据一致性服务器端同步是不可避免的。SharedWorker和Lock API解决的是单个用户在同一浏览器中的一致性问题。总结与展望SharedWorker和Web Locks API是现代 Web 平台提供的强大工具它们共同为前端开发者解决跨 Tab 页强一致性通信这一复杂挑战提供了可靠的方案。SharedWorker作为中心化的状态管理和消息分发枢纽确保了数据源的唯一性和操作的序列化而Lock API则为客户端提供了原子的互斥访问机制有效防止了竞态条件保证了“读-改-写”等临界操作的完整性。通过本文的深入探讨和代码示例我们已经掌握了如何利用这两个 API 构建一个健壮的、高一致性的跨 Tab 页通信系统。在实际应用中开发者应根据业务需求权衡性能与一致性选择最合适的同步策略。理解并熟练运用这些技术将使我们能够构建出更加复杂、更加稳定的富客户端 Web 应用。随着 Web 平台能力的不断增强期待未来有更多创新的解决方案涌现进一步提升用户体验和开发效率。