文件上传
获取文件数据
前端实现文件上传,获取文件的方式主要有两种:
input 选择文件
<button id="btn">点击</button>
<input type="file" id="file" style="display:none" />
// ...
const btnNode = document.getElementById('btn');
const fileNode = document.getElementById('file');
btnNode.addEventListener("click", () => {
fileNode.click();
}, false);
fileNode.addEventListener('change', function (e) {
// e.target.files / fileNode.files 可以拿到文件数据
const file = e.target.files[0];
if (!file) return;
// 可以通过 FileReader 读取文件内容,输出到预览窗口
const fileReader = new FileReader();
// onload 是属性赋值,不是函数调用
fileReader.onload = (event) => {
console.log(event.target.result);
};
fileReader.readAsText(file); // 也可 readAsDataURL(图片预览)/ readAsArrayBuffer
}, false);
拖拽文件
<div id="unload" draggable="true">
将文件拖到此处上传
</div>;
// ...
let unloadNode = document.getElementById("unload");
unloadNode.addEventListener(
"dragenter",
(e) => {
// 禁用浏览器默认事件
e.preventDefault();
e.stopPropagation();
},
false
);
unloadNode.addEventListener(
"dragover",
(e) => {
e.preventDefault();
e.stopPropagation();
},
false
);
unloadNode.addEventListener(
"drop",
function (e) {
e.preventDefault();
e.stopPropagation(); // e.dataTransfer.files
//可以拿到文件数据
},
false
);
以上两种交互的目的是获得文件数据,是一个 File 对象,之后借助 FormData 包装文件数据后通过异步请求发送到后端
const formData = new FormData();
// 注意是 append 而非 add;同名 key 可多次 append 实现多文件
formData.append("file", file);
// fetch
fetch("http://localhost:8080/upload", {
method: "POST", // 也可以是 PUT 请求
body: formData,
})
.then((res) => {})
.catch((err) => {});
// axios
// axios.post("http://localhost:8000/upload", formData);
用
FormData时不要手动设置Content-Type,浏览器会自动加上multipart/form-data和分隔用的boundary,手动设置反而会因缺少 boundary 导致后端解析失败。详见 MDN-Content-Type
上传多个文件
<input type="file" multiple />,遍历 e.target.files 逐个 append 到同一个 FormData 即可。
大文件分片上传(高频场景题)
面试题:几个 G 的大文件如何上传?如何做断点续传和秒传?
核心思路:把大文件用 File.prototype.slice 切成多个小块(chunk),分别上传,后端再合并。
1. 文件分片
const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB 一片
function createChunks(file, size = CHUNK_SIZE) {
const chunks = [];
for (let start = 0; start < file.size; start += size) {
chunks.push(file.slice(start, start + size)); // Blob.slice
}
return chunks;
}
2. 计算文件 hash(秒传 / 唯一标识)
用文件内容算出唯一 hash(如 MD5/SHA),作为文件指纹:
- 秒传:上传前先拿 hash 问后端「这个文件存在吗」,存在则直接返回,无需重复上传。
- 计算 hash 很耗时,应放到 Web Worker(配合
spark-md5)里算,避免卡主线程;超大文件可抽样(首尾 + 中间若干片)做「布隆式」快速 hash。
// worker 中:用 spark-md5 增量计算,避免一次性读入内存
import SparkMD5 from "spark-md5";
async function calcHash(chunks) {
const spark = new SparkMD5.ArrayBuffer();
for (const chunk of chunks) {
spark.append(await chunk.arrayBuffer());
}
return spark.end();
}
3. 断点续传
上传前向后端查询「该 hash 已上传了哪些分片」,跳过已上传的,只传缺失的分片。刷新/断网后续传不用从头再来。
async function uploadFile(file) {
const chunks = createChunks(file);
const hash = await calcHashInWorker(chunks);
// 秒传校验 + 查询已上传分片
const { uploaded, exists } = await checkUploaded(hash);
if (exists) return; // 秒传命中
const tasks = chunks
.map((chunk, index) => ({ chunk, index }))
.filter(({ index }) => !uploaded.includes(index)) // 断点续传:跳过已传
.map(({ chunk, index }) => () => {
const fd = new FormData();
fd.append("file", chunk);
fd.append("hash", hash);
fd.append("index", index);
return fetch("/upload/chunk", { method: "POST", body: fd });
});
// 并发控制上传(见下)
await asyncPool(4, tasks);
// 通知后端合并
await fetch("/upload/merge", {
method: "POST",
body: JSON.stringify({ hash, total: chunks.length, name: file.name }),
});
}
4. 并发控制与进度
- 并发控制:不要一次性
Promise.all几百个请求,用并发池(如限制 4~6 个)避免打满浏览器连接和带宽(实现见 请求处理相关)。 - 进度条:用
xhr.upload.onprogress统计每片进度,汇总得到整体进度(fetch原生不支持上传进度,需用XMLHttpRequest或 axios 的onUploadProgress)。 - 失败重试:单片失败自动重试 N 次,仍失败再整体提示。
三方库可用
vue-simple-uploader、@rpldy/uploady,或对象存储(OSS/COS)的分片上传 SDK。
其它问题
相同文件只能触发一次 onChange
input[file] change 事件触发的判断标准是 value 值发生变化,每次执行完上传逻辑后清空 value 值 fileNode.value = ""