JavaScript实现文件拖拽上传与文件夹过滤详解

作者:代码拾荒

时间:2026-10-22

浏览:332

本文详解JavaScript实现文件拖拽上传的完整流程,包括监听drag/drop事件、阻止浏览器默认打开行为。重点解析Chrome与Safari等浏览器在获取拖拽文件时的差异,特别是针对文件夹上传的兼容性问题。通过DataTransfer API及FileReader异步读取技巧,实现文件夹过滤与多文件处理,并提供完整代码示例。

文章目录

当用户将文件拖拽至浏览器内的特定元素时,JavaScript能够监听相关的拖拽事件并对结果进行处理。本文主要探讨与文件拖拽相关的技术细节,暂不涉及复杂的浏览器兼容性问题处理。

拖拽事件

JavaScript可监听的拖拽事件包括drag、dragend、dragenter、dragexit(目前无浏览器实现)、dragleave、dragover、dragstart以及drop。更详细的信息可参考MDN文档。

其中,与文件拖拽直接相关的事件主要包括dragenter(文件进入)、dragover(文件悬停)、dragleave(文件离开)以及drop(文件放下)。

这些拖拽事件既可以绑定到指定的DOM元素上,也可以绑定到整个页面。


var dropEle = document.querySelector('#dropZone');
dropEle.addEventListener('drop', function (e) {
  // 
}, false);

document.addEventListener('drop', function (e) {
  // 
}, false);

阻止默认行为

通常,我们只需在drop事件的回调中处理拖拽文件的业务逻辑。那么,为何还需要绑定dragenter、dragover和dragleave这三个事件呢?

如果在未处理拖拽事件的浏览器中拖拽文件,浏览器会默认打开该文件。例如,拖拽图片会直接显示图片,拖拽PDF文件(若无阅读器)则会打开PDF。

如果浏览器直接打开拖拽的文件,页面会发生跳转,这并非我们期望的结果。我们希望获取文件本身,而不是让页面跳转。由于打开文件是浏览器的默认行为,因此需要在上述事件中阻止该默认行为。


dropZone.addEventListener("dragenter", function (e) {
  e.preventDefault();
  e.stopPropagation();
}, false);

dropZone.addEventListener("dragover", function (e) {
  e.preventDefault();
  e.stopPropagation();
}, false);

dropZone.addEventListener("dragleave", function (e) {
  e.preventDefault();
  e.stopPropagation();
}, false);

