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

JavaScript/jQuery to download file via POST with JSON data

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

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

  • 将包含JSON数据的文章提交到REST URL。
  • 如果请求指定了JSON响应,则返回JSON。
  • 如果请求指定了PDF/XLS/ETC响应,则返回可下载的二进制文件。
  • 我现在有1&2工作,客户机jquery应用程序通过基于JSON数据创建DOM元素来在网页中显示返回的数据。我还有3从Web服务的角度工作,这意味着如果给定正确的JSON参数,它将创建并返回一个二进制文件。但我不确定在客户机JavaScript代码中处理3的最佳方法。

    是否可以从这样的Ajax调用中获取可下载文件?如何让浏览器下载和保存文件?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    $.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;
        }
    });

    服务器用以下头响应:

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

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

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

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

    必须有一种更简单的方法来实现这一点。思想?

    编辑:在查看了$.ajax的文档之后,我发现响应数据类型只能是xml, html, script, json, jsonp, text中的一个,所以我猜想没有办法使用ajax请求直接下载文件,除非我按照@vinayc answer(我不想这样做)中的建议,将二进制文件嵌入到使用data uri方案中。

    所以我想我的选择是:

  • 不要使用Ajax,而是提交一个表单发布,并将我的JSON数据嵌入表单值中。可能需要处理隐藏的iframes等。

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

  • 使用我上面的其他想法,但通过@naikus答案的建议进行了增强。提交带有一些参数的Ajax请求,这些参数使Web服务知道这是通过Ajax调用调用的。如果从Ajax调用调用Web服务,只需返回带有生成资源URL的JSON。如果直接调用资源,则返回实际的二进制文件。

  • 我想得越多,我就越喜欢最后的选择。通过这种方式,我可以获得有关请求的信息(生成时间、文件大小、错误消息等),并且可以在开始下载之前对这些信息进行操作。缺点是服务器上有额外的文件管理。

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


    Letronje的解决方案只适用于非常简单的页面。document.body.innerHTML +=获取正文的HTML文本,附加iframe html,并将页面的innerhtml设置为该字符串。这将清除页面所具有的所有事件绑定。创建一个元素并使用appendChild

    1
    2
    3
    4
    5
    6
    $.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

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

    实际操作:使用变量post data中的数据执行post-to/create_binary_file.php;如果成功完成了该post,请在页面正文中添加一个新的iframe。假设来自/create_binary_file.php的响应将包含一个值"url",该值是生成的pdf/xls/etc文件可以从中下载的url。向引用该URL的页面添加iframe将导致浏览器升级用户下载该文件,前提是Web服务器具有适当的mime类型配置。


    我一直在玩另一个使用斑点的选项。我已经设法让它下载文本文档,并且我已经下载了PDF(不管它们是如何损坏的)。

    使用BLOBAPI,您将能够执行以下操作:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    $.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中的文件。这将使用锚标记上的下载属性强制浏览器下载它。


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

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

    有更多的工作要做,也许还有更多的处理,但总的来说,我对这个解决方案感觉好多了。

    代码以c/mvc为单位

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
        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);
        }

    在客户身上

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
        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
        }


    有一种更简单的方法,创建一个表单并发布它,如果返回的mime类型是浏览器会打开的类型,那么就有重置页面的风险,但是对于csv这样的类型,它是完美的。

    示例需要下划线和jquery

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    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、文本等内容,请确保mimetype类似于application/octet流。

    PHP代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    <?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;

    简而言之,没有更简单的方法。您需要另一个服务器请求来显示PDF文件。尽管如此,还是有一些选择,但它们并不完美,不能在所有浏览器上工作:

  • 查看数据URI方案。如果二进制数据很小,那么您可以使用javascript打开在URI中传递数据的窗口。
  • Windows/IE唯一的解决方案是使用.NET控件或FileSystemObject将数据保存在本地文件系统上并从本地文件系统打开。

  • 问这个问题已经有一段时间了,但我也有同样的挑战,我想分享我的解决方案。它使用了其他答案中的元素,但我找不到完整的答案。它不使用表单或iframe,但需要post/get请求对。它不在请求之间保存文件,而是保存日志数据。它看起来既简单又有效。

    客户机

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    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;
       });
    });

    服务器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    [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
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    $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
    Unable to download CSV from POST
    "
    + 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('');
        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);
    };

    不完全是对原始文章的回答,而是一个快速而肮脏的解决方案,用于将JSON对象发布到服务器并动态生成下载。

    客户端jquery:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    var download = function(resource, payload) {
         $("#downloadFormPoster").remove();
         $("<iframe name='downloadFormPosterIframe'></iframe>").appendTo('body');
         $("<form action='" + resource +"' target='downloadFormPosterIframe' method='post'>" +
         "<input type='hidden' name='jsonstring' value='" + JSON.stringify(payload) +"'/>" +
         "</form>")
          .appendTo("#downloadFormPoster")
          .submit();
    }

    …然后在服务器端解码JSON字符串并设置下载头(PHP示例):

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


    我认为最好的方法是使用组合,第二种方法似乎是一种优雅的解决方案,其中涉及到浏览器。

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


    使用HTML5,您只需创建一个锚并点击它。不需要将其作为子级添加到文档中。

    1
    2
    3
    4
    const a = document.createElement('a');
    a.download = '';
    a.href = urlForPdfFile;
    a.click();

    都做完了。

    如果您想为下载提供一个特殊的名称,只需在download属性中传递它:

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


    我已经醒了两天了,现在我想知道如何使用jquery with ajax调用下载一个文件。我得到的所有支持对我的处境都无济于事,直到我尝试了这个。

    客户端

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    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);
            }
        });

    }

    服务器端

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
     [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.
        }

    祝你好运。


    另一种方法不是将文件保存在服务器上并检索它,而是使用.NET 4.0+ObjectCache,其有效期很短,直到执行第二个操作为止(此时可以确定地将其转储)。我想使用jqueryajax来进行调用的原因是它是异步的。构建动态PDF文件需要相当长的时间,在此期间我会显示一个繁忙的微调器对话框(它还允许完成其他工作)。使用"success:"中返回的数据创建blob的方法不可靠。这取决于PDF文件的内容。如果响应中的数据不完全是文本的,那么它很容易被破坏,而这正是Ajax所能处理的。