javascript - Last ned en fil asynkront ved hjelp av Ajax
Jeg har en Struts2-handling på serversiden for filnedlasting.
<action name="download" class="com.xxx.DownAction">
<result name="success" type="stream">
<param name="contentType">text/plain</param>
<param name="inputName">imageStream</param>
<param name="contentDisposition">attachment;filename={fileName}</param>
<param name="bufferSize">1024</param>
</result>
</action>
Men når jeg kaller handlingen ved å bruke jQuery:
$.post(
"/download.action",{
para1:value1,
para2:value2
....
},function(data){
console.info(data);
}
);
i Firebug ser jeg at dataene hentes med binær strøm. Jeg lurer på hvordan man åpner nedlastingsvinduet for fil som brukeren kan bruke til å lagre filen lokalt?
Svar #1
2019 oppdatering for moderne nettlesere
Dette er tilnærmingen jeg nå ville anbefalt med noen få forbehold:
- En relativt moderne nettleser er påkrevd
- Hvis filen forventes å være svært stor, bør du sannsynligvis gjøre noe lignende den opprinnelige tilnærmingen (iframe og informasjonskapsel), fordi noen av operasjonene nedenfor sannsynligvis kan bruke systemminne som minst er like stort som filen som lastes ned og/eller andre interessante CPU-bivirkninger.
fetch('https://jsonplaceholder.typicode.com/todos/1')
.then(resp => resp.blob())
.then(blob => {
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.style.display = 'none';
a.href = url;
// the filename you want
a.download = 'todo-1.json';
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
alert('your file has downloaded!'); // or you know, something with better UX...
})
.catch(() => alert('oh no!'));2012 Original jQuery/iframe/Cookie-basert tilnærming
Bluish har helt rett i dette, du kan ikke gjøre det gjennom Ajax fordi JavaScript ikke kan lagre filer direkte på brukerens datamaskin (av sikkerhetsårsaker). Dessverre betyr det å peke hovedvinduets URL til filnedlastingen at du har liten kontroll over hva brukeropplevelsen blir når en filnedlasting skjer.
Jeg laget jQuery File Download som gjør det mulig med en "Ajax-lignende" opplevelse med filnedlastinger, komplett med OnSuccess og OnFailure tilbakekallinger for å gi en bedre brukeropplevelse. Ta en titt på blogginnlegget mitt om det vanlige problemet som pluginet løser og noen måter å bruke det på, samt en demo av jQuery File Download i bruk. Her er kilden
Her er en enkel brukstilfelledemo som bruker plugin-kilden med løfter.
$.fileDownload('some/file.pdf')
.done(function () { alert('File download a success!'); })
.fail(function () { alert('File download failed!'); });
Avhengig av hvilke nettlesere du trenger å støtte, kan du kanskje bruke https://github.com/eligrey/FileSaver.js/ som gir mer eksplisitt kontroll enn IFRAME-metoden jQuery File Download bruker.
Svar #2
Ingen la ut dette @Pekka's løsning... så jeg legger det ut. Det kan hjelpe noen.
Du trenger ikke å gjøre dette gjennom Ajax. Bare bruk
window.location="download.action?para1=value1...."
Svar #3
Du kan med HTML5
NB: Fildataene som returneres MÅ være base64-kodet fordi du ikke kan JSON-kode binærdata
I mitt AJAX-svar har jeg en datastruktur som ser slik ut:
{
result: 'OK',
download: {
mimetype: string(mimetype in the form 'major/minor'),
filename: string(the name of the file to download),
data: base64(the binary data as base64 to download)
}
}
Det betyr at jeg kan gjøre følgende for å lagre en fil via AJAX
var a = document.createElement('a');
if (window.URL && window.Blob && ('download' in a) && window.atob) {
// Do it the HTML5 compliant way
var blob = base64ToBlob(result.download.data, result.download.mimetype);
var url = window.URL.createObjectURL(blob);
a.href = url;
a.download = result.download.filename;
a.click();
window.URL.revokeObjectURL(url);
}
Funksjonen base64ToBlob ble hentet herfra og må brukes i samsvar med denne funksjonen
function base64ToBlob(base64, mimetype, slicesize) {
if (!window.atob || !window.Uint8Array) {
// The current browser doesn't have the atob function. Cannot continue
return null;
}
mimetype = mimetype || '';
slicesize = slicesize || 512;
var bytechars = atob(base64);
var bytearrays = [];
for (var offset = 0; offset < bytechars.length; offset += slicesize) {
var slice = bytechars.slice(offset, offset + slicesize);
var bytenums = new Array(slice.length);
for (var i = 0; i < slice.length; i++) {
bytenums[i] = slice.charCodeAt(i);
}
var bytearray = new Uint8Array(bytenums);
bytearrays[bytearrays.length] = bytearray;
}
return new Blob(bytearrays, {type: mimetype});
};
Dette er bra hvis serveren din dumper fildata for å lagres. Jeg har imidlertid ikke helt funnet ut hvordan man skulle implementere en HTML4-nødutgang.
Svar #4
Den enkle måten å få nettleseren til å laste ned en fil på er å lage forespørselen slik:
function downloadFile(urlToSend) {
var req = new XMLHttpRequest();
req.open("GET", urlToSend, true);
req.responseType = "blob";
req.onload = function (event) {
var blob = req.response;
var fileName = req.getResponseHeader("fileName") //if you have the fileName header available
var link=document.createElement('a');
link.href=window.URL.createObjectURL(blob);
link.download=fileName;
link.click();
};
req.send();
}
Dette åpner nettleserens nedlastingsvindu.
Svar #5
1. Rammeverksuavhengig: Servlet laster ned fil som vedlegg
<!-- with JS -->
<a href="javascript:window.location='downloadServlet?param1=value1'">
download
</a>
<!-- without JS -->
<a href="downloadServlet?param1=value1" >download</a>
2. Struts2-rammeverk: Handling som laster ned fil som vedlegg
<!-- with JS -->
<a href="javascript:window.location='downloadAction.action?param1=value1'">
download
</a>
<!-- without JS -->
<a href="downloadAction.action?param1=value1" >download</a>
Det ville være bedre å bruke <s:a>-taggen som peker med OGNL til en URL laget med <s:url>-taggen:
<!-- without JS, with Struts tags: THE RIGHT WAY -->
<s:url action="downloadAction.action" var="url">
<s:param name="param1">value1</s:param>
</s:ulr>
<s:a href="%{url}" >download</s:a>
I de ovennevnte tilfellene må du skrive Content-Disposition-headeren til responsen, og spesifisere at filen må lastes ned (attachment) og ikke åpnes av nettleseren (inline). Du må også spesifisere Content Type, og du kan ønske å legge til filnavn og lengde (for å hjelpe nettleseren med å tegne en realistisk fremdriftslinje).
For eksempel, når du laster ned en ZIP:
response.setContentType("application/zip");
response.addHeader("Content-Disposition",
"attachment; filename=\"name of my file.zip\"");
response.setHeader("Content-Length", myFile.length()); // or myByte[].length...
Med Struts2 (med mindre du bruker Action som en Servlet, et hack for direkte streaming, for eksempel), trenger du ikke å skrive noe direkte til responsen; det å bare bruke Stream-resultattypen og konfigurere den i struts.xml vil fungere: EKSEMPEL
<result name="success" type="stream">
<param name="contentType">application/zip</param>
<param name="contentDisposition">attachment;filename="${fileName}"</param>
<param name="contentLength">${fileLength}</param>
</result>
3. Rammeverksnøytral (/ Struts2-rammeverk): Servlet(/Action) åpner fil inne i nettleseren
Hvis du vil åpne filen i nettleseren i stedet for å laste den ned, må Content-disposition settes til inline, men målet kan ikke være den gjeldende vindusplasseringen; du må rette det mot et nytt vindu opprettet av JavaScript, en <iframe> på siden, eller et nytt vindu opprettet fortløpende med det «diskuterte» target="_blank":
<!-- From a parent page into an IFrame without javascript -->
<a href="downloadServlet?param1=value1" target="iFrameName">
download
</a>
<!-- In a new window without javascript -->
<a href="downloadServlet?param1=value1" target="_blank">
download
</a>
<!-- In a new window with javascript -->
<a href="javascript:window.open('downloadServlet?param1=value1');" >
download
</a>
Svar #6
Jeg har laget en liten funksjon som en midlertidig løsning (inspirert av @JohnCulviners plugin):
// creates iframe and form in it with hidden field,
// then submit form with provided data
// url - form url
// data - data to form field
// input_name - form hidden input name
function ajax_download(url, data, input_name) {
var $iframe,
iframe_doc,
iframe_html;
if (($iframe = $('#download_iframe')).length === 0) {
$iframe = $("<iframe id='download_iframe'" +
" style='display: none' src='about:blank'></iframe>"
).appendTo("body");
}
iframe_doc = $iframe[0].contentWindow || $iframe[0].contentDocument;
if (iframe_doc.document) {
iframe_doc = iframe_doc.document;
}
iframe_html = "<html><head></head><body><form method='POST' action='" +
url +"'>" +
"<input type=hidden name='" + input_name + "' value='" +
JSON.stringify(data) +"'/></form>" +
"</body></html>";
iframe_doc.open();
iframe_doc.write(iframe_html);
$(iframe_doc).find('form').submit();
}
Demo med klikkehending:
$('#someid').on('click', function() {
ajax_download('/download.action', {'para1': 1, 'para2': 2}, 'dataname');
});
Svar #7
Jeg stod overfor det samme problemet og løste det på en vellykket måte. Mitt brukstilfelle er dette.
"Send JSON-data til serveren og motta en Excel-fil. Den Excel-filen opprettes av serveren og returneres som et svar til klienten. Last ned dette svaret som en fil med egendefinert navn i nettleseren"
$("#my-button").on("click", function(){
// Data to post
data = {
ids: [1, 2, 3, 4, 5]
};
// Use XMLHttpRequest instead of Jquery $ajax
xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
var a;
if (xhttp.readyState === 4 && xhttp.status === 200) {
// Trick for making downloadable link
a = document.createElement('a');
a.href = window.URL.createObjectURL(xhttp.response);
// Give filename you wish to download
a.download = "test-file.xls";
a.style.display = 'none';
document.body.appendChild(a);
a.click();
}
};
// Post data to URL which handles post request
xhttp.open("POST", excelDownloadUrl);
xhttp.setRequestHeader("Content-Type", "application/json");
// You should set responseType as blob for binary responses
xhttp.responseType = 'blob';
xhttp.send(JSON.stringify(data));
});
Ovenstående utdrag gjør bare følgende
- Legge ut et array som JSON til serveren ved hjelp av XMLHttpRequest.
- Etter å ha hentet innhold som en blob (binær), oppretter vi en nedlastbar URL og legger den til en usynlig "a"-lenke for så å klikke på den. Jeg gjorde en POST-forespørsel her. I stedet kan du også bruke en enkel GET. Vi kan ikke laste ned filen gjennom Ajax, må bruke XMLHttpRequest.
Her trenger vi å nøye sette noen ting på serversiden. Jeg satte noen overskrifter i Python Django HttpResponse. Du må sette dem tilsvarende hvis du bruker andre programmeringsspråk.
In python django code
response = HttpResponse(file_content, content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
Siden jeg laster ned xls (Excel) her, justerte jeg contentType til den ovenfor. Du må sette den i henhold til filtypen din. Du kan bruke denne teknikken for å laste ned alle typer filer.
Svar #8
Ok, basert på ndpu sin kode, her er en forbedret (tror jeg) versjon av ajax_download;-)
function ajax_download(url, data) {
var $iframe,
iframe_doc,
iframe_html;
if (($iframe = $('#download_iframe')).length === 0) {
$iframe = $("<iframe id='download_iframe'" +
" style='display: none' src='about:blank'></iframe>"
).appendTo("body");
}
iframe_doc = $iframe[0].contentWindow || $iframe[0].contentDocument;
if (iframe_doc.document) {
iframe_doc = iframe_doc.document;
}
iframe_html = "<html><head></head><body><form method='POST' action='" +
url +"'>"
Object.keys(data).forEach(function(key){
iframe_html += "<input type='hidden' name='"+key+"' value='"+data[key]+"'>";
});
iframe_html +="</form></body></html>";
iframe_doc.open();
iframe_doc.write(iframe_html);
$(iframe_doc).find('form').submit();
}
Bruk dette slik;-
$('#someid').on('click', function() {
ajax_download('/download.action', {'para1': 1, 'para2': 2});
});
Parametrene sendes som riktige post-parametere, som om de kom fra et input, snarere enn som en JSON-kodet streng som i det forrige eksemplet.
ADVARSEL: Vær forsiktig med potensialet for variabelinjeksjon på disse skjemaene. Det kan finnes en sikrere måte å kode disse variablene på. Alternativt kan du vurdere å escape dem.
Svar #9
Min tilnærming er helt basert på jQuery. Problemet for meg var at det måtte være et POST-HTTP-kall. Og jeg ønsket at det skulle gjøres kun med jQuery.
Løsningen:
$.ajax({
type: "POST",
url: "/some/webpage",
headers: {'X-CSRF-TOKEN': csrfToken},
data: additionalDataToSend,
dataType: "text",
success: function(result) {
let blob = new Blob([result], { type: "application/octetstream" });
let a = document.createElement('a');
a.href = window.URL.createObjectURL(blob);
a.download = "test.xml";;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(a.href);
...
},
error: errorDialog
});
Forklaring:
Det jeg og mange andre gjør er å lage en lenke på nettsiden, som indikerer at målet skal lastes ned og sette resultatet av http-forespørselen som målet. Etter det legger jeg til lenken i dokumentet, for så å ganske enkelt klikke på lenken og deretter fjerne lenken. Du trenger ikke et iframe lenger.
Magien ligger i linjene
let blob = new Blob([result], { type: "application/octetstream" });
a.href = window.URL.createObjectURL(blob);
Det interessante er at denne løsningen kun fungerer med en «blob». Som du kan se i andre svar, bruker noen ganske enkelt en blob uten å forklare hvorfor og hvordan man lager den. Som du kan lese f.eks. i dokumentasjonen for Mozilla-utviklere, trenger du en fil, medieressurs eller blob for at funksjonen «createObjectURL()» skal fungere. Problemet er at ditt HTTP-svar kanskje ikke er noen av disse. Derfor er det første du må gjøre å konvertere svaret ditt til en blob. Dette er hva den første linjen gjør. Deretter kan du bruke «createObjectURL» med den nylig opprettede bloben. Hvis du deretter klikker på lenken, vil nettleseren åpne en lagre-fil-dialog, og du kan lagre dataene dine. Selvfølgelig er det mulig at du ikke kan definere et fast filnavn for filen du vil laste ned. Da må du gjøre svaret ditt mer komplekst som i svaret fra Luke.
Og ikke glem å frigjøre minnet, spesielt når du arbeider med store filer. For flere eksempler og informasjon kan du se på detaljene om JS blob-objektet
