解决 Semi Design Upload 组件实现自定义压缩上传文件后无法触发 onChange一、问题背景Semi Design Upload 组件的基本使用Semi Design 是字节跳动开源的企业级 UI 组件库其 Upload 组件提供了丰富的文件上传功能。在实际开发中我们经常需要在文件上传前对图片进行压缩处理以减少带宽消耗和提升用户体验。然而当我们在 Upload 组件的beforeUpload钩子中实现自定义压缩后常常会发现一个棘手的问题压缩后的文件上传后onChange事件无法正常触发。### 1.1 基本 Upload 组件示例让我们先看一个最简单的 Upload 组件使用示例javascriptimport React from react;import { Upload, Button } from douyinfe/semi-ui;function BasicUpload() { const handleChange ({ fileList, file }) { console.log(文件变化:, fileList); }; return ( Upload actionhttps://example.com/upload onChange{handleChange} Button上传文件/Button /Upload );}在这个基础示例中onChange能正常触发因为 Upload 组件内部管理了文件列表的状态变化。但当我们引入自定义压缩逻辑后问题就出现了。## 二、问题深入自定义压缩导致 onChange 失效的原因当我们使用beforeUpload钩子对文件进行压缩时通常会返回一个新的 File 对象。这个新对象与原始文件在内存引用上不同导致 Upload 组件内部的状态管理逻辑无法正确识别文件变化。### 2.1 自定义压缩的常见实现javascriptimport React from react;import { Upload, Button } from douyinfe/semi-ui;function CompressUpload() { // 自定义压缩函数 const compressImage (file) { return new Promise((resolve) { const reader new FileReader(); reader.readAsDataURL(file); reader.onload (e) { const img new Image(); img.src e.target.result; img.onload () { const canvas document.createElement(canvas); const ctx canvas.getContext(2d); // 压缩参数宽高缩小为一半 canvas.width img.width / 2; canvas.height img.height / 2; ctx.drawImage(img, 0, 0, canvas.width, canvas.height); // 转换为 Blob canvas.toBlob((blob) { // 创建新的 File 对象 const compressedFile new File( [blob], file.name, { type: file.type, lastModified: Date.now() } ); resolve(compressedFile); }, file.type, 0.8); // 质量参数 0.8 }; }; }); }; const handleBeforeUpload async ({ file, fileList }) { if (file.type.startsWith(image/)) { const compressed await compressImage(file); return compressed; // 返回新文件 } return file; }; const handleChange ({ fileList }) { console.log(文件列表:, fileList); // 问题这里可能不会触发 }; return ( Upload actionhttps://example.com/upload beforeUpload{handleBeforeUpload} onChange{handleChange} Button上传并压缩/Button /Upload );}这个实现看似合理但实际运行时onChange可能无法正常触发。这是因为beforeUpload返回的新 File 对象与原始文件在 Upload 组件内部的文件列表管理中存在差异。## 三、解决方案手动管理文件列表与 onChange 触发要解决这个问题我们需要理解 Upload 组件的内部机制。核心思路是在beforeUpload中不仅返回压缩后的文件还要手动更新文件列表并触发onChange。### 3.1 完整的解决方案代码下面是经过验证的完整解决方案包含详细的注释javascriptimport React, { useState } from react;import { Upload, Button, Toast } from douyinfe/semi-ui;function CompressUploadSolution() { const [fileList, setFileList] useState([]); // 图片压缩函数 const compressImage (file) { return new Promise((resolve) { // 检查文件类型 if (!file.type.startsWith(image/)) { resolve(file); return; } const reader new FileReader(); reader.readAsDataURL(file); reader.onload (e) { const img new Image(); img.src e.target.result; img.onload () { const canvas document.createElement(canvas); const ctx canvas.getContext(2d); // 设置压缩尺寸示例缩小到原始尺寸的50% const maxWidth img.width * 0.5; const maxHeight img.height * 0.5; canvas.width maxWidth; canvas.height maxHeight; // 绘制压缩后的图片 ctx.drawImage(img, 0, 0, maxWidth, maxHeight); // 转换为 Blob 对象 canvas.toBlob((blob) { // 创建新的 File 对象保留原始文件名 const compressedFile new File( [blob], file.name, { type: file.type, lastModified: Date.now() } ); // 添加自定义属性用于后续识别 compressedFile.compressed true; compressedFile.originalSize file.size; compressedFile.compressedSize blob.size; resolve(compressedFile); }, file.type, 0.7); // 压缩质量 0.7 }; }; reader.onerror () { resolve(file); // 出错时返回原始文件 }; }); }; // 自定义 beforeUpload 处理 const handleBeforeUpload async ({ file, fileList: currentFileList }) { try { // 对图片进行压缩 const compressedFile await compressImage(file); // 手动构建新的文件列表 const newFileList [...currentFileList]; // 查找并替换当前文件 const index newFileList.findIndex( (item) item.uid file.uid || item.name file.name ); if (index ! -1) { // 替换为压缩后的文件 newFileList[index] { ...compressedFile, uid: file.uid || Date.now(), // 保留或生成唯一标识 status: ready, // 设置状态为就绪 percent: 0 }; } else { // 如果是新文件添加到列表 newFileList.push({ ...compressedFile, uid: Date.now(), status: ready, percent: 0 }); } // 手动更新文件列表并触发 onChange setFileList(newFileList); // 返回压缩后的文件用于上传 return compressedFile; } catch (error) { console.error(文件压缩失败:, error); Toast.error(文件压缩失败使用原始文件上传); return file; } }; // 处理文件变化 const handleChange ({ fileList: newFileList, file }) { console.log(文件列表更新:, newFileList); setFileList(newFileList); // 这里可以添加业务逻辑比如显示压缩信息 if (file file.compressed) { const savings ((file.originalSize - file.compressedSize) / file.originalSize * 100).toFixed(1); Toast.success(文件已压缩节省了 ${savings}% 的空间); } }; return ( div style{{ padding: 20px }} Upload actionhttps://example.com/upload fileList{fileList} beforeUpload{handleBeforeUpload} onChange{handleChange} onSuccess{() { Toast.success(上传成功); }} onError{(error) { Toast.error(上传失败: ${error}); }} Button typeprimary选择图片自动压缩/Button /Upload div style{{ marginTop: 20px, color: #666 }} p当前文件数量: {fileList.length}/p p提示: 图片上传前会自动压缩请查看控制台输出/p /div /div );}export default CompressUploadSolution;### 3.2 关键点解析1.手动管理文件列表通过useState维护fileList状态并在beforeUpload中手动更新确保onChange能接收到变化。2.保留文件标识在压缩后创建新 File 对象时保留原始文件的uid或生成新的唯一标识避免文件列表混乱。3.状态管理同步在beforeUpload中手动设置status: ready和percent: 0确保文件状态正确。## 四、进阶技巧批量压缩与进度反馈对于需要处理多个文件的情况我们可以扩展解决方案添加批量压缩和进度反馈功能。javascriptimport React, { useState, useCallback } from react;import { Upload, Button, Progress, Toast } from douyinfe/semi-ui;function BatchCompressUpload() { const [fileList, setFileList] useState([]); const [compressProgress, setCompressProgress] useState(0); const [isCompressing, setIsCompressing] useState(false); // 批量压缩函数 const batchCompress useCallback(async (files) { setIsCompressing(true); const totalFiles files.length; let completedCount 0; const compressedFiles await Promise.all( files.map(async (file) { if (file.type.startsWith(image/)) { const compressed await compressImage(file); completedCount; setCompressProgress((completedCount / totalFiles) * 100); return compressed; } return file; }) ); setIsCompressing(false); setCompressProgress(100); return compressedFiles; }, []); const handleBeforeUpload async ({ file, fileList: currentFileList }) { // 这里可以添加批量处理逻辑 const compressedFile await compressImage(file); return compressedFile; }; return ( div {isCompressing ( Progress percent{compressProgress} / )} Upload actionhttps://example.com/upload fileList{fileList} beforeUpload{handleBeforeUpload} onChange{({ fileList: newFileList }) { setFileList(newFileList); }} multiple // 支持多文件上传 Button批量上传图片/Button /Upload /div );}## 五、常见问题与注意事项### 5.1 文件类型兼容性- 确保压缩函数处理非图片文件时正确返回原始文件- 对于 GIF 等动画格式压缩可能导致动画丢失建议特殊处理### 5.2 性能优化- 对于大文件可以考虑使用 Web Worker 进行压缩避免阻塞主线程- 设置合理的压缩参数平衡文件大小和图片质量### 5.3 错误处理- 在压缩失败时应回退到原始文件确保上传功能不受影响- 添加适当的用户反馈如 Toast 提示或进度条## 六、总结通过本文的分析和实践我们解决了 Semi Design Upload 组件在实现自定义压缩后onChange无法触发的问题。核心解决方案包括1.理解组件内部机制认识到beforeUpload返回新 File 对象会导致文件列表管理混乱。2.手动管理文件列表在beforeUpload中手动更新文件列表状态确保onChange能正常触发。3.保留文件标识与状态为压缩后的文件设置正确的uid、status和percent属性。4.提供完善的用户体验添加进度反馈、错误处理和压缩信息展示。这个解决方案不仅解决了技术问题还保持了代码的可维护性和扩展性。在实际项目中你可以根据具体需求调整压缩算法、添加更多文件类型支持或者集成更复杂的业务逻辑。记住理解组件的工作原理是解决这类问题的关键而手动管理状态则是确保事件正确触发的有效手段。