【转载】文件上传那些事儿,文件ajax无刷上传

文件上传那些事儿,文件ajax无刷上传

最近做了一个vue的文件上传组件,整准备整理一下,微博上看到这篇文章,讲得非常详细,就转载过来了。

导语

  正好新人导师让我看看能否把产品目前使用的FileUploader从老的组件库分离出来的,自己也查阅了相关的各种资料,对文件上传的这些事有了更进一步的了解。把这些知识点总结一下,供自己日后回顾,也供有需要的同学参考,同时也欢迎各位大牛拍砖指点共同学习。

FileUpload 对象

  在网页上传文件,最核心元素就是这个HTML DOM的FileUpload对象了。什么鬼?好像不太熟啊~别急,看到真人就熟了:

<input type="file">

  就是他啊!其实在 HTML 文档中该标签每出现一次,一个 FileUpload 对象就会被创建。该标签包含一个按钮,用来打开文件选择对话框,以及一段文字显示选中的文件名或提示没有文件被选中。

  把这个标签放在标签内,设置form的action为服务器目标上传地址,并点击submit按钮或通过JS调用form的submit()方法就可以实现最简单的文件上传了。

<form id="uploadForm" method="POST" action="upload" enctype="multipart/form-data">
    <input type="file" id="myFile" name="file"></input>
    <input type="submit" value="提交"></input>
</form>

这样就完成功能啦?没错。但是你要是敢提交这样的代码,估计脸要被打肿

  都什么年代了,我们要的是页面无刷新上传!

更优雅的上传

  现代网页通过什么来实现用户与服务器的无刷新交互?

  ——XMLHttpRequest

  对,就是这个你很熟悉的家伙。如果你开发的产品支持的浏览器是现代浏览器,那么恭喜你,文件上传就是这么easy!特别强调强调现代浏览器是因为我们接下来讨论的XMLHttpRequest指的是XMLHttpRequest Level 2。

  那什么是Level 1?为什么不行?因为它有如下限制:

  1. 仅支持文本数据传输,无法传输二进制数据.
  2. 传输数据时,没有进度信息提示,只能提示是否完成.
  3. 受浏览器 同源策略 限制,只能请求同域资源.
  4. 没有超时机制,不方便掌控ajax请求节奏.

而XMLHttpRequest Level 2针对这些缺陷做出了改进:

  1. 支持二进制数据,可以上传文件,可以使用FormData对象管理表单.
  2. 提供进度提示,可通过 xhr.upload.onprogress 事件回调方法获取传输进度.
  3. 依然受 同源策略 限制,这个安全机制不会变. XHR2新提供 Access-Control-Allow-Origin 等headers,设置为 * 时表示允许任何域名请求,从而实现跨域CORS访问(有关CORS详细介绍请耐心往下读).
  4. 可以设置timeout 及 ontimeout,方便设置超时时长和超时后续处理.

      关于XMLHttpRequest的细节就不在这里赘述了,有兴趣可以移步这篇博客。目前,主流浏览器基本上都支持XHR2,除了IE系列需要IE10及更高版本. 因此IE10以下是不支持XHR2的.

      上面提到的FormData就是我们最常用的一种方式。通过在脚本里新建FormData对象,把File对象设置到表单项中,然后利用XMLHttpRequest异步上传到服务器:

var xhr = new XMLHttpRequest();
var formData = new FormData();
var fileInput = document.getElementById("myFile");
var file = fileInput.files[0];
formdata.append('myFile',file);

xhr.open("POST","/upload.php");

xhr.onload = function(){
    if(this.status === 200){
        //对请求成功的处理
    }
}

xhr.send(formData);
xhr = null;

完成最基本的需求无法满足我们对用户体验的追求,所以我们还想要支持上传进度显示和上传图片预览。

上传进度

  因为是XMLHttpRequest Level 2,所以很容易就可以支持对上传进度的监听。细心地小伙伴会发现在chrome的developer tools的console里new一个XHR对象,调用点运算符就可以看到智能提示出来一个onprogress事件监听器,那是不是我们只要绑定XHR对象的progress事件就可以了呢?

  很接近了,但是XHR对象的直属progress事件并不是用来监听上传资源的进度的。XHR对象还有一个属性upload,它返回一个XMLHttpRequestUpload 对象,这个对象拥有下列下列方法:

  1. onloadstart
  2. onprogress
  3. onabort
  4. onerror
  5. onload
  6. ontimeout
  7. 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事件时遇到的一个问题。一开始我设在onprogress事件回调里的断点总是只能走到一次,并且loaded值始终等于total。觉得有点诡异,改用console.log打印loaded值不见效,于是直接加大上传文件的大小到50MB,终于看到了5个不同的百分比值。

