Uso de cargas de archivos HTML5 con AJAX y jQuery

84

Es cierto que hay preguntas similares en Stack Overflow, pero parece que ninguna cumple con mis requisitos.

Esto es lo que estoy buscando hacer:

  • Cargue un formulario completo de datos, uno de los cuales es un solo archivo
  • Trabajar con la biblioteca de carga de archivos de Codeigniter

Hasta aquí todo está bien. Los datos entran en mi base de datos cuando los necesito. Pero también me gustaría enviar mi formulario a través de una publicación AJAX:

  • Usando la API nativa de archivos HTML5, no flash ni una solución iframe
  • Preferiblemente interactuando con el .ajax()método jQuery de bajo nivel

Creo que podría imaginarme cómo hacer esto cargando automáticamente el archivo cuando el valor del campo cambia usando javascript puro, pero prefiero hacerlo todo de una sola vez para enviarlo en jQuery. Estoy pensando que no es posible hacerlo a través de cadenas de consulta, ya que necesito pasar todo el objeto de archivo, pero estoy un poco perdido sobre qué hacer en este momento.

¿Se puede lograr esto?

Joshua Cody
fuente
No tengo idea sobre la parte de Codeigniter, pero para la parte de jQuery, eche un vistazo a este complemento .
BalusC
3
relacionado: stackoverflow.com/questions/166221/…
Timo Huovinen

Respuestas:

93

No es muy dificil. En primer lugar, eche un vistazo a la interfaz FileReader .

Entonces, cuando se envíe el formulario, observe el proceso de envío y

var file = document.getElementById('fileBox').files[0]; //Files[0] = 1st file
var reader = new FileReader();
reader.readAsText(file, 'UTF-8');
reader.onload = shipOff;
//reader.onloadstart = ...
//reader.onprogress = ... <-- Allows you to update a progress bar.
//reader.onabort = ...
//reader.onerror = ...
//reader.onloadend = ...


function shipOff(event) {
    var result = event.target.result;
    var fileName = document.getElementById('fileBox').files[0].name; //Should be 'picture.jpg'
    $.post('/myscript.php', { data: result, name: fileName }, continueSubmission);
}

Luego, en el lado del servidor (es decir, myscript.php):

$data = $_POST['data'];
$fileName = $_POST['name'];
$serverFile = time().$fileName;
$fp = fopen('/uploads/'.$serverFile,'w'); //Prepends timestamp to prevent overwriting
fwrite($fp, $data);
fclose($fp);
$returnData = array( "serverFile" => $serverFile );
echo json_encode($returnData);

O algo parecido. Puedo estar equivocado (y si lo estoy, por favor, corríjame), pero esto debería almacenar el archivo como algo así 1287916771myPicture.jpgen /uploads/su servidor y responder con una variable JSON (a una continueSubmission()función) que contiene el nombre de archivo en el servidor.

Echa un vistazo fwrite()y jQuery.post().

En la página anterior se detalla cómo usar readAsBinaryString(), readAsDataUrl()y readAsArrayBuffer()para sus otras necesidades (por ejemplo, imágenes, videos, etc.).

clarkf
fuente
Oye Clark, ¿estoy entendiendo correctamente? Esto enviará el archivo cargado tan pronto como se cargue en el constructor FileReader desde el sistema de archivos, sin pasar por el controlador .ajax de bajo nivel de jQuery. ¿Entonces el resto del formulario se enviará normalmente?
Joshua Cody
Está bien, entonces estaba equivocado en mi comprensión antes. Ahora estoy tomando readAsDataUrl de una imagen, agregándola a mi cadena de datos en .ajax y enviando toda mi información junta. Mi solución anterior involucraba la clase de entrada de archivo predeterminada de CodeIgniter que tomaba datos de $ _FILES ['campo'], por lo que parece que tendré que cambiar a una solución diferente para analizar los datos de la imagen base64. Cualquier consejo al respecto es bienvenido, votando su respuesta aquí, y una vez que termine la implementación, la marcaré como correcta.
Joshua Cody
1
@Joshua Cody - Actualicé la respuesta para dar un poco más de detalle. Tendrás que perdonar que no he usado CodeIgniter en muchas lunas y no pude decirte cómo integrar esto en su base de código. No estoy seguro de por qué necesita cargar el archivo antes de enviarlo, pero esto al menos debería darle una pista. (También puede insertar la imagen en una base de datos si eso es mejor para usted).
clarkf
@Clarkf, no necesito cargar antes de enviarlo, estaba entendiendo mal tu ejemplo anterior :) Después de que SO se cayó y pasé un tiempo en w3 y HTML5Rocks , comencé a entender. Le daré una oportunidad y volveré aquí.
Joshua Cody
Muy bien, he estado jugando con esto toda la mañana. PHP parece estar devolviendo archivos mal formateados. Vea dos imágenes , una renderizada inmediatamente y la otra después de un $ _POST al servidor y un eco inmediato. Una diferencia en los dos elementos revela esto , que aparentemente PHP está eliminando todos los caracteres "+". Un str_replace corrige esto para una devolución inmediata, pero el archivo que se guarda todavía está dañado y no se puede abrir a través de mi sistema de archivos. Además, siga adelante y marque esto como correcto. Muchas gracias por tu ayuda hasta ahora.
Joshua Cody
6

