File.stream() 与 FormData 技术详解

概述

在现代 Web 开发中,处理文件上传和表单数据是常见需求。File.stream()FormData 是两个关键的 API,分别用于高效读取文件内容和构建多部分表单请求。本文将深入解析这两个技术的原理、用法、兼容性及最佳实践。


一、File.stream() 详解

1.1 什么是 File.stream()

File 接口继承自 Blob,而 Blob.prototype.stream() 方法(在支持的浏览器中可通过 File.stream() 调用)返回一个可读流(ReadableStream),允许以流式方式逐块读取文件内容,避免一次性将整个文件加载到内存中。

⚠️ 注意:File 对象本身没有 stream() 方法,但因其继承自 Blob,所以可以调用 file.stream()

1.2 基本用法

javascript 复制代码
const input = document.querySelector('input[type="file"]');
input.addEventListener('change', async (e) => {
  const file = e.target.files[0];
  if (!file) return;

  const stream = file.stream();
  const reader = stream.getReader();

  let chunks = [];
  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    chunks.push(value);
    console.log('Received chunk:', value); // Uint8Array
  }

  // 合并所有 chunk
  const uint8Array = new Uint8Array(chunks.reduce((acc, chunk) => acc + chunk.length, 0));
  let offset = 0;
  for (const chunk of chunks) {
    uint8Array.set(chunk, offset);
    offset += chunk.length;
  }

  const text = new TextDecoder().decode(uint8Array);
  console.log('Full content:', text);
});

1.3 使用 for await...of 简化代码(需转换为异步迭代器)

javascript 复制代码
const stream = file.stream();
const decoder = new TextDecoder();
let result = '';

for await (const chunk of stream) {
  result += decoder.decode(chunk, { stream: true });
}
result += decoder.decode(); // flush
console.log(result);

✅ 提示:for await...of 需要将 ReadableStream 转换为异步可迭代对象。某些浏览器可能不直接支持,可使用 stream.values() 或 polyfill。

1.4 优势

  • 内存友好:适用于大文件(如视频、日志等),避免 OOM(内存溢出)。
  • 响应式处理:可边读取边处理(如实时上传、加密、压缩)。
  • 与 Fetch API 无缝集成 :可直接作为 fetch 的 body。

1.5 兼容性

浏览器 支持情况
Chrome ≥ 71
Firefox ≥ 69
Safari ≥ 14.1
Edge ≥ 79

📌 参考 MDN Blob.stream()


二、FormData 详解

2.1 什么是 FormData

FormData 是一个用于构造键值对的接口,主要用于通过 XMLHttpRequestfetch 发送表单数据(尤其是包含文件的 multipart/form-data 请求)。

2.2 基本用法

从 HTML 表单构建
html 复制代码
<form id="uploadForm">
  <input name="username" value="alice">
  <input type="file" name="avatar">
</form>
javascript 复制代码
const form = document.getElementById('uploadForm');
const formData = new FormData(form);

// 自动包含所有带 name 的字段
手动构造
javascript 复制代码
const formData = new FormData();
formData.append('username', 'bob');
formData.append('avatar', file); // File 对象
formData.set('token', 'xyz123'); // set 会覆盖同名字段

2.3 与 Fetch 配合上传文件

javascript 复制代码
const response = await fetch('/upload', {
  method: 'POST',
  body: formData
  // 注意:不要手动设置 Content-Type!
  // 浏览器会自动设置 boundary
});

❗ 重要:不要手动设置 Content-Type,否则会破坏 multipart boundary,导致服务器无法解析。

2.4 读取 FormData 内容(调试用)

javascript 复制代码
for (const [key, value] of formData.entries()) {
  console.log(key, value); // value 可能是 string 或 File
}

2.5 兼容性

FormData 在所有现代浏览器中广泛支持(包括 IE10+)。但注意:

  • IE 不支持 entries()keys()values() 等迭代方法。
  • FormData 构造函数接受 <form> 元素作为参数的功能在 IE 中不可用。

三、结合使用:流式上传大文件

虽然 FormData 本身不直接支持流,但我们可以结合 File.stream() 实现分块上传(chunked upload):

示例:分片上传

