上一篇我们实现了最基础的 FormData 上传,体会了前后端的基础联调。但如果在面试中被问到:"如果用户上传了一个 10GB 的文件怎么办?"或者"用户同时选了 100 张图片,浏览器卡死怎么办?"这就需要用到进度监听、双端校验与并发控制。
1 核心概念
文件校验
需要遵循"前端防误操作,后端防恶意攻击"的原则,分为三个层级:
-
第一层:前端 JS 校验 通过 input 标签属性和 JavaScript 读取 file.size 与 file.type。
-
第二层:后端请求头校验 使用 Node.js 的 multer 中间件,读取 HTTP 请求头中的 Content-Type 和限制文件流大小。能拦截超大文件,防止服务器内存溢出。
-
第三层:文件二进制校验 读取文件底层二进制数据的前 8 个字节,与标准文件类型的十六进制码进行比对。这是最高级别的安全校验。无论用户怎么改后缀、改请求头,文件底层的二进制编码是无法伪造的。
并发控制
如果不做控制,直接通过 Promise.all 一次性发送 100 个文件,会导致两个严重问题:
-
浏览器 TCP 连接限制, Chrome 浏览器规定,对同一个域名最多只能同时维持 6 个网络连接。多出的 94
个请求会被强制挂起(Pending)。排队时间过长会导致后面的请求直接超时(Timeout)断开。
-
前端内存占用过高100 个大文件的 FormData 实例会瞬间占满内存,触发浏览器垃圾回收(GC),直接导致页面卡死。
2 前端实现:进度条与取消请求
我们需要使用 Axios/XHR 而不是 fetch, 因为 fetch 具有局限性。fetch API 设计上不支持监听上传进度(只能通过 response.body.getReader() 监听下载进度)。要实现上传进度条,底层必须依赖 XMLHttpRequest 的 upload.onprogress 事件,这也是我们选用 Axios 的原因。
在前端,我们给 Axios 配置 onUploadProgress 来获取进度。同时,利用现代 JS 的 AbortController 来实现"取消上传"功能
ts
<script setup lang="ts">
import axios from 'axios'
import { ref } from 'vue'
const selectedFile = ref<File | null>(null)
const message = ref('')
const progress = ref(0)
// 定义中断控制器
let abortController: AbortController | null = null
const uploadFile = async () => {
if (!selectedFile.value) return
// 1. 前端基础校验:大小限制 10MB
if (selectedFile.value.size > 10 * 1024 * 1024) {
message.value = '文件不能超过 10MB'
return
}
const formData = new FormData()
formData.append('file', selectedFile.value)
abortController = new AbortController()
try {
message.value = '正在上传...'
const res = await axios.post('http://localhost:3000/upload', formData, {
signal: abortController.signal, // 绑定取消信号
// 监听上传进度
onUploadProgress: (progressEvent) => {
if (progressEvent.total) {
progress.value = Math.round((progressEvent.loaded * 100) / progressEvent.total)
}
}
})
message.value = `上传成功: ${res.data.filename}`
} catch (error: any) {
// 捕获取消操作的特殊错误
if (axios.isCancel(error)) {
message.value = '上传已取消'
} else {
message.value = error.response?.data?.msg || '上传失败'
}
}
}
// 取消上传的触发函数
const cancelUpload = () => {
if (abortController) abortController.abort()
}
</script>
在 Vue 3 中,不要把 File 对象直接放到 reactive 里,这会导致性能问题。使用 ref 并通过 .value 操作即可。
3 后端实现:多文件与拦截器(实现并发和安全防线)
后端我们继续使用 Multer,但这次升级为 upload.array() 以支持多文件,并加上了 limits 和 fileFilter。这一步体现了后端对内存和存储的保护。
js
const multer = require("multer");
// 配置安全防线:文件过滤器
const fileFilter = (req, file, cb) => {
const allowedTypes = ["image/jpeg", "image/png", "application/pdf"];
if (allowedTypes.includes(file.mimetype)) {
cb(null, true); // 放行
} else {
cb(new Error("仅支持 JPG/PNG/PDF 文件"), false); // 拒绝
}
};
const upload = multer({
storage: storage,
fileFilter: fileFilter,
limits: {
fileSize: 10 * 1024 * 1024, // 限制单文件最大 10MB
files: 5 // 限制单次请求最多 5 个文件
}
});
// 使用 upload.array 接收多文件,字段名为 "files"
app.post("/upload", upload.array("files", 5), (req, res) => {
res.json({
msg: "上传成功",
files: req.files.map(f => f.filename) // 返回所有文件的名字
});
});