
本文介绍如何通过 http `range` 请求头精准获取 url 资源的前 n 字节(如 1 mb),避免浏览器持续下载冗余数据,并解决重复调用时失效的问题。相比流式读取 + `reader.cancel()`,服务端支持的分块请求更可靠、可重入且无需手动缓冲拼接。
在前端开发中,若仅需下载远程文件的前一部分(例如校验签名、解析头部元信息或预览内容),盲目拉取完整资源不仅浪费带宽,还可能因 fetch 流未正确终止导致后续请求失败——正如原代码中反复调用 download() 后出现的异常:reader.cancel() 并不能真正中断底层网络连接,且已消耗的响应体可能影响缓存状态或服务端连接复用,造成后续请求挂起或返回不完整响应。
推荐方案:优先使用 HTTP Range 请求
现代 Web 服务器(如 nginx、apache、cdn 及多数静态文件托管服务)普遍支持 Range 请求头。通过声明 Range: bytes=0-N,我们可直接向服务端申明“只需第 0 到第 N 字节”,服务端将返回 206 Partial Content 响应,且只传输指定范围的数据:
async function downloadRange(url, maxBytes) { try { const response = await fetch(url, { headers: { Range: `bytes=0-${maxBytes - 1}` } }); if (response.status === 206) { // 成功获取部分数据 const arrayBuffer = await response.arrayBuffer(); const result = new Uint8Array(arrayBuffer); const str = new TextDecoder().decode(result); console.log(`成功下载 ${result.length} 字节:`, str.substring(0, 100) + '...'); return result; } else if (response.status === 200) { // 服务端不支持 Range,返回了完整响应(如小文件) console.warn('Server does not support Range; full response received.'); const arrayBuffer = await response.arrayBuffer(); return new Uint8Array(arrayBuffer).slice(0, maxBytes); } else { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } } catch (error) { console.error('Download failed:', error); throw error; } }
✅ 优势显著:
- ✅ 可重入性强:每次请求都是独立的 HTTP 事务,无 reader 状态残留;
- ✅ 零内存缓冲开销:无需 chunks 数组累积、无需手动 Uint8Array.set() 拼接;
- ✅ 服务端协同节能:真正阻止多余字节下发,降低服务器与网络负载;
- ✅ 语义清晰:符合 HTTP/1.1 规范,调试时可通过 DevTools → Network 面板直观查看 206 Partial Content 及 Content-Range 响应头。
⚠️ 注意事项与兜底策略:
- 检查服务端支持性:并非所有服务都启用 Accept-Ranges: bytes。可在首次请求前发起 HEAD 探测:
async function supportsRange(url) { const headRes = await fetch(url, { method: 'HEAD' }); return headRes.headers.get('accept-ranges')?.toLowerCase() === 'bytes'; } - 处理小文件边界:若文件总大小
- 不支持 Range 时的降级:可结合 AbortController 实现流式中断(需注意其兼容性):
const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 5000); // 超时保护 const response = await fetch(url, { signal: controller.signal }); // ... 后续用 reader.read() + bytesRead 判断,但务必 clearTimeout(timeoutId)
综上,优先采用 Range 请求是解决“下载前 N 字节”问题的最佳实践。它简洁、标准、高效且健壮。仅当明确确认目标服务不支持分块时,再考虑基于 Readablestream 的流控方案,并务必配合 AbortController 与超时机制增强鲁棒性。