javascript 复制代码
async function uploadChunked(file, chunkSize = 1024 * 1024) {
  const totalChunks = Math.ceil(file.size / chunkSize);
  const fileId = crypto.randomUUID();

  for (let i = 0; i < totalChunks; i++) {
    const start = i * chunkSize;
    const end = Math.min(start + chunkSize, file.size);
    
    // 创建文件切片(仍是 File/Blob)
    const chunk = file.slice(start, end, file.type);

    const formData = new FormData();
    formData.append('fileId', fileId);
    formData.append('chunkIndex', i);
    formData.append('totalChunks', totalChunks);
    formData.append('chunk', chunk);

    await fetch('/upload-chunk', {
      method: 'POST',
      body: formData
    });
  }

  // 通知服务器合并
  await fetch('/merge-chunks', {
    method: 'POST',
    body: JSON.stringify({ fileId, filename: file.name }),
    headers: { 'Content-Type': 'application/json' }
  });
}

💡 虽然这里没有直接使用 file.stream(),但 file.slice() 是实现分块的基础。若需更精细控制(如加密每个 chunk),可结合 stream() 读取原始字节。


四、常见问题与最佳实践

Q1: 能否将 ReadableStream 直接放入 FormData

不能FormData.append() 只接受 stringBlobFile 或类似对象。ReadableStream 不是有效的值。

✅ 解决方案:

  • 先将流转换为 Blob(适用于小文件):

    javascript 复制代码
    const blob = await new Response(stream).blob();
    formData.append('file', blob);
  • 或采用分块上传策略(推荐用于大文件)。

Q2: 如何监控上传进度?

使用 XMLHttpRequestupload.onprogress

javascript 复制代码
const xhr = new XMLHttpRequest();
xhr.upload.onprogress = (e) => {
  if (e.lengthComputable) {
    const percent = (e.loaded / e.total) * 100;
    console.log(`Uploaded: ${percent.toFixed(2)}%`);
  }
};
xhr.open('POST', '/upload');
xhr.send(formData);

fetch 原生不支持上传进度,需借助 ReadableStream + 自定义 body 实现(较复杂)。

Q3: 安全注意事项

  • 永远不要信任客户端文件名(file.name 可伪造)。
  • 服务端应校验文件类型(MIME + 文件头),而非仅依赖扩展名。
  • 限制上传大小,防止 DoS 攻击。

五、总结

特性 File.stream() FormData
主要用途 流式读取文件内容 构建 multipart/form-data 请求
内存效率 高(适合大文件) 低(整个文件需加载)
与 Fetch 集成 可作为 body 可作为 body(自动设 Content-Type)
浏览器支持 现代浏览器(Chrome 71+) 广泛(IE10+)
是否支持文件上传 需配合其他逻辑 原生支持

最佳实践建议

  • 小文件(<10MB):直接使用 FormData 上传。
  • 大文件或需进度/断点续传:使用 File.slice() + 分块上传。
  • 需要实时处理文件内容(如压缩、加密):使用 File.stream()

参考资料


FormData到底会不会引起内存溢出

我试了好多次上传大文件,并不会,很多文档说会 ------ 使用 FormData + 原生 <input type="file">File 对象上传大文件,通常不会导致浏览器内存溢出(OOM)。这是现代浏览器为大文件上传专门优化的安全机制。

前提是你没有主动把文件读入 JavaScript 内存。下面详细解释:


✅ 一、为什么 FormData 上传大文件 不会 内存溢出?

浏览器底层做了 流式(streaming)处理

当你这样写:

js 复制代码
const file = document.querySelector('input').files[0]; // File 对象
const formData = new FormData();
formData.append('file', file); // ← 关键:传的是 File 引用,不是内容!

