jQuery.ajaxによるmultipart/formdataの送信

jQueryのajax機能を使ってサーバーサイドのPHPスクリプトにファイルを送信する際に問題が発生しました。 $('#fileinput').attr('files')でFile-Listを取得することはできますが、このDataをサーバーに送信するにはどうすればよいのでしょうか?ファイル入力を使用した場合、サーバーサイドのphpスクリプトの結果の配列($_POST)は0(NULL`)になります。

可能であることはわかっています(ただし、今までjQueryでの解決策は見つからず、Prototyeのコード(http://webreflection.blogspot.com/2009/03/safari-4-multiple-upload-with-progress.html)のみでした)。

これは比較的新しいことのようですので、XHR/Ajaxによるファイルアップロードが不可能であるということは言わないでください、なぜなら間違いなく動作しているからです。

FFやChromeもあればいいのですが、必須ではありません。

今のところ、私のコードは

$.ajax({
    url: 'php/upload.php',
    data: $('#file').attr('files'),
    cache: false,
    contentType: 'multipart/form-data',
    processData: false,
    type: 'POST',
    success: function(data){
        alert(data);
    }
});
ソリューション

Safari 5/Firefox 4からは、FormDataクラスを使うのが最も簡単です。

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

これでFormDataオブジェクトができあがり、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);
    }
});

これにより、jQueryはContent-Typeヘッダーを追加しないようになります。 また、processDataフラグをfalseのままにしておく必要があります。そうしないと、jQueryはFormDataを文字列に変換しようとしますが、これは失敗します。

これで、PHPでファイルを取り出すことができるようになります。

$_FILES['file-0']

(ファイルの入力時に multiple 属性を指定していなければ、ファイルは file-0 の1つだけで、その場合はファイルごとに数字が増えていきます)

古いブラウザ用のFormDataエミュレーションを使用しています

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);

既存のフォームからFormDataを作成する

ファイルを手動で反復する代わりに、既存のフォームオブジェクトの内容を使ってFormDataオブジェクトを作成することもできます。

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

カウンタではなく、PHPネイティブの配列を使用

ファイル要素の名前を同じにして、名前の最後を括弧でくくるだけです。

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

$_FILES['file']`は、アップロードされたすべてのファイルのファイルアップロードフィールドを含む配列になります。私の最初のソリューションよりも、反復処理が簡単なこの方法をお勧めします。

解説 (31)

Raphael'さんの素晴らしい回答に少し付け加えたいことがあります。JavaScriptを使って送信するかどうかにかかわらず、PHPに同じ$_FILESを生成させる方法を紹介します。

HTMLフォーム。


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

PHPは、JavaScriptを使用せずに送信すると、この $_FILES を生成します。

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
                )

        )

)

プログレッシブ・エンハンスメントを行う場合、Raphael'のJSを使ってファイルを送信する...

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);
    }
});

... そのJavaScriptを使用して送信した後の、PHP'の $_FILES 配列は次のようになります。

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
        )

)

これは素敵な配列で、実際に$_FILESを変換する人もいますが、私はJavaScriptが送信に使われたかどうかに関わらず、同じ$_FILESで作業するのが便利だと思っています。 というわけで、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);
});

(2017年4月14日編集:FormData()のコンストラクタからform要素を削除しました -- これでSafariでこのコードが修正されました)

そのコードは2つのことをしています。

1.input の name 属性を自動的に取得し、HTML のメンテナンス性を高める。さて、form が putImages クラスを持っている限り、他のすべてが自動的に処理されます。つまり、input に特別な名前を付ける必要はありません。 2.2. 通常のHTMLが送信する配列形式は、JavaScriptのdata.append行で再現されています。 括弧に注目してください。

これらの変更により、JavaScriptで送信すると、シンプルなHTMLで送信するのとまったく同じ$_FILES配列が生成されるようになりました。

解説 (1)

読んだ情報をもとに、この機能を作ってみました。

使い方は.serialize()のように、.serializefiles();とするだけです。 私のテストでは動作しています。

//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);
解説 (2)