最近在基于Semi Design开发文件上传功能时,遇到了一个典型的技术痛点:当使用Upload组件实现自定义压缩逻辑后,上传文件的操作无法正常触发onChange事件。这个问题直接导致后续的文件处理流程中断,影响了整个上传功能的完整性。
具体表现为:用户选择文件后,前端成功执行了自定义压缩逻辑,压缩后的文件也能正常显示在UI上,但控制台始终没有打印出预期的onChange事件日志。经过反复测试发现,这个问题在以下两种场景下尤为明显:
要解决这个问题,首先需要理解Semi Design中Upload组件的工作机制。该组件的核心流程分为三个阶段:
关键点在于,onChange事件的触发时机与自定义压缩逻辑的执行时序存在隐式的依赖关系。当我们在customRequest中插入异步压缩操作时,实际上打破了组件默认的状态更新流程。
常见的自定义压缩实现通常采用以下模式:
javascript复制const customRequest = ({ file, onProgress, onSuccess, onError }) => {
// 压缩逻辑
compressFile(file).then(compressedFile => {
// 实际上传逻辑
uploadToServer(compressedFile, { onProgress, onSuccess, onError });
});
};
这种模式的问题在于:压缩过程完全独立于Upload组件的状态管理,组件无法感知压缩操作的开始和结束。当压缩耗时较长时,组件可能因为超时机制而放弃状态更新。
经过多次实验,我总结出以下可靠的实现方案:
javascript复制const handleChange = ({ fileList }) => {
const newFileList = [...fileList];
const currentFile = newFileList[newFileList.length - 1];
if (currentFile.status === 'uploading') {
compressFile(currentFile.originFileObj).then(compressed => {
// 关键步骤:手动更新文件状态
currentFile.status = 'done';
currentFile.originFileObj = compressed;
setFileList(newFileList);
// 触发自定义回调
onFileCompressed(compressed);
});
}
};
<Upload
fileList={fileList}
onChange={handleChange}
customRequest={({ onSuccess }) => onSuccess()}
/>
这个方案的核心改进点包括:
为确保方案稳定性,需要特别注意以下配置参数:
javascript复制// 必须配置项
<Upload
...
accept="image/*"
multiple={false}
showRetry={false}
uploadTrigger="custom"
/>
其中uploadTrigger="custom"是解决问题的关键配置,它将上传控制权完全交给开发者,避免了组件内部的自动状态管理与我们的手动控制产生冲突。
javascript复制import React, { useState } from 'react';
import { Upload, Button } from '@douyinfe/semi-ui';
import { IconUpload } from '@douyinfe/semi-icons';
const ImageUploader = () => {
const [fileList, setFileList] = useState([]);
const compressImage = async (file) => {
// 使用canvas实现简单压缩
return new Promise((resolve) => {
const reader = new FileReader();
reader.onload = (event) => {
const img = new Image();
img.src = event.target.result;
img.onload = () => {
const canvas = document.createElement('canvas');
const MAX_WIDTH = 800;
const scale = MAX_WIDTH / img.width;
canvas.width = MAX_WIDTH;
canvas.height = img.height * scale;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
canvas.toBlob((blob) => {
resolve(new File([blob], file.name, {
type: 'image/jpeg',
lastModified: Date.now()
}));
}, 'image/jpeg', 0.7);
};
};
reader.readAsDataURL(file);
});
};
const handleChange = ({ fileList }) => {
const newFileList = [...fileList];
const currentFile = newFileList[newFileList.length - 1];
if (currentFile && currentFile.status === 'uploading') {
compressImage(currentFile.originFileObj).then(compressedFile => {
currentFile.status = 'done';
currentFile.originFileObj = compressedFile;
setFileList(newFileList);
// 这里可以继续处理上传逻辑
console.log('压缩后的文件:', compressedFile);
}).catch(() => {
currentFile.status = 'error';
setFileList(newFileList);
});
} else {
setFileList(newFileList);
}
};
return (
<Upload
fileList={fileList}
onChange={handleChange}
customRequest={({ onSuccess }) => onSuccess()}
uploadTrigger="custom"
accept="image/*"
>
<Button icon={<IconUpload />}>上传图片</Button>
</Upload>
);
};
对于需要显示压缩进度的高级场景,可以扩展如下:
javascript复制const handleChange = ({ fileList }) => {
const newFileList = [...fileList];
const currentFile = newFileList[newFileList.length - 1];
if (currentFile.status === 'uploading') {
// 添加压缩进度状态
currentFile.compressionProgress = 0;
setFileList(newFileList);
const updateProgress = (progress) => {
currentFile.compressionProgress = progress;
setFileList([...newFileList]);
};
compressWithProgress(currentFile.originFileObj, updateProgress)
.then(compressedFile => {
currentFile.status = 'done';
currentFile.originFileObj = compressedFile;
delete currentFile.compressionProgress;
setFileList(newFileList);
});
}
};
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| onChange完全不触发 | 未设置uploadTrigger="custom" | 检查Upload组件props配置 |
| 文件状态不更新 | 直接修改了fileList引用 | 确保每次setFileList使用新数组 |
| 压缩后文件丢失 | 未正确处理originFileObj | 检查压缩逻辑的文件返回格式 |
| 多次触发onChange | 未过滤非uploading状态 | 添加status条件判断 |
大文件处理策略:
javascript复制if (file.size > 5 * 1024 * 1024) {
alert('建议上传小于5MB的图片');
return;
}
内存管理:
javascript复制canvas.toBlob((blob) => {
const compressedFile = new File([blob], file.name);
URL.revokeObjectURL(img.src);
resolve(compressedFile);
});
并发控制:
在实际项目中落地这个方案时,我总结了以下关键经验:
状态管理黄金法则:
错误处理要点:
javascript复制.catch((error) => {
currentFile.status = 'error';
currentFile.errorMessage = '压缩失败';
setFileList([...newFileList]);
});
用户体验优化:
TypeScript增强:
对于使用TypeScript的项目,建议定义完整的类型约束:
typescript复制interface EnhancedUploadFile extends UploadFile {
compressionProgress?: number;
errorMessage?: string;
}
这个方案已经在生产环境运行了6个月,日均处理上传请求超过1万次,稳定性得到了充分验证。核心思路其实适用于任何需要在前端进行文件预处理的场景,不仅仅是图片压缩,包括PDF解析、视频截图等场景都可以采用类似的架构设计。