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

IT技术 javascript jquery ajax rest
2021-01-21 23:25:36

我有一个基于 jquery 的单页 web 应用程序。它通过 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 的文档后,我看到响应 dataType 只能是其中之一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 调用调用的。如果 Web 服务是从 ajax 调用中调用的,只需将带有 URL 的 JSON 返回到生成的资源。如果直接调用资源,则返回实际的二进制文件。

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

还有其他方法可以实现吗?我应该注意这些方法的任何优点/缺点?

6个回答

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。向引用该 URL 的页面添加 iframe 将导致浏览器促使用户下载文件,假设 Web 服务器具有适当的 mime 类型配置。

我喜欢这个概念,但是 Chrome 在控制台中有这样的消息:Resource interpreted as Document but transferred with MIME type application/pdf. 然后它也警告我该文件可能是危险的。如果我直接访问 retData.url,则不会出现警告或问题。
2021-03-20 23:25:36
@Tauren:如果您同意这是一个更好的答案,请注意您可以随时切换已接受的答案 - 甚至完全删除接受标记。只需勾选新答案,接受标记将被转移。(老问题,但最近在旗帜中提出。)
2021-03-28 23:25:36
我同意,这比使用更好+=,我会相应地更新我的应用程序。谢谢!
2021-03-31 23:25:36
retData.url 应该是什么?
2021-04-04 23:25:36
环顾互联网,如果您的服务器未正确设置 Content-Type 和 Content-Disposition,则 iFrame 中的 PDF 似乎可能会出现问题。我自己并没有实际使用过这个解决方案,只是澄清了一个以前的解决方案,所以恐怕没有任何其他建议。
2021-04-05 23:25:36

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

不支持 IE 和 Safari 上的下载属性:((caniuse.com/#feat=download)。您可以打开一个新窗口并将内容放在那里,但不确定 PDF 或 Excel...
2021-03-17 23:25:36
可以使用 mimetypes 来不破坏此处使用的 PDF 信息:alexhadik.com/blog/2016/7/7/l8ztp8kr5lbctf5qns4l8t3646npqh
2021-03-21 23:25:36
它会损坏二进制文件,因为它们使用编码转换为字符串,结果与原始二进制文件不太接近......
2021-03-31 23:25:36
@JoshBerke 这很旧,但对我有用。如何让它立即打开我的目录以保存而不是保存到我的浏览器?还有一种方法可以确定传入文件的名称吗?
2021-04-06 23:25:36
您应该在调用后释放浏览器内存clickwindow.URL.revokeObjectURL(link.href);
2021-04-10 23:25:36

我知道这种老方法,但我想我想出了一个更优雅的解决方案。我有同样的问题。我在建议的解决方案中遇到的问题是,它们都要求将文件保存在服务器上,但我不想将文件保存在服务器上,因为它引入了其他问题(安全性:文件可以通过未经身份验证的用户,清理:您如何以及何时删除文件)。和您一样,我的数据是复杂的、嵌套的 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
    }
我没有仔细研究过这个解决方案,但值得注意的是,使用 create_binary_file.php 的解决方案不需要将文件保存到磁盘。让 create_binary_file.php 在内存中生成二进制文件是完全可行的。
2021-03-20 23:25:36

有一种更简单的方法,创建一个表单并发布它,如果返回的 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
/**
 * 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 文件。尽管如此,几乎没有替代方案,但它们并不完美,并且不适用于所有浏览器:

  1. 查看数据 URI 方案如果二进制数据很小,那么您也许可以使用 javascript 打开窗口,在 URI 中传递数据。
  2. Windows/IE 唯一的解决方案是让 .NET 控件或 FileSystemObject 将数据保存在本地文件系统上并从那里打开它。
谢谢,我不知道数据 URI 方案。看起来这可能是在单个请求中执行此操作的唯一方法。但这并不是我真正想去的方向。
2021-03-16 23:25:36
由于客户端要求,我最终使用响应 ( Content-Disposition: attachment;filename="file.txt") 中的服务器标头解决了这个问题,但我真的想下次尝试这种方法。我以前使用过URI数据作为缩略图,但我从来没有想过要用它来下载 - 感谢分享这个金块。
2021-03-29 23:25:36