拖放文件上传功能总结

学习了下HTML5的拖放API,并做了一个拖放文件上传的Demo,server端使用了node.js,github地址

拖放API

在HTML5中,可以让DOM中的某个元素具有可拖放的属性,或者可以将浏览器外的文件拖放到浏览器中,并利用File API做一些后续的处理。先介绍下HTML5的拖放API。

要让DOM中的元素可拖放,需要设置该元素的draggable属性,并赋值为true,就像这样:

1
<div class="demo" draggable="true"></div>

然后介绍下拖放相关的事件,这是我们主要用来实现拖放逻辑的东西。

事件 产生事件的对象 描述
dragstart 被拖放的元素 开始拖放操作
drag 被拖放的元素 在拖放的过程中
dragend 被拖放的元素 拖放操作结束
dragenter 拖放过程中鼠标经过的元素 被拖放的元素进入该元素
dragover 拖放过程中鼠标经过的元素 被拖放的元素在该元素中移动
dragleave 拖放过程中鼠标经过的元素 被拖放的元素离开该元素
drop 拖放的目标元素 拖放元素被放到了该元素上

拖放操作说到底是一种数据交换的操作,即将被拖放元素上的数据传递到拖放目标上去,那么我就需要dataTransfer对象来做这个数据传递的工作。

首先我们需要设置被拖放元素需要传递的数据,这是我们要利用dragstart事件:

1
2
3
4
5
6
7
var source = document.getElementById('source');
var dest = document.getElementById('dest');

source.addEventListener('dragstart', function(e) {
var dt = e.dataTransfer;
dt.setData('data', 'Hello world');
}, false);

这里,dataTransfer对象作为Event对象的一个属性可被访问,通过setData方法,为要传递的数据提供键值。

接下来,由于浏览器默认是拒绝拖放操作的,需要阻止这个默认事件,这是需要利用dragover事件,希望可以拖放哪个元素中,就在这个元素上注册dragover事件并阻止默认行为:

1
2
3
dest.addEventListener('dragover', function(e) {
e.preventDefault();
}, false);

最后写拖放后的操作,我们这里利用的是drop事件,并使用dataTransfer.getData方法通过键,获取要传递的数据:

1
2
3
4
dest.addEventListener('drop', function(e) {
var dt = e.dataTransfer;
dest.textContent = dt.getData('data');
}, false);

拖放文件上传的实现

我们需要一个server端来帮我们完成上传的后台逻辑,这里我用了node.js,并使用了formidable模块来处理上传请求,再使用node-static模块来处理静态文件请求。详尽的server端实现这边就略过了,有兴趣的可以直接参考github上的代码。

首先介绍下拖放文件和拖放DOM元素的不同之处,就是传递的数据是不需要我们来设置的,我们可以直接访问dataTransfer.files属性来得到被拖放的文件,这边的文件是允许多个的。

这边有两个点需要说明下:

第一点是,异步文件上传,我使用了XMLHttpRequest 2.0,IE10及以上的兼容性,xhr.send()方法可以接受File、Blob、FormData等数据类型了,这里我利用的是FormData,直接将文件append到FormData中去,当使用xhr.send(data)时,Content-Type会自动被置为multipart/form-data并设置boundary,比较方便。当然不用FormData,直接send一个File对象也是可以的,不过这样的话,就需要手动将Content-Type头部设置为multipart/form-data,并且最麻烦的是需要生成一个boundary,并将文件内容分片,过程相对繁琐一些。

1
2
var formData = new FormData();
formData.append('file', file, file.name);

第二点是,在这里我使用的是每个文件一个请求的方式进行上传的,但是可以在一个FormData中append多个文件,并通过一个请求进行上传,不过我还不清楚怎么跟踪每个文件的上传进度并返回给客户端,这点还需要研究和提高。

好了,来看实现吧,这里不需要dragstart事件来设置传递的数据了,直接在drop事件里获取dataTransfer.files即可,不过这里有一点是,文件被拖放到浏览器内的默认行为是试着打开它,不能直接打开,就下载(其实就相当于复制到下载文件夹下),所以我们需要在drop事件里阻止这个默认行为,其实如果一个页面有拖放功能,为了防止误操作,可以在document.ondrop里阻止全局的拖放默认行为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
dest.addEventListener('drop', function(e) {
e.preventDefault();

var dt = e.dataTransfer;
[].forEach.call(dt.files, function(file) {
var xhr = new XMLHttpRequest();
var formData = new FormData();
formData.append('file', file, file.name);

// 省略dom操作

xhr.open('POST', '/upload'); // async default
xhr.upload.onprogress = function(e) {
// 更新进度条
};
xhr.onload = function(e) {
// 上传完成
};
xhr.onerror = function(e) {
// 错误处理
};
xhr.send(formData);
}, false);

这个就是大致的结构,说下其中遇到过的一个小问题,是关于dragenter和dragleave的。我希望文件被移入时,box的边框可以高亮,移出后移除高亮,但是我这个box是有子元素的,那么问题就来了,当拖放到子元素上的时候,会触发box的dragleave一次和dragenter一次(这次是其子元素冒泡上来的),这样的话导致我移入子元素时,边框也同时被移除高亮了,这不是我想要的。

解决方法如下,设置一个计数器,在dragenter时自增1,在dragleave时自减1,在dragleave中,如果这个计数器自减后为0了,那么这时可以移除高亮。这个主要是因为移入子元素时,先触发的是冒泡上来的dragenter,然后再试dragleave,那么在子元素上的时候计数器不会为0,在真正移出box后,计数器才会是0,这样问题就解决了。

1
2
3
4
5
6
7
8
9
var counter = 0;
dest.addEventListener('dragenter', function(e) {
counter++;
dest.classList.add('active');
});
dest.addEventListener('dragleave', function(e) {
counter--;
if(!counter) dest.classList.remove('active');
});

好了,这个拖放文件上传相关的内容就是这样总结分享完了,可以在github上看到完整的项目代码