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

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

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

  1. 向REST url提交包含JSON数据的POST。
  2. 如果请求指定了JSON响应,则返回JSON。
  3. 如果请求指定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, script, json, jsonp, text之一,所以我猜没有办法直接下载一个文件使用ajax请求,除非我嵌入二进制文件使用数据URI方案建议在@VinayC答案(这不是我想做的事情)。

所以我想我的选择是:

  1. 不使用ajax,而是提交一个表单post和嵌入我的JSON数据到表单值。可能需要与隐藏的iframes等混乱。

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

  3. 使用我上面的另一个想法,但从@naikus的回答中得到了建议。提交带有一些参数的AJAX请求,让web-service知道这个请求是通过AJAX调用调用的。如果web服务是从ajax调用调用的,那么只需向生成的资源返回带有URL的JSON。如果直接调用资源,则返回实际的二进制文件。

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

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

408997 次浏览

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

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

简而言之,没有更简单的方法了。您需要发出另一个服务器请求来显示PDF文件。尽管有一些替代方案,但它们并不完美,也不能在所有浏览器上运行:

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

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中的数据执行一个post到/create_binary_file.php;如果该帖子成功完成,则在页面正文中添加一个新的iframe。假设来自/create_binary_file.php的响应将包含一个值'url',这是生成的PDF/XLS/etc文件可以下载的url。向引用该URL的页面添加iframe将导致浏览器促使用户下载该文件,假设web服务器具有适当的mime类型配置。

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

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

我一直在尝试使用blobs的另一种选择。我已经设法让它下载文本文档,我下载了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中下载文件。它使用锚标记上的下载属性强制浏览器下载它。

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

客户端

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

另一种方法是使用。net 4.0+ ObjectCache来代替在服务器上保存文件并检索它,并在第二次Action之前有一个短的过期时间(在这个时间它可以被确定地转储)。我想使用JQuery Ajax进行调用的原因是它是异步的。构建动态PDF文件需要相当多的时间,在此期间我显示了一个繁忙的旋转对话框(它还允许完成其他工作)。使用“success:”中返回的数据创建Blob的方法不可靠。这取决于PDF文件的内容。它很容易被响应中的数据损坏,如果它不是完全文本的,这是Ajax所能处理的。

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

客户端

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

祝你好运。

这并不完全是对原始帖子的回答,而是一种快速而简单的解决方案,用于将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字符串并设置下载头(PHP示例):

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

很久以前在某个地方找到它,它工作得很完美!

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

解决方案

附加的附件似乎对我有用:

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

解决方案

< em >应用程序/八进制< / em >

我有类似的事情发生在我的JSON,对我的服务器端我设置头 自我。set_header(“内容类型”、“application / json”) 然而,当我把它改为:

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

它会自动下载。

还要知道,为了让文件仍然保持.json后缀,你需要在文件名头文件:

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

制作自己的事件的问题

本文提出的许多解决方案都让JavaScript异步运行,并创建一个link元素,然后调用

const a = documet.createElement("a")
a.click()

或者创建一个鼠标事件

new MouseEvent({/* ...some config */})

这看起来很好,对吧?这有什么错呢?

什么是事件源?

事件源在计算中有很多含义,比如基于云架构的发布订阅系统,或者浏览器api EventSource。在浏览器的上下文中 所有事件都有一个源,该源有一个隐藏属性,说明是谁发起了这个事件(用户或站点)

了解了这一点,我们就可以开始理解为什么两次点击事件不能被同等对待

user click*          new MouseEvent()
-----------            -----------
| Event 1 |            | Event 2 |
-----------            -----------
|                      |
|----------------------|
|
|
----------------------
| Permissions Policy |    Available in chrome allows the server to control
----------------------    what features are going to be used by the JS
|
|
----------------------------
| Browser Fraud Protection | The Browser REALLY doesnt like being told to pretend
---------------------------- to be a user. If you will remember back to the early
|              2000s when one click spun off 2000 pop ups. Well here
|              is where popups are blocked, fraudulent ad clicks are
\ /             thrown out, and most importantly for our case stops
v              fishy downloads
JavaScript Event Fires


所以我不能下载一个帖子,这是愚蠢的

不,你当然可以。您只需要给用户一个创建事件的机会。这里有一些模式,您可以使用它们来创建明显且常规的用户流,并且不会被标记为欺诈。(使用JSX抱歉,不抱歉)

一种形式可以用post操作导航到一个url。

const example = () => (
<form
method="POST"
action="/super-api/stuff"
onSubmit={(e) => {/* mutably change e form data but don't e.preventDetfault() */}}
>
{/* relevant input fields of your download */}
</form>
)

预压法如果你的下载是不可配置的,你可能想要考虑将下载预加载到resp.blob()new Blob(resp)中,这告诉浏览器这是一个文件,我们不会对它做任何字符串操作。与其他答案一样,你可以使用window.URL.createObjectURL,这里没有提到的是

createObjectURL可以在JAVASCRIPT中产生内存泄漏

如果你不想让c++恶霸来取笑你,你必须释放这个内存。但我只是个爱好,喜欢收垃圾的人。不用担心,这很简单,如果你在大多数框架中工作(对我来说),你只需要在你的组件和你的权利上注册某种清理效果。

const preload = () => {
const [payload, setPayload] = useState("")
  

useEffect(() => {
fetch("/super-api/stuff")
.then((f) => f.blob())
.then(window.URL.createObjectURL)
.then(setPayload)


return () => window.URL.revokeObjectURL(payload)
}, [])




return (<a href={payload} download disabled={payload === ""}>Download Me</a>)
}

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

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


}
});

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

首先,我运行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");
}