使用 Ajax 和 HTML5 在 MVC 中上传文件






4.93/5 (45投票s)
无需使用 flash 播放器或任何外部文件上传插件,简单的原生文件上传器,带进度条。
只需从 这里 下载项目演示并尽情享用。 :)
引言
我正在开发一个 MVC 应用程序项目,并且我想在不使用 Flash 播放器或任何上传文件插件的情况下,上传文件或多个文件并显示进度条信息。因此,在本文中,我们将解释一种简单的方法来实现
- 上传单个或多个文件并显示进度条信息
- 通过选择文件或拖放文件来上传多个文件
背景
HTML5 通过 File API 规范提供了一种与本地文件交互的标准方法,它为我们提供了访问文件数据或使用客户端验证来检查上传文件的大小/类型机会。
该规范包含几个用于访问文件的接口
- 一个
File
接口,它包含关于文件的只读信息,如名称、类型和字节大小。 - 一个
FileList
接口,它代表用户界面可用于选择的单个文件的列表(文件对象列表),可以通过<input type="file">
或拖放来调用。
XMLHTTPRequest2 是 HTML5 宇宙中的英雄之一,XHR2 与 XMLHttpRequest
相同,但进行了许多更改。它包括以下新功能
- 上传/下载二进制数据。
- 上传过程中的进度事件。此事件包括以下信息
Total
:指定正在传输的总字节数的整数值。Loaded
:指定已上传字节数的整数值。lengthComputable
:一个布尔值,用于检查是否已知上传数据的总大小。
- 跨域请求
这些功能允许 AJAX 信心满满地与 HTML5 的新技术(如 File
API)一起使用,通过将所有这些结合起来,上传文件将非常容易,而且无需使用 Flash 播放器、外部插件或使用普通的 HTML <form>
并依赖服务器端来启用显示上传进度。
在本文中,我们将编写一个小型应用程序,该应用程序能够
- 上传单个文件并提供上传进度信息?
- 在发送到服务器之前创建图像的缩略图预览?
- 通过选择多个文件或拖放文件来上传多个文件?
我们将检查浏览器对 XHR2、File API、FormData 和拖放的支持。
Using the Code
如何上传单个文件并提供上传进度信息?
我们所要做的就是创建一个简单的视图,如下所示
- 表单由文件输入元素和提交按钮组成。
- 用于文件信息和进度条的部分,我使用了 Bootstrap 进度条
<div id="FormContent">
<form id="FormUpload"
enctype="multipart/form-data" method="post">
<span class="btn btn-success fileinput-button">
<i class="glyphicon glyphicon-plus"></i>
<span>Add files...</span>
<input type="file"
name="UploadedFile" id="UploadedFile" />
</span>
<button class="btn btn-primary start"
type="button" id="Submit_btn">
<i class="glyphicon glyphicon-upload"></i>
<span>Start upload</span>
</button>
<button class="btn btn-warning cancel"
type="button" id="Cancel_btn">
<i class="glyphicon glyphicon-ban-circle"></i>
<span>close</span>
</button>
</form>
<div class="progress CustomProgress">
<div id="FileProgress"
class="progress-bar" role="progressbar"
aria-valuenow="0" aria-valuemin="0"
aria-valuemax="100" style="width: 0%;">
<span></span>
</div>
</div>
<div class="InfoContainer">
<div id="Imagecontainer"></div>
<div id="FileName" class="info">
</div>
<div id="FileType" class="info">
</div>
<div id="FileSize" class="info">
</div>
</div>
</div>
然后,我们将添加文件输入元素的 onchange
事件,并将其分配给一个名为 SingleFileSelected
的 JS 方法,如下面的代码片段所示,因此每次用户选择/更改文件时都会调用此方法。在此方法中,我们将选择文件输入元素并访问其 FileList
类型的 files 对象并选择第一个文件(files[0]
),此文件对象提供了关于文件的只读信息,如文件名、文件类型(MIME 类型)。我们可以使用它来限制某些文件以及文件大小(字节)。在我们的方法中,我们将此大小转换为 MB 和 KB,以便在浏览器上显示它们。
function singleFileSelected(evt) {
//var selectedFile = evt.target.files can use this or select input file element
//and access it's files object
var selectedFile = ($("#UploadedFile"))[0].files[0];//FileControl.files[0];
if (selectedFile) {
var FileSize = 0;
var imageType = /image.*/;
if (selectedFile.size > 1048576) {
FileSize = Math.round(selectedFile.size * 100 / 1048576) / 100 + " MB";
}
else if (selectedFile.size > 1024) {
FileSize = Math.round(selectedFile.size * 100 / 1024) / 100 + " KB";
}
else {
FileSize = selectedFile.size + " Bytes";
}
// here we will add the code of thumbnail preview of upload images
$("#FileName").text("Name : " + selectedFile.name);
$("#FileType").text("type : " + selectedFile.type);
$("#FileSize").text("Size : " + FileSize);
}
}
我们还可以使用 File reader 对象将上传的文件内容读入内存,reader 对象有一些事件,如 onload
、onError
,四个读取数据的函数 readAsBinaryString()
、readAsText()
、readAsArrayBuffer()
和 readAsDataURL()
,以及 result
属性,它代表文件的内容。此属性仅在 read
操作完成后才有效,并且数据的格式取决于用于启动 read
操作的哪个方法。
我们将不详细解释 File reader,但我们将在 SingleFileSelected
方法中使用它来预览图像缩略图。请参阅下面的代码片段。
此代码过滤上传的文件中的图像,然后创建一个 Filereader
对象并使用 onload
事件回调来创建图像预览。此回调将在文件读取操作完成后被调用,并将结果分配给 reader.result
属性,然后我们将调用 reader.readAsDataURL()
,它将数据作为编码的 DataURL
返回。
if (selectedFile.type.match(imageType)) {
var reader = new FileReader();
reader.onload = function (e) {
$("#Imagecontainer").empty();
var dataURL = reader.result;
var img = new Image()
img.src = dataURL;
img.className = "thumb";
$("#Imagecontainer").append(img);
};
reader.readAsDataURL(selectedFile);
}
因此,现在我们可以加载文件名、类型、大小和图像预览,如下面的图所示
现在,我们需要将上传的文件发送到服务器,因此我们将添加 onclick
事件并将其分配给名为 uploadFile()
的 JS 方法。请参阅下面的代码片段
function UploadFile() {
//we can create form by passing the form to Constructor of formData object
//or creating it manually using append function
//but please note file name should be same like the action Parameter
//var dataString = new FormData();
//dataString.append("UploadedFile", selectedFile);
var form = $('#FormUpload')[0];
var dataString = new FormData(form);
$.ajax({
url: '/Uploader/Upload', //Server script to process data
type: 'POST',
xhr: function () { // Custom XMLHttpRequest
var myXhr = $.ajaxSettings.xhr();
if (myXhr.upload) { // Check if upload property exists
//myXhr.upload.onprogress = progressHandlingFunction
myXhr.upload.addEventListener('progress', progressHandlingFunction,
false); // For handling the progress of the upload
}
return myXhr;
},
//Ajax events
success: successHandler,
error: errorHandler,
complete:completeHandler,
// Form data
data: dataString,
//Options to tell jQuery not to process data or worry about content-type.
cache: false,
contentType: false,
processData: false
});
}
在此方法中,我们将使用 Form data object 来序列化此类文件值,我们可以通过实例化它来手动创建 formdata,然后通过调用其 append() 方法来追加字段,或者从 HTML 表单检索 FormData
对象,就像我们在上面的函数中所做的那样,将 Form
元素传递给 formdata 构造函数来创建它,我们还可以向其追加更多信息。然后,我们创建具有正确选项的 JQuery AJAX
XHR
:创建自定义XMLHTTPRequest
,检查upload
属性是否存在,然后添加Progress
事件并将其分配给 JS 方法progressHandlingFunction
,该方法将处理upload
属性的进度。processData
:设置为false
以告知 jQuery 不要处理数据。contentType
:设置为false
以告知 jQuery 不要设置contentType
。Data
:将其设置为formdata
对象。
其他选项是常规的 Ajax 选项,并且不言自明。
让我们看一下 progressHandlingFunction
。在此方法中,我们通过 e.lengthComputable
检查是否知道上传数据的总 Size
,然后我们使用 e.loaded
(已上传字节的值)和 e.total
(正在传输的总字节数的值)来计算上传数据的百分比。
function progressHandlingFunction(e) {
if (e.lengthComputable) {
var percentComplete = Math.round(e.loaded * 100 / e.total);
$("#FileProgress").css("width",
percentComplete + '%').attr('aria-valuenow', percentComplete);
$('#FileProgress span').text(percentComplete + "%");
}
else {
$('#FileProgress span').text('unable to compute');
}
}
因此,现在我们能够将数据发送到服务器并提供进度。让我们看一下下面代码片段中的服务器端代码,这是一个名为 uploader
的控制器中一个非常简单的 upload
操作。
在此操作中,我们在 HttpPostedfileBase
对象中接收到文件。此对象包含有关上传文件的信息,如 Filename
属性、Contenttype
属性和包含文件内容的 inputStream
属性,利用这些信息,我们可以在服务器上验证文件并保存文件。
[HttpPost]
public JsonResult Upload(HttpPostedFileBase uploadedFile)
{
if (uploadedFile != null && uploadedFile.ContentLength > 0)
{
byte[] FileByteArray = new byte[uploadedFile.ContentLength];
uploadedFile.InputStream.Read(FileByteArray, 0, uploadedFile.ContentLength);
Attachment newAttchment = new Attachment();
newAttchment.FileName = uploadedFile.FileName;
newAttchment.FileType = uploadedFile.ContentType;
newAttchment.FileContent = FileByteArray;
OperationResult operationResult = attachmentManager.SaveAttachment(newAttchment);
if (operationResult.Success)
{
string HTMLString = CaptureHelper.RenderViewToString
("_AttachmentItem", newAttchment, this.ControllerContext);
return Json(new
{
statusCode = 200,
status = operationResult.Message,
NewRow = HTMLString
}, JsonRequestBehavior.AllowGet);
}
else
{
return Json(new
{
statusCode = 400,
status = operationResult.Message,
file = uploadedFile.FileName
}, JsonRequestBehavior.AllowGet);
}
}
return Json(new
{
statusCode = 400,
status = "Bad Request! Upload Failed",
file = string.Empty
}, JsonRequestBehavior.AllowGet);
}
通过选择文件或拖放文件来上传多个文件?
在本节中,我们将实现相同的上传器,但具有一些新功能
- 允许选择多个文件
- 拖放文件
我们将创建与单文件上传部分相同的视图,但我们需要添加一些内容
- 向文件输入元素添加 multiple 属性,允许选择多个文件
- 添加用于拖放文件的部分,如下面的代码片段所示
<div id="drop_zone">Drop images Here</div>
然后,我们将添加 onchange
事件并将其分配给一个名为 MultiplefileSelected
的 JS 方法,就像我们之前使用 SingleFileSelected
方法所做的那样,但在这里我们将处理 files list 对象中的所有文件并允许拖放文件。请参阅代码片段。
在此方法中,我们将选定的/拖放的文件分配给一个名为 selectedFiles
的全局变量,然后对于 selectedfiles
中的每个文件,我们将使用我们 DataURLreader
对象中的 Read
方法读取文件。我创建了这个对象是为了让代码更清晰易读,而不是在同一个方法中为每个文件创建一个文件读取器。读取文件后,我们可以渲染图像信息或任何错误。让我们看一下 DataURLreader
对象。
function MultiplefileSelected(evt) {
evt.stopPropagation();
evt.preventDefault();
$('#drop_zone').removeClass('hover');
selectedFiles = evt.target.files || evt.dataTransfer.files;
if (selectedFiles) {
$('#Files').empty();
for (var i = 0; i < selectedFiles.length; i++) {
DataURLFileReader.read(selectedFiles[i], function (err, fileInfo) {
if (err != null) {
var RowInfo = '<div id="File_' + i + '"
class="info"><div class="InfoContainer">' +
'<div class="Error">' + err + '</div>' +
'<div data-name="FileName"
class="info">' + fileInfo.name + '</div>' +
'<div data-type="FileType"
class="info">' + fileInfo.type + '</div>' +
'<div data-size="FileSize"
class="info">' + fileInfo.size() +
'</div></div><hr/></div>';
$('#Files').append(RowInfo);
}
else {
var image = '<img src="' + fileInfo.fileContent +
'" class="thumb" title="' +
fileInfo.name + '" />';
var RowInfo = '<div id="File_' + i + '"
class="info"><div class="InfoContainer">' +
'<div data_img="Imagecontainer">' +
image + '</div>' +
'<div data-name="FileName"
class="info">' + fileInfo.name + '</div>' +
'<div data-type="FileType"
class="info">' + fileInfo.type + '</div>' +
'<div data-size="FileSize"
class="info">' + fileInfo.size() +
'</div></div><hr/></div>';
$('#Files').append(RowInfo);
}
});
}
}
}
DataURLFileReader
对象包含 read
方法,该方法将文件和回调方法作为参数。在方法的第一部分,我们创建一个新的 fileReader
并处理其 onload
和 onerror
回调方法,并在函数末尾调用 readAsDataURL
方法来读取文件,我们创建一个名为 fileInfo
的对象,该对象将包含所有文件信息和文件内容(加载后)。
var DataURLFileReader = {
read: function (file, callback) {
var reader = new FileReader();
var fileInfo = {
name: file.name,
type: file.type,
fileContent: null,
size: function () {
var FileSize = 0;
if (file.size > 1048576) {
FileSize = Math.round(file.size * 100 / 1048576) / 100 + " MB";
}
else if (file.size > 1024) {
FileSize = Math.round(file.size * 100 / 1024) / 100 + " KB";
}
else {
FileSize = file.size + " bytes";
}
return FileSize;
}
};
if (!file.type.match('image.*')) {
callback("file type not allowed", fileInfo);
return;
}
reader.onload = function () {
fileInfo.fileContent = reader.result;
callback(null, fileInfo);
};
reader.onerror = function () {
callback(reader.error, fileInfo);
};
reader.readAsDataURL(file);
}
};
使用拖放进行选择
另一种加载文件的方法是从本地计算机本机拖放到浏览器,而且我们不会使用其他插件。现在所有主流浏览器都支持拖放。
要开始使用拖放,我们需要在 drop_zone
元素上添加 dragover 和 drop 事件。
var dropZone = document.getElementById('drop_zone');
dropZone.addEventListener('dragover', handleDragOver, false);
dropZone.addEventListener('drop', MultiplefileSelected, false);
dropZone.addEventListener('dragenter', dragenterHandler, false);
dropZone.addEventListener('dragleave', dragleaveHandler, false);
当文件拖过放置目标时,dragover
事件将被触发。在此方法的处理程序中,我们只需阻止浏览器默认行为并将 datatransfer
对象的 dropEffect
属性设置为 copy,请参阅下面的代码
function handleDragOver(evt) {
evt.preventDefault();
evt.dataTransfer.effectAllowed = 'copy';
evt.dataTransfer.dropEffect = 'copy';
}
然后我们将添加 drop
事件并将其与我们的 MultiplefileSelected
方法关联以处理放置的文件。最好添加 dragenter
和 dragLeave
事件。通过这些事件,您可以在 Dragenter
事件触发时向 Dropzone
元素添加 CSS 类来更改 Drop Zone 的样式,并在 dragleave
事件触发时删除该类。
现在,我们可以点击开始上传按钮将文件发送到服务器。我们只需添加 onclick
事件,当按钮被点击时,它将调用 UploadMultipleFiles
方法。
此方法与我们之前的 Uploadfile
方法类似,除了我们手动创建 formdata
对象来验证/防止发送非图像文件。
function UploadMultipleFiles() {
// here we will create FormData manually to prevent sending mon image files
var dataString = new FormData();
//var files = document.getElementById("UploadedFiles").files;
for (var i = 0; i < selectedFiles.length; i++) {
if (!selectedFiles[i].type.match('image.*')) {
continue;
}
}
// AJAX Request code here
}
现在我们必须做的最后一件事是服务器端代码,它也与我们之前的单个上传服务器端代码类似。唯一不同的是我们将接收一个文件列表,因此我们的操作签名应该是这样的
public JsonResult UplodMultiple(HttpPostedFileBase[] uploadedFiles)
并确保 HttpPostedFileBase
数组的名称与 formdata
对象中 append
方法的对象名称相同。通过这种方式,MVC 可以映射文件数组。
public JsonResult UplodMultiple(HttpPostedFileBase[] uploadedFiles)
dataString.append("uploadedFiles", selectedFiles[i]);
上传大文件
要允许上传大文件。如果您使用的是 IIS 7 或更高版本。您应该修改 web.config 文件并添加以下两个部分
<system.webServer>
<security>
<requestFiltering>
<requestLimits maxAllowedContentLength="2147483648" />
</requestFiltering>
</security>
</system.webServer>
<httpRuntime targetFramework="4.5" maxRequestLength="2097152"/>
这将允许您上传最大为 2 GB 的文件。
注意:maxAllowedContentLength
以字节为单位,而 maxRequestLength
以千字节为单位,这就是为什么值不同的原因(两者都相当于 2 GB)。