FormData上传文件的那些坑我替你踩过了

Top丶杏花 交互 阅读 2,859
赞 21 收藏
二维码
手机扫码查看
反馈

我的写法,亲测靠谱

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;
}

以上是我踩坑后的总结,希望对你有帮助。这种技巧的拓展用法还有很多,后续会继续分享这类博客。有更优的实现方式欢迎评论区交流。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论