news 2026/1/9 22:17:13

跨 Tab 页的强一致性通信:基于 SharedWorker 与 Lock API 的锁竞争实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
跨 Tab 页的强一致性通信:基于 SharedWorker 与 Lock API 的锁竞争实现

尊敬的各位技术同仁,大家好!

在现代复杂的前端应用开发中,我们经常面临一个挑战:如何在用户同时打开的多个浏览器 Tab 页之间,保持数据的强一致性。想象一下,一个用户在一个 Tab 页修改了某个设置,而另一个 Tab 页却依然显示着旧的数据;或者,多个 Tab 页同时尝试更新同一个资源,导致数据冲突或丢失。这些场景轻则影响用户体验,重则引发严重的业务逻辑错误。

今天,我们将深入探讨如何利用 Web 平台提供的两大强大工具——SharedWorkerLock API——来构建一个跨 Tab 页的强一致性通信机制,从而有效解决这些并发与同步问题。我们将从问题的根源出发,逐步剖析这两种技术的原理,最终通过具体的代码示例,展示如何将它们巧妙结合,实现我们所需的高可靠性系统。

跨 Tab 页通信的挑战与强一致性需求

浏览器天然的设计哲学是隔离。每个 Tab 页通常运行在独立的进程或线程中,拥有独立的 JavaScript 运行时、DOM 树和内存空间。这种隔离性保障了安全性与稳定性,但也为跨 Tab 页的数据共享与同步带来了挑战。

传统跨 Tab 页通信手段及其局限

在深入探讨解决方案之前,我们先回顾一下常见的跨 Tab 页通信手段,并分析它们在实现“强一致性”方面的不足:

  1. localStorage/sessionStorage:

    • 优点:简单易用,数据持久化(localStorage),跨 Tab 页共享。
    • 缺点:
      • 非原子性:localStorage的写入操作不是原子的。如果两个 Tab 页几乎同时读取、修改、写入同一个键值,很容易发生竞态条件,导致后写入的数据覆盖前写入的数据,或者基于旧数据进行的计算结果被覆盖。
      • 无通知机制:localStoragestorage事件只能通知到非当前写入的 Tab 页,无法通知到当前写入的 Tab 页。这使得同步逻辑变得复杂。
      • 容量限制:通常为 5-10MB。
    • 强一致性挑战:缺乏原生的锁定机制,无法保证对共享数据的并发访问是安全的。
  2. BroadcastChannelAPI:

    • 优点:专门为跨 Tab 页(同源)广播消息设计,API 简洁。
    • 缺点:
      • 纯消息广播:BroadcastChannel只是一个消息通道,它本身不提供任何状态管理或同步机制。它只能通知其他 Tab 页“某个事件发生了”或“某个数据可能已更新”,但不能保证这些操作的原子性或顺序性。
      • 无原生锁定:同样缺乏对共享资源的锁定能力。如果多个 Tab 页都监听并尝试响应同一消息,仍然可能出现竞态条件。
    • 强一致性挑战:适用于事件通知或非关键数据的同步,但无法独立保证对共享状态的原子性更新。
  3. window.postMessage(配合window.openeriframe):

    • 优点:允许跨窗口/框架通信。
    • 缺点:
      • 限定通信目标:只能与opener窗口或iframe中的内容通信,不适用于任意 Tab 页之间的广播。
      • 复杂性:需要维护窗口引用,处理消息来源。
    • 强一致性挑战:无法提供全局的协调和锁定机制。
  4. IndexedDB:

    • 优点:客户端结构化存储,容量大,支持事务。
    • 缺点:
      • 事务粒度:IndexedDB事务仅在其自身范围内提供原子性。跨 Tab 页的多个IndexedDB事务如果操作同一数据,仍然可能需要额外的协调。
      • 复杂性:API 相对复杂,直接用于通信不如专门的通信 API 方便。
    • 强一致性挑战:虽然其事务机制有助于数据完整性,但要实现跨 Tab 页的逻辑操作的强一致性,仍需额外的同步原语。例如,两个 Tab 页各自在一个事务中读取、修改、写入同一个计数器,没有外部协调仍可能导致错误。

