Обработка загрузки файла с поста ajax

337

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

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

EDIT:

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

$.ajax({
    type: "POST",
    url: url,
    data: params,
    success: function(response, status, request) {
        var disp = request.getResponseHeader('Content-Disposition');
        if (disp && disp.search('attachment') != -1) {
            var form = $('<form method="POST" action="' + url + '">');
            $.each(params, function(k, v) {
                form.append($('<input type="hidden" name="' + k +
                        '" value="' + v + '">'));
            });
            $('body').append(form);
            form.submit();
        }
    }
});

Таким образом, просто создайте HTML-форму с теми же параметрами, которые были использованы в запросе AJAX, и отправьте его.

  • 2
    Нет, это невозможно сделать. Я долго боролся с тем же самым. Я в конечном итоге привел к форме сообщения. Если люди все говорят одно и то же, это, вероятно, правда
  • 4
    Вы не можете скачать с AJAX вызова. Почему бы не вернуть URL-адрес файла и перенаправить оттуда. Если это прямая ссылка на скачивание файла, она даже не отойдет от вашей текущей страницы.
Показать ещё 14 комментариев
Теги:

15 ответов

96
Лучший ответ

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

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

  • 32
    Как указано в вопросе: «Использование простой формы HTML также не вариант».
  • 0
    Вы можете скрыть форму и заполнить ее программно - это все еще не работает?
Показать ещё 8 комментариев
442

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

Изменить 2017-09-28: Обновлен для использования конструктора файлов, когда он доступен, поэтому он работает в Safari >= 10.1.

Редактировать 2015-10-16: jQuery ajax не может корректно обрабатывать двоичные ответы (не может установить responseType), поэтому лучше использовать простой вызов XMLHttpRequest.

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 = typeof File === 'function'
            ? new File([this.response], filename, { type: type })
            : 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 doesn't 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 doesn't 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
        }
    }
});
  • 6
    +1 за отличное решение только для javascript. К сожалению, большинству людей все еще нужно поддерживать не современные браузеры.
  • 2
    +1 отлично работает для современных браузеров. Я бы сделал window.location = downloadUrl; хоть.
Показать ещё 42 комментария
29

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

" Опубликовать данные 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 выше одного. Вам нужно установить его в соответствии с типом файла. Вы можете использовать эту технику для загрузки любых файлов.

28

Какой серверный язык вы используете? В моем приложении я могу легко загрузить файл из вызова 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-адрес, вместо того, чтобы напрямую загружать загрузку, в вашей текущей настройке вам нужно будет обновить как клиент, так и серверную сторону, чтобы сделать это.

  • 0
    Можете ли вы предоставить код JQuery, который выполняет запрос POST? Обычно вы предоставляете функцию обратного вызова, которая обрабатывает ответ сервера, а затем анализируете и обрабатываете этот ответ в соответствии с требованиями вашего приложения. Как именно вы заставите браузер отображать диалог загрузки, как только он получит ответ, предоставленный вами с использованием приведенного выше серверного кода?
  • 0
    @PavlePredic обновил мой ответ обработкой на стороне клиента
Показать ещё 6 комментариев
18

Для тех, кто ищет решение с точки зрения 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();
});
  • 0
    Хорошее решение. Спасибо
  • 0
    Спасибо за это спасло меня 2 дня от удара головой. Ключом был тип ответа: «arraybuffer» не видел ни в одном из других решений для загрузки файлов Angular.
11

Вот как у меня это работает https://stackoverflow.com/questions/1999607/download-and-open-pdf-file-using-ajax

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

Обновленный ответ с использованием download.js

$.ajax({
  url: '<URL_TO_FILE>',
  success: download.bind(true, "<FILENAME_TO_SAVE_WITH_EXTENSION>", "<FILE_MIME_TYPE>")
});
  • 0
    Спасибо за это, я только что использовал это сегодня. Фантастика
  • 0
    Привет, мне нужно иметь jQuery 3.0>, чтобы это работало?
Показать ещё 2 комментария
11

Я вижу, что вы уже нашли решение, но я просто хотел добавить некоторую информацию, которая может помочь кому-то, кто пытается добиться того же самого с большими запросами 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" потоков, которые не дали мне ответа, который я искал, я надеюсь, что эта информация поможет кому-то посмотреть добиться чего-то подобного.

  • 0
    jQuery File Download только перенаправить меня на URL. Я называю это так: jQuery.download("api/ide/download-this-file.php", {filePath: path2Down}, "POST"); ,
5

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

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

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

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

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

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

  • 1
    СПАСИБО, МУЖИК!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
2

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

//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.

2

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

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

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

  • 1
    Это не AJAX пост, так как вопрос использует AJAX
2

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

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

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

Чтобы получить ответ Джонатана Аменса на работу в 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 });

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

1

см. http://www.henryalgus.com/reading-binary-files-using-jquery-ajax/ он вернет blob в качестве ответа, который затем может быть помещен в файл-архив

  • 1
    Хотя это может теоретически ответить на вопрос, было бы предпочтительным включить сюда основные части ответа и предоставить ссылку для справки.
1

Я использовал этот 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.

  • 1
    это не работает с IOS
  • 1
    ..но вы можете скачать файлы обычно в iOS?
0

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

Класс ниже используется для создания 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);
            }  
        }); 

Ещё вопросы

Сообщество Overcoder
Наверх
Меню