JavaScript / jQuery para baixar o arquivo via POST com dados JSON

250

Eu tenho um webapp de página única baseado em jquery. Ele se comunica com um serviço da web RESTful por meio de chamadas AJAX.

Estou tentando fazer o seguinte:

  1. Envie um POST que contenha dados JSON para um URL REST.
  2. Se a solicitação especificar uma resposta JSON, JSON será retornado.
  3. Se a solicitação especificar uma resposta PDF / XLS / etc, um binário para download será retornado.

Eu tenho 1 e 2 trabalhando agora, e o aplicativo jquery do cliente exibe os dados retornados na página da web, criando elementos DOM com base nos dados JSON. Eu também tenho o # 3 trabalhando do ponto de vista do serviço da web, o que significa que ele criará e retornará um arquivo binário se tiver os parâmetros JSON corretos. Mas não tenho certeza da melhor maneira de lidar com o número 3 no código javascript do cliente.

É possível recuperar um arquivo para download a partir de uma chamada ajax como esta? Como obtenho o navegador para baixar e salvar o arquivo?

$.ajax({
    type: "POST",
    url: "/services/test",
    contentType: "application/json",
    data: JSON.stringify({category: 42, sort: 3, type: "pdf"}),
    dataType: "json",
    success: function(json, status){
        if (status != "success") {
            log("Error loading data");
            return;
        }
        log("Data loaded!");
    },
    error: function(result, status, err) {
        log("Error loading data");
        return;
    }
});

O servidor responde com os seguintes cabeçalhos:

Content-Disposition:attachment; filename=export-1282022272283.pdf
Content-Length:5120
Content-Type:application/pdf
Server:Jetty(6.1.11)

Outra idéia é gerar o PDF e armazená-lo no servidor e retornar JSON que inclui uma URL para o arquivo. Em seguida, emita outra chamada no manipulador de sucesso do ajax para fazer algo como o seguinte:

success: function(json,status) {
    window.location.href = json.url;
}

Mas fazer isso significa que eu precisaria fazer mais de uma chamada para o servidor, e meu servidor precisaria criar arquivos para download, armazená-los em algum lugar e, em seguida, limpar periodicamente a área de armazenamento.

Deve haver uma maneira mais simples de conseguir isso. Ideias?


EDIT: Depois de revisar os documentos para $ .ajax, vejo que o dataType de resposta pode ser apenas um deles xml, html, script, json, jsonp, text, então acho que não há como baixar diretamente um arquivo usando uma solicitação ajax, a menos que eu incorpore o arquivo binário usando Esquema de URI de dados, conforme sugerido na resposta @VinayC (que não é algo que eu quero fazer).

Então, acho que minhas opções são:

  1. Não use ajax e, em vez disso, envie uma postagem de formulário e incorpore meus dados JSON aos valores do formulário. Provavelmente precisaria mexer com iframes ocultos e tal.

  2. Não use ajax e, em vez disso, converta meus dados JSON em uma sequência de consultas para criar uma solicitação GET padrão e defina window.location.href para este URL. Pode ser necessário usar event.preventDefault () no meu manipulador de cliques para impedir que o navegador seja alterado a partir do URL do aplicativo.

  3. Use minha outra idéia acima, mas aprimorada com sugestões da resposta @naikus. Envie a solicitação AJAX com algum parâmetro que permita ao serviço da web saber que isso está sendo chamado por meio de uma chamada ajax. Se o serviço da web for chamado a partir de uma chamada ajax, basta retornar JSON com uma URL para o recurso gerado. Se o recurso for chamado diretamente, retorne o arquivo binário real.

Quanto mais eu penso sobre isso, mais eu gosto da última opção. Dessa forma, posso recuperar informações sobre a solicitação (tempo para gerar, tamanho do arquivo, mensagens de erro etc.) e posso agir com base nessas informações antes de iniciar o download. A desvantagem é o gerenciamento extra de arquivos no servidor.

Existem outras maneiras de conseguir isso? Quaisquer prós / contras para esses métodos dos quais devo estar ciente?