综上所述,传统的通信手段在实现“强一致性”时力不从心,主要症结在于缺乏一个统一的协调中心和原子的锁定机制。而这正是SharedWorkerLock 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可以:

  1. 统一管理共享状态:所有跨 Tab 页共享的数据都存储在SharedWorker内部。
  2. 序列化操作:所有对共享状态的修改请求都发送到SharedWorker。由于SharedWorker是单线程的,它会按照接收到的顺序(或内部调度策略)依次处理这些请求,从而避免内部的竞态条件。
  3. 广播更新:当共享状态发生变化时,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 lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>SharedWorker 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[type="text"] { padding: 8px; width: 200px; margin-right: 10px; } #status { margin-top: 15px; padding: 10px; background-color: #f0f0f0; border-radius: 3px; } </style> </head> <body> <h1>SharedWorker 客户端示例</h1> <p>打开多个 Tab 页,观察计数器和消息的同步。</p> <div class="container"> <h2>共享计数器</h2> <p>当前计数器值: <span id="counterValue">0</span></p> <button id="incrementButton">增加计数器 (非强一致性)</button> </div> <div class="container"> <h2>共享消息</h2> <p>当前共享消息: "<span id="messageValue">Hello from SharedWorker!</span>"</p> <input type="text" id="messageInput" placeholder="输入新消息"> <button id="setMessageButton">设置消息 (非强一致性)</button> </div> <div id="status"> <h3>事件日志:</h3> <ul id="eventLog"></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来管理sharedCountersharedMessage。所有连接的 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获得一个独占锁。

场景示例:

  1. Tab A想要执行一个需要强一致性的操作(例如,基于当前计数器的值进行复杂计算后更新计数器)。
  2. Tab A调用navigator.locks.request('my_resource_lock', ...)
  3. 如果锁可用,Tab A成功获取锁,然后执行其临界区代码:
    • SharedWorker发送消息,请求获取当前状态(如果需要)。
    • 基于获取到的状态进行计算。
    • SharedWorker发送消息,请求更新状态。
    • 等待SharedWorker的确认或状态更新广播。
  4. Tab A持有锁期间,如果Tab B也尝试获取'my_resource_lock',它将必须等待。
  5. Tab A的操作完成后(request回调函数执行完毕或其内部 Promise 解决),锁会自动释放。
  6. 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}" (意外情况,因为 ifAvailable=false)`); } }); 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作为客户端协调并发请求的原子锁。是时候将它们结合起来,构建一个真正的强一致性通信方案了。

