Chrome扩展中如何通过XHR流式上传URL指向的文件?
高效流式衔接下载与上传(无内存占用)
这个问题的核心就是要实现无内存拷贝的流式传输——把下载到的文件数据直接“管道”到上传请求里,完全不用把整个文件塞进内存,自然也没有大小限制。我给你两种实用的实现方案,对应不同的服务器接收格式:
方案1:服务器支持直接接收原始文件流
如果你的服务器可以直接接收文件的原始二进制数据(不需要multipart/form-data格式),这个方案最简单,直接把下载的流作为上传请求的body:
async function streamUploadRawFile() { try { // 1. 发起流式下载请求 const downloadResp = await fetch(ctx.srcUrl, { credentials: 'include' // 对应原代码中的xhr.withCredentials = true }); if (!downloadResp.ok) { throw new Error(`下载失败:${downloadResp.status}`); } // 2. 直接把下载的流作为上传请求的body const uploadResp = await fetch('https://endpoint.com/upload', { method: 'POST', credentials: 'include', body: downloadResp.body, // 关键:传递ReadableStream,而非完整Blob headers: { // 复用原文件的Content-Type,让服务器识别文件类型 'Content-Type': downloadResp.headers.get('Content-Type') || 'application/octet-stream' } }); if (!uploadResp.ok) { throw new Error(`上传失败:${uploadResp.status}`); } console.log('流式上传成功!'); } catch (err) { console.error('上传出错:', err); } }
方案2:服务器要求multipart/form-data格式(和原代码兼容)
如果服务器必须用multipart/form-data(也就是原代码中FormData的格式),我们需要手动构造multipart的头部、尾部流,和文件流合并后发送——整个过程依然是流式的,不会把文件存进内存:
async function streamUploadFormData() { try { // 1. 发起流式下载请求 const downloadResp = await fetch(ctx.srcUrl, { credentials: 'include' }); if (!downloadResp.ok) { throw new Error(`下载失败:${downloadResp.status}`); } // 2. 准备multipart相关参数 const boundary = `----WebKitFormBoundary${Math.random().toString(16).slice(2)}`; const fileName = new URL(ctx.srcUrl).pathname.split('/').pop() || 'unknown-file'; const fileStream = downloadResp.body; // 3. 构造multipart的头部流(描述文件字段信息) const headerStream = new Blob([ `--${boundary}\r\n`, `Content-Disposition: form-data; name="upload"; filename="${fileName}"\r\n`, `Content-Type: ${downloadResp.headers.get('Content-Type') || 'application/octet-stream'}\r\n\r\n` ]).stream(); // 4. 构造multipart的尾部流(结束标记) const footerStream = new Blob([`\r\n--${boundary}--\r\n`]).stream(); // 5. 合并头部、文件流、尾部为一个连续的流 const combinedStream = new ReadableStream({ async start(controller) { // 先发送头部 const headerReader = headerStream.getReader(); let result; while (!(result = await headerReader.read()).done) { controller.enqueue(result.value); } // 再发送文件流(核心:边下载边上传) const fileReader = fileStream.getReader(); while (!(result = await fileReader.read()).done) { controller.enqueue(result.value); } // 最后发送尾部 const footerReader = footerStream.getReader(); while (!(result = await footerReader.read()).done) { controller.enqueue(result.value); } controller.close(); } }); // 6. 发起流式上传请求 const uploadResp = await fetch('https://endpoint.com/upload', { method: 'POST', credentials: 'include', body: combinedStream, headers: { 'Content-Type': `multipart/form-data; boundary=${boundary}` } }); if (!uploadResp.ok) { throw new Error(`上传失败:${uploadResp.status}`); } console.log('流式上传成功!'); } catch (err) { console.error('上传出错:', err); } }
关键优势说明
- 零内存占用:整个过程中,浏览器只会缓存一小段文件数据(几KB到几十KB),不会把整个大文件加载到内存里,完全规避了
FileReader的大小限制。 - 兼容性良好:Chrome 65+完全支持这些API,Chrome扩展的运行环境都是较新的Chrome版本,不用担心兼容问题。
- 和原代码逻辑对齐:方案2完全匹配你原有的
uploadFile函数的请求格式,不需要修改服务器端代码。
可选:用XHR替代Fetch
如果你更习惯用XHR,现代Chrome的XHR.send()也支持直接发送ReadableStream,把方案中的Fetch上传部分替换成下面的代码即可:
const xhr = new XMLHttpRequest(); xhr.open('POST', 'https://endpoint.com/upload'); xhr.withCredentials = true; xhr.setRequestHeader('Content-Type', `multipart/form-data; boundary=${boundary}`); // 方案2需要这个,方案1替换成对应Content-Type xhr.send(combinedStream); // 方案2用这个,方案1直接传downloadResp.body // 可以添加监听事件 xhr.onload = () => { if (xhr.status >= 200 && xhr.status < 300) { console.log('上传成功'); } else { console.error('上传失败'); } };
内容的提问来源于stack exchange,提问作者Alexander Craggs
相关产品推荐
相关产品推荐