Tauren
fonte
possível duplicação do download do arquivo Handle do post do ajax
IsmailS
6
este era "um pouco" mais cedo, assim como é uma duplicata
GIM
1
url = 'http://localhost/file.php?file='+ $('input').val(); window.open(url); A maneira mais fácil de obter os mesmos resultados. Coloque o cabeçalho no arquivo php. Não há necessidade de enviar qualquer ajax ou obter pedidos
bagul abhishek

Respostas:

171

A solução da letronje funciona apenas para páginas muito simples. document.body.innerHTML +=pega o texto HTML do corpo, anexa o HTML iframe e define o innerHTML da página para essa sequência. Isso eliminará qualquer ligação de evento que sua página tenha, entre outras coisas. Crie um elemento e use appendChild.

$.post('/create_binary_file.php', postData, function(retData) {
  var iframe = document.createElement("iframe");
  iframe.setAttribute("src", retData.url);
  iframe.setAttribute("style", "display: none");
  document.body.appendChild(iframe);
}); 

Ou usando jQuery

$.post('/create_binary_file.php', postData, function(retData) {
  $("body").append("<iframe src='" + retData.url+ "' style='display: none;' ></iframe>");
}); 

O que isso realmente faz: execute uma postagem no /create_binary_file.php com os dados na variável postData; se essa postagem for concluída com êxito, adicione um novo iframe ao corpo da página. A suposição é que a resposta de /create_binary_file.php incluirá um valor 'url', que é o URL do qual o arquivo PDF / XLS / etc gerado pode ser baixado. A adição de um iframe à página que referencia esse URL resultará no navegador, promovendo o usuário a baixar o arquivo, supondo que o servidor da Web tenha a configuração apropriada do tipo MIME.

SamStephens
fonte
1
Concordo que é melhor do que usar +=e atualizarei meu aplicativo de acordo. Obrigado!
Tauren
3
Eu gosto deste conceito, no entanto Chrome tem esta mensagem no console: Resource interpreted as Document but transferred with MIME type application/pdf. Em seguida, também me avisa que o arquivo pode ser perigoso. Se eu visitar o retData.url diretamente, nenhum aviso ou problema.
22613 Michael Irey
Olhando pela Internet, parece que você pode ter problemas com PDFs no iFrames se o servidor não estiver configurando o Tipo de conteúdo e a Disposição de conteúdo corretamente. Na verdade, eu mesmo não usei essa solução, apenas esclarei uma solução anterior; portanto, não tenho outros conselhos.
SamStephens
1
@ Tauren: Se você concorda que esta é uma resposta melhor, observe que pode mudar a resposta aceita a qualquer momento - ou até mesmo remover completamente a marca de aceitação. Basta marcar a nova resposta e a marca de aceitação será transferida. (Velha questão, mas foi criado em bandeiras recentemente.)
BoltClock
5
qual é o suposto retData.url?
Mark Thien
48

Eu tenho brincado com outra opção que usa blobs. Consegui fazer o download de documentos de texto e baixei PDFs (no entanto, eles estão corrompidos).

Usando a API de blob, você poderá fazer o seguinte:

$.post(/*...*/,function (result)
{
    var blob=new Blob([result]);
    var link=document.createElement('a');
    link.href=window.URL.createObjectURL(blob);
    link.download="myFileName.txt";
    link.click();

});

Este é o IE 10+, Chrome 8+, FF 4+. Vejo https://developer.mozilla.org/en-US/docs/Web/API/URL.createObjectURL

Ele só fará o download do arquivo no Chrome, Firefox e Opera. Isso usa um atributo de download na tag anchor para forçar o navegador a fazer o download.

