← 回總覽

用 Web Locks API 优雅解决跨标签页并发问题

📅 2026-07-27 09:00 前端早读课 软件编程 6 分鐘 6437 字 評分: 82
Web Locks API 前端开发 并发控制 浏览器API 文件上传
📌 一句话摘要 本文通过 OLX 实践案例,介绍用原生 Web Locks API 实现跨标签页文件上传互斥,避免重复上传,包含非阻塞锁与条件锁两种策略及实现细节。 📝 详细摘要 文章源自 OLX 前端工程师的实践分享,由前端早读课编译。作者面临大文件断点续传在多标签页场景下的并发冲突——每个标签页都尝试恢复同一个上传任务,导致重复请求浪费带宽和成本。作者对比了 localStorage+轮询和 Broadcast Channel API 的缺陷后,采用浏览器原生 Web Locks API 构建跨标签页互斥锁。核心策略是:对用户主动发起的全新上传使用非阻塞锁(即使未获取锁也继续上传,不阻

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 日本

!Image 1

介绍了如何利用原生 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 存储桶,带宽被白白浪费,产生不必要的处理成本,而用户则眼睁睁看着上传进度条毫无规律地跳来跳去,一脸困惑。

!Image 2

两个标签页各自恢复后,独立触发了同一个上传任务;由于缺乏协调,视频被上传了两次。

如果你正在现代 Web 应用中构建文件上传体验,大概率遇到过这个场景。今天,我将带你了解我们使用 Web Locks API 实现的一套稳健方案,来解决这个令人抓狂的协调难题。

不过,在深入讲解我们的解决方案之前,我们需要先理解同步与锁的概念。

#### 首先,什么是锁?

锁是一种机制,用于确保共享资源(变量、对象、函数调用等)在同一时刻只能被一个调用者访问,并且每个调用者都能获取到最新的信息。你可以想象一个非常经典的场景 —— 一家银行在共享内存中存有多个账户,同时有多台 ATM 机在进行取款、存款和余额查询操作。如果没有协调机制(同步),结果将一团糟,如下图所示。

!Image 3

为了解决这个混乱局面,我们必须通过对账户的读写操作加锁来控制访问。这样一来,无论执行什么操作(取款、存款还是查询余额),第一个获得锁的调用者将独占资源,其他调用者必须等待,直到第一个持有者完成操作并释放锁。下图展示了对账户访问进行同步后的效果。

!Image 4

现在我们已经理解了同步和锁的工作原理,接下来让我们看看如何将它付诸实践 🚀。

#### 跨标签页协调的技术全景

在深入我们的解决方案之前,先来看看在浏览器标签页之间协调操作有哪些可选方案:

##### 方案一: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

这期前端早读课

对你有帮助,帮”赞“一下,

期待下一期,帮”在看” 一下。 阅读原文

查看原文 → 發佈: 2026-07-27 09:00:00 收錄: 2026-07-27 14:00:58

🤖 問 AI

針對這篇文章提問,AI 會根據文章內容回答。按 Ctrl+Enter 送出。