如何通过HTML5和Ajax异步上传多个文件

接下来我们会上传多个文件到PHP脚本程序,当用户点击“上传文件”时,PHP会处理Ajax上传请求和标准的表单POST。

我们用JavaScript代码来确保只能上传小于3000,000 bytes的JPG图片文件(文件大小在隐藏表单字段MAX_FILE_SIZE中指定).。


JavaScript代码

首先,我们需要在FileSelectHandler( )函数中额外添加一行代码,在选中文件或拖拽文件时它将被调用。在循环遍历FileList时,我们会调用UploadFile( File f ) 函数。

// file selection
function FileSelectHandler(e) {

    // cancel event and hover styling
    FileDragHover(e);

    // fetch FileList object
    var files = e.target.files || e.dataTransfer.files;

    // process all File objects
    for (var i = 0, f; f = files[i]; i++) {
        console.log(f);
        ParseFile(f);
        UploadFile(f);
    }

}

文件上传需要XMLHttpRequest2对象(目前在Firefox和Chrome中可以使用)。在我们做Ajax调用之前,先要确保XMLHttpRequest2的upload方法是可用的,然后确保要有一个小于MAX_FILE_SIZE的JPG文件:

// upload JPEG files
function UploadFile(file) {

    var xhr = new XMLHttpRequest();
    if (xhr.upload && file.type == "image/jpeg" && file.size <= $id("MAX_FILE_SIZE").value) {

        // start upload
        xhr.open("POST", $id("upload").action, true);
        xhr.setRequestHeader("X_FILENAME", file.name);
        xhr.send(file);

    }

}

XMLHttpRequest.open( )方法,第一参数是请求方法,第二参数是接收请求的PHP脚本程序之URL,即form表单的action属性;额外的,在HTTP请求头部中设置X_FILENAME文件名并传递File对象给send方法。


PHP

php文件“upload.php”将检查HTTP头部是否有X_FILENAME,以此来区分Ajax请求和标准的表单form POST数据。

$fn = (isset($_SERVER['HTTP_X_FILENAME']) ? $_SERVER['HTTP_X_FILENAME'] : false);
if ($fn) {

    // AJAX call
    file_put_contents(
        'uploads/' . $fn,
        file_get_contents('php://input')
    );
    echo "$fn uploaded";
    exit();

}else {

    // form submit
    $files = $_FILES['fileselect'];

    foreach ($files['error'] as $id => $err) {
        if ($err == UPLOAD_ERR_OK) {
            $fn = $files['name'][$id];
            move_uploaded_file(
                $files['tmp_name'][$id],
                'uploads/' . $fn
            );
            echo "<p>File $fn uploaded.</p>";
        }
    }

}