因为xhr.upload.onprogress在上传阶段(即xhr.send()之后,xhr.readystate=2之前)触发,每50ms触发一次。所以文件太小网络环境好的时候是直接到100%的。

图片预览

普通青年的图片预览方式是待文件上传成功后,后台返回上传文件的url,然后把预览图片的img元素的src指向该url。这其实达不到预览的效果和目的。

属于文艺青年的现代浏览器又登场了:“使用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);

多文件支持

什么?一个一个添加文件太烦?别急,打开一个开关就好了。别忘了我们文章一开头就登场的FileUpload对象,它有一个multiple属性。只要这样

<input id="myFile" type="file" multiple>

我们就能在打开的文件选择对话框中选中多个文件了。然后你在代码里拿到的FileUpload对象的files属性就是一个选中的多文件的数组了。

var fileInput = document.getElementById("myFile");
var files = fileInput.files;
var formData = new FormData();

for(var i = 0; i < files.length; i++) {
    var file = files[i];
    formData.append('files[]',file,file.name);
}

FormData的append方法提供第三个可选参数用于指定文件名,这样就可以使用同一个表单项名,然后用文件名区分上传的多个文件。这样也方便前后台的循环操作。

二进制上传

有了FileReader,其实我们还有一种上传的途径,读取文件内容后直接以二进制格式上传。

var reader = new FileReader();
reader.onload = function(){
    xhr.sendAsBinary(this.result);
}
// 把从input里读取的文件内容,放到fileReader的result字段里
reader.readAsBinaryString(file);

不过chrome已经把XMLHttpRequest的sendAsBinary方法移除了。所以可能得自行实现一个

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);
}

这段代码将字符串转成8位无符号整型,然后存放到一个8位无符号整型数组里面,再把整个数组发送出去。

到这里,我们应该可以结合业务需求实现一个比较优雅的文件上传组件了。等等,哪里优雅了?都不支持拖拽!

拖拽的支持

利用HTML5的drag & drop事件,我们可以很快实现对拖拽的支持。首先我们可能需要确定一个允许拖放的区域,然后绑定相应的事件进行处理。看代码

var dropArea;

dropArea = document.getElementById("dropArea");
dropArea.addEventListener("dragenter",handleDragenter,false);
dropArea.addEventListener("dragover",handleDragover,false);
dropArea.addEventListener("drop",handleDrop,false);

// 阻止dragenter和dragover的默认行为,这样才能使drop事件被触发
function handleDragenter(e) {
    e.stopPropagation();
    e.preventDefault();
}

function handleDragover(e) {
    e.stopPropagation();
    e.preventDefault();
}

function handleDrop(e) {
    e.stopPropagation();
    e.preventDefault();

    var dt = e.dataTransfer;
    var files = dt.files;

    // handle files ...
}

这里可以把通过事件对象的dataTransfer拿到的files数组和之前相同处理,以实现预览上传等功能。有了这些事件回调,我们也可以在不同的事件给我们UI元素添加不同的class来实现更好交互效果。

好了,一个比较优雅的上传组件可以进入生产模式了。什么?还要支持IE9?好吧,让我们来看看IE10以下的浏览器如何实现无刷新上传。

借用iframe

之前说了要实现文件上传使用FileUpload对象(选择文件)即可。这在低版本的IE里也是适用的。那我们为什么还要用iframe呢?

因为在现代浏览器中我们可以用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;

好了,实现一个文件上传组件的基本知识点大致总结了一下。在这些基础知识之上我们开始可以为我们的业务开发各种酷炫的File Uploader了。在之后的开发中会把相关的更细的知识点也总结进来,不足之处也欢迎大家指正。

原文地址:https://www.qcloud.com/community/article/985614
作者:谭伟华

如果转载侵犯版权请私信告知

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


