首页 文章

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

提问于
浏览
223

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

我正在努力完成以下任务:

  • 将包含JSON数据的POST提交到REST URL .

  • 如果请求指定了JSON响应,则返回JSON .

  • 如果请求指定了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的文档后,我看到响应dataType只能是 xml, html, script, json, jsonp, text 之一,所以我猜测没有办法使用ajax请求直接下载文件,除非我嵌入了二进制文件使用@VinayC答案中建议的数据URI方案(这不是我想要做的事情) .

所以我想我的选择是:

  • 不使用ajax而是提交表单帖子并将我的JSON数据嵌入到表单值中 . 可能需要搞乱隐藏的iframe等 .

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

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

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

还有其他方法可以做到这一点吗我应该注意这些方法的任何利弊?

12 回答

  • 2

    有一种更简单的方法,创建一个表单并发布它,如果返回的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;
    
  • 15

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

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

  • 1

    简而言之,没有更简单的方法 . 您需要另一个服务器请求来显示PDF文件 . 虽然,但是它们很少有替代品,但它们并不完美,并且不适用于所有浏览器:

    • data URI scheme . 如果二进制数据很小,那么您可以使用javascript打开URI中传递数据的窗口 .

    • Windows / IE唯一的解决方案是使用.NET控件或FileSystemObject将数据保存在本地文件系统上并从那里打开它 .

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

    我一直在玩另一个使用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中下载该文件 . 这使用锚标记上的下载属性来强制浏览器下载它 .

  • 9

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

    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

    不完全是对原始帖子的回答,而是将json-object发布到服务器并动态生成下载的快速而肮脏的解决方案 .

    客户端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并设置 Headers 以供下载(PHP示例):

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

    我已经醒了两天,现在试图弄清楚如何使用带有ajax调用的jquery下载文件 . 在我尝试这个之前,我得到的所有支持都无法帮助我 .

    Client Side

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

    Server Side

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

    祝好运 .

  • 7

    我知道这种陈旧,但我想我已经提出了一个更优雅的解决方案 . 我有同样的问题 . 我对解决方案提出的问题是,他们都要求将文件保存在服务器上,但我不想将文件保存在服务器上,因为它引入了其他问题(安全性:文件可以通过以下方式访问:未经过身份验证的用户,清理:如何以及何时删除文件) . 和你一样,我的数据是复杂的,嵌套的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
        }
    
  • 0

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

    客户

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

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

相关问题