/** * 使用XMLHttpRequest上传文件并提交表单数据 (ES5兼容版本) * @param {string} url - 上传接口地址 * @param {object} formData - 要提交的表单数据对象 * @param {HTMLInputElement|File|File[]|FileList} fileInput - 要上传的文件对象 * @param {function} callback - 上传完成后的回调函数 (response,error) * @param {object} [options] - 可选配置项 * @param {object} [options.fileTypeMap] - 文件类型映射表 {扩展名: MIME类型} * @param {number} [options.maxFileSize] - 最大文件大小(字节) * @param {number} [options.timeout] - 请求超时时间(毫秒) * @param {function} [options.onProgress] - 上传进度回调 * @param {string} [options.fieldName] - 文件字段名称(默认'FileData') * @param {object} [options.headers] - 自定义请求头 * @returns {object} 返回包含xhr和abort方法的对象 */ function XHRUploadFile(url, formData, fileInput, callback, options) { // 辅助函数:安全地检查对象自身属性 function hasOwn(obj, prop) { return obj != null && Object.prototype.hasOwnProperty.call(obj, prop); }; // 辅助函数:格式化文件大小显示 function formatFileSize(bytes) { if (typeof bytes !== 'number' || bytes === 0) return '0 Bytes'; var k = 1024; var sizes = ['Bytes', 'KB', 'MB', 'GB']; var i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; }; // 获取文件扩展名 function getExtension(filename) { if (!filename) return ''; // 处理undefined/null var lastDot = filename.lastIndexOf('.'); // 确保: 1. 存在扩展名 2. 不是以点开头的隐藏文件 return (lastDot > 0) ? filename.slice(lastDot + 1).toLowerCase() : ''; }; // 默认类型映射表 var defaultTypeMap = { 'csv': 'text/csv', 'xls': 'application/vnd.ms-excel', 'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }; // 获取CSRF Token var csrfMeta = document.querySelector('meta[name="csrf-token"]'); var csrfToken = (csrfMeta && csrfMeta.content) || ''; // 获取文件类型(增强版) function getFileType(file) { // 1. 优先使用文件的type属性 if (file.type) return file.type; // 2. 通过扩展名查找 var ext = getExtension(file.name); return config.fileTypeMap[ext] || ''; }; // 替代 Object.assign() 的 ES5 兼容实现 function mergeObjects() { var result = {}; for (var i = 0; i < arguments.length; i++) { var obj = arguments[i]; if (obj) { for (var key in obj) { // if (obj.hasOwnProperty(key)) result[key] = obj[key]; if (hasOwn(obj, key)) { result[key] = obj[key]; } } } } return result; }; // 默认配置 var defaults = { fileTypeMap: defaultTypeMap, maxFileSize: null, timeout: 60000 * 3, // 3分钟 fieldName: 'FileData', onProgress: null, headers: { 'X-Requested-With': 'XMLHttpRequest', 'X-CSRF-Token': csrfToken } }; // 合并配置 var config = mergeObjects({}, defaults, options); // 参数验证 if (!url) { var error = new Error('上传地址不能为空'); error.name = 'InvalidURL'; if (typeof callback === 'function') { return callback(null, error); }; throw error; } // 获取文件列表 (ES5兼容实现) var files = []; if (fileInput instanceof File) { files = [fileInput]; } else if (Array.isArray(fileInput)) { files = fileInput.filter(function (f) { return f instanceof File; }); } else if (fileInput instanceof FileList) { for (var i = 0; i < fileInput.length; i++) { files.push(fileInput[i]); } } else if (fileInput && fileInput.tagName === 'INPUT' && fileInput.type === 'file' && fileInput.files) { for (var j = 0; j < fileInput.files.length; j++) { files.push(fileInput.files[j]); } } // 验证文件 if (files.length === 0) { var error = new Error('没有可上传的有效文件'); error.name = 'NoValidFiles'; if (typeof callback === 'function') { return callback(null, error); }; throw error; } // 检查文件类型(使用typeMap模式) var invalidFiles = files.filter(function (file) { var fileType = getFileType(file); var ext = getExtension(file.name); var hasExtension = ext !== ''; // 情况1:有扩展名但不在typeMap中 if (hasExtension && !config.fileTypeMap[ext]) return true; // 情况2:类型不匹配且不是通配符 var expectedType = config.fileTypeMap[ext]; return !(fileType && (expectedType === fileType || expectedType === '*')); }); if (invalidFiles.length > 0) { var error = new Error('文件类型不支持: ' + invalidFiles.map(function (f) { return f.name + ' (' + (f.type || '未知类型') + ')'; }).join(', ')); error.name = 'InvalidFileType'; if (typeof callback === 'function') { return callback(null, error); }; throw error; }; // 检查文件大小 if (config.maxFileSize) { var oversizedFiles = files.filter(function (file) { return file.size > config.maxFileSize; }); if (oversizedFiles.length > 0) { var error = new Error('文件大小超过限制: ' + oversizedFiles.map(function (f) { return f.name + ' (' + formatFileSize(f.size) + ' > ' + formatFileSize(config.maxFileSize) + ')'; }).join(', ')); error.name = 'FileSizeExceeded'; if (typeof callback === 'function') { return callback(null, error); }; throw error; }; }; // 构建FormData var bodyData = new FormData(); // 添加表单数据 if (formData && typeof formData === 'object') { for (var key in formData) { if (hasOwn(formData, key)) { bodyData.append(key, formData[key]); } } }; // 添加文件到FormData files.forEach(function (file, index) { var fieldName = files.length === 1 ? config.fieldName : config.fieldName + '_' + index; bodyData.append(fieldName, file, file.name); // 第三个参数指定文件名 }); // 创建XHR对象 var xhr = new XMLHttpRequest(); xhr.open('POST', url, true); xhr.timeout = config.timeout; // 设置请求头 if (config.headers) { for (var header in config.headers) { if (hasOwn(config.headers, header)) { xhr.setRequestHeader(header, config.headers[header]); } } }; // 进度跟踪变量 var lastProgress = 0; // 上传进度事件处理 xhr.upload.onprogress = function (event) { if (event.lengthComputable) { lastProgress = event.loaded / event.total; var percentComplete = Math.round(lastProgress * 100); if (typeof config.onProgress === 'function') { config.onProgress({ percent: percentComplete, loaded: event.loaded, total: event.total, event: event }); } else { console.log('上传进度: ' + percentComplete + '%'); }; } else { console.log('无法计算上传进度'); }; }; // 响应处理 xhr.onload = function () { if (xhr.status >= 200 && xhr.status < 300) { var response; try { response = JSON.parse(xhr.responseText); } catch (e) { response = xhr.responseText; } if (typeof callback === 'function') { callback(response); } else { console.log('上传完成', response) }; // 上传成功清空表单 if (fileInput && fileInput.tagName === 'INPUT') { fileInput.value = ''; }; } else { handleError(new Error('上传失败: ' + xhr.status)); } }; xhr.onerror = function () { handleError(new Error('请求过程中发生错误')); }; xhr.ontimeout = function () { handleError(new Error('请求超时: ' + xhr.timeout + '毫秒内未完成上传')); }; xhr.onabort = function () { var message = '上传中止: 已上传 ' + xhr.upload.loaded + ' 字节'; var error = new Error(message); error.name = 'UploadAborted'; error.uploadedBytes = xhr.upload.loaded; error.totalBytes = xhr.upload.total; handleError(error); }; // 错误处理函数 function handleError(error) { if (!error.name) error.name = 'UploadError';// 添加错误类型标识 error.cause = error;// 保留原始错误对象 error.status = xhr.status; error.responseText = xhr.responseText; error.xhr = xhr; if (typeof callback === 'function') { return callback(null, error); }; throw error; } // 发送请求 xhr.send(bodyData); // 返回包含XHR对象和abort方法的对象 // 在返回对象中增强: return { xhr: xhr, abort: function () { xhr.abort(); }, getProgress: function () { return lastProgress; }, cleanup: function () { // 清理上传事件 xhr.upload.onprogress = null; // 清理所有事件监听(优化版) var events = ['load', 'error', 'timeout', 'abort', 'readystatechange']; for (var i = 0; i < events.length; i++) { xhr['on' + events[i]] = null; } // 清理FormData引用 bodyData = null; } }; }; /** * 使用示例 try { const xhr = XHRUploadFile('/upload', {'TaskID':'123'}, files, (response, error) => { if (error) { console.error('上传出错:', error.message); return; } console.log('上传成功', response); }); // 如果需要可以中止上传 // xhr.abort(); } catch (error) { console.error('初始化上传时出错:', error.message); } */