如果设置了文件名,PHP能接收post过来的数据然后把它输出到一个新的文件夹,这可以用一个代码来完成file_put_contents( dest, file_get_contents(‘php://input’));

标准的HTML multipart/form-data POST请求可以使用通常的PHP函数对$_FILES超全局变量做操作。

(end)

在客户端用JavaScript加载文件内容

为什么要用JavaScript打开本地文件?

人们经常上传几megabyte大小的图片到web服务器。假定用户通过input控件去选择文件,上传过程可能耗费几分钟,最后发现是个它错误的文件、不支持的格式或者文件大于受限值。直到现在,开发者不得不去依赖Flash或者其他插件来提供一个更好的用户体验。

通过JavaScript做文件上传的预处理的一些好处:

1.本地文件的处理很快;

2.通过文件分析来确保它们是正确的格式和低于指定的文件大小;

3.部分文件(例如图片)可以再上传之前被预览;

4.可以在canvas元素上对图片做裁剪和缩放,然后再上传最终的文件。


FileReader对象

FileReader对象是W3C File API的组成之一,提供了四个对File对象进行异步加载操作的方法。

1. .readAsText( File f, [encoding] )以某种可选编码读取文件到一个字符串中;

2. .readAsDataURL( File f ) 读取文件并编码为URL格式(比如:把它的返回值设置为img标签的src属性,这样就可以预览图片了);

3. .readAsBinaryString( File f ) 以二进制字符串形式读取文件;

4. .readAsArrayBuffer( File f ) 读取文件并返回ArrayBuffer对象;

通常使用前两个函数来显示字符串和图片文件


 

用JavaScript异步打开文件

当拖拽或者选择一个文件时,原来的ParseFile( File f )函数接收一个File对象,代码如下:

function ParseFile(file) {

    Output(

            "<p>File information: <strong>" + file.name +
            "</strong> last modefied:<strong>" + file.lastModified +
            "</strong> type: <strong>" + file.type +
            "</strong> size: <strong>" + file.size +
            "</strong> bytes</p>"
    );
}

紧随代码更新,我们要检查是否有一个文本文件(text/plain, text/html, text/css, etc ), 通过FileReader.ReadAsText()方法来加载它,然后显示结果(在转义escaping<和>字符以后):

function ParseFile(file) {

    Output(

            "<p>File information: <strong>" + file.name +
            "</strong> last modefied:<strong>" + file.lastModified +
            "</strong> type: <strong>" + file.type +
            "</strong> size: <strong>" + file.size +
            "</strong> bytes</p>"
    );

    // display text
    if (file.type.indexOf("text") == 0) {
        var reader = new FileReader();
        reader.onload = function(e) {
            Output(
                    "<p><strong>" + file.name + ":</strong></p><pre>" +
                    e.target.result.replace(/</g, "&lt;").replace(/>/g, "&gt;") +
                    "</pre>"
            );
        }
        reader.readAsText(file);
    }
}

同样的,我们能检查是否有一个图片文件(image/* , image/jpeg, image/png , etc) , 通过FileReader.readAsDataURL( ) 方法加载图片数据,然后传递结果给img标签作为src属性值:

    // display an image
if (file.type.indexOf("image") == 0) {
    var reader = new FileReader();
    reader.onload = function(e) {
        Output(
                "<p><strong>" + file.name + ":</strong><br />" +
                '<img src="' + e.target.result + '" /></p>'
        );
    }
    reader.readAsDataURL(file);
}

(end)

 

如何使用HTML5的文件拖拽上传

目标:

允许拖拽文件到网页的某个元素上;

用JS分析被拽进来的文件

在客户端加载和解析文件

用Ajax异步上传文件到服务端

上传的时候显示一个图形化的进度条

使用“渐进坚强”来确保文件上传表单在各种浏览器都能使用(对IE6用户来说是好事)

写代码实现,不引入任何JS函数库


拖拽事件的浏览器支持问题:

1)Chrome,Firefox都支持所有特性;

2) IE和桌面版的Safari都不支持;

3)Apple iPhone和iPAdy已经禁用了Safari的 HTML文件上传表单,有人知道为什么吗?


HTML和CSS

以下是一个标准的HTML文件上传表单,唯一的HTML5特性就是“multiple”属性,它允许用户选择任意数量的文件。隐藏字段MAX_FILE_SIZE是为了防止上传大文件。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title></title>
    <style>
        /*拖拽区域*/
        #filedrag
        {
            display: block;
            font-weight: bold;
            text-align: center;
            padding: 1em 0;
            margin: 1em 0;
            color: #555;
            border: 2px dashed #555;
            border-radius: 7px;
            cursor: default;
        }
        #filedrag.hover
        {
            color: #f00;
            border-color: #f00;
            border-style: solid;
            box-shadow: inset 0 3px 4px #888;
        }
    </style>
</head>
<body>
<form id="upload" action="upload.php" method="POST" enctype="multipart/form-data">

    <fieldset>
        <legend>HTML File Upload</legend>

        <input type="hidden" id="MAX_FILE_SIZE" name="MAX_FILE_SIZE" value="300000" />

        <div>
            <label for="fileselect">Files to upload:</label>
            <input type="file" id="fileselect" name="fileselect[]" multiple="multiple" />
            <div id="filedrag">or drop files here</div>
        </div>

        <div id="submitbutton">
            <button type="submit">Upload Files</button>
        </div>

    </fieldset>

</form>

<div id="messages">
    <p>Status Messages</p>
</div>
</body>
</html>

文件API

W3C File API 提供了多个对象 ( http://www.w3.org/TR/file-upload/ ),会用到:

FIleList,表示一组被选中的文件;

File,表示一个指定的文件;

FileReader,一个允许在客户端用JS读取文件数据的接口;


绑定JS事件

先构建两个辅助函数,用以代替jQuery;

// getElementById
function $id(id) {
    return document.getElementById(id);
}

// output information
function Output(msg) {
    var m = $id("messages");
    m.innerHTML = msg + m.innerHTML;
}

 

然后检查浏览器的File API是否可用,可用的则执行初始化Init( )函数:

// call initialization file
if (window.File && window.FileList && window.FileReader) {
    Init();
}

// initialize
function Init() {

    var fileselect = $id("fileselect"),
            filedrag = $id("filedrag"),
            submitbutton = $id("submitbutton");

    // file select
    fileselect.addEventListener("change", FileSelectHandler, false);

    // is XHR2 available?
    var xhr = new XMLHttpRequest();
    if (xhr.upload) {

        // file drop
        filedrag.addEventListener("dragover", FileDragHover, false);
        filedrag.addEventListener("dragleave", FileDragHover, false);
        filedrag.addEventListener("drop", FileSelectHandler, false);
        filedrag.style.display = "block";

        // remove submit button
        submitbutton.style.display = "none";
    }

}

Init函数:

1. 给文件上传元素设置“change”事件监听器;

2.显示拖拽区域#filedrag的元素;

3.给拖拽区域设置“dragover”和“dragleave”事件监听器,用来改变拖拽区域#filedrag的元素样式;

4.给拖拽区域设置“drop”放置事件监听器,

5.隐藏表单提交按钮–文件被选中之后,在我们分析和上传文件之前是不需要显示提交按钮的。

对XMLHttpRequest.upload方法的检查是为了防止Opera浏览器出现问题。浏览器支持FileList、File和FileReader对象,但不支持drag和drop事件或者XMLHttpRequest2对象。它依然能显示文件信息,但我们不想显示拖拽区域,且要移除submit按钮。


 

文件拖拽样式的变化

很少用户有在网页浏览器拖拽文件的体验。事实上,体验 的网页用户获取并不认为这是可以做到的。因此,我们使用一个“拖放文件到这里”的元素来提示用户。当一个文件被拖拽到拖拽区域上时,我们同样想通过修改它的显示样式来暗示用户。

// file drag hover
function FileDragHover(e) {
    e.stopPropagation();
    e.preventDefault();
    e.target.className = (e.type == "dragover" ? "hover" : "");
}

分析被拖拽或选择的多个文件

我们使用同一个FileSelectHandler( )函数来处理不管一个还是多个被选中的文件。比如通过“浏览文件”或者拖拽到拖拽区域#filedrag上。

// file selection
function FileSelectHandler(e) {

    // cancel event and hover styling
    FileDragHover(e);

    // fetch FileList object
    var files = e.target.files || e.dataTransfer.files;

    // process all File objects
    for (var i = 0, f; f = files[i]; i++) {
        console.log(f);
        ParseFile(f);
    }

}

FileSelectHandler函数功能:

1.调用FileDragHover( )函数一处hover的样式,并取消浏览器事件的默认行为,组织事件冒泡;

2.获取一个FilesList对象,这要么来自input输入框(e.target.files),要么来自拖拽区域#filedrag元素的数据传输(e.dataTransfer,files);

3.最后,遍历FilesList对象中的每一个File对象,把File对象作为实际参数传递给ParseFile( ) 函数。

function ParseFile(file) {

    Output(

            "<p>File information: <strong>" + file.name +
            "</strong> last modefied:<strong>" + file.lastModified +
            "</strong> type: <strong>" + file.type +
            "</strong> size: <strong>" + file.size +
            "</strong> bytes</p>"
    );
}

ParseFile函数输出File对象的只读属性:

.name文件名,

.type文件类型,

.size文件大小(字节),

.lastModified(最后修改时间),

.lastModifiedDate(获取最后修改时间的时间);

(end)