我有一个基于 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 的文档后,我看到响应数据类型只能是 xml, html, script, json, jsonp, text
之一,所以我猜没有办法使用 ajax 请求直接下载文件,除非我嵌入二进制文件按照@VinayC 答案中的建议(这不是我想做的事情),使用 Data URI 方案提交文件。
所以我想我的选择是:
不使用 ajax 而是提交表单帖子并将我的 JSON 数据嵌入到表单值中。可能需要处理隐藏的 iframe 等。不使用 ajax 而是将我的 JSON 数据转换为查询字符串以构建标准 GET 请求并将 window.location.href 设置为此 URL。可能需要在我的点击处理程序中使用 event.preventDefault() 以防止浏览器从应用程序 URL 更改。使用我上面的其他想法,但通过@naikus 答案的建议得到了增强。提交带有一些参数的 AJAX 请求,让 web 服务知道这是通过 ajax 调用调用的。如果 Web 服务是从 ajax 调用中调用的,则只需返回带有指向生成资源的 URL 的 JSON。如果直接调用资源,则返回实际的二进制文件。
我想得越多,我就越喜欢最后一个选项。通过这种方式,我可以获取有关请求的信息(生成时间、文件大小、错误消息等),并且可以在开始下载之前对这些信息采取行动。缺点是服务器上的额外文件管理。
还有其他方法可以做到这一点吗?我应该注意这些方法的任何优点/缺点?
url = 'http://localhost/file.php?file='+ $('input').val(); window.open(url);
获得相同结果的简单方法。将标题放在php文件中。无需发送任何 ajax 或获取请求
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 的页面将导致浏览器促使用户下载文件。
我一直在玩另一个使用 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 中的文件。这使用锚标记上的下载属性来强制浏览器下载它。
click
后释放浏览器内存:window.URL.revokeObjectURL(link.href);
我知道这种旧的,但我想我想出了一个更优雅的解决方案。我有同样的问题。我在建议的解决方案中遇到的问题是它们都需要将文件保存在服务器上,但我不想将文件保存在服务器上,因为它引入了其他问题(安全性:然后可以通过未经身份验证的用户,清理:您如何以及何时摆脱文件)。和你一样,我的数据是复杂的、嵌套的 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;
自从提出这个问题以来已经有一段时间了,但我遇到了同样的挑战并想分享我的解决方案。它使用其他答案中的元素,但我无法完整找到它。它不使用表单或 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");
}
简而言之,没有更简单的方法。您需要发出另一个服务器请求来显示 PDF 文件。虽然,几乎没有替代品,但它们并不完美,并且不适用于所有浏览器:
查看数据 URI 方案。如果二进制数据很小,那么您也许可以使用 javascript 打开在 URI 中传递数据的窗口。 Windows/IE 唯一的解决方案是使用 .NET 控件或 FileSystemObject 将数据保存在本地文件系统上并从那里打开它。
Content-Disposition: attachment;filename="file.txt"
) 解决了这个问题,但我真的很想下次尝试这种方法。我以前使用 URI
数据作为缩略图,但我从来没有想过将它用于下载 - 感谢分享这个金块。
不完全是对原始帖子的回答,而是一个快速而肮脏的解决方案,用于将 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 并设置标题以供下载(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);
};
我认为最好的方法是使用组合,您的第二种方法似乎是涉及浏览器的优雅解决方案。
因此,取决于调用方式。 (无论是浏览器还是 Web 服务调用)您可以结合使用两者,将 URL 发送到浏览器并将原始数据发送到任何其他 Web 服务客户端。
很久以前在某个地方找到了它,而且效果很好!
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();
我已经醒了两天了,现在试图弄清楚如何使用 jquery 和 ajax 调用来下载文件。在我尝试这个之前,我得到的所有支持都无济于事。
客户端
函数 exportStaffCSV(t) { var postData = { checkOne: t }; $.ajax({ type: "POST", url: "/Admin/Staff/exportStaffAsCSV", data: postData, success: function (data) { SuccessMessage("文件下载将在几秒后开始.."); var url = '/Admin/Staff/DownloadCSV?data=' + data; window.location = url; },传统:true,错误:function (xhr, status, p3, p4) { var err = "Error " + " " +状态 + " " + 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.
}
祝你好运。
另一种方法不是将文件保存在服务器上并检索它,而是使用 .NET 4.0+ ObjectCache,其有效期很短,直到第二个 Action(此时可以明确地转储它)。我想使用 JQuery Ajax 进行调用的原因是它是异步的。构建我的动态 PDF 文件需要相当多的时间,在此期间我会显示一个忙碌的微调器对话框(它还允许完成其他工作)。使用“成功:”中返回的数据来创建 Blob 的方法不能可靠地工作。这取决于 PDF 文件的内容。如果响应中的数据不完全是 Ajax 可以处理的全部文本,那么它很容易被响应中的数据破坏。
解决方案
内容处置附件似乎对我有用:
self.set_header("Content-Type", "application/json")
self.set_header("Content-Disposition", 'attachment; filename=learned_data.json')
解决方法
应用程序/八位字节流
我在使用 JSON 时发生了类似的事情,对于服务器端的我来说,我将标头设置为 self.set_header("Content-Type", "application/json") 但是当我将其更改为:
self.set_header("Content-Type", "application/octet-stream")
它自动下载了它。
还要知道,为了使文件仍然保留 .json 后缀,您需要在文件名标头上添加它:
self.set_header("Content-Disposition", 'filename=learned_data.json')
制作自己的活动的问题
本文提出的许多解决方案让 JavaScript 异步运行并创建一个链接元素,然后调用
const a = documet.createElement("a")
a.click()
或创建鼠标事件
new MouseEvent({/* ...some config */})
这看起来不错吧?这有什么问题?
什么是事件溯源?
事件溯源在计算中具有多种含义,例如基于云的架构中的 pub sub 系统或浏览器 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 会在 JAVASCRIPTsource 中造成内存泄漏
如果你不想让 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;
}
然后,当我调用 window.location 时,我将 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");
}
使用 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();
+=
更好,我会相应地更新我的应用程序。谢谢!Resource interpreted as Document but transferred with MIME type application/pdf
。然后它还警告我该文件可能很危险。如果我直接访问 retData.url,则不会出现警告或问题。