javascript - Hantera filnedladdning från ajax-post


Jag har en JavaScript-app som skickar AJAX POST-förfrågningar till en viss URL. Svar kan vara en JSON-sträng eller det kan vara en fil (som en bilaga). Jag kan enkelt upptäcka Content-Type och Content-Disposition i mitt AJAX-anrop, men när jag väl upptäcker att svaret innehåller en fil, hur kan jag erbjuda klienten att ladda ner den? Jag har läst ett antal liknande trådar här men ingen av dem ger det svar jag letar efter.

Snälla, snälla, snälla posta inte svar som föreslår att jag inte borde använda ajax för detta eller att jag borde omdirigera webbläsaren, eftersom inget av detta är ett alternativ. Att använda ett vanligt HTML-formulär är heller inte ett alternativ. Det jag behöver är att visa en nedladdningsdialog för klienten. Kan detta göras och hur?

Svar #1

Ge inte upp så snabbt, eftersom detta kan göras (i moderna webbläsare) med delar av FileAPI:

var xhr = new XMLHttpRequest();
xhr.open('POST', url, true);
xhr.responseType = 'blob';
xhr.onload = function () {
    if (this.status === 200) {
        var blob = this.response;
        var filename = "";
        var disposition = xhr.getResponseHeader('Content-Disposition');
        if (disposition && disposition.indexOf('attachment') !== -1) {
            var filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
            var matches = filenameRegex.exec(disposition);
            if (matches != null && matches[1]) filename = matches[1].replace(/['"]/g, '');
        }

        if (typeof window.navigator.msSaveBlob !== 'undefined') {
            // IE workaround for "HTML7007: One or more blob URLs were revoked by closing the blob for which they were created. These URLs will no longer resolve as the data backing the URL has been freed."
            window.navigator.msSaveBlob(blob, filename);
        } else {
            var URL = window.URL || window.webkitURL;
            var downloadUrl = URL.createObjectURL(blob);

            if (filename) {
                // use HTML5 a[download] attribute to specify filename
                var a = document.createElement("a");
                // safari doesn't support this yet
                if (typeof a.download === 'undefined') {
                    window.location.href = downloadUrl;
                } else {
                    a.href = downloadUrl;
                    a.download = filename;
                    document.body.appendChild(a);
                    a.click();
                }
            } else {
                window.location.href = downloadUrl;
            }

            setTimeout(function () { URL.revokeObjectURL(downloadUrl); }, 100); // cleanup
        }
    }
};
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
xhr.send($.param(params, true));

Eller om du använder jQuery.ajax:

$.ajax({
    type: "POST",
    url: url,
    data: params,
    xhrFields: {
        responseType: 'blob' // to avoid binary data being mangled on charset conversion
    },
    success: function(blob, status, xhr) {
        // check for a filename
        var filename = "";
        var disposition = xhr.getResponseHeader('Content-Disposition');
        if (disposition && disposition.indexOf('attachment') !== -1) {
            var filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
            var matches = filenameRegex.exec(disposition);
            if (matches != null && matches[1]) filename = matches[1].replace(/['"]/g, '');
        }

        if (typeof window.navigator.msSaveBlob !== 'undefined') {
            // IE workaround for "HTML7007: One or more blob URLs were revoked by closing the blob for which they were created. These URLs will no longer resolve as the data backing the URL has been freed."
            window.navigator.msSaveBlob(blob, filename);
        } else {
            var URL = window.URL || window.webkitURL;
            var downloadUrl = URL.createObjectURL(blob);

            if (filename) {
                // use HTML5 a[download] attribute to specify filename
                var a = document.createElement("a");
                // safari doesn't support this yet
                if (typeof a.download === 'undefined') {
                    window.location.href = downloadUrl;
                } else {
                    a.href = downloadUrl;
                    a.download = filename;
                    document.body.appendChild(a);
                    a.click();
                }
            } else {
                window.location.href = downloadUrl;
            }

            setTimeout(function () { URL.revokeObjectURL(downloadUrl); }, 100); // cleanup
        }
    }
});

Svar #2

Skapa ett formulär, använd POST-metoden, skicka formuläret - det behövs ingen iframe. När serversidan svarar på begäran, skriv en svarshuvud för filens MIME-typ, och det kommer att visa en nedladdningsdialog - jag har gjort detta flera gånger.

Du vill ha innehållstypen application/download - sök bara efter hur man tillhandahåller en nedladdning för vilket språk du än använder.

Svar #3

Jag stötte på samma problem och löste det framgångsrikt. Mitt användningsfall är detta.

"Skicka JSON-data till servern och ta emot en excel-fil. Den excel-filen skapas av servern och returneras som ett svar till klienten. Ladda ner det svaret som en fil med ett anpassat namn i webbläsaren"

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

Ovanstående utdrag gör bara följande

  • Skicka en array som JSON till servern med XMLHttpRequest.
  • Efter att ha hämtat innehåll som en blobb (binär), skapar vi en nedladdningsbar URL och fäster den på en osynlig "a"-länk och klickar sedan på den.

Här behöver vi noggrant ställa in några saker på serversidan. Jag har ställt in några headers i Python Django HttpResponse. Du behöver ställa in dem på motsvarande sätt om du använder andra programmeringsspråk.

In python django code
response = HttpResponse(file_content, content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")

Eftersom jag laddar ner xls (Excel) här, justerade jag contentType till ovanstående. Du behöver ställa in det enligt din filtyp. Du kan använda denna teknik för att ladda ner alla typer av filer.

Svar #4

Här är hur jag fick detta att fungera https://stackoverflow.com/a/27563953/2845977

$.ajax({
  url: '<URL_TO_FILE>',
  success: function(data) {
    var blob=new Blob([data]);
    var link=document.createElement('a');
    link.href=window.URL.createObjectURL(blob);
    link.download="<FILENAME_TO_SAVE_WITH_EXTENSION>";
    link.click();
  }
});

Uppdaterat svar med hjälp av download.js

$.ajax({
  url: '<URL_TO_FILE>',
  success: download.bind(true, "<FILENAME_TO_SAVE_WITH_EXTENSION>", "<FILE_MIME_TYPE>")
});

Svar #5

Vilket serversidespråk använder du? I min app kan jag enkelt ladda ner en fil från ett AJAX-anrop genom att ställa in rätt headers i PHPs svar:

Ställa in rubriker på serversidan

header("HTTP/1.1 200 OK");
header("Pragma: public");
header("Cache-Control: must-revalidate, post-check=0, pre-check=0");

// The optional second 'replace' parameter indicates whether the header
// should replace a previous similar header, or add a second header of
// the same type. By default it will replace, but if you pass in FALSE
// as the second argument you can force multiple headers of the same type.
header("Cache-Control: private", false);

header("Content-type: " . $mimeType);

// $strFileName is, of course, the filename of the file being downloaded. 
// This won't have to be the same name as the actual file.
header("Content-Disposition: attachment; filename=\"{$strFileName}\""); 

header("Content-Length: " . mb_strlen($strFile));

// $strFile is a binary representation of the file that is being downloaded.
echo $strFile;

Detta kommer faktiskt att 'omdirigera' webbläsaren till denna nedladdningssida, men som @ahren redan sa i sin kommentar, kommer det inte att navigera bort från den aktuella sidan.

Det handlar helt om att ställa in rätt headers, så jag är säker på att du kommer att hitta en lämplig lösning för serversidans språk som du använder om det inte är PHP.

Hantera svaret på klientsidan

Förutsatt att du redan vet hur man gör ett AJAX-anrop, utför du på klientsidan en AJAX-begäran till servern. Servern genererar sedan en länk från vilken denna fil kan laddas ner, t.ex. den 'forward'-URL som du vill hänvisa till. Till exempel svarar servern med:

{
    status: 1, // ok
    // unique one-time download token, not required of course
    message: 'http://yourwebsite.com/getdownload/ska08912dsa'
}

När du bearbetar svaret, infogar du en iframe i din kropp och sätter iframe:s SRC till URL:en du just mottagit på detta sätt (med jQuery för enkelhetens skull i detta exempel):

$("body").append("<iframe src='" + data.message +
  "' style='display: none;' ></iframe>");

Om du har ställt in rätt rubriker som visas ovan kommer iframe att tvinga fram en nedladdningsdialog utan att navigera webbläsaren bort från den aktuella sidan.

Anteckning

Extra tillägg i förhållande till din fråga; jag tycker att det är bäst att alltid returnera JSON när man begär saker med AJAX-teknologi. Efter att du har mottagit JSON-svaret kan du sedan bestämma på klientsidan vad du vill göra med det. Kanske, till exempel, vill du senare att användaren klickar på en nedladdningslänk till URL:en istället för att tvinga nedladdningen direkt, i din nuvarande uppsättning skulle du behöva uppdatera både klient- och serversidan för att göra det.

Svar #6

För dem som letar efter en lösning från ett Angular-perspektiv fungerade detta för mig:

$http.post(
  'url',
  {},
  {responseType: 'arraybuffer'}
).then(function (response) {
  var headers = response.headers();
  var blob = new Blob([response.data],{type:headers['content-type']});
  var link = document.createElement('a');
  link.href = window.URL.createObjectURL(blob);
  link.download = "Filename";
  link.click();
});

Svar #7

För de som letar efter en mer modern metod kan du använda fetch API. Följande kod visar hur man laddar ner en kalkylarksfil.

fetch(url, {
    body: JSON.stringify(data),
    method: 'POST',
    headers: {
        'Content-Type': 'application/json; charset=utf-8'
    },
})
.then(response => response.blob())
.then(response => {
    const blob = new Blob([response], {type: 'application/application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'});
    const downloadUrl = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = downloadUrl;
    a.download = "file.xlsx";
    document.body.appendChild(a);
    a.click();
})

Jag tror att detta tillvägagångssätt är mycket lättare att förstå än andra XMLHttpRequest-lösningar. Dessutom har det en liknande syntax som jQuery-metoden, utan behov av att lägga till några ytterligare bibliotek.

Självklart skulle jag råda dig att kontrollera vilken webbläsare du utvecklar för, eftersom denna nya metod inte fungerar på IE. Du kan hitta hela listan över webbläsarkompatibilitet på följande länk.

Viktigt: I det här exemplet skickar jag en JSON-förfrågan till en server som lyssnar på den angivna url. Denna url måste vara inställd, i mitt exempel antar jag att du känner till denna del. Tänk också på de headers som behövs för att din förfrågan ska fungera. Eftersom jag skickar en JSON måste jag lägga till Content-Type-headern och sätta den till application/json; charset=utf-8, för att låta servern veta vilken typ av förfrågan den kommer att ta emot.

Svar #8

Jag ser att du redan har hittat en lösning, men jag ville bara lägga till lite information som kan hjälpa någon som försöker uppnå samma sak med stora POST-förfrågningar.

Jag hade samma problem för ett par veckor sedan, och det är verkligen inte möjligt att uppnå en "ren" nedladdning genom AJAX. Filament Group skapade ett jQuery-plugin som fungerar precis som du redan har upptäckt, det kallas jQuery File Download, men det finns en nackdel med denna teknik.

Om du skickar stora förfrågningar via AJAX (till exempel filer på över 1 MB) kommer det att påverka responsiviteten negativt. På långsamma internetanslutningar kommer du att behöva vänta mycket tills förfrågan skickas och även vänta på att filen laddas ner. Det är inte som ett ögonblickligt "klick" => "popup" => "nedladdning start". Det är mer som "klick" => "vänta tills data skickas" => "vänta på svar" => "nedladdning start" vilket gör att filen verkar dubbelt så stor eftersom du måste vänta på att förfrågan skickas via AJAX och få tillbaka den som en nedladdningsbar fil.

Om du arbetar med små filstorlekar <1MB kommer du inte att märka detta. Men som jag upptäckte i min egen app, för större filstorlekar är det nästan outhärdligt.

Min app tillåter användare att exportera bilder som genereras dynamiskt, dessa bilder skickas genom POST-förfrågningar i base64-format till servern (det är det enda möjliga sättet), sedan bearbetas de och skickas tillbaka till användarna i form av .png- och .jpg-filer, base64-strängar för bilder större än 1 MB är stora, detta tvingar användare att vänta längre än nödvändigt på att filen ska börja laddas ner. Vid långsamma Internetanslutningar kan det vara riktigt irriterande.

Min lösning på detta var att tillfälligt skriva filen till servern, och när den är klar, dynamiskt generera en länk till filen i form av en knapp som växlar mellan "Vänligen vänta..." och "Ladda ner"-tillstånd och samtidigt skriva ut base64-bilden i ett förhandsgranskningsfönster så att användare kan "högerklicka" och spara den. Detta gör all väntetid mer uthärdlig för användarna, och snabbar också upp processen.

Uppdatering 30 sep 2014:

Månader har gått sedan jag skrev detta, äntligen har jag hittat ett bättre sätt att snabba upp arbetet med stora base64-strängar. Jag lagrar nu base64-strängar i databasen (med hjälp av longtext- eller longblog-fält), sedan skickar jag dess post-ID genom jQuery File Download, och slutligen på nedladdningsskriptfilen frågar jag databasen med detta ID för att hämta base64-strängen och skickar den genom nedladdningsfunktionen.

Ladda ner skriptexempel:

<?php
// Record ID
$downloadID = (int)$_POST['id'];
// Query Data (this example uses CodeIgniter)
$data       = $CI->MyQueries->GetDownload( $downloadID );
// base64 tags are replaced by [removed], so we strip them out
$base64     = base64_decode( preg_replace('#\[removed\]#', '', $data[0]->image) );
// This example is for base64 images
$imgsize    = getimagesize( $base64 );
// Set content headers
header('Content-Disposition: attachment; filename="my-file.png"');
header('Content-type: '.$imgsize['mime']);
// Force download
echo $base64;
?>

Jag vet att detta är långt utöver vad originalposter (OP) frågade om, men jag kände att det vore bra att uppdatera mitt svar med mina fynd. När jag sökte efter lösningar på mitt problem, läste jag många "Ladda ner från AJAX POST-data"-trådar som inte gav mig det svar jag letade efter, jag hoppas att denna information hjälper någon som vill uppnå något liknande.

Relaterade artiklar