Afsendelse af multipart/formdata med jQuery.ajax

Jeg har et problem med at sende en fil til et serverside PHP-script ved hjælp af jQuery's ajax-funktion. Det'er muligt at få File-List med $('#fileinput').attr('files') men hvordan er det muligt at sende disse data til serveren? Det resulterende array ($_POST) på serverside php-scriptet er 0 (NULL) når man bruger file-input.

Jeg ved, at det er muligt (selvom jeg ikke fandt nogen jQuery-løsninger indtil nu, kun Prototye-kode (http://webreflection.blogspot.com/2009/03/safari-4-multiple-upload-with-progress.html)).

Dette ser ud til at være relativt nyt, så nævn venligst ikke at filopload ville være umuligt via XHR/Ajax, for det'er helt sikkert at det virker.

Jeg har brug for funktionaliteten i Safari 5, FF og Chrome ville være rart, men er ikke afgørende.

Min kode for nu er:

$.ajax({
    url: 'php/upload.php',
    data: $('#file').attr('files'),
    cache: false,
    contentType: 'multipart/form-data',
    processData: false,
    type: 'POST',
    success: function(data){
        alert(data);
    }
});
Løsning

Fra og med Safari 5/Firefox 4 er det nemmest at bruge klassen FormData:

var data = new FormData();
jQuery.each(jQuery('#file')[0].files, function(i, file) {
    data.append('file-'+i, file);
});

Så nu har du et FormData-objekt, der er klar til at blive sendt sammen med XMLHttpRequest.

jQuery.ajax({
    url: 'php/upload.php',
    data: data,
    cache: false,
    contentType: false,
    processData: false,
    method: 'POST',
    type: 'POST', // For jQuery < 1.9
    success: function(data){
        alert(data);
    }
});

Det er vigtigt at du sætter contentType indstillingen til false, hvilket tvinger jQuery til ikke at tilføje en Content-Type header for dig, ellers vil den afgrænsende streng mangle i den. Du skal også lade processData-flaget være indstillet til false, ellers vil jQuery forsøge at konvertere dine FormData til en streng, hvilket vil mislykkes.

Du kan nu hente filen i PHP ved hjælp af:

$_FILES['file-0']

(Der er kun én fil, file-0, medmindre du har angivet attributten multiple på din filindtastning, i hvilket tilfælde tallene vil blive øget med hver fil.)

Anvendelse af FormData-emulering for ældre browsere

var opts = {
    url: 'php/upload.php',
    data: data,
    cache: false,
    contentType: false,
    processData: false,
    method: 'POST',
    type: 'POST', // For jQuery < 1.9
    success: function(data){
        alert(data);
    }
};
if(data.fake) {
    // Make sure no text encoding stuff is done by xhr
    opts.xhr = function() { var xhr = jQuery.ajaxSettings.xhr(); xhr.send = xhr.sendAsBinary; return xhr; }
    opts.contentType = "multipart/form-data; boundary="+data.boundary;
    opts.data = data.toString();
}
jQuery.ajax(opts);

Skab FormData fra en eksisterende formular

I stedet for manuelt at iterere filerne kan FormData-objektet også oprettes med indholdet af et eksisterende formularobjekt:

var data = new FormData(jQuery('form')[0]);

Brug et PHP-nativt array i stedet for en tæller

Du skal blot navngive dine filelementer på samme måde og afslutte navnet i parentes:

jQuery.each(jQuery('#file')[0].files, function(i, file) {
    data.append('file[]', file);
});

$_FILES['file'] vil derefter være et array, der indeholder filoploadfelterne for hver fil, der uploades. Jeg anbefaler faktisk dette frem for min oprindelige løsning, da det er enklere at iterere over.

Kommentarer (31)

Jeg vil bare gerne tilføje lidt til Raphael's gode svar. Her er hvordan du får PHP til at producere den samme $_FILES, uanset om du bruger JavaScript til at sende.

HTML-formular:


   <input name="media[]" type="file" multiple/>
   <input class="button" type="submit" alt="Upload" value="Upload" />

PHP producerer denne $_FILES, når den sendes uden JavaScript:

Array
(
    [media] => Array
        (
            [name] => Array
                (
                    [0] => Galata_Tower.jpg
                    [1] => 518f.jpg
                )

            [type] => Array
                (
                    [0] => image/jpeg
                    [1] => image/jpeg
                )

            [tmp_name] => Array
                (
                    [0] => /tmp/phpIQaOYo
                    [1] => /tmp/phpJQaOYo
                )

            [error] => Array
                (
                    [0] => 0
                    [1] => 0
                )

            [size] => Array
                (
                    [0] => 258004
                    [1] => 127884
                )

        )

)

Hvis du laver progressiv forbedring ved hjælp af Raphael's JS til at indsende filerne...

var data = new FormData($('input[name^="media"]'));       
jQuery.each($('input[name^="media"]')[0].files, function(i, file) {
    data.append(i, file);
});

$.ajax({
    type: ppiFormMethod,
    data: data,
    url: ppiFormActionURL,
    cache: false,
    contentType: false,
    processData: false,
    success: function(data){
        alert(data);
    }
});

... sådan ser PHP's $_FILES-array ud, efter at JavaScript er blevet brugt til at indsende:

Array
(
    [0] => Array
        (
            [name] => Galata_Tower.jpg
            [type] => image/jpeg
            [tmp_name] => /tmp/phpAQaOYo
            [error] => 0
            [size] => 258004
        )

    [1] => Array
        (
            [name] => 518f.jpg
            [type] => image/jpeg
            [tmp_name] => /tmp/phpBQaOYo
            [error] => 0
            [size] => 127884
        )

)

Det er et fint array, og det er faktisk det, som nogle mennesker omdanner $_FILES til, men jeg synes, det er nyttigt at arbejde med det samme $_FILES, uanset om JavaScript blev brugt til at indsende. Så her er nogle mindre ændringer til JS:

// match anything not a [ or ]
regexp = /^[^[\]]+/;
var fileInput = $('.putImages input[type="file"]');
var fileInputName = regexp.exec( fileInput.attr('name') );

// make files available
var data = new FormData();
jQuery.each($(fileInput)[0].files, function(i, file) {
    data.append(fileInputName+'['+i+']', file);
});

(14. april 2017 edit: Jeg fjernede formularelementet fra konstruktøren af FormData() -- det løste denne kode i Safari.)

Den kode gør to ting.

  1. Henter input-navn-attributten automatisk, hvilket gør HTML'en mere vedligeholdelsesvenlig. Nu, så længe form har klassen putImages, bliver alt andet ordnet automatisk. Det vil sige, at input behøver ikke at have noget specielt navn.
  2. Det array-format, som normal HTML indsender, genskabes af JavaScript i data.append-linjen. Bemærk parenteserne.

Med disse ændringer giver indsendelse med JavaScript nu præcis det samme array $_FILES som indsendelse med simpel HTML.

Kommentarer (1)

Jeg har lige bygget denne funktion på baggrund af nogle oplysninger, jeg har læst.

Brug den ligesom at bruge .serialize(), i stedet skal du bare sætte .serializefiles();.
Arbejder her i mine tests.<br /&>

//USAGE: $("#form").serializefiles();
(function($) {
$.fn.serializefiles = function() {
    var obj = $(this);
    /* ADD FILE TO PARAM AJAX */
    var formData = new FormData();
    $.each($(obj).find("input[type='file']"), function(i, tag) {
        $.each($(tag)[0].files, function(i, file) {
            formData.append(tag.name, file);
        });
    });
    var params = $(obj).serializeArray();
    $.each(params, function (i, val) {
        formData.append(val.name, val.value);
    });
    return formData;
};
})(jQuery);
Kommentarer (2)