Скачивание файла вручную из сообщения ajax

У меня есть приложение javascript, которое отправляет запросы POST ajax на определенный URL. Ответ может быть строкой JSON или файлом (в виде вложения). Я легко могу определить Content-Type и Content-Disposition в своем вызове ajax, но как только я обнаружу, что ответ содержит файл, как я могу предложить клиенту загрузить его? Я прочитал несколько похожих тем здесь, но ни одна из них не дает ответ, который я ищу.

Пожалуйста, пожалуйста, пожалуйста, не публикуйте ответы, предлагающие мне не использовать ajax для этого или что я должен перенаправить браузер, потому что ничего из этого не вариант. Использование простой формы HTML также не вариант. Что мне нужно, это показать клиенту диалог загрузки. Можно ли это сделать и как?

Ответ 1

Создайте форму, используйте метод POST, отправьте форму - нет необходимости в iframe. Когда страница сервера отвечает на запрос, напишите заголовок ответа для типа mime файла, и он представит диалог загрузки - я сделал это несколько раз.

Вы хотите, чтобы тип содержимого приложения/загрузка - просто найдите, как обеспечить загрузку для любого языка, который вы используете.

Ответ 2

Не сдавайтесь так быстро, потому что это можно сделать (в современных браузерах), используя части FileAPI:

var xhr = new XMLHttpRequest();
xhr.open('POST', url, true);
xhr.responseType = 'arraybuffer';
xhr.onload = function () {
    if (this.status === 200) {
        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, '');
        }
        var type = xhr.getResponseHeader('Content-Type');

        var blob;
        if (typeof File === 'function') {
            try {
                blob = new File([this.response], filename, { type: type });
            } catch (e) { /* Edge */ }
        }
        if (typeof blob === 'undefined') {
            blob = new Blob([this.response], { type: type });
        }

        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 does not support this yet
                if (typeof a.download === 'undefined') {
                    window.location = downloadUrl;
                } else {
                    a.href = downloadUrl;
                    a.download = filename;
                    document.body.appendChild(a);
                    a.click();
                }
            } else {
                window.location = downloadUrl;
            }

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

Вот старая версия с использованием jQuery.ajax. Он может искажать двоичные данные, когда ответ преобразуется в строку некоторого набора символов.

$.ajax({
    type: "POST",
    url: url,
    data: params,
    success: function(response, 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, '');
        }

        var type = xhr.getResponseHeader('Content-Type');
        var blob = new Blob([response], { type: type });

        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 does not support this yet
                if (typeof a.download === 'undefined') {
                    window.location = downloadUrl;
                } else {
                    a.href = downloadUrl;
                    a.download = filename;
                    document.body.appendChild(a);
                    a.click();
                }
            } else {
                window.location = downloadUrl;
            }

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

Ответ 3

Я столкнулся с той же проблемой и успешно ее разрешил. Мой прецедент - это.

" Опубликовать данные JSON на сервере и получить файл excel. Этот файл excel создается сервером и возвращается как ответ клиенту. Загрузите этот ответ как файл с пользовательским именем в браузере "

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

Вышеприведенный фрагмент просто выполняет следующие

  • Проводка массива как JSON на сервер с использованием XMLHttpRequest.
  • После извлечения содержимого в виде blob (двоичного) мы создаем загружаемый URL-адрес и прикрепляем его к невидимой ссылке "a", а затем щелкаем по ней.

Здесь нам нужно тщательно задать несколько вещей на стороне сервера. Я установил несколько заголовков в Python Django HttpResponse. Вы должны установить их соответственно, если используете другие языки программирования.

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

Так как я загружаю xls (excel) здесь, я скорректировал contentType выше одного. Вам нужно установить его в соответствии с типом файла. Вы можете использовать эту технику для загрузки любых файлов.

Ответ 4

Какой серверный язык вы используете? В моем приложении я могу легко загрузить файл из вызова AJAX, установив правильные заголовки в ответе PHP:

