最近遇到了一个文件上传的问题,为了更好地解决问题。
自己也查阅了相关的各种资料,对文件上传的这些事有了更进一步的了解。
把这些知识点总结如下,我们开始吧。
FileUpload 对象
在网页上传文件,最核心元素就是这个HTML DOM的FileUpload对象了。
1 | <input type="file"> |
就是他啊!其实在 HTML 文档中该标签每出现一次,一个 FileUpload 对象就会被创建。
该标签包含一个按钮,用来打开文件选择对话框,以及一段文字显示选中的文件名或提示没有文件被选中。
把这个标签放在<form>
标签内,设置form的action为服务器目标上传地址,并点击submit按钮或通过JS调用form的submit()方法就可以实现最简单的文件上传了。
1 | <form id="uploadForm" method="POST" action="upload" enctype="multipart/form-data"> |
但是我们现在要的都是页面无刷新上传!
XMLHttpRequest
我们现在使用的大都是XMLHttpRequest Level 2。
Level 1,有如下限制:
仅支持文本数据传输, 无法传输二进制数据.
传输数据时, 没有进度信息提示, 只能提示是否完成.
受浏览器 同源策略 限制, 只能请求同域资源.
没有超时机制, 不方便掌控ajax请求节奏.
而XMLHttpRequest Level 2针对这些缺陷做出了改进:
支持二进制数据, 可以上传文件, 可以使用
FormData
对象管理表单.
提供进度提示, 可通过xhr.upload.onprogress
事件回调方法获取传输进度.
依然受 同源策略 限制, 这个安全机制不会变. XHR2新提供Access-Control-Allow-Origin
等headers, 设置为 * 时表示允许任何域名请求, 从而实现跨域CORS访问
可以设置timeout
及ontimeout
, 方便设置超时时长和超时后续处理.
IE10以下是不支持XHR2的
上面提到的FormData就是我们最常用的一种方式。通过在脚本里新建FormData对象,把File对象设置到表单项中,然后利用XMLHttpRequest异步上传到服务器:
1 | var xhr = new XMLHttpRequest(); |
上传进度
XHR对象还有一个属性upload, 它返回一个XMLHttpRequestUpload 对象,这个对象拥有下列下列方法:
onloadstart
onprogress
onabort
onerror
onload
ontimeout
onloadend
这些方法在XHR对象中都存在同名版本,区别是后者是用于加载资源时,而前者用于资源上传时。
其中onprogress 事件回调方法可用于跟踪资源上传的进度,它的event参数对象包含两个重要的属性loaded和total。
分别代表当前已上传的字节数(number of bytes)和文件的总字节数。比如我们可以这样计算进度百分比:
1 | xhr.upload.onprogress = function(event) { |
其中事件的lengthComputable属性代表文件总大小是否可知。
如果 lengthComputable 属性的值是 false,那么意味着总字节数是未知并且 total 的值为零。
如果是现代浏览器,可以直接配合HTML5提供的元素使用,方便快捷的显示进度条。
1 | <progress id="myProgress" value="50" max="100"></progress> |
其value属性绑定上面代码中的percentComplete的值即可。再进一步我们还可以对<progress>
的样式统一调整,实现优雅降级方案,具体参见这篇文章。
图片预览
普通的图片预览方式是待文件上传成功后,后台返回上传文件的url,然后把预览图片的img元素的src指向该url。这其实达不到预览的效果和目的。
此时现代浏览器又登场了:“使用HTML5的FileReader API吧!” 让我们直接上代码,直奔主题:
1 | function handleImageFile(file) { |
这里我们使用FileReader来处理图片的异步加载。
在创建新的FileReader对象之后,我们建立了onload函数,然后调用readAsDataURL()开始在后台进行读取操作。
当图像文件加载后,转换成一个 data: URL,并传递到onload回调函数中设置给img的src。
另外我们还可以通过使用对象URL来实现预览
1 | var img = document.createElement("img"); |
多文件支持
文章一开头就登场的FileUpload对象,它有一个multiple属性。只要这样
1 | <input id="myFile" type="file" multiple> |
我们就能在打开的文件选择对话框中选中多个文件了。
然后你在代码里拿到的FileUpload对象的files属性就是一个选中的多文件的数组了。
1 | var fileInput = document.getElementById("myFile"); |
FormData
的append
方法提供第三个可选参数用于指定文件名,这样就可以使用同一个表单项名,然后用文件名区分上传的多个文件。这样也方便前后台的循环操作。
二进制上传
有了FileReader,其实我们还有一种上传的途径,读取文件内容后直接以二进制格式上传。
1 | var reader = new FileReader(); |
不过chrome已经把XMLHttpRequest的sendAsBinary方法移除了。所以可能得自行实现一个
1 | XMLHttpRequest.prototype.sendAsBinary = function(text){ |
这段代码将字符串转成8位无符号整型,然后存放到一个8位无符号整型数组里面,再把整个数组发送出去。
拖拽的支持
利用HTML5的drag & drop事件,我们可以很快实现对拖拽的支持。
首先我们可能需要确定一个允许拖放的区域,然后绑定相应的事件进行处理。看代码
1 | var dropArea; |
这里可以把通过事件对象的dataTransfer拿到的files数组和之前相同处理,以实现预览上传等功能。有了这些事件回调,我们也可以在不同的事件给我们UI元素添加不同的class来实现更好交互效果。
借用iframe,实现IE10以下的浏览器如何实现无刷新上传
之前说了要实现文件上传使用FileUpload对象即可。
这在低版本的IE里也是适用的。那我们为什么还要用iframe呢?
因为在现代浏览器中我们可以用XMLHttpRequest Level 2来支持二进制数据,异步文件上传,并且动态创建FormData。
而低版本的IE里的XMLHttpRequest是Level 1。所以我们通过XHR异步向服务器发上传请求的路走不通了。只能老老实实的用form的submit。
而form的submit会导致页面的刷新。原因分析好了,那么答案就近在咫尺了。我们能不能让form的submit不刷新整个页面呢?答案就是利用iframe。
把form的target指定到一个看不见的iframe,那么返回的数据就会被这个iframe接受,于是乎就只有这个iframe会刷新。而它又是看不见的,用户自然就感知不到了。
1 | window.__iframeCount = 0; |
然后响应iframe的onload事件,获取response
1 | hiddenframe.onload = function(){ |
iframe的实现大致如此,但是如果文件上传的地址与当前页面不在同一个域下就会出现跨域问题。
导致iframe的onload回调里的访问服务返回的数据失败。
这时我们再祭出JSONP这把利剑,来解决跨域问题。
首先在上传之前注册一个全局的函数,把函数名发给服务器。
服务器需要配合在response里让浏览器直接调用这个函数。
1 | // 生成全局函数名,避免冲突 |
- 本文作者: Jambo
- 版权声明: 本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。转载请注明出处!