axios.post('/upload', formData);
  • file 是一个 File 对象 ,它只是一个文件句柄(handle),包含路径、大小、类型等元数据
  • 文件内容并未加载到 JS 堆内存中
  • 浏览器在发送 HTTP 请求时,会直接从磁盘分块读取 → 网络发送 (类似 fs.createReadStream()
  • 整个过程 内存占用恒定(通常几 MB),与文件大小无关

📌 这是 W3C 标准设计,专为大文件上传优化。


⚠️ 二、什么情况下 导致内存溢出?

❌ 错误做法:用 FileReader / ArrayBuffer 读整个文件

js 复制代码
// 危险!将整个文件加载到内存
const reader = new FileReader();
reader.onload = (e) => {
  const arrayBuffer = e.target.result; // ← 1GB 文件 = 占用 1GB JS 内存!
  const blob = new Blob([arrayBuffer]);
  formData.append('file', blob);
  axios.post('/upload', formData);
};
reader.readAsArrayBuffer(file);

💥 后果:

  • 上传 2GB 文件 → 浏览器可能卡死、崩溃、标签页被杀
  • 内存监控(DevTools Memory)会看到明显峰值

❌ 其他危险操作:

  • await file.arrayBuffer()(ES2022)
  • new Response(file).arrayBuffer()
  • 把文件转成 Base64:btoa(await file.text())

这些都会强制将文件全部载入内存


✅ 三、如何安全上传超大文件(如 10GB)?

方案 1:直接用 FormData(推荐,适用于 ≤ 几 GB)

js 复制代码
const file = input.files[0];
const fd = new FormData();
fd.append('file', file); // 安全!

// 可加进度监听
axios.post('/upload', fd, {
  onUploadProgress: (e) => {
    console.log(`${(e.loaded / e.total * 100).toFixed(1)}%`);
  }
});

✅ 优点:简单、原生支持

⚠️ 注意:单个请求上传超大文件可能受服务器/网络限制(如 Nginx 默认 1MB)


方案 2:分片上传(Chunked Upload)------ 用于 >5GB 或断点续传

js 复制代码
const chunkSize = 10 * 1024 * 1024; // 10MB 每片
for (let start = 0; start < file.size; start += chunkSize) {
  const chunk = file.slice(start, start + chunkSize); // ← slice 不复制内存!
  const fd = new FormData();
  fd.append('chunk', chunk);
  await axios.post('/upload-chunk', fd);
}

file.slice() 返回的是 新的 File/Blob 引用不会复制数据到内存

后端需合并分片(如用 fs.createWriteStream 追加写入)。


🔍 四、验证:如何确认没占内存?

  1. 打开 Chrome DevTools → Memory 面板
  2. 点击 "Take heap snapshot" 记录初始内存
  3. 上传一个 1GB+ 文件 (用 FormData + File
  4. 上传过程中再拍快照
  5. 对比:JS Heap 大小几乎不变

如果你看到内存暴涨 → 说明你无意中读了文件内容!


🛡️ 五、服务端注意事项(避免 Node.js OOM)

虽然浏览器安全 ,但Node.js 服务端可能 OOM,如果:

  • 使用 multer.memoryStorage()(文件进内存)
  • 上传 1GB 文件 → 占用 1GB Node.js 堆内存

✅ 正确做法:用磁盘存储

js 复制代码
const upload = multer({
  dest: 'uploads/', // 文件直接写磁盘,不进内存
  limits: { fileSize: 5 * 1024 * 1024 * 1024 } // 限制 5GB
});

✅ 总结

上传方式 是否内存安全 适用场景
FormData + File(直接 append) ✅ 安全 小到中大文件(≤ 几 GB)
FileReader 读整个文件 ❌ 危险 应避免
分片上传(file.slice() ✅ 安全 超大文件、断点续传
服务端用 memoryStorage ❌ 危险 改用 diskStorage

🎯 只要你不主动读文件内容,FormData 上传任意大小文件都不会导致浏览器内存溢出。


如果你正在实现大文件上传,建议:

  1. 前端用 FormData + File
  2. 后端用 multer({ dest: '...' })
  3. 超大文件(>2GB)考虑分片 + 断点续传
相关推荐
Evand J14 小时前
MATLAB例程【二维,UKF,速度滤波】DVL与IMU的融合例程,模拟速度和惯导的融合,适用于二维平面、非线性的运动轨迹
开发语言·matlab·滤波·定位
TDengine (老段)14 小时前
TDengine JAVA 语言连接器入门指南
java·大数据·开发语言·数据库·python·时序数据库·tdengine
董世昌4114 小时前
如何声明一个类?类如何继承?
java·开发语言·前端
企微自动化14 小时前
企业微信 API 开发:如何实现外部群消息主动推送
java·开发语言·spring
love530love14 小时前
EPGF 新手教程 04一个项目一个环境:PyCharm 是如何帮你“自动隔离”的?(全 GUI,新手零命令)
运维·开发语言·ide·人工智能·python·pycharm
2501_9419820514 小时前
企业微信 API 外部群主动推送技术解析
前端·chrome
Grassto14 小时前
Go 在哪里找第三方包?Module 查找顺序详解
开发语言·后端·golang
小鸡脚来咯14 小时前
后端开发vue速成
开发语言·前端·javascript
糯诺诺米团14 小时前
C++多线程打包成so给JAVA后端(Ubuntu)<2>
java·开发语言·c++