文件异步上传
本文摘抄自网络
XHR对象还有一个属性upload, 它返回一个XMLHttpRequestUpload 对象,这个对象拥有下列下列方法:
-
onloadstart
-
onprogress
-
onabort
-
onerror
-
onload
-
ontimeout
-
onloadend
这些方法在XHR对象中都存在同名版本,区别是后者是用于加载资源时,而前者用于资源上传时。其中onprogress 事件回调方法可用于跟踪资源上传的进度,它的event参数对象包含两个重要的属性loaded和total。分别代表当前已上传的字节数(number of bytes)和文件的总字节数。比如我们可以这样计算进度百分比:
xhr.upload.onprogress = function(event) { if (event.lengthComputable) { var percentComplete = (event.loaded / event.total) * 100; // 对进度进行处理 } }
其中事件的lengthComputable属性代表文件总大小是否可知。如果 lengthComputable 属性的值是 false,那么意味着总字节数是未知并且 total 的值为零。
如果是现代浏览器,可以直接配合HTML5提供的
<progress id="myProgress" value="50" max="100"> </progress>
其value属性绑定上面代码中的percentComplete的值即可。再进一步我们还可以对<progress>
的样式统一调整,实现优雅降级方案,具体参见这篇文章。
再说说我在测试这个progress事件时遇到的一个问题。一开始我设在onprogress事件回调里的断点总是只能走到一次,并且loaded值始终等于total。觉得有点诡异,改用console.log打印loaded值不见效,于是直接加大上传文件的大小到50MB,终于看到了5个不同的百分比值。
因为xhr.upload.onprogress在上传阶段(即xhr.send()之后,xhr.readystate=2之前)触发,每50ms触发一次。所以文件太小网络环境好的时候是直接到100%的。
关于文件预览,现代浏览器,使用HTML5的FileReader API可以实现。
function handleImageFile(file) { var previewArea = document.getElementById('previewArea'); var img = document.createElement('img'); var fileInput = document.getElementById("myFile"); var file = fileInput.files[0]; img.file = file; previewArea.appendChild(img); var reader = new FileReader(); reader.onload = (function(aImg) { return function(e) { aImg.src = e.target.result; } })(img); reader.readAsDataURL(file); }
这里我们使用FileReader来处理图片的异步加载。
在创建新的FileReader对象之后,我们建立了onload函数,然后调用readAsDataURL()开始在后台进行读取操作。当图像文件加载后,转换成一个 data: URL,并传递到onload回调函数中设置给img的src。
另外我们还可以通过使用对象URL来实现预览
var img = document.createElement("img"); img.src = window.URL.createObjectURL(file);; img.onload = function() { // 明确地通过调用释放 window.URL.revokeObjectURL(this.src); } previewArea.appendChild(img);
二进制上传
XMLHttpRequest.prototype.sendAsBinary = function(text){ var data = new ArrayBuffer(text.length); var ui8a = new Uint8Array(data, 0); for (var i = 0; i < text.length; i++){ ui8a[i] = (text.charCodeAt(i) & 0xff); } this.send(ui8a); }
因为在现代浏览器中我们可以用XMLHttpRequest Level 2来支持二进制数据,异步文件上传,并且动态创建FormData。
而低版本的IE里的XMLHttpRequest是Level 1。所以我们通过XHR异步向服务器发上传请求的路走不通了。只能老老实实的用form的submit。
而form的submit会导致页面的刷新。原因分析好了,那么答案就近在咫尺了。我们能不能让form的submit不刷新整个页面呢?答案就是利用iframe。把form的target指定到一个看不见的iframe,那么返回的数据就会被这个iframe接受,于是乎就只有这个iframe会刷新。而它又是看不见的,用户自然就感知不到了。
window.__iframeCount = 0; var hiddenframe = document.createElement("iframe"); var frameName = "upload-iframe" + ++window.__iframeCount; hiddenframe.name = frameName; hiddenframe.id = frameName; hiddenframe.setAttribute("style", "width:0;height:0;display:none"); document.body.appendChild(hiddenframe); var form = document.getElementById("myForm"); form.target = frameName;
然后响应iframe的onload事件,获取response
hiddenframe.onload = function(){ // 获取iframe的内容,即服务返回的数据 var resData = this.contentDocument.body.textContent || this.contentWindow.document.body.textContent; // 处理数据 。。。 //删除iframe setTimeout(function(){ var _frame = document.getElementById(frameName); _frame.parentNode.removeChild(_frame); }, 100); }
iframe的实现大致如此,但是如果文件上传的地址与当前页面不在同一个域下就会出现跨域问题。导致iframe的onload回调里的访问服务返回的数据失败。
这时我们再祭出JSONP这把利剑,来解决跨域问题。首先在上传之前注册一个全局的函数,把函数名发给服务器。服务器需要配合在response里让浏览器直接调用这个函数
// 生成全局函数名,避免冲突 var CALLBACK_NAME = 'CALLBACK_NAME'; var genCallbackName = (function () { var i = 0; return function () { return CALLBACK_NAME + ++i; }; })(); var curCallbackName = genCallbackName(); window[curCallbackName] = function(res) { // 处理response 。。。 // 删除iframe var _frame = document.getElementById(frameName); _frame.parentNode.removeChild(_frame); // 删除全局函数本身 window[curCallbackName] = undefined; } // 如果已有其他参数,这里需要判断一下,改为拼接 &callback= form.action = form.action + '?callback=' + curCallbackName;