AJAX 文件上传器,带有纯 HTML5 的进度通知






4.87/5 (16投票s)
一个纯HTML5(无iframe,无外部库)实现的AJAX文件上传器,带有进度通知(已完成百分比、上传速度、预计剩余时间、剩余字节数、已发送总字节数和待发送总字节数)
引言
自从AJAX技术进入互联网应用开发领域以来,它允许向服务器发送请求并更新网页的一部分而无需重新加载整个页面,开发人员利用它编写了许多出色的应用程序。
然而,尽管这项技术非常出色,但它有一个主要限制,即无法上传文件,直到HTML5引入了FileReader
、FormData
和ArrayBuffer
才得以解决。在HTML5之前,开发人员不得不使用各种方法伪造AJAX文件上传。其中最流行的方法之一是使用嵌入到文档中的iFrame,并通过JSONP响应从服务器加载回iFrame,以伪造onreadystatechange
方法。
借助HTML5的FileReader
、FormData
和ArrayBuffer
,AJAX现在能够上传文件,本文将展示:
- 如何使用各种方法通过AJAX上传文件
- 如何获取上传进度通知
- 这些方法的一些缺点以及何时使用和不使用特定方法
假设
- 您已经熟悉HTML5
- 您已经熟悉AJAX的基本概念
- (不是假设,只是事实)本文在服务器端使用PHP,但可以使用任何服务器,无论是ASP、ASP.NET、ColdFusion,任何有价值的服务器都可以。
上传文件
在AJAX中可以使用各种方式上传文件,具体取决于可用的DOMElement
。例如,您可能拥有单个File
对象("<input type='file' …/>")的DOMElement
,或者包含多个File
对象的HTMLFormElement
("<form></form>")的DOMElement
。
使用readAsDataURL
当您拥有单个File
对象时,FileReader
对象的成员readAsDataURL
可以与AJAX一起用于文件上传。
readAsDataURL
将文件内容转换为base64编码字符串,然后可以轻松地发送到服务器并解码回原始内容。
function sendFile()
{
var url = "upload.php";
var file = document.getElementById("file1").files[0];
xhr = new XMLHttpRequest();
xhr.open("POST", url, true);
xhr.setRequestHeader("Content-type",
"application/x-www-form-urlencoded"); //very important
xhr.onreadystatechange = function()
{
if(xhr.readyState == 4 && xhr.status == 200)
{
document.getElementById("serverresponse").innerHTML = xhr.responseText;
}
}
//Read the file and send to server
var fileObj = new FileReader();
fileObj.onload = function()
{
/* Don't do this, it will cause serious
hugging of CPU resources when the file is very large
var param = "file1=" + fileObj.result; xhr.send(param); */
xhr.send("file1=" + fileObj.result); //Send to server
}
fileObj.readAsDataURL(file);
}
readAsDataURL的服务器端
也许我应该在讨论服务器端代码之前先谈谈这种方法的缺点。
好的,这实际上不是方法本身的缺点,而是PHP处理base64编码字符串中空格的方式(所以如果您不在服务器端使用PHP,这可能与您无关)。为了防止使用readAsDataURL
在PHP中发送的数据损坏,编码字符串中的每个空格
字符必须在解码前替换为“+
”字符(我花了几个小时阅读PHP网站上base64_decode的用户贡献笔记才弄清楚为什么我发送的文件总是损坏)。
话虽如此,要开始编写代码,发送的数据可以通过PHP中的$_POST
变量访问。您会注意到我像这样从AJAX发送文件xhr.send("file1=" + fileObj.result);
,这意味着我可以使用$_POST
的索引“file1
”访问发送的数据。也就是说,$_POST['file1']
将保存base64编码的字符串数据,但其前面会附加一些MIME
信息,因此在解码数据之前,我们必须删除这些MIME
信息。
发送的数据总是这种格式
data:<MIME info>;base64,<base64 string>
如果数据是jpg图像,它将看起来像
data:image/jpeg;base64,/9j/4AAQSkZJRg...
我们需要做的是删除逗号之前的数据(包括逗号),然后解码剩余的数据(如果您使用的是PHP,请记住将所有空格替换为“+”)
<?php
$data = $_POST['file1'];
//Split the data into two. Data format is "data:<MIME
// info>;base64,<base64 encoded string>"
$data = explode(",", $data);
//PHP handles spaces in base64 encoded string differently
//so to prevent corruption of data, convert spaces to +
$data[1] = str_replace(' ', '+', $data[1]);
//now we can decode our data and write it to disk
//You can get the right extention to use from the MIME info
//in data[0] but I will assume our data is a jpg picture
$fh = fopen("picture.jpg", "w");
fwrite($fh, base64_decode($data[1])); //decode and write to file
fclose($fh);
echo "File written successfully";
?>
如果您不知道要接收的文件类型(以便知道使用正确的文件扩展名),请记住$data的第零个元素包含MIME信息,但请注意,如果文件类型对发送请求的浏览器来说是陌生的,数据的实际MIME部分有时可能为空。
data:;base64,AxYtd...
上传进度通知
上传通知对所有方法都相同,所以我不会重复了。
已完成百分比,已发送总字节数,待发送总字节数,剩余待发送字节数
上传完成的百分比可以通过向XMLHttpRequest对象的上传方法附加进度eventListener
来完成。
xhr.upload.addEventListener('progress', uploadProgress, false);
在回调函数“uploadProgress”中,可以从事件对象中获取要上传的总字节数和已上传的总字节数(分别为e.total
和e.loaded
)。通过此,可以计算已完成的百分比。
var uploaded = 0, prevUpload = 0, speed = 0, total = 0, remainingBytes = 0, timeRemaining = 0; function uploadProgress(e) { if (e.lengthComputable) { uploaded = e.loaded; total = e.total; //percentage var percentage = Math.round((e.loaded / e.total) * 100); document.getElementById('progress_percentage').innerHTML = percentage + '%'; document.getElementById('progress').style.width = percentage + '%'; document.getElementById("remainingbyte").innerHTML = j.BytesToStructuredString(e.total - e.loaded);//remaining bytes document.getElementById('uploadedbyte').innerHTML = j.BytesToStructuredString(e.loaded);//uploaded bytes document.getElementById('totalbyte').innerHTML = j.BytesToStructuredString(e.total);//total bytes } }
上传速度和ETR(估计剩余时间)
为了获取上传速度(以<字节>/秒为单位)和ETR,必须每秒调用一个函数。由于e.loaded保存已发送的总字节数,因此速度可以这样计算:
- 每秒调用一个函数
- 有一个变量prevUpload (var prevUpload = 0)
- 在函数第一次调用时,获取已发送的总字节数
- 速度将是“当前总上传值(e.loaded)– 1秒前的总上传值(prevUpload)”
- 将prevUpload等于e.loaded
- 重复 3 – 6
function UploadSpeed() { //speed speed = uploaded - prevUpload; prevUpload = uploaded; document.getElementById("speed").innerHTML = j.SpeedToStructuredString(speed); //Calculating ETR remainingBytes = total - uploaded; timeRemaining = remainingBytes / speed; document.getElementById("ETR").innerHTML = i.SecondsToStructuredString(timeRemaining); }
使用readAsArrayBuffer
当您拥有单个File对象时,可以使用此方法。它使用readAsArrayBuffer
将文件内容读取到ArrayBuffer
中。然后可以将此ArrayBuffer
转换为BinaryString
,并将BinaryString
编码为base64(或您喜欢的任何其他格式,例如压缩文件),然后发送到服务器(有些人甚至在发送到服务器之前将文件打包成tarball)。现在的问题是,既然可以使用readAsDataURL
,为什么要经历所有这些麻烦呢?原因是您现在可以控制发送到服务器的内容,您可以轻松地操作数据。
这种方法唯一的缺点是您必须将文件内容读取到一个变量中才能对其进行操作,如果文件很大,这将导致严重的性能问题。
function sendFile()
{
var url = "upload.php";
var file = document.getElementById("file1").files[0];
xhr = new XMLHttpRequest();
xhr.open("POST", url, true);
xhr.setRequestHeader("Content-type",
"application/x-www-form-urlencoded"); //very important
xhr.onreadystatechange = function()
{
if(xhr.readyState == 4 && xhr.status == 200)
{
document.getElementById("serverresponse").innerHTML = xhr.responseText;
}
}
//Read the file content convert to BinaryString, encode BinaryString to base64 and send
var fileObj = new FileReader();
fileObj.onload = function()
{
//Convert ArrayBuffer to BinaryString
var data = "";
bytes = new Uint8Array(fileObj.result);
var length = bytes.byteLength;
for(var i = 0; i < length; i++)
{
data += String.fromCharCode(bytes[i]);
}
//Encode BinaryString to base64
data = btoa(data);
xhr.send("file1=" + data); //Send to server
}
fileObj.readAsArrayBuffer(file);
}
readAsArrayBuffer的服务器端
由于您可以选择数据的发送格式,服务器端现在更容易了,但请记住,如果您使用PHP,请将所有空格替换为“+”。
<?php
$data = $_POST['file1'];
//PHP handles spaces in base64 encoded string differently
//so to prevent corruption of data, convert spaces to +
$data = str_replace(' ', '+', $data);
//now we can decode our data and write it to disk
$fh = fopen("picture.jpg", "w");
fwrite($fh, base64_decode($data)); //decode and write to file
fclose($fh);
echo "File written successfully";
?>
使用FormData
这是所有方法中最简单的,因为您无需操作任何数据,并且上传在服务器端被视为传统上传,但您只能在拥有HTMLFormElement
时使用它。
这种方法也没有任何性能问题,使其成为始终使用的最佳方法。
<form id="form1" name="form1" enctype="multipart/form-data">
<input type="file" id="file1" name="file1">
<input type="file" id="file2" name="file2">
<input type="text" id="uname" name="uname" placeholder="Your name">
<input type="button" value="Send" onclick="sendFile();" />
</form>
AJAX部分将是
function sendFile()
{
var url = "upload.php";
var formData = new FormData(document.getElementById("form1"));
xhr = new XMLHttpRequest();
xhr.open("POST", url, true);
//xhr.setRequestHeader("Content-type",
// "application/x-www-form-urlencoded"); //no longer necessary here
xhr.onreadystatechange = function()
{
if(xhr.readyState == 4 && xhr.status == 200)
{
document.getElementById("serverresponse").innerHTML = xhr.responseText;
}
}
xhr.send(formData); //Send to server
}
您应该注意的第一件事是XMLHttpRequest
对象的setRequestHeader("Content-type", "application/x-www-form-urlencoded");
不再需要,但已被的
enctype="multipart/form-data"
属性替换。此方法唯一的缺点是您无法获得单个文件的进度通知,报告的进度通知是针对整个表单数据的。
FormData的服务器端
服务器端处理方式与传统文件上传相同。您可以从$_FILES
变量访问上传的文件,并从$_POST
变量访问其他表单元素。
<?php
//This is just like a normal form submission
//You can access the uploaded files through $_FILES
if(isset($_FILES["file1"]))
move_uploaded_file($_FILES["file1"]["tmp_name"], "picture1.jpg");
if(isset($_FILES["file2"]))
move_uploaded_file($_FILES["file2"]["tmp_name"], "picture2.jpg");
//You can access other form element through $_POST
echo "Thanks {$_POST['uname']}! Files received successfully.";
?>
关于readAsBinaryString/sendAsBinary的说明
请注意,FileReader
对象的readAsBinaryString
和XMLHttpRequest
的sendAsBinary
不是标准。它们仅在Firefox(和readAsBinaryString
仅在Chrome)中受支持。它们所做的事情与我们在readAsArrayBuffer
方法中所做的完全相同。这只是Firefox通过帮助开发人员编写一些代码来让他们生活更轻松的方式
结论
请注意,这些只是我自己的结论,它们不是标准。
- 当您只能访问File对象时,使用
readAsDataURL
方法发送大文件。 - 使用
readAsArrayBuffer
方法发送较小的文件,因为将其用于大文件可能会导致性能问题,因为文件内容必须首先读取到变量中。 - 如果您不需要操作数据,希望对服务器端处理上传的传统方式进行很少的更改,并且当然可以访问
HTMLFormElement
,请使用FormData
。在我看来,这是最好的使用方法。