FormData上传文件的那些坑我替你踩过了
我的写法,亲测靠谱
FormData 这玩意儿,说简单也简单,说复杂也复杂。我之前做文件上传功能的时候,被它折腾得够呛,后来慢慢摸清了一些门道。
最开始我喜欢这么写:
const formData = new FormData();
formData.append('name', '张三');
formData.append('age', '25');
formData.append('file', fileInput.files[0]);
这样写没问题,但是遇到复杂一点的场景就开始头疼了。比如要传数组、对象,或者需要批量处理表单数据的时候。
我现在一般会封装一个工具函数:
function createFormData(data, formData = new FormData(), parentKey = '') {
for (let key in data) {
if (data.hasOwnProperty(key)) {
const value = data[key];
const fullKey = parentKey ? ${parentKey}[${key}] : key;
if (value instanceof File || value instanceof Blob) {
formData.append(fullKey, value);
} else if (Array.isArray(value)) {
value.forEach((item, index) => {
const arrayKey = ${fullKey}[${index}];
if (typeof item === 'object' && item !== null) {
createFormData(item, formData, arrayKey);
} else {
formData.append(arrayKey, item);
}
});
} else if (typeof value === 'object' && value !== null && !(value instanceof Date)) {
createFormData(value, formData, fullKey);
} else {
formData.append(fullKey, value);
}
}
}
return formData;
}
// 使用示例
const postData = {
user: {
name: '张三',
age: 25
},
tags: ['前端', 'javascript'],
avatar: fileInput.files[0]
};
const formData = createFormData(postData);
这个写法的好处是能处理各种复杂的数据结构,而且不用手动一个个 append,减少了出错的可能性。
这几种错误写法,别再踩坑了
最常见的错误就是忘记处理数组和对象:
// 错误写法1:直接转字符串
const formData = new FormData();
const user = { name: '张三', age: 25 };
formData.append('user', JSON.stringify(user)); // 后端接收的时候还要解析,麻烦死了
// 错误写法2:数组处理不当
const tags = ['前端', 'javascript'];
tags.forEach(tag => {
formData.append('tags', tag); // 后端可能收不到数组格式
});
还有个大坑是文件对象的处理。我之前遇到过这种情况:
// 错误写法:直接传递FileList
formData.append('files', fileInput.files); // FileList不是File对象,后端收不到
// 正确写法
for (let i = 0; i < fileInput.files.length; i++) {
formData.append('files[]', fileInput.files[i]);
}
还有一个经常被忽略的问题是编码问题。特别是中文字段名:
// 错误写法:中文字段名可能会有问题
formData.append('用户名', '张三');
// 正确写法:用英文字段名
formData.append('username', '张三');
实际项目中的坑
在真实项目中,我发现最麻烦的是错误处理和进度监控。很多人只关注成功的场景,忽略了失败情况。
async function uploadWithProgress(data, onProgress) {
const formData = createFormData(data);
try {
const xhr = new XMLHttpRequest();
return new Promise((resolve, reject) => {
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
const percentComplete = (e.loaded / e.total) * 100;
onProgress && onProgress(percentComplete);
}
});
xhr.addEventListener('load', () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(JSON.parse(xhr.responseText));
} else {
reject(new Error(上传失败: ${xhr.statusText}));
}
});
xhr.addEventListener('error', () => {
reject(new Error('网络错误'));
});
xhr.open('POST', 'https://jztheme.com/api/upload');
xhr.send(formData);
});
} catch (error) {
throw new Error(创建请求失败: ${error.message});
}
}
这里要注意几个点:
- 一定要监听 error 事件,不然网络异常的时候用户完全不知道发生了什么
- progress 事件只有在 upload 对象上才有,getResponseHeader 事件是针对响应的
- 记得判断 e.lengthComputable,有时候获取不到总大小
另一个实际项目中的问题是如何验证文件类型和大小。我一般会在提交前做个预检查:
function validateFiles(fileList, maxSize = 10 * 1024 * 1024, allowedTypes = []) {
for (let i = 0; i < fileList.length; i++) {
const file = fileList[i];
if (file.size > maxSize) {
throw new Error(${file.name} 文件太大,超过 ${maxSize / 1024 / 1024}MB);
}
if (allowedTypes.length > 0 && !allowedTypes.includes(file.type)) {
throw new Error(${file.name} 文件类型不支持);
}
}
}
这个验证函数可以在用户选择文件后立即执行,避免后面上传失败再提示用户。
性能优化的一些小心得
在处理大文件上传的时候,我发现了几个影响性能的地方。
首先是内存占用问题。如果同时上传多个大文件,FormData 会把所有文件都加载到内存里:
// 不太好的做法:一次性添加大量文件
const formData = new FormData();
for (let file of largeFiles) {
formData.append('files[]', file); // 所有文件都在内存里
}
// 更好的做法:分批处理
async function batchUpload(files, batchSize = 3) {
const results = [];
for (let i = 0; i < files.length; i += batchSize) {
const batch = files.slice(i, i + batchSize);
const batchResult = await uploadBatch(batch);
results.push(...batchResult);
}
return results;
}
还有个细节是关于取消上传的功能。我发现很多人都没考虑到用户想取消上传的情况:
class UploadManager {
constructor() {
this.xhrs = new Map();
}
async upload(id, data) {
const xhr = new XMLHttpRequest();
this.xhrs.set(id, xhr);
// ... 其他代码
return new Promise((resolve, reject) => {
xhr.addEventListener('load', () => {
this.xhrs.delete(id);
// ... 处理响应
});
xhr.open('POST', 'https://jztheme.com/api/upload');
xhr.send(createFormData(data));
});
}
cancel(id) {
const xhr = this.xhrs.get(id);
if (xhr) {
xhr.abort();
this.xhrs.delete(id);
}
}
}
这个 UploadManager 可以让我们随时取消某个上传任务,用户体验会好很多。
最后的小贴士
我一般还会加一些调试功能,方便排查问题:
function debugFormData(formData) {
console.group('FormData 内容:');
for (let [key, value] of formData.entries()) {
if (value instanceof File) {
console.log(${key}: File(${value.name}, ${value.size} bytes));
} else {
console.log(${key}: ${value});
}
}
console.groupEnd();
}
这些小工具在开发阶段很有用,特别是遇到后端说参数不对的时候,可以直接查看 FormData 里面到底是什么内容。
还有个小技巧是关于重试机制的。网络不稳定的情况下,上传失败很常见:
async function uploadWithRetry(data, maxRetries = 3) {
let lastError;
for (let i = 0; i < maxRetries; i++) {
try {
return await uploadData(data);
} catch (error) {
lastError = error;
if (i < maxRetries - 1) {
await sleep(1000 * Math.pow(2, i)); // 指数退避
}
}
}
throw lastError;
}
以上是我踩坑后的总结,希望对你有帮助。这种技巧的拓展用法还有很多,后续会继续分享这类博客。有更优的实现方式欢迎评论区交流。

暂无评论