JavaScript / jQuery通过JSON数据通过POST下载文件


250

我有一个基于jquery的单页webapp。它通过AJAX调用与RESTful Web服务进行通信。

我正在尝试完成以下任务:

  1. 将包含JSON数据的POST提交到REST URL。
  2. 如果请求指定JSON响应,则返回JSON。
  3. 如果请求指定PDF / XLS / etc响应,则返回可下载的二进制文件。

我现在有1&2,并且客户端jquery应用通过基于JSON数据创建DOM元素来在网页中显示返回的数据。从Web服务的角度来看,我也有#3工作,这意味着如果给出正确的JSON参数,它将创建并返回一个二进制文件。但是我不确定在客户端JavaScript代码中处理#3的最佳方法。

是否可以通过这样的ajax调用取回可下载文件?如何让浏览器下载并保存文件?

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

服务器使用以下标头进行响应:

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

另一个想法是生成PDF并将其存储在服务器上,并返回包含文件URL的JSON。然后,在ajax成功处理程序中发出另一个调用,以执行以下操作:

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

但这意味着我需要对服务器进行多次调用,并且服务器需要构建可下载文件,将它们存储在某个地方,然后定期清理该存储区域。

必须有一种更简单的方法来完成此任务。有想法吗?


编辑:审查$ .ajax的文档后,我看到响应数据类型只能是之一xml, html, script, json, jsonp, text,因此我猜测没有办法直接使用ajax请求下载文件,除非我将二进制文件嵌入到@VinayC答案中建议的数据URI方案(我不想这样做)。

所以我想我的选择是:

  1. 不使用ajax,而是提交一个表单发布,并将我的JSON数据嵌入到表单值中。可能需要弄乱隐藏的iframe等。

  2. 不使用ajax,而是将我的JSON数据转换为查询字符串以构建标准的GET请求,并将window.location.href设置为此URL。可能需要在我的点击处理程序中使用event.preventDefault(),以防止浏览器从应用程序URL更改。

  3. 使用上面我的其他想法,但是在@naikus答案中的建议得到了增强。提交带有一些参数的AJAX请求,该参数使Web服务知道通过ajax调用正在调用该请求。如果从ajax调用中调用了Web服务,则只需将带有URL的JSON返回到生成的资源即可。如果直接调用资源,则返回实际的二进制文件。

我想得越多,我越喜欢最后一个选择。这样,我可以获取有关请求的信息(生成时间,文件大小,错误消息等),并且可以在开始下载之前根据该信息进行操作。缺点是服务器上的额外文件管理。

还有其他方法可以做到这一点吗?我应该知道这些方法的利弊吗?



6
这个是“较早”的,所以它如何重复
GiM 2015年

1
url = 'http://localhost/file.php?file='+ $('input').val(); window.open(url); 获得相同结果的简单方法。将标题放在php文件中。无需发送任何AJAX或get请求
阿布舍克bagul

Answers:


171

letronje的解决方案仅适用于非常简单的页面。document.body.innerHTML +=接受正文的HTML文本,附加iframe HTML,然后将页面的innerHTML设置为该字符串。这将清除您的页面具有的所有事件绑定。创建一个元素并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);
}); 

或使用jQuery

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

这实际上是做什么的:使用变量postData中的数据执行到/create_binary_file.php的发布;如果该帖子成功完成,则在页面正文中添加一个新的iframe。假设来自/create_binary_file.php的响应将包含值'url',这是可从中下载生成的PDF / XLS / etc文件的URL。假设Web服务器具有适当的mime类型配置,则将iframe添加到引用该URL的页面将导致浏览器提示用户下载文件。


1
我同意,这比使用更好+=,我将相应地更新我的应用程序。谢谢!
牛头人

3
我喜欢这个概念,但是Chrome在控制台中显示以下消息:Resource interpreted as Document but transferred with MIME type application/pdf。然后它也警告我该文件可能很危险。如果我直接访问retData.url,则没有警告或问题。
Michael Irey

