使用drag api实现向页面中拖入本地文件夹获取文件

806 阅读2分钟

一.Input

    项目一开始的需求定的是拖动单个文件夹到页面中收集文件。

    使用的是input解决

<input type="file" ref="updateFile" multiple webkitdirectory @change="updateFile" />
//multiple 允许多文件   
//webkitdirectory  选择目录
updateFile(e) {  
  console.log(e.target.files)
}

\

    这样我们拿到了文件夹里的文件信息,并且根据webkitRelativePath属性里的数据也能还原这些文件原本的层级关系和文件夹的名称

    但是!需求变了!需要支持多个文件夹一块拖入。

    试了下input多文件夹拖入后获取的文件只读取了第一个文件夹的文件,其余文件夹文件并没有读取,查阅了很多文章都没有找到解决办法,所以只能换一种思路。

二.DragEvent 

    查看了MDN的drag and drop API后,发现可以实现这个需求,过程:

drop(e) {
      // 1.获取DataTransfer对象。
      // 2.DataTransfer对象的items属性,提供一个包含所有拖动数据列表的 DataTransferItemList 对象。
      let DataTransferItemList = e.dataTransfer.items


      // 3.DataTransferItemList 对象是一组代表被拖动项的DataTransferItem 对象的列表。
      for (let index = 0; index < DataTransferItemList.length; index++) {
        const DataTransferItem = DataTransferItemList[index]


        // 4.DataTransferItem的kind属性可以判断拖拽项的种类,string 或是 file。
        // 判断文件
        if (DataTransferItem.kind === 'file') {
          // 5.文件返回FileSystemFileEntry,文件夹返回FileSystemDirectoryEntry
          //   上面两个entry具有的属性{filesystem,fullPath,isDirectory,isFile,name}
          let entry = DataTransferItem.webkitGetAsEntry()
          // 业务需求是拖进文件夹,所以判断下
          if (entry.isDirectory) {
            this.readDirectory(entry)
          }
        }
      }
    },
    readDirectory(directory) {
      let _this = this
      // 6.调用createReader方法 创建一个reader
      let dirReader = directory.createReader()


      let getEntries = function () {
        // 7.readEntries返回一个数组,文件返回FileSystemFileEntry,文件夹返回FileSystemDirectoryEntry。
        dirReader.readEntries(
          results => {
            if (results.length) {
              results.forEach(item => {
                // 判断是否为文件 FileSystemFileEntry
                if (item.isFile) {
                  // 8.FileSystemFileEntry.file()读取file
                  item.file(
                    file => {
                      _this.fileList.push(file)
                    },
                    error => {}
                  )
                }
              })
              getEntries()
            }
          },
          error => {
            /* handle error — error is a FileError object */
          }
        )
      }


      getEntries()
    }

    这样就从用户拖拽进来的文件夹里获取到文件的file对象。当然根据业务需求需要进行一些改造。比如要判断什么时候读取完,就可以包一层promise;要判断文件夹层级,封装的方法可以添加层级参数,然后递归等等。

    常见问题

    1.阻止默认事件。

        不想打开新页面,不仅要在ondrop上添加阻止,还要在ondragover上添加

<div class="drag_box" @drop.prevent="drop" @dragover.prevent></div>

    2.异步。

        readEntries(),file()这几个都是异步的,所以要判断是否读取完成时需要注意。

        而且safari中reader读取文件完成的顺序是乱的,比如同样的代码和文件,如果是判断最后一个文件读取结束,在谷歌中确实是都结束了,但是safari中有的文件还没有读取完成。所以如果要判断所有文件是否完成也要注意这一点---不要以数组最后一个文件读取是否完成来判断所有文件的读取状态。

    3.文件数量太多时(>100),文件缺失

let dirReader = directory.createReader()


let getEntries = function () {
    dirReader.readEntries(
    results => {
      if (results.length) {
        getEntries()
      }
    },
    error => {
      /* handle error — error is a FileError object */
    }
  )
}


getEntries()

    这个是MDN文档中的写法,一定要封装个函数判断递归!直到返回的数组中再也没有文件为止!

   经过测试,返回的数组长度最大为100

    兼容性

    MDN和caniuse网站提供数据显示:

api不兼容的浏览器
DataTransfer.itemsIE
FileSystemDirectoryEntry.createReader()IE,Opera
FileSystemFileEntry.file()IE,Opera

    但是我测试了Opera(版本号89.0.4447.71)可以正常使用,数据正常

    所以兼容性:完美