Service Worker · API
Background Fetch API
发布于
Background Fetch API 让页面或 service worker 把一组请求作为一个有名字的下载任务交给浏览器,标签页关闭后任务继续进行;浏览器在自己的界面中显示进度与取消控件,并在任务落定时在 service worker 中触发事件。它自 Chrome 74、Edge 79、Samsung Internet 11 起可用,Firefox 与 Safari 均未实现(BCD api.BackgroundFetchManager),因此它只能作为普通 fetch() 下载之上的增强,而不是替代。
// 页面或 service worker 中;registration 来自 navigator.serviceWorker.readyconst bgFetch = await registration.backgroundFetch.fetch(id, requests, options);const existing = await registration.backgroundFetch.get(id); // 没有该 id 的任务时为 undefinedconst ids = await registration.backgroundFetch.getIds(); // string[]
// service worker 中self.addEventListener('backgroundfetchsuccess', (event) => { /* event.registration */ });self.addEventListener('backgroundfetchfail', (event) => { /* event.registration.failureReason */ });self.addEventListener('backgroundfetchabort', (event) => { /* 用户或应用取消 */ });self.addEventListener('backgroundfetchclick', (event) => { /* 用户点击了浏览器界面 */ });fetch() 在浏览器接受任务后立即返回 Promise<BackgroundFetchRegistration>,远早于任何字节到达。任务落定时,backgroundfetchsuccess、backgroundfetchfail、backgroundfetchabort 三者恰好触发一个;backgroundfetchclick 只在用户点击浏览器的下载界面时触发。前两个是 BackgroundFetchUpdateUIEvent 实例,其 updateUI({ title, icons }) 可修改完成后的通知;后两个是普通的 BackgroundFetchEvent 实例。
fetch() 接受两个必填参数和一个选项对象。
| 名称 | 类型 | 说明 |
|---|---|---|
id |
string |
开发者自定的标识符,之后传给 get(id)。每个 id 同时只能有一个进行中的注册;第一个任务未完成时复用同一 id 会被拒绝。 |
requests |
RequestInfo | RequestInfo[] |
一个 Request 或 URL 字符串,或它们的数组。所有请求作为一个用户可见的任务一起下载。mode: 'no-cors' 的请求会被拒绝。 |
options.title |
string |
浏览器进度界面中显示的标题。 |
options.icons |
ImageResource[] |
含 src 以及可选的 sizes、type、label 的对象,用于进度界面。 |
options.downloadTotal |
number |
预估总大小(字节)。它驱动进度条,同时也是硬上限:已下载字节一旦超过它,浏览器就会中止任务,并把 failureReason 置为 "download-total-exceeded"。 |
返回的 BackgroundFetchRegistration 暴露 id、downloadTotal、downloaded、uploadTotal、uploaded、result(""、"success" 或 "failure")、failureReason(""、"aborted"、"bad-status"、"fetch-error"、"quota-exceeded" 或 "download-total-exceeded")、recordsAvailable、abort()、match()、matchAll(),以及一个在任一计数器或 result 变化时触发的 progress 事件。
fetch() 在以下情况拒绝其 Promise(MDN,BackgroundFetchManager.fetch())。
TypeError:没有给出任何请求、某个请求使用了mode: 'no-cors'、注册上没有 service worker、同一id的注册已存在,或请求对象无法创建。AbortError(DOMException):任务在被接受之前就被中止。NotAllowedError(DOMException):用户代理未授予该源发起后台下载的权限;规范把这项权限检查留给浏览器,因此具体条件由浏览器定义。QuotaExceededError:存储请求时超出了该源的存储配额。响应在 service worker 读取之前保存在与 Cache Storage 同类的存储中,因此与caches计入同一份配额。
下载失败不会让任何 Promise 被拒绝:它触发 backgroundfetchfail,并设置 event.registration.failureReason。"bad-status" 表示某个响应的状态码不是 ok;"fetch-error" 表示网络请求本身在重试后仍然失败。
两个示例都假定已在 /sw.js 注册了 service worker,且页面处于安全源。
把一集音频与封面图作为一个任务下载
Section titled “把一集音频与封面图作为一个任务下载”页面以同一个 id 请求音频文件及其封面,浏览器因此只显示一条进度通知;downloadTotal 是此前从服务端 API 取得的两个 Content-Length 之和。
async function downloadEpisode(episode) { const registration = await navigator.serviceWorker.ready; const bgFetch = await registration.backgroundFetch.fetch( `episode-${episode.id}`, [episode.audioUrl, episode.artworkUrl], { title: episode.title, icons: [{ src: '/icons/download-192.png', sizes: '192x192', type: 'image/png', label: 'Episode download' }], downloadTotal: episode.audioBytes + episode.artworkBytes, }, ); bgFetch.addEventListener('progress', () => { const percent = Math.round((bgFetch.downloaded / bgFetch.downloadTotal) * 100); document.querySelector('#progress').value = percent; });}progress 监听器只在这个页面打开期间运行;标签页关闭后,浏览器自己的界面就是唯一的进度指示,这正是该 API 的意义所在。
任务落定后保存响应
Section titled “任务落定后保存响应”service worker 在 backgroundfetchsuccess 中把每个响应复制到一个命名缓存,然后修改通知文字;失败时记录原因并保持通知原样,让用户看到浏览器的失败状态。
self.addEventListener('backgroundfetchsuccess', (event) => { event.waitUntil((async () => { const cache = await caches.open('episodes'); const records = await event.registration.matchAll(); await Promise.all(records.map(async (record) => { const response = await record.responseReady; await cache.put(record.request, response); })); await event.updateUI({ title: `${event.registration.id} is ready to play` }); })());});
self.addEventListener('backgroundfetchfail', (event) => { console.warn('background fetch failed:', event.registration.failureReason);});
self.addEventListener('backgroundfetchclick', () => { clients.openWindow('/downloads/');});record.responseReady 是一个 Promise,因为部分失败的任务触发事件时,某条记录的响应可能仍在传输;在 waitUntil() 内 await 它可以让 worker 存活到复制完成。
检测支持并改为前台下载
Section titled “检测支持并改为前台下载”Firefox 与 Safari 的注册对象上没有 backgroundFetch 属性。回退方案执行普通 fetch(),并通过临时的 <a download> 触发浏览器的保存对话框;它只在页面保持打开时有效,界面应明确告知这一点。
async function downloadWithFallback(id, urls, title) { const registration = await navigator.serviceWorker.ready; if ('backgroundFetch' in registration) { return registration.backgroundFetch.fetch(id, urls, { title }); } for (const url of urls) { const response = await fetch(url); if (!response.ok) throw new Error(`${url}: ${response.status}`); const link = document.createElement('a'); link.href = URL.createObjectURL(await response.blob()); link.download = new URL(url, location.href).pathname.split('/').pop(); link.click(); URL.revokeObjectURL(link.href); }}前台路径会在保存前把整个文件缓冲在内存中,因此超大文件应逐个下载,并提醒用户不要离开页面。
- Background Fetch specification: fetch() method(wicg.github.io)
- Introducing Background Fetch(developer.chrome.com)
- Debug background services(developer.chrome.com)
- Background Fetch 浏览器支持
- Background Sync API
- Cache API
- 配额与 StorageManager.estimate()
规范
| 规范 | 状态 |
|---|---|
| Background Fetch(后台获取) | WICG 草案 |
- 图例
- 支持
- 部分支持
- 需开启标志
- 不支持
- 未知
| 浏览器 / 平台 | 支持 | 版本 | 置信度 | 来源 | 备注 |
|---|---|---|---|---|---|
| Chrome (Desktop) | 支持 | 74 | 高 | 来源 | — |
| Chrome (Android) | 支持 | 74 | 高 | 来源 | 1 |
| Edge (Desktop) | 支持 | 79 | 高 | 来源 | 2 |
| Firefox (Desktop) | 不支持 | — | 高 | 来源 | 3 |
| Firefox (Android) | 不支持 | — | 高 | 来源 | 45 |
| Safari (macOS) | 不支持 | — | 高 | 来源 | 6 |
| Safari (iOS) | 不支持 | — | 高 | 来源 | 78 |
| Samsung Internet | 支持 | 11.0 | 高 | 来源 | 9 |
| WebView (Android) | 不支持 | — | 高 | 来源 | 10 |
- 由 browser-compat-data 镜像自 Chrome 的数据推导。
- 由 browser-compat-data 镜像自 Chrome 的数据推导。
- browser-compat-data 未记录 Firefox 的支持。
- browser-compat-data 未记录 Firefox for Android 的支持。
- 由 browser-compat-data 镜像自 Firefox 的数据推导。
- browser-compat-data 未记录 Safari 的支持。
- browser-compat-data 未记录 iOS 版 Safari 的支持。
- 由 browser-compat-data 镜像自 Safari 的数据推导。
- 由 browser-compat-data 镜像自 Chrome Android 的数据推导。
- browser-compat-data 未记录 WebView Android 的支持。