JoshBerke
fonte
1
Não há suporte para o atributo de download no IE e Safari :( ( caniuse.com/#feat=download ). Você pode abrir uma nova janela e colocar o conteúdo lá, mas não tem certeza de um PDF ou Excel ...
Marçal Juan
1
Você deve liberar memória do navegador após ligar para click:window.URL.revokeObjectURL(link.href);
Edward Brey
@ JoshBerke Isso é antigo, mas funciona do meu lado. Como posso abrir meu diretório imediatamente para salvar em vez de salvar no meu navegador? Também existe uma maneira de determinar o nome do arquivo recebido?
Jo Ko
2
Será arquivos binários corruptos, porque eles são convertidos para cadeias usando a codificação, e resultado não é muito perto de binário original ...
Gobol
2
Pode usar mimetypes para não corromper a informação como usado aqui com PDFs: alexhadik.com/blog/2016/7/7/l8ztp8kr5lbctf5qns4l8t3646npqh
Mateo Tibaquira
18

Conheço esse tipo de idade, mas acho que encontrei uma solução mais elegante. Eu tive o mesmo problema. O problema que eu estava tendo com as soluções sugeridas era que todas exigiam que o arquivo fosse salvo no servidor, mas eu não queria salvar os arquivos no servidor, porque isso introduzia outros problemas (segurança: o arquivo poderia ser acessado por usuários não autenticados, limpeza: como e quando você se livra dos arquivos). E, como você, meus dados eram objetos JSON complexos e aninhados que seriam difíceis de colocar em um formulário.

O que eu fiz foi criar duas funções de servidor. O primeiro validou os dados. Se houvesse um erro, ele seria retornado. Se não houve um erro, retornei todos os parâmetros serializados / codificados como uma string base64. Em seguida, no cliente, tenho um formulário que possui apenas uma entrada oculta e envia para uma segunda função de servidor. Defino a entrada oculta para a string base64 e submeto o formato. A segunda função do servidor decodifica / desserializa os parâmetros e gera o arquivo. O formulário pode ser enviado para uma nova janela ou um iframe na página e o arquivo será aberto.

Há um pouco mais de trabalho envolvido, e talvez um pouco mais de processamento, mas, no geral, me senti muito melhor com esta solução.

O código está em C # / MVC

    public JsonResult Validate(int reportId, string format, ReportParamModel[] parameters)
    {
        // TODO: do validation

        if (valid)
        {
            GenerateParams generateParams = new GenerateParams(reportId, format, parameters);

            string data = new EntityBase64Converter<GenerateParams>().ToBase64(generateParams);

            return Json(new { State = "Success", Data = data });
        }

        return Json(new { State = "Error", Data = "Error message" });
    }

    public ActionResult Generate(string data)
    {
        GenerateParams generateParams = new EntityBase64Converter<GenerateParams>().ToEntity(data);

        // TODO: Generate file

        return File(bytes, mimeType);
    }

no cliente

    function generate(reportId, format, parameters)
    {
        var data = {
            reportId: reportId,
            format: format,
            params: params
        };

        $.ajax(
        {
            url: "/Validate",
            type: 'POST',
            data: JSON.stringify(data),
            dataType: 'json',
            contentType: 'application/json; charset=utf-8',
            success: generateComplete
        });
    }

    function generateComplete(result)
    {
        if (result.State == "Success")
        {
            // this could/should already be set in the HTML
            formGenerate.action = "/Generate";
            formGenerate.target = iframeFile;

            hidData = result.Data;
            formGenerate.submit();
        }
        else
            // TODO: display error messages
    }
Amersk
fonte
1
Ainda não dei uma boa olhada nessa solução, mas vale a pena notar que nada na solução usando create_binary_file.php exige que os arquivos sejam salvos em disco. Seria perfeitamente possível que o create_binary_file.php gere arquivos binários na memória.
SamStephens
11

Existe uma maneira mais simples: criar um formulário e publicá-lo; isso corre o risco de redefinir a página se o tipo mime de retorno for algo que um navegador abriria, mas para csv e assim é perfeito

Exemplo requer sublinhado e jquery

var postData = {
    filename:filename,
    filecontent:filecontent
};
var fakeFormHtmlFragment = "<form style='display: none;' method='POST' action='"+SAVEAS_PHP_MODE_URL+"'>";
_.each(postData, function(postValue, postKey){
    var escapedKey = postKey.replace("\\", "\\\\").replace("'", "\'");
    var escapedValue = postValue.replace("\\", "\\\\").replace("'", "\'");
    fakeFormHtmlFragment += "<input type='hidden' name='"+escapedKey+"' value='"+escapedValue+"'>";
});
fakeFormHtmlFragment += "</form>";
$fakeFormDom = $(fakeFormHtmlFragment);
$("body").append($fakeFormDom);
$fakeFormDom.submit();

Para coisas como html, texto e coisas do tipo, verifique se o tipo de mimet é algo como application / octet-stream

código php

<?php
/**
 * get HTTP POST variable which is a string ?foo=bar
 * @param string $param
 * @param bool $required
 * @return string
 */
function getHTTPPostString ($param, $required = false) {
    if(!isset($_POST[$param])) {
        if($required) {
            echo "required POST param '$param' missing";
            exit 1;
        } else {
            return "";
        }
    }
    return trim($_POST[$param]);
}

$filename = getHTTPPostString("filename", true);
$filecontent = getHTTPPostString("filecontent", true);

header("Content-type: application/octet-stream");
header("Content-Disposition: attachment; filename=\"$filename\"");
echo $filecontent;
aqm
fonte
8

Em suma, não há maneira mais simples. Você precisa fazer outra solicitação do servidor para mostrar o arquivo PDF. No entanto, existem poucas alternativas, mas elas não são perfeitas e não funcionam em todos os navegadores:

  1. Veja o esquema de URI de dados . Se os dados binários forem pequenos, talvez você possa usar o javascript para abrir os dados que passam pela janela no URI.
  2. A única solução do Windows / IE seria ter o controle .NET ou o FileSystemObject para salvar os dados no sistema de arquivos local e abri-los a partir daí.
VinayC
fonte
Obrigado, eu não estava ciente do esquema de URI de dados. Parece que essa pode ser a única maneira de fazer isso em uma única solicitação. Mas não é realmente a direção que quero seguir.
Tauren
Devido aos requisitos do cliente, acabei resolvendo esse problema usando os cabeçalhos do servidor na resposta ( Content-Disposition: attachment;filename="file.txt"), mas realmente quero tentar essa abordagem na próxima vez. Eu tinha usado URIdados para miniaturas antes, mas nunca me ocorreu usá-los para downloads - obrigado por compartilhar essa pepita.
Brichins
8

Já faz um tempo desde que essa pergunta foi feita, mas eu tive o mesmo desafio e quero compartilhar minha solução. Ele usa elementos de outras respostas, mas não consegui encontrá-lo na íntegra. Ele não usa um formulário ou iframe, mas requer um par de solicitação de postagem / obtenção. Em vez de salvar o arquivo entre as solicitações, ele salva os dados da postagem. Parece ser simples e eficaz.

cliente

var apples = new Array(); 
// construct data - replace with your own
$.ajax({
   type: "POST",
   url: '/Home/Download',
   data: JSON.stringify(apples),
   contentType: "application/json",
   dataType: "text",

   success: function (data) {
      var url = '/Home/Download?id=' + data;
      window.location = url;
   });
});

servidor

[HttpPost]
// called first
public ActionResult Download(Apple[] apples)
{
   string json = new JavaScriptSerializer().Serialize(apples);
   string id = Guid.NewGuid().ToString();
   string path = Server.MapPath(string.Format("~/temp/{0}.json", id));
   System.IO.File.WriteAllText(path, json);

   return Content(id);
}

// called next
public ActionResult Download(string id)
{
   string path = Server.MapPath(string.Format("~/temp/{0}.json", id));
   string json = System.IO.File.ReadAllText(path);
   System.IO.File.Delete(path);
   Apple[] apples = new JavaScriptSerializer().Deserialize<Apple[]>(json);

   // work with apples to build your file in memory
   byte[] file = createPdf(apples); 

   Response.AddHeader("Content-Disposition", "attachment; filename=juicy.pdf");
   return File(file, "application/pdf");
}
Frank Rem
fonte
1
Eu acho que gosto mais desta solução. No meu caso, embora eu esteja gerando o arquivo, acho que tentarei manter o arquivo na memória até que seja acessado e liberar a memória após o download, para que eu nunca precise gravar no disco.
precisa saber é o seguinte
5
$scope.downloadSearchAsCSV = function(httpOptions) {
  var httpOptions = _.extend({
    method: 'POST',
    url:    '',
    data:   null
  }, httpOptions);
  $http(httpOptions).then(function(response) {
    if( response.status >= 400 ) {
      alert(response.status + " - Server Error \nUnable to download CSV from POST\n" + JSON.stringify(httpOptions.data));
    } else {
      $scope.downloadResponseAsCSVFile(response)
    }
  })
};
/**
 * @source: https://github.com/asafdav/ng-csv/blob/master/src/ng-csv/directives/ng-csv.js
 * @param response
 */
$scope.downloadResponseAsCSVFile = function(response) {
  var charset = "utf-8";
  var filename = "search_results.csv";
  var blob = new Blob([response.data], {
    type: "text/csv;charset="+ charset + ";"
  });

  if (window.navigator.msSaveOrOpenBlob) {
    navigator.msSaveBlob(blob, filename); // @untested
  } else {
    var downloadContainer = angular.element('<div data-tap-disabled="true"><a></a></div>');
    var downloadLink      = angular.element(downloadContainer.children()[0]);
    downloadLink.attr('href', window.URL.createObjectURL(blob));
    downloadLink.attr('download', "search_results.csv");
    downloadLink.attr('target', '_blank');

    $document.find('body').append(downloadContainer);

    $timeout(function() {
      downloadLink[0].click();
      downloadLink.remove();
    }, null);
  }

  //// Gets blocked by Chrome popup-blocker
  //var csv_window = window.open("","","");
  //csv_window.document.write('<meta name="content-type" content="text/csv">');
  //csv_window.document.write('<meta name="content-disposition" content="attachment;  filename=data.csv">  ');
  //csv_window.document.write(response.data);
};
James McGuigan
fonte
3

Não é totalmente uma resposta para a postagem original, mas uma solução rápida e suja para postar um objeto json no servidor e gerar dinamicamente um download.

JQuery do lado do cliente:

var download = function(resource, payload) {
     $("#downloadFormPoster").remove();
     $("<div id='downloadFormPoster' style='display: none;'><iframe name='downloadFormPosterIframe'></iframe></div>").appendTo('body');
     $("<form action='" + resource + "' target='downloadFormPosterIframe' method='post'>" +
      "<input type='hidden' name='jsonstring' value='" + JSON.stringify(payload) + "'/>" +
      "</form>")
      .appendTo("#downloadFormPoster")
      .submit();
}

..e decodificando a string json no servidor e definindo cabeçalhos para download (exemplo PHP):

$request = json_decode($_POST['jsonstring']), true);
header('Content-Type: application/csv');
header('Content-Disposition: attachment; filename=export.csv');
header('Pragma: no-cache');
ralftar
fonte
Eu gosto desta solução. Na pergunta do OP, parece que o solicitante sabe se deve esperar um download de arquivo ou dados JSON, para que ele possa decidir no final do cliente e postar em um URL diferente, em vez de decidir no final do servidor.
22415 Sam
2

