React Dropzone:Safari中onDrop函数无法触发问题求助
React Dropzone onDrop在Safari中无响应的排查与修复
问题描述
使用带自定义验证逻辑的React Dropzone组件时,onDrop函数中的alert在Chrome浏览器中正常触发,但在Safari中完全无响应,且控制台无任何报错信息。
核心排查方向
- 异步流程Promise状态异常:Safari对未处理的Promise拒绝或pending状态的处理逻辑与Chrome不同,若
getFilesFromEvent中的Promise一直处于pending状态,会阻塞React Dropzone后续流程(包括onDrop)的执行。 - 原生File对象可扩展性限制:Safari可能禁止通过
Object.defineProperty修改原生File对象的属性,导致validator无法读取isValid标记,触发未预期的验证阻塞。 - 视频元数据加载浏览器差异:Safari的
<video>元素loadeddata事件触发逻辑与Chrome不同,可能因视频格式、元数据缺失等问题导致getVideoDuration中的Promise无法resolve。
调试步骤
- 在
getFilesFromEvent关键节点添加console.log,确认Safari下是否执行到返回结果的步骤:async getFilesFromEvent(event) { console.log('进入getFilesFromEvent'); let files = await fromEvent(event); console.log('从事件获取的文件:', files); const fileRes = await Promise.all( files.map(async (file) => { console.log('处理文件:', file.name); const duration = await getVideoDuration(file).catch(err => { console.log('获取时长错误:', err); return 0; }); console.log('文件时长:', duration); const isValid = duration < 15 * 60; console.log('文件是否有效:', isValid); return Object.defineProperty(file, "isValid", { value: isValid, writable: false }); }) ); console.log('最终处理结果:', fileRes); return fileRes; } - 开启Safari控制台的「捕获所有异常」选项,查看是否有被吞掉的Promise拒绝信息。
- 单独测试
getVideoDuration函数,传入Safari中出现问题的视频文件,确认是否能正常resolve。
修复方案
方案1:给视频元数据加载添加超时与错误处理
防止getVideoDuration的Promise因Safari中的视频加载问题一直pending:
async function getVideoDuration(videoFile: File | Blob | DataTransferItem) { return new Promise<number>((res, rej) => { // 5秒超时,避免无限等待 const timeoutId = setTimeout(() => { rej('视频元数据加载超时'); }, 5000); if (!videoFile.type.startsWith("video/")) { clearTimeout(timeoutId); rej("文件必须是视频格式"); } const video = document.createElement("video"); video.preload = "metadata"; video.onloadeddata = function () { clearTimeout(timeoutId); res(video.duration); }; // 捕获视频加载错误 video.onerror = function() { clearTimeout(timeoutId); rej('视频加载失败'); }; let file; if (videoFile instanceof DataTransferItem) { file = videoFile.getAsFile(); } else { file = videoFile; } if (!file) { clearTimeout(timeoutId); return res(1); } video.src = URL.createObjectURL(file); }); }
方案2:避免修改原生File对象,使用包装对象传递验证状态
Safari对原生File对象的属性修改有严格限制,改用包装对象传递isValid标记:
async getFilesFromEvent(event) { let files = await fromEvent(event); const fileRes = await Promise.all( files.map(async (file) => { try { const duration = await getVideoDuration(file); const isValid = duration < 15 * 60; // 返回包装对象,不修改原生File return { file, isValid }; } catch (err) { return { file, isValid: false }; } }) ); return fileRes; } // 调整validator适配包装对象 validator(item) { const { file, isValid } = item; if (file.size > 5e8) { return { message: "文件过大(最大500MB)", code: "FileTooLarge" }; } if (!isValid) { return { message: "视频时长需小于15分钟", code: "LargeFileDuration" }; } return null; } // 调整onDrop处理包装对象 onDrop(items) { // 若需使用原始文件,可在此提取 const files = items.map(item => item.file); alert("文件已上传"); }
完整修复后代码
import "./styles.css"; import { useDropzone } from "react-dropzone"; import { fromEvent } from "file-selector"; async function getVideoDuration(videoFile: File | Blob | DataTransferItem) { return new Promise<number>((res, rej) => { const timeoutId = setTimeout(() => { rej('视频元数据加载超时'); }, 5000); if (!videoFile.type.startsWith("video/")) { clearTimeout(timeoutId); rej("文件必须是视频格式"); } const video = document.createElement("video"); video.preload = "metadata"; video.onloadeddata = function () { clearTimeout(timeoutId); res(video.duration); }; video.onerror = function() { clearTimeout(timeoutId); rej('视频加载失败'); }; let file; if (videoFile instanceof DataTransferItem) file = videoFile.getAsFile(); else file = videoFile; if (!file) { clearTimeout(timeoutId); return res(1); } video.src = URL.createObjectURL(file); }); } function Basic(props) { const { getRootProps, getInputProps, isDragActive, isDragReject, fileRejections } = useDropzone({ accept: { "video/*": [] }, noDragEventsBubbling: true, maxSize: 5e8, maxFiles: 1, multiple: false, async getFilesFromEvent(event) { let files = await fromEvent(event); const fileRes = await Promise.all( files.map(async (file) => { try { const duration = await getVideoDuration(file); const isValid = duration < 15 * 60; return { file, isValid }; } catch (err) { return { file, isValid: false }; } }) ); return fileRes; }, validator(item) { const { file, isValid } = item; if (file.size > 5e8) return { message: "文件过大(最大500MB)", code: "FileTooLarge" }; if (!isValid) return { message: "视频时长需小于15分钟", code: "LargeFileDuration" }; return null; }, onDrop(items) { alert("文件已上传"); } }); return ( <section className="container"> <div {...getRootProps({ className: "dropzone" })}> <input {...getInputProps()} /> <p>拖拽文件到此处,或点击选择文件</p> </div> <aside> <h4>文件状态</h4> <strong style={{ color: "red" }}> { fileRejections[fileRejections.length - 1]?.errors[ fileRejections[fileRejections.length - 1].errors.length - 1 ]?.message } </strong> </aside> </section> ); } export default function App() { return <Basic />; }
内容的提问来源于stack exchange,提问作者plutolaser
相关产品推荐
相关产品推荐

