JavaScript/jQuery для загрузки файла через POST с данными JSON
У меня есть одностраничное веб-приложение на основе jquery. Он связывается с веб-сервисом RESTful через вызовы AJAX.
Я пытаюсь сделать следующее:
- Отправьте сообщение POST, содержащее данные JSON, в URL-адрес REST.
- Если в запросе указан ответ JSON, возвращается JSON.
- Если в запросе указан ответ PDF/XLS/etc, возвращается загружаемый двоичный файл.
У меня сейчас работают 1 и 2, и клиентское приложение jquery отображает возвращенные данные на веб-странице, создавая элементы DOM на основе данных JSON. У меня также есть #3, работающий с точки зрения веб-сервиса, то есть он создаст и вернет двоичный файл, если ему будут заданы правильные параметры JSON. Но я не уверен, что лучший способ справиться с #3 в клиентском JavaScript-коде.
Можно ли получить загружаемый файл обратно с помощью вызова 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-файл, сохранить его на сервере и вернуть JSON, содержащий URL-адрес файла. Затем выполните другой вызов в обработчике успеха ajax, чтобы сделать что-то вроде следующего:
success: function(json,status) {
window.location.href = json.url;
}
Но это означает, что мне нужно будет сделать более одного вызова на сервер, и мой сервер должен будет создавать загружаемые файлы, хранить их где-то, а затем периодически очищать эту область хранения.
Должен быть более простой способ сделать это. Идеи?
РЕДАКТИРОВАТЬ: после просмотра документов для $.ajax, я вижу, что ответ dataType может быть только один из xml, html, script, json, jsonp, text
поэтому я предполагаю, что нет способа напрямую загрузить файл с помощью ajax-запроса, если только я не внедряю двоичный файл в схему Data URI, как предложено в ответе @VinayC (что я не хочу делать).
Итак, я думаю, что мои варианты:
Не используйте ajax, вместо этого отправьте сообщение формы и вставьте мои данные JSON в значения формы. Вероятно, придется возиться со скрытыми фреймами и тому подобное.
Не используйте ajax, а вместо этого преобразуйте мои данные JSON в строку запроса для построения стандартного запроса GET и задайте для window.location.href этот URL. Может потребоваться использовать event.preventDefault() в моем обработчике кликов, чтобы браузер не менял URL приложения.
Используйте мою другую идею выше, но дополненную предложениями из ответа @naikus. Отправьте запрос AJAX с некоторым параметром, который позволяет веб-сервису узнать, что он вызывается с помощью вызова ajax. Если веб-служба вызывается из вызова ajax, просто верните JSON с URL-адресом сгенерированного ресурса. Если ресурс вызывается напрямую, верните фактический двоичный файл.
Чем больше я об этом думаю, тем больше мне нравится последний вариант. Таким образом, я могу получить информацию о запросе (время генерации, размер файла, сообщения об ошибках и т. Д.), И я могу обработать эту информацию перед началом загрузки. Недостатком является дополнительное управление файлами на сервере.
Есть ли другие способы сделать это? Любые плюсы / минусы этих методов, которые я должен знать?
17 ответов
Решение letronje работает только для очень простых страниц. document.body.innerHTML +=
берет HTML-текст тела, добавляет HTML-код iframe и устанавливает 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>");
});
Что это на самом деле делает: выполните запись в /create_binary_file.php с данными в переменной postData; если этот пост завершается успешно, добавьте новый iframe в тело страницы. Предполагается, что ответ из /create_binary_file.php будет содержать значение 'url', которое является URL-адресом, с которого можно загрузить сгенерированный файл PDF/XLS/etc. Добавление iframe на страницу, которая ссылается на этот URL, приведет к тому, что браузер предложит пользователю загрузить файл, предполагая, что веб-сервер имеет соответствующую конфигурацию MIME-типа.
Я играл с другим вариантом, который использует капли. Мне удалось получить его для загрузки текстовых документов, и я загрузил PDF (однако они повреждены).
Используя API BLOB-объектов, вы сможете сделать следующее:
$.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 и отправил формат. Вторая серверная функция декодирует / десериализует параметры и генерирует файл. Форма может быть отправлена в новом окне или в фрейме на странице, и файл откроется.
Требуется немного больше работы и, возможно, немного больше обработки, но в целом я почувствовал себя намного лучше с этим решением.
Код находится в 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, текст и тому подобное, убедитесь, что 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 для сохранения данных в локальной файловой системе и открытия их оттуда.
$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);
};
Не совсем ответ на оригинальную статью, а быстрое и грязное решение для публикации 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');
Давно где-то нашел, работает отлично!
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();
Я думаю, что лучший подход - это использовать комбинацию. Ваш второй подход выглядит элегантным решением, когда задействованы браузеры.
Так что в зависимости от того, как сделан звонок. (будь то браузер или вызов веб-службы) вы можете использовать комбинацию этих двух способов, отправив URL-адрес в браузер и отправив необработанные данные любому другому клиенту веб-службы.
Мне понравилась идея Фрэнка, и я решил внести в нее свой собственный поворот. Поскольку попытка сделать это в одном сообщении очень сложна, я использую метод двух сообщений, но только один раз обращаюсь к базе данных, и мне не нужно сохранять файл или очищать файл после завершения.
Сначала я запускаю запрос ajax для извлечения данных, но вместо возврата данных из контроллера я возвращаю GUID, который привязан к хранилищу записей TempData.
$.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();
Я не спал уже два дня, пытаясь понять, как загрузить файл с помощью 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.
}
Удачи.
Проблемы с созданием собственных мероприятий
Во многих решениях, предложенных в этой статье, JavaScript выполняется асинхронно и создает элемент ссылки, а затем вызывает
const a = documet.createElement("a")
a.click()
или создание события мыши
new MouseEvent({/* ...some config */})
Казалось бы, это нормально, правда? Что может быть в этом плохого?
Что такое Event-Sourcing?
событий имеют множество значений в вычислениях, таких как система 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, извините, не пожалейте)
Форму можно использовать для перехода к 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источнике
Если вы не хотите, чтобы хулиганы С ++ над вами смеялись, вы должны освободить эту память. Ах, но я просто хобби, которому нравится его сборщик мусора. Не бойтесь, это очень просто, если вы работаете в большинстве фреймворков (на мой взгляд), вы просто регистрируете какой-то эффект очистки для вашего компонента и вашего права, как дождь.
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 () {
}
});
Решение
Мне кажется, что приложение Content-Disposition работает:
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')
Другой подход, вместо сохранения файла на сервере и его извлечения, заключается в использовании.NET 4.0+ ObjectCache с коротким сроком действия до второго действия (когда он может быть окончательно выгружен). Причина, по которой я хочу использовать JQuery Ajax для вызова, заключается в том, что он асинхронный. Создание моего динамического PDF-файла занимает довольно много времени, и в течение этого времени я отображаю диалоговое окно с вращающимся вертелом (оно также позволяет выполнять другую работу). Подход с использованием данных, возвращаемых в "success:", для создания BLOB-объектов не работает надежно. Это зависит от содержимого файла PDF. Он легко искажается данными в ответе, если он не полностью текстовый, и это все, с чем может работать Ajax.