离线传输的Service Worker文件缓存
使用Service Workers实现离线文件传输能力。缓存策略、后台同步与渐进式Web应用模式。
Service Worker 让文件传输应用在网络断开时仍能继续工作:用 Cache API 缓存 HTML 外壳和 JS,用 Background Sync API 排队失败的上传,将部分分块存入 IndexedDB,连接恢复时重放所有内容。Worker 运行在独立线程上,有自己的事件循环,拦截其作用域内的 fetch 事件,在标签页关闭后仍然存活。对于上传工具,正确的方案是:应用外壳采用过期即重新验证策略,在途传输使用 IndexedDB 支持的分块队列,并注册每15分钟重试一次的周期性 Background Sync,直到上传成功。
Service Worker 对传输应用的实际价值
最大的收益是 navigator.serviceWorker 能在标签页刷新、离线期间乃至手机进入睡眠状态后仍然存活。当用户在不稳定的高铁 Wi-Fi 上开始2GB上传时,你希望已成功的分块保持已成功状态,正在传输的分块在重连时重试,整个状态在浏览器关闭标签页以回收内存后仍可恢复。Service Worker——独立于任何具体标签页运行——是实现这一切的关键。
该 API 提供三个构建块:Cache 用于按 URL 存储响应,IndexedDB 用于结构化数据(分块队列、会话状态),SyncManager 用于在设备在线时触发重试。
注册和版本控制 Worker
在应用加载时注册一次,显式处理更新:
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js', { scope: '/' })
.then((reg) => reg.addEventListener('updatefound', () => {
const sw = reg.installing;
sw.addEventListener('statechange', () => {
if (sw.state === 'installed' && navigator.serviceWorker.controller) {
// 新版本就绪,提示用户刷新
}
});
}));
}
对缓存键进行版本管理(transfer-v7),让新部署使旧资源失效,同时不留下过时的 JavaScript。经典 bug:index.html 永久缓存,用户从未获得更新,你花好几周靠微信群调试。将缓存锁定到构建哈希,并在 activate 事件中清理旧缓存。
应用外壳与用户数据的缓存策略
不同资源需要不同策略:
- 应用外壳(HTML、CSS、JS、图标):缓存优先配网络回退。即时加载,离线可用。
- API 元数据(
/shares/:id):网络优先配缓存回退,60秒 TTL。在线时获取最新,离线时可用。 - 文件字节:从不缓存。文件通常数GB,Cache API 有源配额(通常是可用磁盘的60%)。
- CDN 字体:过期即重新验证。快速且自动更新。
在 fetch 处理器中:
self.addEventListener('fetch', (e) => {
const url = new URL(e.request.url);
if (url.pathname.startsWith('/assets/')) {
e.respondWith(cacheFirst(e.request, 'shell-v7'));
} else if (url.pathname.startsWith('/api/shares/')) {
e.respondWith(networkFirst(e.request, 'api-v1', 60));
}
});
永远不要拦截二进制文件上传的请求——通过检查 e.request.method === 'PUT' 并提前返回来绕过它们。通过 Worker 代理 GB 级 PUT 请求是内存灾难。
用 Background Sync 排队失败的上传
SyncManager 是构建弹性上传的关键。当某个分块 PUT 失败时,将其暂存到 IndexedDB 并注册同步:
// 在页面代码中
const reg = await navigator.serviceWorker.ready;
await reg.sync.register('flush-uploads');
// 在 sw.js 中
self.addEventListener('sync', (event) => {
if (event.tag === 'flush-uploads') {
event.waitUntil(flushPendingUploads());
}
});
浏览器在网络恢复后触发 sync 事件,采用指数退避直至约24小时。Chrome 和 Edge 支持此功能;Safari 在17.5版本下通过"Background Fetch"标志支持子集,但需要用户权限。对于 Safari,回退到在下次页面可见时通过 visibilitychange 事件重试。
Background Fetch API 是专门针对大文件操作的独立工具——它在浏览器 UI 中显示持久通知,让用户即使关闭标签页后也能追踪进度。超过500MB的上传值得使用。
在 IndexedDB 中存储部分上传
IndexedDB 是你的持久临时空间。打开一个小型数据库,暂存会话元数据和分块偏移量:
const db = await openDB('transfers', 1, {
upgrade(db) {
db.createObjectStore('sessions', { keyPath: 'id' });
db.createObjectStore('chunks', { keyPath: ['sessionId', 'index'] });
}
});
await db.put('sessions', {
id, fileName, fileSize, fileFingerprint, createdAt: Date.now(),
completedIndexes: [], partUrls
});
不要存储原始分块字节——它们来自 File 句柄,IndexedDB 可以将其作为结构化克隆引用持久化,在重新加载后仍然有效。存储句柄避免在数据库中重复2GB的字节。
源存储有限制:桌面 Chrome 约为可用磁盘的60%,iOS Safari 每源1GB,之后触发驱逐压力。调用 navigator.storage.persist() 获得浏览器不会自动驱逐的"持久"存储桶。
处理离线和在线切换
在页面和 Service Worker 中监听 online 和 offline 事件:
// 页面
window.addEventListener('online', () => {
ui.showBanner('已重新连接——继续上传');
navigator.serviceWorker.controller?.postMessage({ type: 'resume' });
});
window.addEventListener('offline', () => {
ui.showBanner('已离线——上传已暂停');
});
navigator.onLine 在企业强制门户下出了名的不可靠——当设备有本地网络连接但没有互联网时,它报告 true。对于可靠的检测,发起一个带3秒超时的小 fetch('/ping', { cache: 'no-store' })。
让它成为真正的 PWA
提供一个带 display: standalone、图标集和 start_url: / 的 manifest.json。为 iOS 添加 apple-touch-icon 链接。声明文件处理,让操作系统可以将你的应用与特定扩展名关联:
{
"name": "Hex Transfer",
"file_handlers": [{
"action": "/share-target",
"accept": { "application/*": [".pdf", ".zip", ".docx"] }
}]
}
结合 Web Share Target,这让用户可以从操作系统的分享菜单直接分享文件到你的应用。在 Chrome Android 和桌面 Chromium 上,PWA 可以注册为你声明的文件类型的默认处理程序。这将浏览器页面变成行为类似原生上传工具的应用。
测试离线场景
三个需要手动测试的场景,因为自动化离线测试容易不稳定:
- 在快速 Wi-Fi 上开始500MB上传,在30%时切换到飞行模式,等待30秒,再打开 Wi-Fi。上传应该从停止的地方恢复,不需要用户操作。
- 在60%时开始上传并关闭标签页,等待2分钟后重新打开。提供恢复会话的选项。
- 在手机上开始上传,锁屏5分钟。解锁时 Background Sync 应该触发并完成传输。
Chrome DevTools 的"离线"复选框和Application > Service Workers > Update on reload不可或缺。网络面板的"节流"配置文件让你模拟快速3G和慢速3G,观察错误 UI 的表现。
HexaTransfer 的 Web 应用使用 Service Worker 进行应用外壳缓存,使用 IndexedDB 存储在途会话状态,因此刷新和短暂离线不会丢失上传进度。免费试用 hexatransfer.com——无需注册,单次最大10GB。
值得了解的陷阱
Service Worker 有一小堆坑会让新手吃苦:只在 HTTPS 上运行(localhost 除外),各浏览器缓存配额差异巨大,iOS Safari 不能可靠地为 Background Sync 唤醒 Worker,DevTools 会激进地缓存过时 Worker(开发时务必点击"Bypass for network"),importScripts 在安装期间同步运行,所以不要在那里请求慢速第三方脚本。编写一个小集成测试,检查 Worker 是否激活、是否接管客户端、是否提供离线页面——这一个测试能捕获你在生产中遇到的80%的回归问题。