Настройка заголовков на стороне сервера

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-Transfer-Encoding: binary");
header("Content-Length: " . mb_strlen($strFile));

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

Это фактически переадресовывает браузер на эту страницу загрузки, но, как сказал в своем комментарии @ahren alread, он не будет перемещаться от текущей страницы.

Все о настройке правильных заголовков, поэтому я уверен, что вы найдете подходящее решение для серверного языка, который вы используете, если это не PHP.

Обработка клиентской части ответа

Предполагая, что вы уже знаете, как сделать вызов AJAX, на стороне клиента вы выполняете запрос AJAX на сервер. Затем сервер генерирует ссылку, с которой можно загрузить этот файл, например. URL 'forward', на который вы хотите указать. Например, сервер отвечает:

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

При обработке ответа вы вводите iframe в свой организм и устанавливаете SRC iframe на URL-адрес, который вы только что получили (используя jQuery для удобства этого примера):

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

Если вы установили правильные заголовки, как показано выше, iframe заставит диалоговое окно загрузки без перемещения браузера от текущей страницы.

Примечание

Дополнительное дополнение по отношению к вашему вопросу; Я считаю, что лучше всегда возвращать JSON при запросе материала с помощью технологии AJAX. После того, как вы получили ответ JSON, вы можете решить, на какой стороне клиент, что с ним делать. Возможно, например, в дальнейшем вы хотите, чтобы пользователь нажал ссылку для загрузки на URL-адрес, вместо того, чтобы напрямую загружать загрузку, в вашей текущей настройке вам нужно будет обновить как клиент, так и серверную сторону, чтобы сделать это.

Ответ 5

Для тех, кто ищет решение с точки зрения Angular, это сработало для меня:

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

Ответ 6

Вот как у меня это работает fooobar.com/questions/23182/...

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

Ответ 7

Я вижу, что вы уже нашли решение, но я просто хотел добавить некоторую информацию, которая может помочь кому-то, кто пытается добиться того же самого с большими запросами POST.

У меня была такая же проблема пару недель назад, действительно, невозможно добиться "чистой" загрузки через AJAX, Filament Group создала плагин jQuery, который работает именно так, как вы уже узнали, это называемый jQuery File Download, однако есть недостатки этой техники.

Если вы отправляете большие запросы через AJAX (скажем, файлы + 1 МБ), это будет отрицательно влиять на отзывчивость. В медленных подключениях к Интернету вам придется много ждать, пока не будет отправлен запрос, а также дождитесь загрузки файла. Это не похоже на мгновение "click" = > "popup" = > "начало загрузки". Это больше похоже на "click" = > "подождите, пока не будут отправлены данные" = > "wait for response" = > "start start", что делает его двойным по размеру, потому что вам придется ждать отправки запроса через AJAX и вернуть его в качестве загружаемого файла.

Если вы работаете с небольшими размерами файлов < 1MB, вы этого не заметите. Но, как я обнаружил в своем приложении, для больших размеров файлов это почти невыносимо.

Мое приложение позволяет пользователям экспортировать изображения, динамически генерируемые, эти изображения отправляются через запросы POST в формате base64 на сервер (это единственный возможный способ), а затем обрабатываются и отправляются пользователям в виде .png,.jpg файлы, base64 строки для изображений + 1 МБ огромны, это заставит пользователей ждать больше, чем необходимо для начала загрузки файла. В медленных интернет-соединениях это может быть очень неприятно.

Моим решением для этого было временно записать файл на сервер, как только он будет готов, динамически создать ссылку на файл в виде кнопки, которая изменяется между состояниями "Подождите..." и "Загрузить" и в то же время напечатайте образ base64 во всплывающем окне предварительного просмотра, чтобы пользователи могли "щелкнуть правой кнопкой мыши" и сохранить его. Это делает все время ожидания более приемлемым для пользователей, а также ускоряет работу.

Обновление 30 сентября 2014 года:

Месяцы прошли с тех пор, как я опубликовал это, наконец, я нашел лучший подход к ускорению работы при работе с большими строками base64. Теперь я храню строки base64 в базе данных (используя longtext или longblog fields), затем передаю свой идентификатор записи через загрузку файла jQuery, наконец, в файле загрузки script я запрашиваю базу данных, используя этот идентификатор, чтобы вытащить строку base64 и передать через функцию загрузки.

Загрузить script Пример:

<?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;
?>

Я знаю, что это далеко не то, что задал ОП, однако я чувствовал, что было бы полезно обновить мой ответ своими выводами. Когда я искал решения моей проблемы, я читал много "Скачать из AJAX POST data" потоков, которые не дали мне ответа, который я искал, я надеюсь, что эта информация поможет кому-то посмотреть добиться чего-то подобного.

Ответ 8

Я хочу указать на некоторые трудности, которые возникают при использовании техники в принятом ответе, то есть при использовании формы сообщения:

  1. Вы не можете установить заголовки по запросу. Если ваша схема аутентификации включает заголовки, Json-Web-Token, переданный в заголовке Authorization, вам нужно будет найти другой способ отправить его, например, в качестве параметра запроса.

  2. Вы не можете точно сказать, когда запрос закончился. Что ж, вы можете использовать cookie, который устанавливается в ответ, как это делает jquery.fileDownload, но это далеко от совершенства. Он не будет работать для одновременных запросов и сломается, если ответ так и не поступит.

  3. Если сервер отвечает с ошибкой, пользователь будет перенаправлен на страницу ошибки.

  4. Вы можете использовать только те типы контента, которые поддерживаются формой. Это означает, что вы не можете использовать JSON.

В итоге я использовал метод сохранения файла на S3 и отправки предварительно подписанного URL, чтобы получить файл.

Ответ 9

Это трехлетний вопрос, но сегодня у меня такая же проблема. Я посмотрел ваше отредактированное решение, но я думаю, что он может пожертвовать производительностью, потому что он должен сделать двойной запрос. Поэтому, если кому-то нужно другое решение, которое не подразумевает двойную вызов службы, то это так, как я сделал это:

<form id="export-csv-form" method="POST" action="/the/path/to/file">
    <input type="hidden" name="anyValueToPassTheServer" value="">
</form>

Эта форма используется только для вызова службы и не используется window.location(). После этого вам просто нужно отправить форму из jquery, чтобы вызвать службу и получить файл. Это довольно просто, но таким образом вы можете сделать загрузку с помощью POST. Теперь я понял, что это может быть проще, если служба, которую вы вызываете, является GET, но это не мое дело.

Ответ 10

Вот мое решение, используя временную скрытую форму.

//Create an hidden form
var form = $('<form>', {'method': 'POST', 'action': this.href}).hide();

//Add params
var params = { ...your params... };
$.each(params, function (k, v) {
    form.append($('<input>', {'type': 'hidden', 'name': k, 'value': v}));
});

//Make it part of the document and submit
$('body').append(form);
form.submit();

//Clean up
form.remove();

Обратите внимание, что я широко использую JQuery, но вы можете сделать то же самое с встроенным JS.

Ответ 11

Как заявили другие, вы можете создать и отправить форму для загрузки через запрос POST. Однако вам не нужно делать это вручную.

Одна действительно простая библиотека для выполнения именно этого jquery.redirect. Он предоставляет API, похожий на стандартный метод jQuery.post:

$.redirect(url, [values, [method, [target]]])

Ответ 12

Я использовал этот FileSaver.js. В моем случае с файлами csv я сделал это (в coffescript):

  $.ajax
    url: "url-to-server"
    data: "data-to-send"
    success: (csvData)->
      blob = new Blob([csvData], { type: 'text/csv' })
      saveAs(blob, "filename.csv")

Я думаю, что для самого сложного случая данные должны обрабатываться должным образом. Под капотом FileSaver.js реализует тот же подход ответа Jonathan Amend.

