Usando uploads de arquivos HTML5 com AJAX e jQuery

84

É certo que existem questões semelhantes no Stack Overflow, mas parece que nenhuma atende aos meus requisitos.

Aqui está o que procuro fazer:

  • Faça upload de toda uma forma de dados, uma das quais é um único arquivo
  • Trabalhe com a biblioteca de upload de arquivos da Codeigniter

Até aqui está tudo bem. Os dados são armazenados em meu banco de dados conforme preciso. Mas também gostaria de enviar meu formulário por meio de uma postagem AJAX:

  • Usando a API de arquivo HTML5 nativa, não flash ou uma solução iframe
  • Interface preferível com o .ajax()método jQuery de baixo nível

Acho que poderia imaginar como fazer isso enviando automaticamente o arquivo quando o valor do campo muda usando puro javascript, mas prefiro fazer tudo de uma só vez para enviar em jQuery. Estou pensando que não é possível fazer por meio de strings de consulta, pois preciso passar o objeto de arquivo inteiro, mas estou um pouco perdido sobre o que fazer neste momento.

Isso pode ser alcançado?

Joshua Cody
fonte
Não tenho ideia sobre a parte do Codeigniter, mas para a parte do jQuery, dê uma olhada neste plugin .
BalusC
3
relacionado: stackoverflow.com/questions/166221/…
Timo Huovinen

Respostas:

93

Não é muito difícil. Em primeiro lugar, dê uma olhada na Interface do FileReader .

Então, quando o formulário for enviado, pegue o processo de envio e

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

Então, no lado do servidor (ou seja, 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);

Ou algo parecido. Posso estar enganado (e se estiver, por favor, corrija-me), mas isso deve armazenar o arquivo como algo como 1287916771myPicture.jpgin /uploads/em seu servidor e responder com uma variável JSON (para uma continueSubmission()função) contendo o fileName no servidor.

Confira fwrite()e jQuery.post().

Na página acima, ele detalha como usar readAsBinaryString(), readAsDataUrl()e readAsArrayBuffer()para suas outras necessidades (por exemplo, imagens, vídeos, etc.).

clarkf
fonte
Ei Clark, estou entendendo bem? Isso enviará o arquivo carregado assim que ele for carregado no construtor FileReader do sistema de arquivos, ignorando o manipulador .ajax de baixo nível do jQuery. Então o resto do formulário será enviado normalmente?
Joshua Cody
Tudo bem, então eu estava errado em meu entendimento antes. Agora estou pegando o readAsDataUrl de uma imagem, adicionando-o à minha cadeia de dados em .ajax e enviando todas as minhas informações juntas. Minha solução anterior envolveu a classe de entrada de arquivo padrão do CodeIgniter que pegou dados de $ _FILES ['field'], então parece que vou precisar mudar para uma solução diferente para analisar os dados da imagem base64. Qualquer conselho sobre isso é bem-vindo, votando a favor de sua resposta aqui e, assim que terminar a implementação, marcarei como correto.
Joshua Cody
1
@Joshua Cody - Atualizei a resposta para dar um pouco mais de detalhes. Você terá que perdoar por não usar o CodeIgniter em muitas luas e não poder dizer a você como integrá-lo em sua base de código. Não sei por que você precisa fazer o upload do arquivo antes do envio, mas isso deve pelo menos lhe dar uma pista. (Você também pode inserir a imagem em um banco de dados, se for melhor para você).
clarkf
@Clarkf, não preciso fazer o upload antes do envio, não entendi bem o seu exemplo anterior :) Depois que o SO caiu e passei um tempo no w3 e HTML5Rocks , comecei a entender. Vou tentar e volto aqui.
Joshua Cody
Tudo bem, mexi com isso a manhã toda. O PHP parece estar retornando arquivos mal formatados. Veja duas imagens , uma renderizada imediatamente e a outra após $ _POST para o servidor e eco imediato. Uma diferença nos dois elementos revela isso , que aparentemente o PHP está removendo todos os caracteres "+". Um str_replace corrige isso para retorno imediato, mas o arquivo que foi salvo ainda está corrompido e não pode ser aberto através do meu sistema de arquivos. Além disso, vá em frente e marque isso como correto. Muito obrigado por sua ajuda até agora.
Joshua Cody
6

Com jQuery (e sem API FormData) você pode usar algo assim:

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

Com a API FormData você só precisa adicionar todos os campos do seu formulário ao objeto FormData e enviá-lo via $ .ajax ({url: url, data: formData, processData: false, contentType: false, type: "POST"})

Gheljenor
fonte
1
Esta solução não aborda a limitação que XMLHttpRequest.send () impõe aos dados canalizados por meio dele. Quando passada uma string (como sua multiparte), send () não suporta dados binários. Sua multiparte aqui será tratada como uma string utf-8 e irá engasgar ou corromper dados binários que não são utf-8 válido. Se você realmente precisa evitar FormData, você precisa usar XMLHttpRequest.sendAsBinary () ( polyfill disponível . Infelizmente, isso significa que usar jQuery para a chamada ajax se torna muito mais difícil.