在用户拖拽文件到浏览器的某个元素上时, js 可以监听到与拖拽相关的事件, 并对拖拽结果进行处理, 本文讨论下和拖拽文件相关的一些问题, 不过没有处理太多关于兼容性的问题
拖拽事件
js 能够监听到拖拽的事件有 dragdragenddragenterdragexit(没有浏览器实现)dragleavedragoverdragstartdrop, 详细的内容可以看 MDN
其中, 与拖拽文件相关的事件有 dragenter(文件拖拽进)dragover(文件拖拽在悬浮)dragleave(文件拖拽离开)drop(文件拖拽放下)
拖拽事件可以绑定到指定的 DOM 元素上, 可以绑定到整个页面中
- var dropEle = document.querySelector('#dropZone');
- dropEle.addEventListener('drop', function (e) {
- //
- }, false);
- document.addEventListener('drop', function (e) {
- //
- }, false);
阻止默认行为
一般来说, 我们只需要把处理拖拽文件的业务逻辑写到 drop 事件中就可以了, 为什么还要绑定 dragenterdragoverdragleave 这三个事件呢?
因为当你拖拽一个文件到没有对拖拽事件进行处理的浏览器中的时候, 浏览器会打开这个文件, 比如拖拽一张图片浏览器会打开这个图片, 在没有 PDF 阅读器的时候也可以拖拽一个 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 对象有四个属性: lastModifiednamesizetype, 其中 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 实例, 通过这个实例对文件进行读取, 我测试读取一个 1G 多的文件要 3S 多, 时间有点长, 就用 slice 截取了前 3 个字符, 为什么是前 3 个不是前 2 个或者前 4 个呢, 因为代码是我写的, 我开心这么写呗~
如果 load 事件触发了, 就说明拖拽过来的东西是文件, 如果 error 事件触发了, 就说明是文件夹, 为了防止其他可能的潜在错误, 用 try 包起来这段代码
三方应用的一点点小 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);
来源: http://www.jb51.net/article/135119.htm