环顾互联网,如果您的服务器未正确设置Content-Type和Content-Disposition,则iFrame中的PDF可能会出现问题。我本人实际上并没有使用此解决方案,只是澄清了以前的解决方案,所以恐怕没有其他建议。
SamStephens

1
@Tauren:如果您同意这是一个更好的答案,请注意,您可以随时切换接受的答案-甚至完全删除接受标记。只需勾选新答案,即可接受标记。(旧问题,但最近在国旗中提出。)
BoltClock

5
retData.url应该是什么?
马克·蒂恩

48

我一直在玩另一个使用blob的选项。我设法下载了文本文档,并下载了PDF(但是它们已损坏)。

使用blob API,您将可以执行以下操作:

$.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();

});

这是IE 10 +,Chrome 8 +,FF 4+。参见https://developer.mozilla.org/en-US/docs/Web/API/URL.createObjectURL

它将仅在Chrome,Firefox和Opera中下载文件。这将使用定位标记上的下载属性来强制浏览器下载它。


1
对IE和Safari浏览器下载属性不支持:((caniuse.com/#feat=download)您可以打开一个新窗口,并把内容有,但不知道一个PDF或Excel ...
马萨尔胡安

1
您应在致电后释放浏览器的内存clickwindow.URL.revokeObjectURL(link.href);
爱德华·布雷 Edward Brey)2015年

@JoshBerke这很旧,但对我有效。我如何才能立即打开目录以保存而不是保存到浏览器?还有一种确定传入文件名称的方法吗?
Jo Ko

2
它将破坏二进制文件,因为它们使用编码被转换为字符串,并且结果与原始二进制文件不太接近...
Gobol

2
可能会使用mimetypes而不破坏此处与PDF 一起
Mateo Tibaquira

18

我知道这种旧的,但我想我想出了一个更优雅的解决方案。我有同样的问题。我所提出的解决方案存在的问题是,它们都需要将文件保存在服务器上,但是我不想将文件保存在服务器上,因为它引入了其他问题(安全性:文件可以由未经身份验证的用户,清除:如何以及何时清除文件)。和您一样,我的数据是复杂的,嵌套的JSON对象,很难将其放入表单中。

我所做的是创建两个服务器功能。第一个验证了数据。如果有错误,将被返回。如果不是错误,我将所有序列化/编码为base64字符串的参数返回。然后,在客户端上,我有一个仅包含一个隐藏输入并张贴到第二个服务器功能的表单。我将隐藏的输入设置为base64字符串,然后提交格式。第二个服务器功能对参数进行解码/反序列化并生成文件。表单可以提交到新窗口或页面上的iframe,然后文件将打开。

涉及到更多的工作,也许还有更多的处理,但是总的来说,这种解决方案让我感觉好多了。

代码在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);
    }

在客户端上

    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
    }

1
我对这个解决方案没有很好的了解,但是值得注意的是,使用create_binary_file.php的解决方案并不需要将文件保存到磁盘。让create_binary_file.php在内存中生成二进制文件是完全可行的。
SamStephens

11

有一种更简单的方法,创建一个表单并将其发布,如果返回的mime类型是浏览器可以打开的,则冒着重置页面的风险,但是对于csv而言,这是完美的

示例需要下划线和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();

对于html,text之类的东西,请确保mimetype是诸如application / octet-stream之类的东西

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;

8

简而言之,没有更简单的方法。您需要发出另一个服务器请求以显示PDF文件。虽然,几乎没有其他选择,但是它们并不是完美的,并且不能在所有浏览器上都可以使用:

  1. 看一下数据URI方案。如果二进制数据很小,则可以使用javascript打开以URI形式传递数据的窗口。
  2. Windows / IE唯一的解决方案是具有.NET控件或FileSystemObject来将数据保存在本地文件系统上,然后从那里打开它。

谢谢,我不知道数据URI方案。看起来这可能是在单个请求中执行此操作的唯一方法。但这并不是我真正想去的方向。
牛头人