Acho que a melhor abordagem é usar uma combinação. Sua segunda abordagem parece ser uma solução elegante em que os navegadores estão envolvidos.

Portanto, dependendo de como a chamada é feita. (seja um navegador ou uma chamada de serviço da web), você pode usar uma combinação dos dois, enviando um URL para o navegador e enviando dados brutos para qualquer outro cliente de serviço da web.

naikus
fonte
Minha segunda abordagem também parece mais atraente para mim agora. Obrigado por confirmar que é uma solução que vale a pena. Você está sugerindo que eu transmita um valor adicional no objeto json que indique que esta solicitação está sendo feita a partir de um navegador como uma chamada ajax em vez de uma chamada de serviço da web? Existem várias maneiras de conseguir isso, mas que técnica você usaria?
Tauren
isso não pode ser determinado pelo cabeçalho http do agente do usuário? Ou qualquer outro cabeçalho http?
Naikus
1

Estou acordado há dois dias tentando descobrir como baixar um arquivo usando o jquery com chamada ajax. Todo o apoio que recebi não pôde ajudar minha situação até que eu tente isso.

Lado do Cliente

function exportStaffCSV(t) {
   
    var postData = { checkOne: t };
    $.ajax({
        type: "POST",
        url: "/Admin/Staff/exportStaffAsCSV",
        data: postData,
        success: function (data) {
            SuccessMessage("file download will start in few second..");
            var url = '/Admin/Staff/DownloadCSV?data=' + data;
            window.location = url;
        },
       
        traditional: true,
        error: function (xhr, status, p3, p4) {
            var err = "Error " + " " + status + " " + p3 + " " + p4;
            if (xhr.responseText && xhr.responseText[0] == "{")
                err = JSON.parse(xhr.responseText).Message;
            ErrorMessage(err);
        }
    });

}