$.AJAX()方法中的PROCESSDATA参数 在使用jQuery的$.ajax()方法的时候参数processData默认为true(该方法为jQuery独有的) 默认情况下会将发送的数据序列化以适应默认的内容类型application/x-www-form-urlencoded 如果想发送不
form表单提交的几种方式 表单提交方式一:直接利用form表单提交 html页面代码: &lt;!DOCTYPE html&gt; &lt;html&gt; &lt;head&gt; &lt;meta charset=&quot;UTF-8&quot; /&gt; &lt;title&gt;Ins
文章浏览阅读1.3k次。AJAX的无刷新机制使得在注册系统中对于注册名称的检测能即时显示。常见的用户注册是用户输入用户名,后台程序检测数据库中用户名是否重复而做出注册的成功与失败之提示(当用户注册重名时将返回重新注册),或者稍微人性化一点就是在用户名文本框后添加一个检测按钮,让用户检测后再做注册。以上操作,对于用户体验方面来说是比较“差劲”的,一个很好的用户体验就是:当用户输入完注册用户名后,Web系统应能即时检查并即时_用户注册 实时异步检测
文章浏览阅读1.2k次。 本文将解释如何使用AJAX和JSON分析器在客户端和服务器之间创建复杂的JSON数据传输层。一、 引言毫无疑问,AJAX已经成为当今Web开发中一种强有力的用户交互技术,但是它的许多可能性应用仍然鲜为人知。在本文中,我们将来共同探讨如何 使用JavaScript对象标志(JSON)和JSON分析器在服务器和客户端AJAX引擎之间创建复杂而强有力的JSON数据传输层。我们将_ajax技术可行性
文章浏览阅读2.2k次。/************************** 创建XMLHttpRequest对象 **************************/function CreateRequest(){ var xmlObj = null; try { xmlObj = new XMLHttpRequest(); } catch(e) {
文章浏览阅读3.7k次。在ajax应用中,通常一个页面要同时发送多个请求,如果只有一个XMLHttpRequest对象,前面的请求还未完成,后面的就会把前面的覆盖 掉,如果每次都创建一个新的XMLHttpRequest对象,也会造成浪费。解决的办法就是创建一个XMLHttpRequset的对象池,如果池里有 空闲的对象,则使用此对象,否则将创建一个新的对象。下面是我最近写的一个简单的类:* XMLHttpReques_xmlhttprequest发送多个请求
文章浏览阅读3.1k次。Ajax 同一页面如何同时执行多个 XMLHTTP 呢,比如博客页,需要同时利用 Ajax 读取作者信息、文章信息、评论信息……我们的第一反应可能是创建多个全局 XMLHTTP 对象,但这并不现实。其实实现方式非常简单,就是给 onreadystatechange 对应的回调函数加上参数,以下代码是解决方案中一个函数中的一段代码。xmlhttp.open("GET", "ajax_proc_ajax响应多个mxl文件
文章浏览阅读1.5k次。数据岛指的是存在Html网页中的xml代码段,它在Html中形成了一个数据的集合,数据岛允许我们在Html网页中集成xml,对xml编写脚本.数据岛有它特有的形式,由标记xml开始,在开始标记中要有一个ID属性,用于指定该指定数据岛的名称。 (当然要以/xml结束).元素xml包含的内容就是xml代码。数据岛也分为2种:1)内嵌的数据岛形式2)外嵌的数据岛形式说了那么多废话,还_数据中岛计算模式
文章浏览阅读2.1k次。AJAX 流行之后,总想好好学习一下。但是众多的框架实在难以选择。说明一下 ASP.NET AJAX 并不包括在 AJAX 框架之中。刚开始学了 JQuqery, 众多的 $get(),...等等符号早已把我搞晕了。暂时就放弃了。后来学习 ASP.NET AJAX ,在微软的领导下,逐渐由服务器端转向客户端编程。 激起我客户端编程的兴趣,才想起学习一下了 Jquery. 随着WEB2._jquery ajax asp.net 认证
文章浏览阅读1.7k次。前段时间在用google map api的函数库的时候,发现里面的downloadUrl函数非常好用,所以自己写了一个。用腻了那些什么框架什么池,到头来发现越简单的东西越是适合我这种懒人。downloadUrl(url, callback, data);参数说明: url不用说了; callback是回调函数,函数调用的时候会有两个参数:data, responseCode,data就_xmlhttprequest downloadurl
文章浏览阅读956次。前些时间写了几篇关于XMLHTTP运用的实例.(可以到http://dev.csdn.net/user/wanghr100看之前的几编关于XMLHTTP的介绍.)近来看论坛上经常有人提问关于如何无刷新,自动更新数据.传统上,我们浏览网页,如果加入最新的数据.只能是等我们重新向服务器端请求时才能显示出来.但是,对于一些时效性很强的网站.传统的这种做法是不能满足的.我们可以让程序自动刷新.定时_后端xml怎么实现数据有救新增,没有就更新
文章浏览阅读3.3k次。 XMLHttpRequest调用XMLHttpRequest Call ●●●调用,回调,下载,抓取,实时,查询,远程通信(Remoting),远程通信脚本(RemoteScripting),同步,上传,XMLHttpRequest图6-2:XMLHttpRequest调用 目标故事Reta正在一个批发商网站上购买商品。每次她添加一个商品到购物车时,web站点发出_createxmlhttpre
文章浏览阅读1.3k次。function clearitem(){ var drp1 = document.getElementById("drp1"); while(drp1.options.length>0) { drp1.options.remove(0); } }//动态更改方法(根据城市代码取得该市商业区并添加到DropDownList中_dropdownlist根據動態變化
文章浏览阅读1.9k次。因為 Json.net 是有附原始碼的,他也附了單元測試的專案,底下是我額外增加的UnitTest,我的目標就是讓底下的測試可以pass,而且原來的Test 也要都能通過。 ValueTypeTest.csusing System;using NUnit.Framework;namespace Newtonsoft.Json.Test { [TestFixture] public cl_vb 無效的 json 基本型別
文章浏览阅读844次。利用XMLHTTP无刷新获取数据. 客户端和服务器端数据的交互有几种方法.1.提交,通过提交到服务器端.也称"有刷新"吧.2.通过XMLHTTP无刷新提交到服务器端,并返回数据.也称"无刷新"吧.利用XMLHTTP我们可以实现很多很强大的应用.这文章主要介绍它的一些简单的应用.附:因为XMLHTTP是IE5.0+支持的对象.所以你必须要有IE5.0+才能看到效果.client.htm_xmlhttp取源码没有更新
文章浏览阅读1.8k次。Json.Net 無法序列基本型別(string, int),Asp.Net Ajax 無法正確序列日期,AjaxPro序列出我不想要的_type字串 1. Json.Net 是我最常使用的序列/反序列json套件,標榜速度快,對於一對多關係的object 也都能正常運作, 己能滿足我平日的需要,但前幾天突然有個情況,我要序列的是一個泛型參數,該參數不一定是物object型別,有可能是st_token string in state start
文章浏览阅读1.3k次。转载自:http://www.cnblogs.com/JeffreyZhao/archive/2007/01/31/update_the_updatepanels_by_js.html众 所周知,UpdatePanel是通过Trigger来更新的。被设定为Trigger的控件在PostBack之后会被客户端所截获,并且使用 XMLHttpRequest对象发送内容,然后服务器端由ScriptMan_web.ui.updatepanel 和 updatepanel 的区别
文章浏览阅读1.9k次。有些时候,只是需要更新页面的一个部分甚至只是更新中间的几个数据却需要从服务器DOWN整个页面,导致各种资源的浪费。使用数据岛技术可以很好的解决这个问题:通过定时器或用户事件触发数据岛(XML对象)象服务器获取数据,在数据获取完成后,适时更新相关数据。示例HTML部分:http://localhost/WebService/LoadData/FeaturedService.asmx/GetScore_web数据岛
文章浏览阅读1k次。在页面上使用ActiveXObject的代价是很大的,如果我们的无刷新页面使用xmlhttp技术,我们或许需要频繁的建立xmlhttp对象,当然 我们也可以使用全局变量来cache一个xmlhttp对象实例。但是这样的方法适合于同步方式xmlhttp通信,而对于异步方式xmlhttp通信将 会出现问题。由于没有了进程的堵塞,用户可能再次调用同一个xmlhttp实例,如果这时前一个通信未完成,那么就
文章浏览阅读998次。 by Lokesh Dhakar 译: croc查看原文概要:Lightbox JS 是一个简单而又谦恭的用来把图片覆盖在当前页面上的脚本. 它能被快速安装并且运作于所有流行的浏览器.最新更新 Version 2.0 图片集: 分组相关的图片并且能轻松的导航它们 视觉特效: 奇特的自适应调整 向后兼容: yes! 点击这里查看实例_on lightbox 2 by lokesh dhakar