核心思想与工作流程
  1. SharedWorker 作为唯一数据源:所有的共享状态都只在SharedWorker内部维护。
  2. 客户端通过 Lock API 协调请求:当任何客户端(Tab 页)需要执行一个涉及共享状态修改的“临界操作”时,它必须首先请求一个独占锁。
  3. 锁内操作与 SharedWorker 交互:只有在成功获取锁之后,客户端才能向SharedWorker发送修改请求。这个请求可能包含读取当前状态、基于当前状态计算新值、然后提交新值的整个流程。
  4. SharedWorker 处理并广播:SharedWorker接收到请求后,更新其内部状态,然后将最新的状态广播给所有连接的客户端。
  5. 客户端释放锁:客户端在收到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 lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>SharedWorker + 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[type="text"] { padding: 8px; width: 200px; margin-right: 10px; } input[type="number"] { 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> <h1>SharedWorker + Lock API 客户端示例</h1> <p>打开多个 Tab 页,通过锁定机制协调对共享数据的更新。</p> <div class="container"> <h2>共享计数器</h2> <p>当前计数器值: <span id="counterValue">0</span></p> <button id="incrementButton">增加计数器 (非强一致性)</button> <hr> <h3>强一致性递增 (使用 Lock API)</h3> <p>递增值: <input type="number" id="incrementAmount" value="1" min="1"></p> <p>最大限制: <input type="number" id="maxLimit" value="10" min="1"></p> <button id="incrementStronglyButton">强一致性递增</button> <span id="strongIncrementStatus" style="margin-left: 10px;"></span> </div> <div class="container"> <h2>共享消息</h2> <p>当前共享消息: "<span id="messageValue">Hello from SharedWorker!</span>"</p> <input type="text" id="messageInput" placeholder="输入新消息"> <button id="setMessageButton">设置消息 (非强一致性)</button> </div> <div id="status"> <h3>事件日志:</h3> <ul id="eventLog"></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示例中,我们新增了一个“强一致性递增”按钮。当用户点击此按钮时:

  1. 客户端首先使用navigator.locks.request('counter_update_lock', ...)尝试获取一个名为counter_update_lock的独占锁。
  2. AbortSignal.timeout(5000)用于设置锁获取的超时时间,防止因其他 Tab 页长时间持有锁而导致当前 Tab 页无限等待。
  3. 一旦成功获取锁(lock参数存在),客户端进入临界区。
  4. 在临界区内,客户端向SharedWorker发送一个INCREMENT_COUNTER_COMPLEX消息。这个消息包含了递增值和最大限制,让SharedWorker执行一个带条件的递增操作。
  5. 客户端通过监听SharedWorker的消息,等待INCREMENT_COMPLEX_ACK响应。这是一个Promise包装的等待过程,确保客户端在锁释放前,已经知道SharedWorker的操作结果。
  6. SharedWorker处理请求,更新sharedCounter,并广播STATE_UPDATE给所有客户端。
  7. 客户端收到INCREMENT_COMPLEX_ACK消息后,Promise解决。
  8. navigator.locks.request的回调函数执行完毕,浏览器自动释放counter_update_lock
  9. 其他等待该锁的 Tab 页现在可以尝试获取它。

通过这种机制,即使多个 Tab 页同时点击“强一致性递增”按钮,它们也会按照顺序,一个接一个地获取锁,向SharedWorker发送请求,从而确保每次递增操作都是原子且基于最新的共享状态进行的。例如,如果计数器最大限制是10,当前是9,两个Tab页同时请求递增2。只有一个Tab页能获取锁,它发送请求,SharedWorker处理后发现9+2=11 > 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时,它需要立即获取当前的最新状态。在我们的示例中,SharedWorkeronconnect事件中立即发送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 页持有的所有锁,这有效防止了死锁。
性能考量

虽然SharedWorkerLock API提供了强大的同步能力,但它们并非没有开销。

  • 消息传递开销:postMessage涉及数据的序列化和反序列化。对于大量或复杂的数据,这可能产生性能负担。
  • 锁竞争开销:频繁的锁竞争会导致一些 Tab 页等待,这在高并发场景下可能影响响应速度。
  • 权衡:这种机制最适用于需要严格一致性的关键业务操作。对于非核心、允许最终一致性的数据同步,BroadcastChannellocalStorage配合事件监听可能更简单高效。
替代方案与混合模式
  • IndexedDB+Lock API:如果共享状态需要持久化,并且SharedWorker的生命周期不够(例如,用户关闭所有 Tab 页后状态丢失),可以将IndexedDB作为后端存储,然后使用Lock API来协调对IndexedDB事务的访问。SharedWorker依然可以作为协调者,但数据源变为IndexedDB
  • 服务器端同步:对于需要跨浏览器、跨设备甚至跨用户的数据一致性,服务器端同步是不可避免的。SharedWorkerLock API解决的是单个用户在同一浏览器中的一致性问题。

总结与展望

SharedWorkerWeb Locks API是现代 Web 平台提供的强大工具,它们共同为前端开发者解决跨 Tab 页强一致性通信这一复杂挑战提供了可靠的方案。SharedWorker作为中心化的状态管理和消息分发枢纽,确保了数据源的唯一性和操作的序列化;而Lock API则为客户端提供了原子的互斥访问机制,有效防止了竞态条件,保证了“读-改-写”等临界操作的完整性。

通过本文的深入探讨和代码示例,我们已经掌握了如何利用这两个 API 构建一个健壮的、高一致性的跨 Tab 页通信系统。在实际应用中,开发者应根据业务需求,权衡性能与一致性,选择最合适的同步策略。理解并熟练运用这些技术,将使我们能够构建出更加复杂、更加稳定的富客户端 Web 应用。随着 Web 平台能力的不断增强,期待未来有更多创新的解决方案涌现,进一步提升用户体验和开发效率。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2025/12/15 21:07:39

多传感器信息融合,卡尔曼滤波算法的轨迹跟踪与估计 AEKF——自适应扩展卡尔曼滤波算法

多传感器信息融合&#xff0c;卡尔曼滤波算法的轨迹跟踪与估计AEKF——自适应扩展卡尔曼滤波算法 AUKF——自适应无迹卡尔曼滤波算法 UKF——无迹卡尔曼滤波算法 三种不同的算法实现轨迹跟踪轨迹跟踪这活儿听起来高端&#xff0c;实际干起来全是坑。传感器数据像一群不听话的…

作者头像 李华
网站建设 2025/12/15 21:06:34

【NGS数据质控黄金法则】:10个R语言关键步骤确保分析可靠性

第一章&#xff1a;NGS数据质控的核心意义与R语言优势高通量测序&#xff08;NGS&#xff09;技术的迅猛发展为基因组学研究提供了前所未有的数据规模&#xff0c;但原始测序数据中常包含接头污染、低质量碱基和PCR重复等问题&#xff0c;直接影响后续分析的准确性。因此&#…

作者头像 李华
网站建设 2025/12/15 21:06:21

boost获取dll导出函数调用(C++源码)

1、概述 boost获取dll导出函数并调用,4个步骤。 1、包含头文件 2、加载dll 3、获取函数地址 4、调用函数 与windows 的GetProcessAdress方式相比,感觉boost更麻烦一点,于是用ai搜索了下区别,我觉得其中一个好处就是支持跨平台吧。 由于boost::dll::shared_library::get&…

作者头像 李华