Lado do servidor

 [HttpPost]
    public string exportStaffAsCSV(IEnumerable<string> checkOne)
    {
        StringWriter sw = new StringWriter();
        try
        {
            var data = _db.staffInfoes.Where(t => checkOne.Contains(t.staffID)).ToList();
            sw.WriteLine("\"First Name\",\"Last Name\",\"Other Name\",\"Phone Number\",\"Email Address\",\"Contact Address\",\"Date of Joining\"");
            foreach (var item in data)
            {
                sw.WriteLine(string.Format("\"{0}\",\"{1}\",\"{2}\",\"{3}\",\"{4}\",\"{5}\",\"{6}\"",
                    item.firstName,
                    item.lastName,
                    item.otherName,
                    item.phone,
                    item.email,
                    item.contact_Address,
                    item.doj
                    ));
            }
        }
        catch (Exception e)
        {

        }
        return sw.ToString();

    }

    //On ajax success request, it will be redirected to this method as a Get verb request with the returned date(string)
    public FileContentResult DownloadCSV(string data)
    {
        return File(new System.Text.UTF8Encoding().GetBytes(data), System.Net.Mime.MediaTypeNames.Application.Octet, filename);
        //this method will now return the file for download or open.
    }

Boa sorte.

Otis-iDev
fonte
2
Isso funcionará, para arquivos pequenos. Mas se você tiver um arquivo grande, encontrará o problema de o URL ser muito longo. Experimente com 1.000 registros, ele irá quebrar e exportar apenas parte dos dados.
Michael Merrell
1

