html5 文件拖拽上传是个老话题了,网上有很多例子,我一开始的代码也是网上找来改的,只是踩了几个坑之后就想把过程记录下来。
功能实现
下面主要介绍从浏览器外拖拽文件到浏览器进行上传的实现。
拖拽事件
拖拽事件有下面这些:
dragstart
:当用户开始拖动对象时触发。dragenter
: 当鼠标第一次经过目标元素,且有拖动发生时触发。此事件的监听者应指明在这个位置上是否允许drop,或者监听者不执行任何操作,那么drop默认是不允许的。dragover
:当鼠标经过一个元素时,且有拖动发生时触发 。dragleave
:当鼠标离开一个元素,且有拖动在发生时触发。drag
: 当对象被拖动,每次移动鼠标时触发。drop
:在drag操作的最后发生drop时,在元素上触发此事件。监听者应该负责检索拖动的数据,并插入drop的位置。dragend
: 在拖动对象时放开鼠标按键时触发。
从浏览器外拖拽文件到浏览器时,必须要绑定的事件有 dragover
和 drop
,其他的都可以不绑定。** dragover
和 drop
事件的处理函数内必须调用事件的 preventDefault()
函数**,要不然浏览器会进行默认处理,比如文本类型的文件直接打开,非文本的可能弹出一个下载文件框。
DataTransfer对象
拖拽对象用来传递数据的媒介,通过拖拽事件的 event.dataTransfer
获取。
dataTransfer.dropEffect [ = value ]
:返回当前选择的操作类型,可以设置新的值来修改已选择的操作。可选的值有:none, copy, link, move
。dataTransfer.effectAllowed [ = value ]
:返回允许的操作类型,可修改。可选的值有:none, copy, copyLink, copyMove, link, linkMove, move, all, uninitialized
。dataTransfer.types
:返回一个DOMString,列出在dragstart事件里设置的所有格式。另外,如果有文件被拖动,那么其中一个类型的字符串将是“Files”。dataTransfer.clearData( [ format ] )
:移除指定格式的数据。如果忽略参数则移除所有数据。dataTransfer.setData(format, data)
:添加指定的数据。data = dataTransfer.getData(format)
:返回指定的数据。如果没有这样的数据,则返回空字符串。dataTransfer.files
:返回被拖拽的FileList,如果有。dataTransfer.setDragImage(element, x, y)
:用指定的元素来更新drag反馈,替换之前指定的反馈(feedback)。dataTransfer.addElement(element)
:添加指定元素到用于渲染drag反馈的元素列表。
在这个用例里,最重要的就是 dataTransfer.files
属性,它是用户拖拽进浏览器的文件列表,是个 FileList
对象,有 length
属性,可以通过下标访问。
FormData
FormData
代表一个表单,可以通过 append('fieldName', value)
函数往表单里添加参数,参数的只不仅可以是字符串,还可以是File对象,甚至二进制数据。
XMLHttpRequest level 2
新版本的XMLHttpRequest对象,这里说的XMLHttpRequest都是指新版的。
XMLHttpRequest可以向不同域名的服务器发出HTTP请求。这叫做 “跨域资源共享”(Cross-origin resource sharing,简称CORS)。
浏览器有个著名的同源策略,这里浏览器安全的基础,CORS 除了需要浏览器支持外,还要服务器同意。
XMLHttpRequest 支持直接发送FormData,就像浏览器进行表单提交一样。
XMLHttpRequest 还支持进度信息(progress
事件),进度分为上传进度和下载进度,上传进度的事件是在 XMLHttpRequest.upload
对象上,下载进度的事件是在 XMLHttpRequest
对象。每个进度事件都有三个属性:
lengthComputable
:可计算的已上传字节 数total
:总的字节 数loaded
:到目前为止上传的字节 数
除了进度事件,还支持下面五个事件:
load
事件:传输成功完成。abort
事件:传输被用户取消。error
事件:传输中出现错误。loadstart
事件:传输开始。loadend
事件:传输结束,但是不知道成功还是失败。
同 progress
事件一样,属于上传操作的事件处理函数绑定在XMLHttpRequest.upload
对象上,属性下载的直接绑定在 XMLHttpRequest
对象。
具体代码
服务器端
服务器端需要写个Servlet来接收上传的表单。 /html5/FileUploadServlet
客户端代码
<html>
<head>
<title> drag drop upload demo
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
</head>
<body>
<div id= "progressBarZone">请将文件拖拽进浏览器内! <br/></ div>
</body>
<script>
var progressBarZone = document.getElementById('progressBarZone');
function sendFile(files) {
if (!files || files.length < 1) {
return;
}
var percent = document.createElement('div' );
progressBarZone.appendChild(percent);
var formData = new FormData(); // 创建一个表单对象FormData
formData.append( 'submit', '中文' ); // 往表单对象添加文本字段
var fileNames = '' ;
for ( var i = 0; i < files.length; i++) {
var file = files[i]; // file 对象有 name, size 属性
formData.append( 'file[' + i + ']' , file); // 往FormData对象添加File对象
fileNames += '《' + file.name + '》, ' ;
}
var xhr = new XMLHttpRequest();
xhr.upload.addEventListener( 'progress',
function uploadProgress(evt) {
// evt 有三个属性:
// lengthComputable – 可计算的已上传字节数
// total – 总的字节数
// loaded – 到目前为止上传的字节数
if (evt.lengthComputable) {
percent.innerHTML = fileNames + ' upload percent :' + Math.round((evt.loaded / evt.total) * 100) + '%
' ;
}
}, false); // false表示在事件冒泡阶段处理
xhr.upload.onload = function() {
percent.innerHTML = fileNames + '上传完成。
' ;
};
xhr.upload.onerror = function(e) {
percent.innerHTML = fileNames + ' 上传失败。
' ;
};
xhr.open( 'post', 'http://cross.site.com:8080/html5/FileUploadServlet' , true);
xhr.send(formData); // 发送表单对象。
}
document.addEventListener("dragover", function(e) {
e.stopPropagation();
e.preventDefault(); // 必须调用。否则浏览器会进行默认处理,比如文本类型的文件直接打开,非文本的可能弹出一个下载文件框。
}, false);
document.addEventListener("drop", function(e) {
e.stopPropagation();
e.preventDefault(); // 必须调用。否则浏览器会进行默认处理,比如文本类型的文件直接打开,非文本的可能弹出一个下载文件框。
sendFile(e.dataTransfer.files);
}, false);
</script>
</html>
如果上面的代码都部署在同一个网站下,那是没有问题的。可是我要做的上传操作是要把文件传到另一个网站上,坑也就产生了。
踩过的坑
坑其实只有一个,只是表现出来的形态很多。比如前面的代码如果在 xhr.upload 上绑定了事件处理函数时,请求都不发送;在xhr上绑定的事件也没有回调。一开始总是在Firefox下测试,怎么改代码都不行,因为不是代码问题,上面的代码是没有问题。
看不出是什么问题。不过在Chrome下看就可以发现问题所在了:
从上面可以看到,问题在于浏览器的同源策略,也就是跨域的问题。
同源策略
同源策略阻止从一个源加载的文档或脚本获取或设置另一个源加载的文档的属性。这是浏览器安全的基础。
源
如果两个页面的协议、端口(如果指明了的话)和主机名都相同,认为这两个页面拥有相同的源。
下表给出了相对 http://store.company.com/dir/page.html
同源检测的结果:
URL 结果 原因 http://store.company.com/dir2/other.html 成功 http://store.company.com/dir/inner/another.html 成功 https://store.company.com/secure.html 失败 协议不同 http://store.company.com:81/dir/etc.html 失败 端口不同 http://news.company.com/dir/other.html 失败 主机名不同
同源策略的例外
在同源策略中有一个例外,脚本可以设置 document.domain
的值为当前域的一个后缀,比如域 store.company.com
的后缀可以是 company.com
。如果这样做的话,短的域将作为后续同源检测的依据。例如,假设在 http://store.company.com/dir/other.html
中的一个脚本执行了下列语句:
document.domain = "company.com";
这条语句执行之后,页面将会成功地通过对 http://company.com/dir/page.html
的同源检测。而同理,company.com
不能设置 document.domain
为 othercompany.com
.
浏览器对跨域请求的处理
当浏览器要执行一个跨域请求资源时,分两步来执行:
- 首先用
HTTP OPTIONS
方法请求该资源,获取响应头,这一步不会携带实际的参数。如果响应头有Access-Control-Allow-Origin
域,且其值为*
或者与请求头的Origin
域的值进行同源检测是同源的,则进行第二步,否则终止请求。 - 发送实际的请求去获取资源。
** Access-Control-Allow-Origin
域的值为*
表示允许任意站点请求该服务器的资源;*
只能独立使用,不能作为域的一部分使用,如 *.site.com
与 cross.site.com
是不同源的。 **
更多信息可以参考:
- 客户端设置,HTTP access control (CORS) https://developer.mozilla.org/en-US/docs/HTTP/Access_control_CORS?redirectlocale=en-US&redirectslug=HTTP_access_control
- 服务器端设置,Server-Side Access Control https://developer.mozilla.org/en-US/docs/Server-Side_Access_Control
测试验证
可以查看我部署的测试demo: http://coderbee.net/html5/crossdomain.html
建议在Chrome下观察,xhr请求的http://198.56.238.193/html5/crossdomain.php,在处理OPTIONS请求时会动态返回Access-Control-Allow-Origin响应头:
$randnum = rand(5, 10);
if ($_SERVER['REQUEST_METHOD']=='OPTIONS' && ($randnum % 2) == 1) {
header("Access-Control-Allow-Origin: http://198.56.238.193");
} else {
header("Access-Control-Allow-Origin: http://coderbee.net");
}
欢迎关注我的微信公众号: coderbee笔记,可以更及时回复你的讨论。
真心不错~我要改造楼主的代码~