Ответ 14

Чтобы получить ответ Джонатана Аменса на работу в Edge, я внес следующие изменения:

var blob = typeof File === 'function'
    ? new File([this.response], filename, { type: type })
    : new Blob([this.response], { type: type });

к этому

var f = typeof File+"";
var blob = f === 'function' && Modernizr.fileapi
    ? new File([this.response], filename, { type: type })
    : new Blob([this.response], { type: type });

Я бы предпочел опубликовать это как комментарий, но у меня недостаточно репутации для этого

Ответ 15

Вот подробный ответ на ваш вопрос. позвольте мне начать с кода на стороне сервера:

Класс ниже используется для создания pdf с произвольным содержимым и возврата эквивалентного байтового массива outputtream.

public class pdfgen extends AbstractPdfView{

 private static ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();

public ByteArrayOutputStream showHelp() throws Exception {
    Document document = new Document();
   // System.IO.MemoryStream ms = new System.IO.MemoryStream();
    PdfWriter.getInstance(document,byteArrayOutputStream);
    document.open();
    document.add(new Paragraph("table"));
    document.add(new Paragraph(new Date().toString()));
    PdfPTable table=new PdfPTable(2);

    PdfPCell cell = new PdfPCell (new Paragraph ("table"));

    cell.setColspan (2);
    cell.setHorizontalAlignment (Element.ALIGN_CENTER);
    cell.setPadding (10.0f);
    //cell.setBackgroundColor (new BaseColor (140, 221, 8));                                  

    table.addCell(cell);                                    
    ArrayList<String[]> row=new ArrayList<String[]>();
    String[] data=new String[2];
    data[0]="1";
    data[1]="2";
    String[] data1=new String[2];
    data1[0]="3";
    data1[1]="4";
    row.add(data);
    row.add(data1);

    for(int i=0;i<row.size();i++) {
      String[] cols=row.get(i);
      for(int j=0;j<cols.length;j++){
        table.addCell(cols[j]);
      }
    }

    document.add(table);
    document.close();

    return byteArrayOutputStream;   
}

}

Затем следует код контроллера: здесь bytearrayoutputstream преобразуется в bytearray и отправляется на клиентскую сторону с использованием объекта ответа с соответствующими заголовками.

@RequestMapping(path="/home")
public ResponseEntity<byte[]> render(HttpServletRequest request , HttpServletResponse response) throws IOException
{
  pdfgen pg=new pdfgen();
    response.setContentType("application/pdf");
    response.setHeader("Content-Disposition", "attachment:filename=report.pdf");
    try {
            OutputStream out = response.getOutputStream();
    }
  catch (IOException e){
        e.printStackTrace();
    }
    byte[] contents = null;
    try {
        contents = pg.showHelp().toByteArray();
    } 
  catch (Exception e) {
        e.printStackTrace();
    }
  //These 3 lines are used to write the byte array to pdf file
  /*FileOutputStream fos = new FileOutputStream("/Users/naveen-pt2724/desktop/nama.pdf");
  fos.write(contents);
  fos.close();*/
    HttpHeaders headers = new HttpHeaders();
    headers.setContentType(MediaType.parseMediaType("application/pdf"));
//Here you have to set the actual filename of your pdf
    String filename = "output.pdf";
    headers.setContentDispositionFormData(filename, filename);
    headers.setCacheControl("must-revalidate, post-check=0, pre-check=0");
    ResponseEntity<byte[]> respons = new ResponseEntity<byte[]>(contents, headers, HttpStatus.OK);
    return respons;
}

Заголовок должен быть установлен на "application/pdf"