由于客户的要求,我最终使用response(Content-Disposition: attachment;filename="file.txt")中的服务器标头解决了这个问题,但是我真的想下次尝试这种方法。我以前曾将URI数据用作缩略图,但我从未想过将其用于下载-感谢您分享此掘金。
brichins 2013年

8

自问这个问题已经有一段时间了,但是我面临着同样的挑战,想分享我的解决方案。它使用了其他答案中的元素,但我无法完整找到它。它不使用表单或iframe,但确实需要发布/获取请求对。而不是在请求之间保存文件,而是保存发布数据。这似乎既简单又有效。

客户

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

服务器

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

1
我认为我最喜欢这种解决方案。就我而言,尽管我正在生成文件,所以我认为我将尝试将文件保留在内存中直到被访问,然后在下载文件后释放内存,因此我不必写磁盘。
BVernon

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

3

这不完全是对原始帖子的答案,而是一种用于将json对象发布到服务器并动态生成下载的快速而肮脏的解决方案。

客户端jQuery:

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

..然后在服务器端解码json-string并设置要下载的标头(PHP示例):

$request = json_decode($_POST['jsonstring']), true);
header('Content-Type: application/csv');
header('Content-Disposition: attachment; filename=export.csv');
header('Pragma: no-cache');

我喜欢这个解决方案。在OP的问题中,听起来好像请求者知道是要下载文件还是JSON数据,因此他可以在客户端决定并发布到其他URL,而不是在服务器端决定。
山姆

2

我认为最好的方法是结合使用。您的第二种方法似乎是涉及浏览器的优雅解决方案。

因此,取决于呼叫的方式。(无论是浏览器还是Web服务调用),您可以将两者结合使用,将URL发送到浏览器,并将原始数据发送到任何其他Web服务客户端。


我的第二种方法现在对我也更具吸引力。感谢您确认这是一个值得的解决方案。您是否建议我在json对象中传递一个附加值,以指示此请求是从浏览器发出的,所以它是ajax调用而不是Web服务调用?我可以想到几种方法来实现此目的,但是您将使用哪种技术?
牛头人

不能由User-Agent http标头确定吗?还是其他任何HTTP标头?
naikus

1

我已经醒了两天,试图弄清楚如何使用带有ajax调用的jquery下载文件。在尝试此方法之前,我获得的所有支持都无法帮助我解决问题。

客户端

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

}

服务器端

 [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.
    }

祝好运。


2
这适用于小文件。但是,如果文件很大,则会遇到URL太长的问题。尝试使用1,000条记录,它将中断并且仅导出部分数据。
Michael Merrell

1

在很久以前的某个地方找到它,并且效果很好!

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

0

替代将文件保存在服务器上并进行检索的另一种方法是,使用.NET 4.0+ ObjectCache的有效期短,直到执行第二个Action(此时可以将其明确转储)。我想使用JQuery Ajax进行调用的原因是它是异步的。构建动态PDF文件需要花费大量时间,并且在此期间,我会显示一个繁忙的微调器对话框(它还允许完成其他工作)。使用“成功:”中返回的数据创建Blob的方法无法可靠地工作。这取决于PDF文件的内容。如果响应不是完全文本的,则很容易被响应中的数据破坏,这是Ajax可以处理的全部。


0

内容处置附件似乎对我有用:

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

解决方法

应用/八位字节流

我使用JSON发生了类似的事情,对于我来说,在服务器端,我将标头设置为self.set_header(“ Content-Type”,“ application / json ”),但是当我将其更改为:

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

它会自动下载它。

还知道为了使文件仍然保留.json后缀,您需要在文件名标头上添加它:

self.set_header("Content-Disposition", 'filename=learned_data.json')

-1

使用HTML5,您只需创建一个锚点并单击它即可。无需将其作为子项添加到文档中。

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

全做完了。

如果要为下载使用特殊名称,只需在download属性中传递它即可:

const a = document.createElement('a');
a.download = 'my-special-name.pdf';
a.href = urlForPdfFile;
a.click();

4
问题是关于POST方法
DOVID
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.