想在网页中实现大文件后台下载,让用户切换到其他标签页甚至关闭浏览器后,下载任务依然持续运行?Background Fetch API 是一个强大的选项,但它并非开箱即用。今天,我们来探讨如何避开这些常见陷阱,真正把这套功能用起来。

Background Fetch API 是否可用,先看浏览器和注册状态
首先得明确一个限制:Background Fetch 目前仅在 Chrome 105+ 和 Edge 105+ 中可用,Firefox 和 Safari 暂不支持。而且,它必须在 HTTPS 环境或本地 localhost 下才能正常运行。
更重要的是,它并非注册了 Service Worker 就能自动生效的“隐形”功能。你需要主动、显式地调用 backgroundFetch.fetch()。调用前,务必确保两件事:navigator.backgroundFetch 这个对象存在,并且 navigator.serviceWorker.ready 这个 Promise 已经成功解析(意味着 Service Worker 已经激活就绪)。
很多开发者容易在这里踩坑,常见的错误现象有两种:要么是 TypeError: navigator.backgroundFetch is undefined,要么是注册后调用 fetch() 却报出 InvalidStateError。
- 兼容性检查是第一步:动手前务必用
if ('backgroundFetch' in navigator)判断一下。如果不支持,就得准备降级方案,比如使用StreamSaver.js配合 Blob URL。 - Service Worker 脚本路径有讲究:最好将 Service Worker 脚本注册在根路径下(例如
/sw.js),否则backgroundFetch的权限可能会受到限制。 - 耐心等待激活:Service Worker 注册后,不要急着立即调用
fetch()。一定要等navigator.serviceWorker.ready这个 Promise 完成,否则会因 Service Worker 尚未激活而直接失败。
如何发起一个带元数据的后台下载任务
backgroundFetch.fetch() 的用法与普通的 fetch() 有所不同。它需要三个参数:一个唯一的任务 id、一个 requests 数组(包含要下载的资源),以及一个配置 options 对象。
这里有几个容易踩的坑:直接把字符串 URL 丢进 requests 数组(比如 ['/large.zip'])会报错;忘了在 options 里设置 title,会导致系统通知栏没有标题;没提供 icons 数组,在 Android 上就无法显示自定义图标。
requests必须是 Request 对象:数组里的每个元素都得用new Request(url, { method: 'GET' })来构造。如果需要认证,headers 里必须显式带上(因为 Service Worker 里读不到页面的 cookie)。options的必填项:title是必需的,用于通知显示。icons虽然不是必须,但强烈建议至少提供一个 192x192 的 PNG 图标,否则系统会用默认图标。id的唯一性:同一个id重复调用会覆盖前一个任务。如果需要并发多个下载任务,务必保证每个任务的id是唯一的,可以拼接时间戳或 UUID。
const registration = await na vigator.serviceWorker.ready;
const bgFetch = await registration.backgroundFetch.fetch(
'download-123',
[new Request('https://example.com/file.zip')],
{
title: '正在下载大文件',
icons: [{ src: '/icon-192.png', sizes: '192x192', type: 'image/png' }],
downloadTotal: 1024 * 1024 * 100 // 可选,用于进度估算
});
如何监听进度与处理完成事件
后台下载的进度监听机制有些特殊。进度事件(backgroundfetchprogress)的回调仅在 Service Worker 脚本内部触发,页面脚本无法直接监听。这意味着,如果你想在页面上更新进度条,就必须通过 postMessage 让 Service Worker 把进度数据“传递”出来。
常见的困惑点就在这里:在页面里写 navigator.backgroundFetch.addEventListener('progress', ...) 是完全没有效果的。另一个常见问题是,任务明明完成了,但页面 UI 没更新,原因就是没处理好 backgroundfetchsuccess 或 backgroundfetchfail 事件。
- 在 Service Worker 里监听事件:在 Service Worker 脚本中,监听
backgroundfetchsuccess和backgroundfetchfail事件。然后,通过clients.matchAll()找到所有活跃的页面客户端,再用postMessage把结果或进度发过去。 - 理解进度数据:
onprogress回调的参数对象里包含developerId和progress信息。progress里有totalBytes和transferredBytes。注意,如果服务器没返回Content-Length头,totalBytes可能为 0。 - 获取任务结果:任务完成后,可以通过
registration.backgroundFetch.get(id)获取对应的BackgroundFetchRegistration对象。该对象里的result属性会告诉你任务是成功('success')还是失败('failure'),failedRecords数组则包含了失败的请求记录。
为什么下载完文件却打不开?注意响应体提取限制
这是最关键也最容易出问题的环节。Background Fetch 下载的文件并不会自动保存到用户的磁盘,也不会生成一个可以直接访问的 URL。它只是把响应体临时存储在浏览器的内部空间里。
你必须在 Service Worker 的 backgroundfetchsuccess 事件处理函数中,调用 registration.matchAll() 来获取匹配的 Response 对象,然后再用 response.blob() 等方法把内容提取出来。但麻烦的是,这个过程已经脱离了原始的请求上下文,像服务器返回的 Content-Disposition 头(通常包含文件名)等信息,在 Service Worker 里是读不到的。
还有一个关键限制:在 Service Worker 环境中,你无法直接触发浏览器的原生下载行为。也就是说,在 SW 里执行 location.href = URL.createObjectURL(blob) 是无效的。你必须把提取出来的 Blob 数据发回给页面,让页面来处理下载。
- 在页面中触发下载:页面收到 Service Worker 发来的 Blob 后,需要用
URL.createObjectURL()为其创建一个临时的对象 URL。然后创建一个隐藏的标签,设置其href为这个 URL,并设置download属性为想要的文件名,最后模拟点击。别忘了,使用完毕后要用URL.revokeObjectURL()释放该 URL。 - 文件名的处理:如果服务器通过
Content-Disposition: attachment; filename="report.pdf"指定了文件名,这个信息在 Service Worker 里是拿不到的。因此,文件名要么由前端提前约定好,要么从请求的 URL 里解析出来,再通过postMessage一并传给页面。 - 大文件的内存警告:对于超大文件(比如超过 500MB),在 Service Worker 里一次性提取成 Blob 可能会引发内存压力,导致 Chrome 终止 Service Worker。针对这种情况,建议考虑分块下载,或者直接使用
StreamSaver.js这类库进行流式写入。
说到底,使用 Background Fetch 的真正难点,不在于发起一个下载任务,而在于让整个链路形成闭环:从页面触发,到 Service Worker 注册并执行下载,再到进度同步回页面,最后在下载成功后安全地将文件落地为用户可下载的实体。这其中的任何一环没处理好——比如忘了用 postMessage 通信、没处理 failedRecords、创建了 Blob URL 却没及时释放——都会让用户感觉操作“卡住了”或者“没反应”。
