关键时刻可以救命的Web Locks API

这篇文章发布于 2026年09月10日,星期四,18:37,归类于 JS API。 阅读 55 次, 今日 53 次 没有评论

 

一、大文件断点续传

大文件上传,为了用户体验良好,通常需要接入断点续传。

相关前端实现我十几年前就有介绍过“HTTP协议下文件上传断点续传”。

不过本文要讲的重点不是断点续传本身,而是其中可能遇到的一个用户体验问题。

那就是如果用户同时打开多个同一个URL地址的文件续传标签页,此时该怎么办?

统一标签上传

如果我们放任不管,那么这几个页面都会自主主张给后端发送上传数据。

那还得了,每个页面的上传进度不一样的,后端返回岂不是一会儿30%,一会儿25%,一会儿又是35%。

于是就会看到进度条像得了羊癫疯一样,疯狂跳动,来,社会摇,走起来。

这种疯癫的交互效果就算是精神小妹都无法容忍。

显然需要处理?

该怎么处理呢。

二、跨浏览器标签页开发常见方案

我们不妨看看市面上现有的,跨页面的通信交互方案,能不能优雅地解决此问题。

1. 本地localStorage + 轮询

// 示意代码,勿使用
if (!localStorage.getItem('upload-lock')) { 
  localStorage.setItem('upload-lock', Date.now()); 
  // 开始上传
  // ...
}

问题在于:

  • 竞态条件:localStorage 的读/写操作不是原子操作。所谓原子操作,指的是一次性完整执行完毕的操作。例如:
    // 逻辑:读取计数 +1 再存回去
    let count = Number(localStorage.getItem('counter')); // ①读
    count = count + 1;                                    // ②内存修改
    localStorage.setItem('counter', count);               // ③写
    

    上面代码的读本身,和写本身可以看成是原子操作,但是,整个任务却不是原子的,因为读和写中间有一段业务计算,整套操作被拆分,中间可以被别的标签页抢占。

  • 过期锁定:如果标签页崩溃,锁定状态将永久保留。
  • 轮询开销:您需要持续检查锁定状态
  • 事件限制:存储事件只会在其他标签页中触发,而不会在当前标签页中触发。

2. Broadcast Channel API

广播通道API我去年也介绍过,详见“Broadcast Channel API简介,可实现Web页面广播通信”一文。

BroadcastChannel兼容性

如果是针对当前需求,我们可能会这么处理:

const channel = new BroadcastChannel('upload-coordination');
channel.postMessage({ type: 'REQUEST_UPLOAD_PERMISSION' });

看起来代码还挺简单,不过问题也比较致命。

那就是同时发送的消息可能会造成冲突,需要手动处理心跳检测、崩溃检测和冲突解决。


可以看到,那些常见的跨页面通信手段,都无法真正解决这里的共享文件上传问题。

其实,类似这种多页面操作统一资源的场景,毫无疑问,最好的解决方案就是Web Locks API。

三、了解“锁”的概念

“锁”是计算机开发中的一个重要概念,前端开发人员接触的不多,但是如果是数据库开发,那这个可太熟了。

锁其实是一种机制,它确保共享资源(变量、对象、函数调用等)一次只能被一个对象访问,并且保证每个调用者都能获得最新信息。

比方说:一家银行在共享内存中存储了多个账户,并且有多台ATM机同时进行取款、存款和查询余额的操作。

如果没有协调(同步),这将导致混乱,如下图所示。

取款混乱示意

此时,我们可以通过锁定账户的读写权限来控制访问。

这样一来,无论执行什么操作(取款、充值或查询余额),第一个获得锁的调用者都将获得该锁。

其他调用者必须等到第一个获得锁的调用者完成操作后,锁才会被释放。

下图展示了同步账户访问后的情况。

有了锁之后

四、navigator.locks与上传锁定

OK,万事具备,就看Web Locks API如何在代码层面解决我们的上传冲突问题了,其实代码很简单。

// 只有成功获取锁才继续处理
navigator.locks.request(LOCK_NAME, { ifavailable: true }
  async (lock) => {
    if (lock) {
      // 执行上传
      upload(); 
      await holdLockUntilComplete(abortController);
    }
    // 获取不到锁则静默跳过 - 其他标签页正在处理
    // ...
  }
);

看到没有,就几行代码。

使用非常简单,就是把原来的实现代码,使用navigator.locks.request()方法包一下就好了。

此时,浏览器会自动加锁和锁判断。

锁的生命周期管理示意

上述代码中的holdLockUntilComplete()可以用来中断上传请求,或者用来管理整个上传的生命周期。

使用示意,供大家参考。

const holdLockUntilComplete = (abortController) => {
  return new Promise((resolve) => {
    const cleanup = () => {
      // 移除所有的监听事件
      uploader.off("complete", cleanup);
      resolve();
    };

    // 上传完成事件
    uploader.on("complete", cleanup);
    // 其他事件略...

    // 组件卸载时也释放
    abortController.signal.addEventListener("abort", cleanup);
  });
};

使用 LockManager 的好处

  • 真正解决冲突问题,零重复上传
  • 更好的用户体验,由于协调逻辑,上传过程不会再出现卡顿。
  • 降低基础设施成本,不再需要重复处理和存储
  • 更简洁的代码,没有复杂的状态机或轮询逻辑

五、一些可选参数说明

navigator.locks.request()方法是支持一些可选参数的,语法如下:

request(name, callback)
request(name, options, callback)

其中,options支持以下一些参数,这些参数大多数场景下都用不到,大家一眼扫一下就可以了。

mode

"exclusive""shared" 之一。默认值是 "exclusive",表示排他,也就是一次只能一个锁。"shared" 表示共享锁。

ifAvailable

如果为 true,则只有在尚未持有锁的情况下才会授予锁请求。如果无法授予,则将使用 null 而不是 Lock 实例来调用回调。默认值为 false

steal

如果为 true,将释放所有同名已持有的锁,并授予该请求。默认值为 false

警告:小心使用!之前在锁内运行的代码会继续运行,并且可能与现在持有锁的代码发生冲突。

signal

一个 AbortSignalAbortControllersignal 属性);如果指定并且 AbortController 被中止,则锁请求将被丢弃(如果尚未授予)。

其他适合使用Locks API的场景

除了本文提到的大文件断点续传,还有以下这些需求场景适合使用 Web Locks API.

  • 当多个标签页同时操作 Indexed Database API 或 LocalStorage 时;
  • 在网页端文档编辑器中,若用户在两个标签页中打开了同一份文档;
  • 在单页面内按顺序执行复杂的异步任务(如批量文件上传、连续串联请求)时;

还有下图所示的情况,也有必要上锁

众女围绕一个资源

六、兼容性以及其他说明

Web Locks API已经出现很多年了,兼容性还是相当OK的,以目前的AI能力,此API使用是没有任何困扰的。

Web Locks API兼容性

除了上面介绍过的request()方法,navigator.locks还有个query()方法,此方法执行后返回一个个Promise,该Promise解析后得到一个对象,其中包含有关已持有锁和挂起锁的信息。

具体有哪些信息,不展开介绍,因为没有意义,正遇到类似需求,直接让AI去运行代码就好了。

好了,本文内容已经比预期的多多了。

其实没必要扯那么多东西的,如今AI这么强,技术细节并没有那么重要。

你是好人

(本篇完)

分享到:


发表评论(目前没有评论)