JavaScript/jQuery för att ladda ner fil via POST med JSON-data
Jag har en jQuery-baserad enkelsidig webbapp. Den kommunicerar med en RESTful webbtjänst via AJAX-anrop.
Jag försöker uppnå följande:
- Skicka en POST som innehåller JSON-data till en REST-URL.
- Om begäran specificerar ett JSON-svar, returneras då JSON.
- Om begäran specificerar ett PDF/XLS/etc.-svar, returneras då en nedladdningsbar binär fil.
Jag har 1 & 2 fungerande nu, och klientens jQuery-app visar de returnerade uppgifterna på webbsidan genom att skapa DOM-element baserat på JSON-data. Jag har också #3 fungerande från webbtjänstens synvinkel, vilket betyder att den kommer att skapa och returnera en binär fil om den får de korrekta JSON-parametrarna. Men jag är osäker på det bästa sättet att hantera #3 i klientens JavaScript-kod.
Är det möjligt att få tillbaka en nedladdningsbar fil från ett ajax-anrop som detta? Hur får jag webbläsaren att ladda ner och spara filen?
$.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;
}
});
Servern svarar med följande rubriker:
Content-Disposition:attachment; filename=export-1282022272283.pdf
Content-Length:5120
Content-Type:application/pdf
Server:Jetty(6.1.11)
En annan idé är att generera PDF:en och lagra den på servern och returnera JSON som inkluderar en URL till filen. Sedan kan man göra ett annat anrop i ajax-svarshanteraren för att göra något liknande följande:
success: function(json,status) {
window.location.href = json.url;
}
Men att göra det innebär att jag skulle behöva göra mer än ett samtal till servern, och min server skulle behöva bygga nedladdningsbara filer, lagra dem någonstans och sedan periodiskt rensa det lagringsområdet.
Det måste finnas ett enklare sätt att åstadkomma detta. Några idéer?
REDIGERA: Efter att ha granskat dokumentationen för $.ajax, ser jag att svarsdata-typen endast kan vara en av xml, html, script, json, jsonp, text, så jag gissar att det inte finns något sätt att direkt ladda ner en fil med en ajax-begäran, om jag inte bäddar in den binära filen med hjälp av Data URI-schemat som föreslagits i @VinayC:s svar (vilket inte är något jag vill göra).
Så jag antar att mina alternativ är:
Använd inte ajax och skicka istället ett formulär via post och bädda in mina JSON-data i formvärdena. Skulle antagligen behöva pilla med dolda iframes och liknande.
Inte använda ajax och istället konvertera mina JSON-data till en frågesträng för att bygga en standard GET-förfrågan och sätta window.location.href till denna URL. Kan behöva använda event.preventDefault() i min klickhanterare för att förhindra att webbläsaren byter från applikationens URL.
Använd min andra idé ovan, men förbättrad med förslag från @naikus svar. Skicka AJAX-förfrågan med någon parameter som låter webbservicen veta att detta anropas via ett ajax-anrop. Om webbservicen anropas från ett ajax-anrop, returnera helt enkelt JSON med en URL till den genererade resursen. Om resursen anropas direkt, returnera då den faktiska binära filen.
Ju mer jag tänker på det, desto mer gillar jag det sista alternativet. På det här sättet kan jag få information tillbaka om förfrågan (tid för att generera, filstorlek, felmeddelanden, etc.) och jag kan agera utifrån den informationen innan jag startar nedladdningen. Nackdelen är extra filhantering på servern.
Finns det några andra sätt att uppnå detta? Finns det några för- och nackdelar med dessa metoder som jag bör känna till?
Svar #1
letronjes lösning fungerar bara för väldigt enkla sidor. document.body.innerHTML += tar HTML-texten från body, lägger till iframe-HTML:n och sätter innerHTML för sidan till den strängen. Detta kommer att radera alla händelsebindningar din sida har, bland annat. Skapa ett element och använd appendChild istället.
$.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);
});
Eller använda jQuery
$.post('/create_binary_file.php', postData, function(retData) {
$("body").append("<iframe src='" + retData.url+ "' style='display: none;' ></iframe>");
});
Vad detta faktiskt gör: utför en post till /create_binary_file.php med datan i variabeln postData; om den posten slutförs framgångsrikt, lägg till en ny iframe i sidans kropp. Antagandet är att svaret från /create_binary_file.php kommer att inkludera ett värde 'url', vilket är URL:en som den genererade PDF-/XLS-/etc-filen kan laddas ner från. Att lägga till en iframe på sidan som refererar till den URL:en kommer att resultera i att webbläsaren uppmanar användaren att ladda ner filen, förutsatt att webbservern har lämplig mime-typkonfiguration.
Svar #2
Jag har lekt med ett annat alternativ som använder blobs. Jag har lyckats få det att ladda ner textdokument, och jag har laddat ner PDF-filer (Men de är korrupta).
Med blob API:et kommer du att kunna göra följande:
$.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();
});
Detta är IE 10+, Chrome 8+, FF 4+. Se https://developer.mozilla.org/en-US/docs/Web/API/URL.createObjectURL
Det kommer endast att ladda ner filen i Chrome, Firefox och Opera. Detta använder ett nedladdningsattribut på ankartaggen för att tvinga webbläsaren att ladda ner den.
Svar #3
Jag vet att det här är ganska gammalt, men jag tror att jag har kommit på en mer elegant lösning. Jag hade exakt samma problem. Problemet jag hade med de föreslagna lösningarna var att de alla krävde att filen sparades på servern, men jag ville inte spara filerna på servern eftersom det introducerade andra problem (säkerhet: filen skulle då kunna nås av icke-auktoriserade användare, städning: hur och när blir man av med filerna). Och precis som du var mina data komplexa, nästlade JSON-objekt som skulle vara svåra att lägga in i ett formulär.
Det jag gjorde var att skapa två serverfunktioner. Den första validerade datan. Om det fanns ett fel skulle det returneras. Om det inte var ett fel returnerade jag alla parametrar serialiserade/kodade som en base64-sträng. Sedan, på klienten, har jag ett formulär som bara har ett dolt fält och skickar till en andra serverfunktion. Jag sätter det dolda fältet till base64-strängen och skickar formuläret. Den andra serverfunktionen avkodar/deserialiserar parametrarna och genererar filen. Formuläret kan skickas till ett nytt fönster eller ett iframe på sidan och filen kommer att öppnas.
Det innebär lite mer arbete och kanske lite mer bearbetning, men överlag kände jag mig mycket bättre med denna lösning.
Koden är i 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);
}
på klienten
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
}
Svar #4
Det finns ett enklare sätt, skapa ett formulär och skicka det, detta medför risken att sidan återställs om retur-mime-typen är något som en webbläsare skulle öppna, men för csv och liknande är det perfekt
Exempel kräver underscore och 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();
För saker som html, text och liknande, se till att mimetypen är något som application/octet-stream
php-kod
<?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;