Затем идет код на стороне клиента: где вы можете сделать ajax-запрос к серверу, чтобы открыть файл PDF в новой вкладке браузера

 $.ajax({
            url:'/PDFgen/home',
            method:'POST',
            cache:false,
             xhrFields: {
                    responseType: 'blob'
                  },
              success: function(data) {
                  //alert(data);
                let blob = new Blob([data], {type: 'application/pdf'}); //mime type is important here
                let link = document.createElement('a'); //create hidden a tag element
                let objectURL = window.URL.createObjectURL(blob); //obtain the url for the pdf file
                link.href = objectURL; // setting the href property for a tag
                link.target = '_blank'; //opens the pdf file in  new tab
                link.download = "fileName.pdf"; //makes the pdf file download
                (document.body || document.documentElement).appendChild(link); //to work in firefox
                link.click(); //imitating the click event for opening in new tab
              },
            error:function(xhr,stats,error){
                 alert(error);
            }  
        }); 

Ответ 16

Вот мое решение, собранное из разных источников: Реализация на стороне сервера:

    String contentType = MediaType.APPLICATION_OCTET_STREAM_VALUE;
    // Set headers
    response.setHeader("content-disposition", "attachment; filename =" + fileName);
    response.setContentType(contentType);
    // Copy file to output stream
    ServletOutputStream servletOutputStream = response.getOutputStream();
    try (InputStream inputStream = new FileInputStream(file)) {
        IOUtils.copy(inputStream, servletOutputStream);
    } finally {
        servletOutputStream.flush();
        Utils.closeQuitely(servletOutputStream);
        fileToDownload = null;
    }

Реализация на стороне клиента (с использованием jquery):

$.ajax({
type: 'POST',
contentType: 'application/json',
    url: <download file url>,
    data: JSON.stringify(postObject),
    error: function(XMLHttpRequest, textStatus, errorThrown) {
        alert(errorThrown);
    },
    success: function(message, textStatus, response) {
       var header = response.getResponseHeader('Content-Disposition');
       var fileName = header.split("=")[1];
       var blob = new Blob([message]);
       var link = document.createElement('a');
       link.href = window.URL.createObjectURL(blob);
       link.download = fileName;
       link.click();
    }
});   

Ответ 17

есть другое решение для загрузки веб-страницы в ajax. Но я имею в виду страницу, которая должна быть сначала обработана, а затем загружена.

Для начала нужно отделить обработку страницы от загрузки результатов.

1) В вызове ajax выполняются только расчеты страницы.

$.post("CalculusPage.php", { calculusFunction: true, ID: 29, data1: "a", data2: "b" },

       function(data, status) 
       {
            if (status == "success") 
            {
                /* 2) In the answer the page that uses the previous calculations is downloaded. For example, this can be a page that prints the results of a table calculated in the ajax call. */
                window.location.href = DownloadPage.php+"?ID="+29;
            }               
       }
);

// For example: in the CalculusPage.php

    if ( !empty($_POST["calculusFunction"]) ) 
    {
        $ID = $_POST["ID"];

        $query = "INSERT INTO ExamplePage (data1, data2) VALUES ('".$_POST["data1"]."', '".$_POST["data2"]."') WHERE id = ".$ID;
        ...
    }

// For example: in the DownloadPage.php

    $ID = $_GET["ID"];

    $sede = "SELECT * FROM ExamplePage WHERE id = ".$ID;
    ...

    $filename="Export_Data.xls";
    header("Content-Type: application/vnd.ms-excel");
    header("Content-Disposition: inline; filename=$filename");

    ...

Я надеюсь, что это решение может быть полезным для многих, как и для меня.

Ответ 18

Если ответом является буфер массива, попробуйте выполнить это в событии onsuccess в Ajax:

 if (event.data instanceof ArrayBuffer) {
          var binary = '';
          var bytes = new Uint8Array(event.data);
          for (var i = 0; i < bytes.byteLength; i++) {
              binary += String.fromCharCode(bytes[i])
          }
          $("#some_id").append("<li><img src=\"data:image/png;base64," + window.btoa(binary) + "\"/></span></li>");
          return;
      }
  • где event.data - ответ, полученный в функции успеха события xhr.