dropZone.addEventListener("drop", function (e) {
  e.preventDefault();
  e.stopPropagation();
  // 处理拖拽文件的逻辑
}

实际上,即使不阻止dragenter的默认行为,浏览器也不会打开文件。但为了规避某些浏览器可能存在的兼容性问题,建议在整个拖拽周期内的所有事件中均阻止默认行为并阻止事件冒泡。

获得拖拽的文件

在drop事件的回调函数中,可以通过事件对象获取到文件对象。

事件对象中包含一个e.dataTransfer属性,其类型为DataTransfer,具备以下属性:

属性 类型 说明
dropEffect String 用来hack某些兼容性问题
effectAllowed String 暂时不用
files FileList 拖拽的文件列表
items DataTransferItemList 拖拽的数据(有可能是字符串)
types Array 拖拽的数据类型 该属性在Safari下比较混乱

在Chrome浏览器中,我们通过items对象获取文件;而在其他浏览器中,则使用files属性获取文件。这主要是为了处理拖拽文件夹的问题。通常建议禁止用户拖拽文件夹,因为文件夹内可能包含子文件夹,递归上传耗时较长。若不递归查找仅上传第一层文件,用户可能误以为上传完成,实则子目录文件未上传。因此,禁止上传文件夹是较好的处理方式,下文将介绍具体做法。

Chrome获取文件


dropZone.addEventListener("drop", function (e) {
  e.preventDefault();
  e.stopPropagation();
  
  var df = e.dataTransfer;
  var dropFiles = []; // 存放拖拽的文件对象
  
  if(df.items !== undefined) {
    // Chrome有items属性,对Chrome的单独处理
    for(var i = 0; i < df.items.length; i++) {
      var item = df.items[i];
      // 用webkitGetAsEntry禁止上传目录
      if(item.kind === "file" && item.webkitGetAsEntry().isFile) {
        var file = item.getAsFile();
        dropFiles.push(file);
      }
    }
  }
}

其他浏览器获取文件

本文仅测试了Safari浏览器,其他浏览器的情况并未逐一测试。但阅读完本文后,读者应能掌握处理其他浏览器兼容性的思路。


dropZone.addEventListener("drop", function (e) {
  e.preventDefault();
  e.stopPropagation();
  
  var df = e.dataTransfer;
  var dropFiles = []; // 存放拖拽的文件对象
  
  if(df.items !== undefined) {
    // Chrome拖拽文件逻辑
  } else {
    for(var i = 0; i < df.files.length; i++) {
      dropFiles.push(df.files[i]);
    }
  }
}

由于Safari没有item属性,自然也就没有webkitGetAsEntry方法,因此在Safari中无法直接确定拖拽的是文件还是文件夹。

非Chrome内核浏览器判断目录的方法

浏览器获取到的每个file对象包含四个属性:lastModified、name、size和type。其中type是文件的MIME Type,文件夹的type为空。然而,部分文件也没有MIME Type,若仅依据type是否为空来判断是否为文件夹,会误伤部分文件,因此该方法并不可靠。

那么还有什么其他方法呢?大致思路如下:用户拖拽的文件和文件夹在行为上应有区别,使用File API操作时也应有所不同。例如,在对文件进行某些操作时,文件能正常处理,而文件夹则会报错。通过捕获这些错误,即可判断拖拽的是文件还是文件夹。基于此思路,我们编写相关代码。


dropZone.addEventListener("drop", function (e) {
  e.preventDefault();
  e.stopPropagation();

  var df = e.dataTransfer;
  var dropFiles = [];
  
  if(df.items !== undefined){
    // Chrome拖拽文件逻辑
  } else {
    for(var i = 0; i < df.files.length; i++){
      var dropFile = df.files[i];
      if ( dropFile.type ) {
        // 如果type不是空串,一定是文件
        dropFiles.push(dropFile);
      } else {
        try {
          var fileReader = new FileReader();
          fileReader.readAsDataURL(dropFile.slice(0, 3));

          fileReader.addEventListener('load', function (e) {
            console.log(e, 'load');
            dropFiles.push(dropFile);
          }, false);

          fileReader.addEventListener('error', function (e) {
            console.log(e, 'error,不可以上传文件夹');
          }, false);

        } catch (e) {
          console.log(e, 'catch error,不可以上传文件夹');
        }
      }
    }
  }
}, false);

上述代码创建了一个FileReader实例,通过该实例读取文件。测试读取一个1GB以上的文件耗时3秒多,时间较长,因此使用slice截取前3个字符。至于为何是前3个而非前2个或前4个,纯属个人习惯。

如果触发load事件,说明拖拽过来的是文件;如果触发error事件,说明是文件夹。为防止其他潜在错误,使用try...catch包裹这段代码。

三方应用的一点点小hack

经过测试,通过Mac的Finder拖拽文件没有问题。但有时文件并非来自Finder,而是来自某些应用程序。例如,有一个名为“圈点”的应用,其用户反馈文件拖拽失效。查阅其他开源文件上传源码后,发现了一行关键代码:


dropZone.addEventListener("dragover", function (e) {
  e.dataTransfer.dropEffect = 'copy'; // 兼容某些三方应用,如圈点
  e.preventDefault();
  e.stopPropagation();
}, false);

需要将dropEffect设置为copy。上网搜索该问题,源码文档中并未说明原因,有兴趣的同学可以探究一下。

可以拿来就用的代码

由于使用了FileReader读取文件,这是一个异步IO操作。为了记录当前处理了多少个文件以及何时触发拖拽结束的回调,编写了一个checkDropFinish方法,持续比较已处理文件数量与文件总数。确定所有文件处理完毕后,调用完成回调。

另外,在调试异步处理时,我发现Safari中的断点调试会导致异步回调不触发。需要自行调试定制功能的同学请注意这一点。


// 获得拖拽文件的回调函数
function getDropFileCallBack (dropFiles) {
  console.log(dropFiles, dropFiles.length);
}

var dropZone = document.querySelector("#dropZone");
dropZone.addEventListener("dragenter", function (e) {
  e.preventDefault();
  e.stopPropagation();
}, false);

dropZone.addEventListener("dragover", function (e) {
  e.dataTransfer.dropEffect = 'copy'; // 兼容某些三方应用,如圈点
  e.preventDefault();
  e.stopPropagation();
}, false);

dropZone.addEventListener("dragleave", function (e) {
  e.preventDefault();
  e.stopPropagation();
}, false);

dropZone.addEventListener("drop", function (e) {
  e.preventDefault();
  e.stopPropagation();

  var df = e.dataTransfer;
  var dropFiles = []; // 拖拽的文件,会放到这里
  var dealFileCnt = 0; // 读取文件是个异步的过程,需要记录处理了多少个文件了
  var allFileLen = df.files.length; // 所有的文件的数量,给非Chrome浏览器使用的变量

  // 检测是否已经把所有的文件都遍历过了
  function checkDropFinish () {
    if ( dealFileCnt === allFileLen-1 ) {
      getDropFileCallBack(dropFiles);
    }
    dealFileCnt++;
  }

  if(df.items !== undefined){
    // Chrome拖拽文件逻辑
    for(var i = 0; i < df.items.length; i++) {
      var item = df.items[i];
      if(item.kind === "file" && item.webkitGetAsEntry().isFile) {
        var file = item.getAsFile();
        dropFiles.push(file);
        console.log(file);
      }
    }
  } else {
    // 非Chrome拖拽文件逻辑
    for(var i = 0; i < allFileLen; i++) {
      var dropFile = df.files[i];
      if ( dropFile.type ) {
        dropFiles.push(dropFile);
        checkDropFinish();
      } else {
        try {
          var fileReader = new FileReader();
          fileReader.readAsDataURL(dropFile.slice(0, 3));

          fileReader.addEventListener('load', function (e) {
            console.log(e, 'load');
            dropFiles.push(dropFile);
            checkDropFinish();
          }, false);

          fileReader.addEventListener('error', function (e) {
            console.log(e, 'error,不可以上传文件夹');
            checkDropFinish();
          }, false);

        } catch (e) {
          console.log(e, 'catch error,不可以上传文件夹');
          checkDropFinish();
        }
      }
    }
  }
}, false);

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章