service worker 能拦截 xmlhttprequest 的 post 请求但无法缓存其响应,因请求体单向消耗导致 fetch 失败;cache.put() 对上传无效因其响应无重放价值;应改用 fetch + 预读 arraybuffer 构造可复用请求,并由前端控制离线队列与 sync 重发。

Service Worker 能否拦截 XMLHttpRequest 的 POST 请求?
不能直接缓存上传请求的响应体,但可以拦截并阻止默认行为——关键在于:浏览器对 XMLHttpRequest(尤其是带 send(body) 的 POST)的请求体读取是单向且不可复用的。一旦你在 fetch 事件中调用 event.request.clone().arrayBuffer() 或类似方法,原始请求体就被消耗掉了,后续 fetch(event.request) 会失败并抛出 TypeError: Failed to execute 'fetch' on 'Window': Request body is already used。
为什么 cache.put() 对上传请求基本无效?
因为缓存的是响应,而 XML 上传这类请求的目标通常是服务端处理动作(如提交表单、触发任务),其响应往往无意义(200 OK 空体、204 No Content),或含动态时间戳/ID,不具备可重放性。强行缓存会导致:
- 重复提交(用户刷新页面后 Service Worker 返回旧响应,但服务端已执行过)
- 缓存击穿(响应体为空或极小,
cache.put()成功但无实际价值) - 请求体丢失导致 fetch 失败,降级为离线失败
真正可行的替代方案:用 fetch() + FormData + Request 构造可拦截的上传
必须放弃原生 XMLHttpRequest,改用 fetch() 发起上传,并确保请求体可被多次读取。核心是:不直接传原始 FormData 或 Blob,而是提前转为 ArrayBuffer 或 Uint8Array,再构造新 Request:
self.addEventListener('fetch', event => {
if (event.request.method === 'POST' && event.request.url.includes('/upload')) {
event.respondWith((async () => {
try {
// 1. 提前读取 body(只做一次)
const bodyArray = await event.request.arrayBuffer();
// 2. 构造可复用的新 Request(注意:headers 需手动复制,Content-Length 会自动计算)
const newRequest = new Request(event.request.url, {
method: 'POST',
headers: Object.fromEntries(event.request.headers),
body: bodyArray
});
// 3. 可选:先尝试缓存匹配(对上传场景通常跳过)
// const cached = await caches.match(newRequest);
// if (cached) return cached;
// 4. 转发请求到网络
const response = await fetch(newRequest);
// 5. 缓存响应?仅当响应有明确重用价值(如上传后返回资源 URL)
// if (response.status === 200) {
// const cache = await caches.open('uploads');
// await cache.put(newRequest, response.clone());
// }
return response;
} catch (err) {
return new Response('Upload failed', { status: 503 });
}
})());
}
});
注意:event.request.arrayBuffer() 是唯一安全读取方式;用 text() 或 json() 会破坏二进制数据;FormData 实例无法被 clone() 后重复使用,必须转底层字节。
离线上传的兜底逻辑必须由前端主动控制
Service Worker 不适合自动重放上传请求。正确做法是:
- 前端在发起
fetch('/upload', { method: 'POST', body: formData })前,先检查navigator.onLine - 若离线,将序列化后的请求数据(URL、headers、body ArrayBuffer base64)存入
IndexedDB - Service Worker 中监听
sync事件,在恢复联网后主动拉取待上传队列并重发 - 避免在
fetch事件里自动重试——用户可能已关闭页面,或数据已过期
真正的难点不在拦截,而在语义:上传不是 GET,它代表状态变更。缓存它的“结果”不如保证它的“送达”。fetch 事件里能做的只是可控转发,而非魔法缓存。










