我有一个基于jquery的单页web应用程序。它通过AJAX调用与基于rest的web服务通信。

我正在努力实现以下目标:

向REST url提交包含JSON数据的POST。 如果请求指定了JSON响应,则返回JSON。 如果请求指定PDF/XLS/etc响应,则返回可下载的二进制文件。

我有1和2工作现在,和客户端jquery应用程序通过创建基于JSON数据的DOM元素在网页中显示返回的数据。我还让#3从web服务的角度工作,这意味着如果给定正确的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,我看到响应dataType只能是xml, html,脚本,json, jsonp,文本之一,所以我猜没有办法直接下载一个文件使用ajax请求,除非我嵌入二进制文件使用数据URI方案建议在@VinayC答案(这不是我想做的事情)。

所以我想我的选择是:

Not use ajax and instead submit a form post and embed my JSON data into the form values. Would probably need to mess with hidden iframes and such. Not use ajax and instead convert my JSON data into a query string to build a standard GET request and set window.location.href to this URL. May need to use event.preventDefault() in my click handler to keep browser from changing from the application URL. Use my other idea above, but enhanced with suggestions from the @naikus answer. Submit AJAX request with some parameter that lets web-service know this is being called via an ajax call. If the web service is called from an ajax call, simply return JSON with a URL to the generated resource. If the resource is called directly, then return the actual binary file.

我越想越喜欢最后一个选项。通过这种方式,我可以获得关于请求的信息(生成时间、文件大小、错误消息等),并且可以在开始下载之前对这些信息采取行动。缺点是服务器上需要额外的文件管理。

还有其他方法吗?我应该知道这些方法的优缺点吗?


当前回答

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

What this actually does: perform a post to /create_binary_file.php with the data in the variable postData; if that post completes successfully, add a new iframe to the body of the page. The assumption is that the response from /create_binary_file.php will include a value 'url', which is the URL that the generated PDF/XLS/etc file can be downloaded from. Adding an iframe to the page that references that URL will result in the browser promoting the user to download the file, assuming that the web server has the appropriate mime type configuration.

其他回答

我喜欢弗兰克的想法,并决定做出自己的改变。因为尝试在一篇文章中做到这一点是非常复杂的,我使用了两篇文章的方法,但只击中数据库一次,不需要保存文件或完成后清理文件。

首先,我运行ajax请求来检索数据,但不是从控制器返回数据,而是返回一个绑定到记录的TempData存储的GUID。

$.get("RetrieveData", { name: "myParam"} , function(results){
    window.location = "downloadFile?id=" + results
});

public string RetrieveData(string name)
{
    var data = repository.GetData(name);
    string id = Guid.NewGuid().ToString();
    var file = new KeyValuePair<string, MyDataModel>(name, data);
    TempData[id]=file;
    return id;
}

然后当我呼叫窗口的时候。我将Guid传递给新方法,并从TempData获取数据。执行此方法后,TempData将是空闲的。

public ActionResult DownloadFile(string id)
{
   var file = (KeyValuePair<string,MyDataModel>)TempData[id];
   var filename = file.Key;
   var data = file.Value;
   var byteArray = Encoding.UTF8.GetBytes(data);
   ...
   return File(byteArray, "text/csv", "myFile.csv");
}

我想我已经接近了,但是有些东西正在破坏文件(图像),无论如何,也许有人可以揭示这种方法的问题

$.ajax({
            url: '/GenerateImageFile',
            type: 'POST',
            cache: false,
            data: obj,
            dataType: "text",
            success: function (data, status, xhr) {
                let blob = new Blob([data], { type: "image/jpeg" });

                let a = document.createElement('a');
                a.href = window.URL.createObjectURL(blob);
                a.download = "test.jpg";
                document.body.appendChild(a);
                a.click();
                document.body.removeChild(a);
                window.URL.removeObjectURL(a.href);
            },
            complete: function () {

            },
            beforeSend: function () {

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

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

Another approach instead of saving the file on the server and retrieving it, is to use .NET 4.0+ ObjectCache with a short expiration until the second Action (at which time it can be definitively dumped). The reason that I want to use JQuery Ajax to do the call, is that it is asynchronous. Building my dynamic PDF file takes quite a bit of time, and I display a busy spinner dialog during that time (it also allows other work to be done). The approach of using the data returned in the "success:" to create a Blob does not work reliably. It depends on the content of the PDF file. It is easily corrupted by data in the response, if it is not completely textual which is all that Ajax can handle.