You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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。

调试步骤

  1. 在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;
    }
    
  2. 开启Safari控制台的「捕获所有异常」选项,查看是否有被吞掉的Promise拒绝信息。
  3. 单独测试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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.19 04:07:28