这篇文章是我翻译的外文,非本人原创
网上看到很多博客都有转载这篇文章
不过转载的都是英文
所以我就决定翻译一下
(翻译和格式化也耗费了一番心血 (~﹃~)~zZ)
英文能力有限,大家凑合看吧(翻译有略微改动)
(ps:关于下面版本兼容的问题请无视,毕竟这篇文章也算有一段时间了)
译文原址:传送门
HTML5终于解决了在上传文件的同时也能显示上传进度的问题
如今,大多网站都使用Flash播放器来实现这一功能
一些网站仍然使用html的form标签
(enctype=multipart/form-data)
但是它需要服务器端改动来使用户显示上传进度
实际上,我们需要做的是挂接到服务器的字节流
服务器正在接收一个文件
所以我们可以知道有多少已接收字节
并以某种方式传达信息返回给客户端浏览器
而它仍然是正在上传的文件
这种解决方案非常好(尤其对于大文件)
然而,它相当复杂
因为你基本上是接管整个服务器端处理(当你挖掘到字节流)
HTML5上传文件
XMLHttpRequest对象已经得到HTML5规范的扩展
特别是XMLHttpRequest的2级标准(目前最新版本)
已经包括了以下新特点:
- 处理字节流(如File、Blob和FormData对象在上传/下载过程中)
- 上传/下载过程中的进度事件
- 跨域请求
- 匿名请求(不发送HTTP请求)
- 超时请求
在这篇文章中,我们主要用到前两条
特别是上传文件中要使用XMLHttpRequest并且向用户提供上传进度的信息
注意,这种解决办法不需要服务器端做出任何变动
顶多也就是是处理multipart / form-data协议这种程度
所以,现有的服务器端逻辑应保持不变
这就使得这种技术更容易适应
【图1:文件上传(上传刚开始)】
【图2:文件上传(上传已完成)】
上面的图片提供给用户以下信息:
- 文件信息
- 文件名
- 文件大小
- MIME类型
- 进度条(已完成百分比)
- 上传速度/上传带宽
- 剩余估计时间
- 已上传字节
- 服务器端响应(橘色框内)
最后一条看起来不重要,但其实这种环境下十分重要
请记住,用户通常不会提交HTML表单后(ps:这里我觉得应该是文件上传完毕)就马上从该页面离开
因为我们使用的XMLHttpRequest上传发生在后台,网页仍旧没有发生变化
如果你的业务流程可以用到它
那将会带来很好的用户体验
HTML5进度事件
按照HTML5的进度事件规范
HTML5的进度事件提供以下相关信息
- total - 被转移总字节数
- loaded - 已上传字节
- lengthComputable - 指定上传总数据大小(上传文件总大小已知)
你应该注意到,我们需要使用这两条信息来计算出所有我们要显示给用户的其他信息
这是十分简单的计算
但它确实涉及很多额外代码,并且需要建立定时器
考虑到用户应该也可以了解进度信息
HTML5的进度事件规范也注意到了这一需求
同时对于浏览器厂商来说,在每次进度增加一点后提供额外信息十分简单
所以我建议进度事件应修改如下:
- total - 被转移总字节数
- loaded - 已上传字节
- lengthComputable - 指定上传总数据大小(上传文件总大小已知)
- transferSpeed - 传输速度
- timeRemaining - 剩余时间
功能实现
首先是一个非常简单的HTML表单
<!DOCTYPE html>
<html>
<head>
<title>Upload Files using XMLHttpRequest - Minimal</title>
</head>
<body>
<form id="form1" enctype="multipart/form-data" method="post" action="Upload.aspx">
<div class="row">
<label for="fileToUpload">Select a File to Upload</label><br>
<input type="file" name="fileToUpload" id="fileToUpload" onchange="fileSelected();">
</div>
<div id="fileName"></div>
<div id="fileSize"></div>
<div id="fileType"></div>
<div class="row">
<input type="button" onclick="uploadFile()" value="Upload">
</div>
<div id="progressNumber"></div>
</form>
</body>
</html>
【代码1:HTML】
注意,<input type="file">
标签具有onchange事件绑定了js函数fileSelected()
每次用户通过浏览本地系统上的文件就会触发此事件
fileSelected()函数如下:
function fileSelected() {
var file = document.getElementById('fileToUpload').files[0];
if (file) {
var fileSize = 0;
if (file.size > 1024 * 1024){
fileSize = (Math.round(file.size * 100 / (1024 * 1024)) / 100).toString() + 'MB';
}else{
fileSize = (Math.round(file.size * 100 / 1024) / 100).toString() + 'KB';
}
document.getElementById('fileName').innerHTML = 'Name: ' + file.name;
document.getElementById('fileSize').innerHTML = 'Size: ' + fileSize;
document.getElementById('fileType').innerHTML = 'Type: ' + file.type;
}
}
【代码2:fileSelected()函数】
函数中的第一行做了一些你以前可能没见过或没做过的事情
实际上,一旦你有了一个<input type="file">
元素的应用
你就可以访问一个叫做FileList的对象,这是HTML5新规范的一部分
(HTML5 File API 的一部分)
FileList对象是文件的集合
更具体地说,它是File对象的集合
File对象有以下属性:
- name - 文件名称(包含任意路径)
- type - 文件MIME类型(小写)
- size - 文件字节大小
这越来越有趣了,不是吗?
我们可以在客户端访问这些信息
事实上,File API(使用FileReader对象)同样可以让我们访问的客户端文件内容(文件流或字节)
在这个例子中我们不会用FileReader来处理,所以我不打算用其他新对象来击垮你的耐心
我们还有几个新对象没有讨论
对你们来说,可以使用File对象提供的信息
来阻止用户上传超过一定大小的文件
或者你可以使用type属性找出用户试图上传和修改的文件类型
以上的JavaScript方法填充于灰色文本(上面的图1和图2)
(使用File对象获取选中文件的信息)
文件的大小是字节,所以有办法将其转换为我们更容易理解的形式
(例如使用15.65MB代替15795748864字节)
用户选择文件后,会通过点击上传按钮来上传文件
这里还要注意上传按钮onclick事件绑定了js函数uploadFile()
uploadFile()函数如下:
function uploadFile() {
var xhr = new XMLHttpRequest();
var fd = document.getElementById('form1').getFormData();
/* 事件监听 */
xhr.upload.addEventListener("progress", uploadProgress, false);
xhr.addEventListener("load", uploadComplete, false);
xhr.addEventListener("error", uploadFailed, false);
xhr.addEventListener("abort", uploadCanceled, false);
/* 下面的url一定要改成你要发送文件的服务器url */
xhr.open("POST", "UploadMinimal.aspx");
xhr.send(fd);
}
【代码3:uploadFile()函数】
在这个函数的第二行,你将会看到另一个对象FormData
我们从来没有见过,也没有使用过它
其实你没有真正地看到它,但我们通过一个方法getFormData()得到了它的一个引用
FormData对象类似于字典(数据结构)、名/值对的集合
名字的部分就是表单的名称(HTML中定义)
值的部分就是是该字段的值
值的部分可以是一个字符串、数字、甚至是File对象(可以看代码4)
所以,当你在表单调用了getFormData()方法就能获得FormData对象的引用(表单的键值对集合)
你可以使用这个引用作为你要发送给服务器端的信息
这使得提交整个表单变得十分简单
现在,你可以手动创建一个FormData实例对象发送给服务器
或者在发送之前向FormData对象中添加额外信息
关于FormData的注意事项
需要注意的是,写这篇文章时Chrome6支持使用新的API上传文件
貌似不支持getFormDate()方法,但支持FormData对象
所以在代码6中手动创建了一个FormData实例
另一种实例化FormData的方法是在表单元素中传递引用
像这样var fd = new FormData(document.getElementById('form1'));
这个构造函数的优点是可以用所有表单填充FormData实例(不必手动完成)
(类似于前面讨论表单元素的getFormData方法,写这篇文章时,Firefox支持这个方法)
代码4向你展示了如何创建一个FormData实例
分配任意字段和值,包括File对象的引用(通过用户<input type="file">
选择的文件)
var fd = new FormData();
fd.append("author", "Shiv Kumar");
fd.append("name", "Html 5 File API/FormData");
fd.append("fileToUpload", document.getElementById('fileToUpload').files[0]);
【代码4:手动创建一个FormData实例】
让我们回到代码3,你会发现我们已经订阅了XMLHttpRequest对象的一些事件
尤其要注意代码中“事件监听”注释后的第一行
xhr.upload.addEventListener("progress", uploadProgress, false);
我们订阅的进度事件并不是XMLHttpRequest实例的,而是在XMLHttpRequest实例的upload属性上
这是一个XMLHttpRequestUpload类型
(这个事件目标有一个progress事件,我们可以订阅它来获取当前发生的上传信息)
我们不必在这个对象上过于烦恼
只要记得,当使用XMLHttpRequest上传数据到服务器时
你必须订阅它upload属性的progress事件
有关progress事件的注意事项
XMLHttpRequest对象也有它自己的progress事件
当你从服务器下载数据时可以订阅该事件(我们这里不会涉及)
function uploadProgress(evt) {
if (evt.lengthComputable) {
var percentComplete = Math.round(evt.loaded * 100 / evt.total);
document.getElementById('progressNumber').innerHTML = percentComplete.toString() + '%';
}
else {
document.getElementById('progressNumber').innerHTML = '无法计算';
}
}
function uploadComplete(evt) {
/* 当服务器响应后,这个事件就会被触发 */
alert(evt.target.responseText);
}
function uploadFailed(evt) {
alert("上传文件发生了错误尝试");
}
function uploadCanceled(evt) {
alert("上传被用户取消或者浏览器断开连接");
}
【代码5:各种事件处理函数的实现】
上面的代码已经写得很清楚了,就不再赘述了
完整简约的解决方案
下面的代码清单是包括能够支持的最小文件与进度指示器上传所需的JavaScript和HTML整个页面
我尽量保证它的简洁
所以如果你想要使用自己的布局和显示信息可以借此扩展
HTML5还引入了progress标签用于显示进度
progress元素有max和value属性,因此使用它可以更方便的显示进度
但是,在写这篇文章的时候,只有Chrome6支持这个元素
所以我在这个简约的解决方案没有使用它
更改服务器端脚本的URL
请务必将URL更改为指向你上传文件的服务器端URL
在下面的代码清单中
UploadMinimal.aspx的uploadFile()方法:
xhr.open("POST", "UploadMinimal.aspx");
<!DOCTYPE html>
<html>
<head>
<title>Upload Files using XMLHttpRequest - Minimal</title>
<script type="text/javascript">
function fileSelected() {
var file = document.getElementById('fileToUpload').files[0];
if (file) {
var fileSize = 0;
if (file.size > 1024 * 1024)
fileSize = (Math.round(file.size * 100 / (1024 * 1024)) / 100).toString() + 'MB';
else
fileSize = (Math.round(file.size * 100 / 1024) / 100).toString() + 'KB';
document.getElementById('fileName').innerHTML = 'Name: ' + file.name;
document.getElementById('fileSize').innerHTML = 'Size: ' + fileSize;
document.getElementById('fileType').innerHTML = 'Type: ' + file.type;
}
}
function uploadFile() {
var fd = new FormData();
fd.append("fileToUpload", document.getElementById('fileToUpload').files[0]);
var xhr = new XMLHttpRequest();
xhr.upload.addEventListener("progress", uploadProgress, false);
xhr.addEventListener("load", uploadComplete, false);
xhr.addEventListener("error", uploadFailed, false);
xhr.addEventListener("abort", uploadCanceled, false);
xhr.open("POST", "UploadMinimal.aspx");
xhr.send(fd);
}
function uploadProgress(evt) {
if (evt.lengthComputable) {
var percentComplete = Math.round(evt.loaded * 100 / evt.total);
document.getElementById('progressNumber').innerHTML = percentComplete.toString() + '%';
}
else {
document.getElementById('progressNumber').innerHTML = '无法计算';
}
}
function uploadComplete(evt) {
/* 当服务器响应后,这个事件就会被触发 */
alert(evt.target.responseText);
}
function uploadFailed(evt) {
alert("上传文件发生了错误尝试");
}
function uploadCanceled(evt) {
alert("上传被用户取消或者浏览器断开连接");
}
</script>
</head>
<body>
<form id="form1" enctype="multipart/form-data" method="post" action="Upload.aspx">
<div class="row">
<label for="fileToUpload">Select a File to Upload</label><br />
<input type="file" name="fileToUpload" id="fileToUpload" onchange="fileSelected();"/>
</div>
<div id="fileName"></div>
<div id="fileSize"></div>
<div id="fileType"></div>
<div class="row">
<input type="button" onclick="uploadFile()" value="Upload" />
</div>
<div id="progressNumber"></div>
</form>
</body>
</html>
【代码6:完整简约的代码清单】
嗯,这是几乎涵盖了所有新版HTML5功能的简约版本
在图片2中大家也看到了,获取其他信息需要数学知识
这是相当多的额外的工作,不仅仅获得这些信息需要用到数学,显示和动画等等也要用
例如获取上传的速率(上传速度)
我做了以下几点:
- 在uploadProgress(evt)事件中,存储evt.loaded和evt.total作为全局变量
- 创建了一个每秒触发的计时器事件
- 在定时器的回调中,获取了传输字节的差值(与1s之前的差)
- 每秒上传字节数得到上传速度
在这里,我还设置了定时器每500毫秒获取从而更加精细
我就不再说剩余时间怎样求了
但是demo中都有(ps:应该是说他图片里的酷炫版本)
希望这篇文章对大家有所帮助
若翻译有不好的地方,还请大家包涵指正