Title: 【第 3733 期】用 Web Locks API 优雅解决跨标签页并发问题 | BestBlogs.dev
URL Source: https://www.bestblogs.dev/article/c481fdf5de?amp%3Butm_medium=feed&%3Butm_campaign=resources&%3Bentry=rss_article_item
Published Time: 2026-07-27 09:00:00
Markdown Content: 82
Through an OLX practical case study, this article introduces how to use the native Web Locks API to implement mutual exclusion for cross-tab file uploads to prevent duplicate uploads, covering both non-blocking and conditional lock strategies and their implementation details. 前 前端早读课
Today 3083 words (about 13 min) View Source →
Sign in to highlight text and take notes as you read. Sign in now
Cesar Contreras 2026-07-27 09:00 日本
介绍了如何利用原生 Web Locks API 实现跨标签页并发互斥,高效解决大文件重复上传问题。
前言
大文件断点续传遇上多标签页,每个标签页都争着恢复同一个上传,重复请求接踵而至。localStorage 有竞态风险,BroadcastChannel 又过于复杂。本文分享如何用浏览器原生 Web Locks API 实现原子级跨标签页互斥,优雅杜绝重复上传问题。今日前端早读课文章由 @Cesar Contreras 分享,@飘飘编译。
> 用于解决实际工作中的 前端多标签页状态同步 / 资源竞争 问题,最值得借鉴的一点是:对用户主动触发的操作使用 “非阻塞锁”(有锁就持有,无锁也继续),而对系统自动恢复的操作使用 “条件锁”,既保障体验又不产生冗余。
译文从这开始~~
编程中的并发,是指同时管理和执行多个任务或进程的能力(或至少看起来如此),使程序保持响应迅速且高效运行。你可以把它想象成一位忙碌厨房里的大厨:他不会把每道菜从头到尾按顺序一道道做完,而是一边切着蔬菜,一边看着这口锅在炖煮、那口锅在烘烤 —— 同时推进着好几道菜的进度。
我是 OLX 的一名前端工程师,日常主要使用 React 和 Next.js,但我的技术背景其实是 Java 并发编程。开发多线程应用是件让人头疼的事,要跟锁、信号量打交道…… 还有我的 "最爱"—— 死锁(在大型代码库里简直是噩梦)。当我完全转向 Web 开发时,从没想过还会遇到类似的场景。
Web 开发已经演变成了一个极其强大的生态系统。从早期那个只有用 HTML marquee 做些花哨滚动动画的年代(这里有点玩笑的意思 😆),从 JQuery 一统天下的时代,发展到如今这个强大的环境 —— 你可以使用 Workers、用 WebGL 和 Canvas 做 2D 和 3D 动画、用 Service Workers 实现离线功能,等等不一而足。这充分说明,Web 如今已经能够应对真正复杂的挑战,遇到类似问题只是时间早晚的事。
#### 问题所在
最近,我们在 OLX 面临了一个大文件上传的挑战。在处理上传时想要提供最佳用户体验并非易事,现实中大多数场景都要求用户留在页面上等待上传完成,但我们不想这样。由于文件体积可能非常大(最高达 20GB),我们希望用户可以自由地去做其他事情,即使关闭了浏览器或标签页再重新打开,上传也能自动继续,无需任何手动操作。为此,我们需要提供一种断点续传机制。
但麻烦也随之而来。如果用户打开了你应用的多个标签页会怎样?每个标签页都会尝试恢复并继续同一个上传任务。后果呢?多个重复的上传请求打到你的 S3 存储桶,带宽被白白浪费,产生不必要的处理成本,而用户则眼睁睁看着上传进度条毫无规律地跳来跳去,一脸困惑。
两个标签页各自恢复后,独立触发了同一个上传任务;由于缺乏协调,视频被上传了两次。
如果你正在现代 Web 应用中构建文件上传体验,大概率遇到过这个场景。今天,我将带你了解我们使用 Web Locks API 实现的一套稳健方案,来解决这个令人抓狂的协调难题。
不过,在深入讲解我们的解决方案之前,我们需要先理解同步与锁的概念。
#### 首先,什么是锁?
锁是一种机制,用于确保共享资源(变量、对象、函数调用等)在同一时刻只能被一个调用者访问,并且每个调用者都能获取到最新的信息。你可以想象一个非常经典的场景 —— 一家银行在共享内存中存有多个账户,同时有多台 ATM 机在进行取款、存款和余额查询操作。如果没有协调机制(同步),结果将一团糟,如下图所示。
为了解决这个混乱局面,我们必须通过对账户的读写操作加锁来控制访问。这样一来,无论执行什么操作(取款、存款还是查询余额),第一个获得锁的调用者将独占资源,其他调用者必须等待,直到第一个持有者完成操作并释放锁。下图展示了对账户访问进行同步后的效果。
现在我们已经理解了同步和锁的工作原理,接下来让我们看看如何将它付诸实践 🚀。
#### 跨标签页协调的技术全景
在深入我们的解决方案之前,先来看看在浏览器标签页之间协调操作有哪些可选方案:
##### 方案一:localStorage + 轮询
这是一种 "经典" 做法,利用 localStorage 作为共享锁机制:
// 简化示例 - 请勿在生产环境使用 if (!localStorage.getItem('upload-lock')) { localStorage.setItem('upload-lock', Date.now()); // 开始上传 }
存在的问题:
* 竞态条件:localStorage 的读写操作不是原子性的
* 过期锁:如果标签页崩溃,锁会永远残留
* 轮询开销:需要持续检查锁的状态
* 事件局限:storage 事件只在其他标签页触发,当前标签页收不到
##### 方案二:Broadcast Channel API
这种方式通过 Broadcast Channel API 在标签页之间发送消息来进行协调:
const channel = new BroadcastChannel('upload-coordination'); channel.postMessage({ type: 'REQUEST_UPLOAD_PERMISSION' });
面临的挑战:
* 缺少互斥原语:你需要从零开始构建状态机
* 竞态条件:同时发出的消息可能产生冲突
* 协议复杂:需要手动处理心跳、崩溃检测和冲突解决
* 容易出错:消息排序和时序问题非常常见
#### Web Locks API 登场:浏览器原生互斥锁
Web Locks API 恰好提供了我们所需要的东西:一个真正的、原子性的互斥锁系统,直接内置于浏览器中。它的优势在于:
* 原子性锁获取 —— 没有竞态条件
* 自动清理 —— 标签页关闭或崩溃时锁会自动释放
* 非阻塞选项 —— 避免 UI 冻结
* API 简洁 —— 无需实现复杂协议
* 浏览器兼容性 —— 所有主流浏览器均已支持 😃
#### 我们的实现:useUploadCoordinator
以下是我们通过自定义 React Hook 解决该问题的方式:
##### 核心策略
针对两种场景采用两种不同的处理方式:
// 1. 全新上传(用户主动发起):非阻塞 // 尝试获取锁,但无论是否获取到都继续执行 navigator.locks.request(LOCK_NAME, { ifAvailable: true }, async (lock) => { if (lock) { // 持有锁直到上传完成 await holdLockUntilComplete(abortController); } // 无论如何都继续上传 - 绝不阻塞用户 } ); // 2. 恢复上传:有条件执行 // 只有获取到锁才继续 navigator.locks.request(LOCK_NAME, { ifAvailable: true }, async (lock) => { if (lock) { uploader.emit("restore-confirmed"); // 继续恢复上传 await holdLockUntilComplete(abortController); } // 如果未获取到锁,静默跳过 - 另一个标签页正在处理 } );
##### 关键实现细节
锁的生命周期管理:
const holdLockUntilComplete = (abortController: AbortController) => { return new Promise<void>((resolve) => { const cleanup = () => { // 移除所有监听器并 resolve(释放锁) uploader.off("complete", cleanup); uploader.off("error", cleanup); uploader.off("cancel-all", cleanup); resolve(); }; // 在任何终态事件发生时释放锁 uploader.on("complete", cleanup); uploader.on("error", cleanup); uploader.on("cancel-all", cleanup); // 组件卸载时也释放锁 abortController.signal.addEventListener("abort", cleanup); }); };
浏览器兼容性与优雅降级:
const checkWebLocksAvailable = (): boolean => { return typeof navigator !== 'undefined' && 'locks' in navigator; }; // 降级处理 if (!checkWebLocksAvailable()) { // 允许恢复操作继续(接受可能出现重复的风险) uploader.emit("restore-confirmed"); return; }
#### 实际收益
自从上线这套方案以来,我们看到了:
* 跨标签页协调导致的重复上传归零
* 更好的用户体验 —— 上传永远不会因协调逻辑而卡住
* 基础设施成本降低 —— 不再有重复的处理和存储开销
* 代码更简洁 —— 不再需要复杂的状态机或轮询逻辑
现在,我们的方案效果如下:
!Image 5通过共享锁,只有第一个获取到锁的标签页会继续执行上传;第二个标签页检测到锁已被占用后,便悄然退让。
#### 实际应用中的注意事项
##### 适用场景:
* 支持持久化 / 恢复的文件上传
* 后台同步操作
* 任何在同一时间只应在一个标签页中运行的单例操作
##### 性能影响:
* 极小 —— 没有轮询开销
* 自动清理机制避免内存泄漏
* 非阻塞设计确保 UI 始终流畅响应
##### 测试策略:
* 在所有目标浏览器中进行测试
* 验证标签页意外崩溃时的行为表现
* 确认在不支持该 API 的环境中能优雅降级
#### 结语
跨标签页协调不必成为一场噩梦。Web Locks API 提供了一个稳健的、浏览器原生的解决方案,免去了自定义协调协议的复杂性和脆弱性。
在构建这套方案的过程中,我们总结出跨标签页协调成功的关键在于:
1、绝不阻塞用户操作 —— 对用户主动发起的操作使用非阻塞锁
2、安全地失败 —— 优雅降级胜过功能崩溃
3、保持简单 —— 善用平台原生能力,而非重新造轮子
Web Locks API 不仅仅适用于上传协调;任何时候你需要在标签页之间实现互斥,都值得考虑使用它。随着浏览器支持的持续完善,它正在成为现代 Web 开发者工具箱中越来越实用的利器。
关于本文
译者:@飘飘
作者:@Cesar Contreras
原文:https://tech.olx.com/handling-concurrency-on-the-web-with-web-locks-api-163b7e07eddd
这期前端早读课
对你有帮助,帮”赞“一下,
期待下一期,帮”在看” 一下。 阅读原文