← 返回题目列表

Node.js 如何处理文件上传和大文件传输?

中等 第 21 / 27 题 更新于 2026/07/29
Node.js文件上传大文件Stream

简化版

Node.js 处理大文件要优先使用流式处理,避免把完整文件读入内存。上传场景要限制大小、类型、并发和路径,落盘或上传对象存储时要处理背压、临时文件清理、校验和安全扫描。

详细版

大文件上传最怕一次性读入内存。 一个 500 MB 文件如果完整放进 Buffer,几个并发就可能把进程打爆。 应使用 multipart 解析库或 stream 管道,把数据分块写入磁盘或对象存储。

import { pipeline } from 'node:stream/promises'
import fs from 'node:fs'

await pipeline(req, fs.createWriteStream('/tmp/upload.bin'))

真实项目还要做文件大小限制、扩展名和 MIME 校验、随机文件名、目录穿越防护、失败清理、断点续传或分片上传。

完整版教学

一、大文件问题的本质是内存和背压

Node.js 很适合 I/O,但不代表可以随便把大文件放进内存。 如果每个上传 200 MB,同时 20 个用户上传,理论数据量就是 4 GB。 这远超多数 Node 进程的安全堆内存。

错误思路:
读取完整文件 -> Buffer -> 再写入

正确思路:
请求流 -> 分块处理 -> 写入磁盘/对象存储

流式处理让内存占用接近分块大小,而不是文件总大小。 这也是 Node 处理大文件的核心姿势。

二、pipeline 能正确处理错误和背压

手动监听 data 事件容易忽略背压。 pipeline 可以把 readable、transform、writable 串起来,并在错误时自动销毁管道。 它比简单 req.pipe(writeStream) 更适合工程代码。

import { pipeline } from 'node:stream/promises'
import { createWriteStream } from 'node:fs'

await pipeline(
  req,
  createWriteStream(filePath)
)
写法优点风险
data 手动拼接简单直观易爆内存
pipe流式处理错误处理容易漏
pipeline处理背压和错误推荐工程使用

背压的意思是写入端处理不过来时,读取端应该慢下来。 否则内存会堆积越来越多待写数据。

三、multipart 上传需要专门解析

浏览器表单上传常用 multipart/form-data。 请求体里不只有文件字节,还有边界、字段名、文件名等元数据。 不要自己从零解析,通常使用 busboy、multer、formidable 等成熟库。

multipart body
├─ field: userId
├─ file: avatar.png
└─ field: type

解析库的配置要设置文件大小、字段数量、文件数量限制。 如果没有限制,攻击者可以上传超大文件或构造大量字段消耗资源。

四、文件名和路径必须做安全处理

用户上传的原始文件名不能直接作为服务端路径。 可能包含 ../、特殊字符、重复名或控制字符。 服务端应生成随机文件名,原始文件名只作为展示元数据保存。

import crypto from 'node:crypto'

const safeName = `${crypto.randomUUID()}.bin`
风险示例防护
路径穿越../../app.js不信任原始文件名
覆盖文件avatar.png 重名使用随机名
类型伪造.jpg 实为脚本内容嗅探和白名单
超大文件10 GB 上传大小限制

文件上传是安全入口,不是单纯 I/O 功能。 面试里补这些点会很加分。

五、失败和取消时要清理临时文件

上传过程中可能网络断开、校验失败、磁盘写满、对象存储失败。 如果临时文件不清理,磁盘会慢慢被打满。 因此上传流程要有 finally 清理和后台巡检。

let completed = false
try {
  await pipeline(req, createWriteStream(tmpPath))
  completed = true
} finally {
  if (!completed) {
    await fs.promises.rm(tmpPath, { force: true })
  }
}

假设每天失败上传 1000 次,每次残留 20 MB,一天就是 20 GB。 临时文件清理不是小事。

六、大文件下载也要流式输出

下载同样不能一次性 readFile 大文件。 应使用 createReadStream 或对象存储签名 URL。 如果文件很大,优先让客户端直传直下对象存储,Node 只负责签名和鉴权。

记忆钩子:大文件别进内存,Node 只当传送带;上传要限,失败要扫尾。

服务端如果必须中转,就用流和 pipeline。 如果能走对象存储直传,架构上更省资源。

七、常见误区与追问

  • 误区:Node.js 处理文件就是把 Buffer 拼起来。 大文件拼 Buffer 会占用大量内存,应流式处理。
  • 误区:前端限制文件大小就够了。 服务端必须再次限制大小、数量、类型和并发。
  • 误区:用户上传文件名可以直接落盘。 原始文件名不可信,应使用随机名并防路径穿越。
  • 追问:pipeline 比 pipe 好在哪里? pipeline 更完整地处理背压、错误传播和资源销毁。
  • 追问:对象存储直传有什么好处? 减少 Node 中转流量和带宽压力,Node 只做鉴权和签名。
  • 追问:上传失败要注意什么? 清理临时文件、释放流、记录失败原因,并避免磁盘残留。

八、加强记忆

文件上传题按“流式、安全、清理”记。大文件不要进内存,用 stream 和 pipeline;上传要限制大小、类型、数量和路径;失败取消要清理临时文件;大流量场景优先对象存储直传。