Encontrado em algum lugar há muito tempo e funciona perfeitamente!

let payload = {
  key: "val",
  key2: "val2"
};

let url = "path/to/api.php";
let form = $('<form>', {'method': 'POST', 'action': url}).hide();
$.each(payload, (k, v) => form.append($('<input>', {'type': 'hidden', 'name': k, 'value': v})) );
$('body').append(form);
form.submit();
form.remove();
Den Nikitin
fonte
0

Outra abordagem, em vez de salvar o arquivo no servidor e recuperá-lo, é usar o .NET 4.0+ ObjectCache com uma expiração curta até a segunda ação (no momento, ele pode ser descartado definitivamente). O motivo pelo qual desejo usar o JQuery Ajax para fazer a chamada é que é assíncrono. A criação do meu arquivo PDF dinâmico leva bastante tempo e eu exibo uma caixa de diálogo movimentada durante esse período (também permite que outro trabalho seja feito). A abordagem de usar os dados retornados no "success:" para criar um Blob não funciona de maneira confiável. Depende do conteúdo do arquivo PDF. É facilmente corrompido pelos dados na resposta, se não for completamente textual, o que é tudo o que o Ajax pode manipular.

Wray Smallwood
fonte
0

Solução

O anexo Disposição de conteúdo parece funcionar para mim:

self.set_header("Content-Type", "application/json")
self.set_header("Content-Disposition", 'attachment; filename=learned_data.json')

Gambiarra

application / octet-stream

Eu tinha algo semelhante acontecendo comigo com um JSON, para mim, no lado do servidor, eu estava definindo o cabeçalho como self.set_header ("Content-Type", " application / json "), no entanto, quando eu mudei para:

self.set_header("Content-Type", "application/octet-stream")

Ele baixou automaticamente.

Saiba também que, para que o arquivo ainda mantenha o sufixo .json, você precisará dele no cabeçalho do nome do arquivo:

self.set_header("Content-Disposition", 'filename=learned_data.json')
Mr-Programs
fonte
-1

Com o HTML5, você pode apenas criar uma âncora e clicar nela. Não há necessidade de adicioná-lo ao documento quando criança.

const a = document.createElement('a');
a.download = '';
a.href = urlForPdfFile;
a.click();

Tudo feito.

Se você deseja ter um nome especial para o download, basta passá-lo no downloadatributo:

const a = document.createElement('a');
a.download = 'my-special-name.pdf';
a.href = urlForPdfFile;
a.click();
reescrito
fonte
4
a pergunta é sobre o método POST #
22419