Con jQuery (y sin API FormData) puedes usar algo como esto:

function readFile(file){
   var loader = new FileReader();
   var def = $.Deferred(), promise = def.promise();

   //--- provide classic deferred interface
   loader.onload = function (e) { def.resolve(e.target.result); };
   loader.onprogress = loader.onloadstart = function (e) { def.notify(e); };
   loader.onerror = loader.onabort = function (e) { def.reject(e); };
   promise.abort = function () { return loader.abort.apply(loader, arguments); };

   loader.readAsBinaryString(file);

   return promise;
}

function upload(url, data){
    var def = $.Deferred(), promise = def.promise();
    var mul = buildMultipart(data);
    var req = $.ajax({
        url: url,
        data: mul.data,
        processData: false,
        type: "post",
        async: true,
        contentType: "multipart/form-data; boundary="+mul.bound,
        xhr: function() {
            var xhr = jQuery.ajaxSettings.xhr();
            if (xhr.upload) {

                xhr.upload.addEventListener('progress', function(event) {
                    var percent = 0;
                    var position = event.loaded || event.position; /*event.position is deprecated*/
                    var total = event.total;
                    if (event.lengthComputable) {
                        percent = Math.ceil(position / total * 100);
                        def.notify(percent);
                    }                    
                }, false);
            }
            return xhr;
        }
    });
    req.done(function(){ def.resolve.apply(def, arguments); })
       .fail(function(){ def.reject.apply(def, arguments); });

    promise.abort = function(){ return req.abort.apply(req, arguments); }

    return promise;
}

var buildMultipart = function(data){
    var key, crunks = [], bound = false;
    while (!bound) {
        bound = $.md5 ? $.md5(new Date().valueOf()) : (new Date().valueOf());
        for (key in data) if (~data[key].indexOf(bound)) { bound = false; continue; }
    }

    for (var key = 0, l = data.length; key < l; key++){
        if (typeof(data[key].value) !== "string") {
            crunks.push("--"+bound+"\r\n"+
                "Content-Disposition: form-data; name=\""+data[key].name+"\"; filename=\""+data[key].value[1]+"\"\r\n"+
                "Content-Type: application/octet-stream\r\n"+
                "Content-Transfer-Encoding: binary\r\n\r\n"+
                data[key].value[0]);
        }else{
            crunks.push("--"+bound+"\r\n"+
                "Content-Disposition: form-data; name=\""+data[key].name+"\"\r\n\r\n"+
                data[key].value);
        }
    }

    return {
        bound: bound,
        data: crunks.join("\r\n")+"\r\n--"+bound+"--"
    };
};

//----------
//---------- On submit form:
var form = $("form");
var $file = form.find("#file");
readFile($file[0].files[0]).done(function(fileData){
   var formData = form.find(":input:not('#file')").serializeArray();
   formData.file = [fileData, $file[0].files[0].name];
   upload(form.attr("action"), formData).done(function(){ alert("successfully uploaded!"); });
});

Con la API de FormData solo tiene que agregar todos los campos de su formulario al objeto FormData y enviarlo a través de $ .ajax ({url: url, data: formData, processData: false, contentType: false, type: "POST"})

Gheljenor
fuente
1
Esta solución no aborda la limitación que XMLHttpRequest.send () impone a los datos canalizados a través de ella. Cuando se pasa una cadena (como su multiparte), send () no admite datos binarios. Su multiparte aquí se tratará como una cadena utf-8 y se ahogará o corromperá los datos binarios que no son utf-8 válidos. Si realmente necesita evitar FormData, debe usar XMLHttpRequest.sendAsBinary () ( polyfill disponible . Desafortunadamente, esto significa que usar jQuery para la llamada ajax se vuelve mucho más difícil.