JavaScript / jQuery для загрузки файла через POST с данными JSON

250

У меня есть одностраничное веб-приложение на основе jquery. Он связывается с веб-сервисом RESTful через вызовы AJAX.

Я пытаюсь сделать следующее:

  1. Отправьте сообщение POST, содержащее данные JSON, в URL-адрес REST.
  2. Если в запросе указан ответ JSON, возвращается JSON.
  3. Если в запросе указан ответ PDF / XLS / etc, возвращается загружаемый двоичный файл.

У меня сейчас работают 1 и 2, и клиентское приложение jquery отображает возвращенные данные на веб-странице, создавая элементы DOM на основе данных JSON. У меня также есть # 3, работающий с точки зрения веб-сервиса, то есть он создаст и вернет двоичный файл, если ему будут заданы правильные параметры JSON. Но я не уверен, что лучший способ справиться с # 3 в клиентском JavaScript-коде.

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

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

Сервер отвечает следующими заголовками:

Content-Disposition:attachment; filename=export-1282022272283.pdf
Content-Length:5120
Content-Type:application/pdf
Server:Jetty(6.1.11)

Другая идея состоит в том, чтобы сгенерировать PDF-файл и сохранить его на сервере и вернуть JSON, содержащий URL-адрес файла. Затем выполните другой вызов в обработчике успеха ajax, чтобы сделать что-то вроде следующего:

success: function(json,status) {
    window.location.href = json.url;
}

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

Должен быть более простой способ сделать это. Идеи?


РЕДАКТИРОВАТЬ: После просмотра документов для $ .ajax, я вижу, что ответ dataType может быть только одним из xml, html, script, json, jsonp, text, поэтому я предполагаю, что нет способа напрямую загрузить файл с помощью запроса ajax, если я не внедряю двоичный файл в использование Схема URI данных, предложенная в ответе @VinayC (что я не хочу делать).

Итак, я думаю, что мои варианты:

  1. Не используйте ajax, вместо этого отправьте сообщение формы и вставьте мои данные JSON в значения формы. Вероятно, придется возиться со скрытыми фреймами и тому подобное.

  2. Не используйте ajax, а вместо этого преобразуйте мои данные JSON в строку запроса для создания стандартного запроса GET и задайте для window.location.href этот URL. Может потребоваться использовать event.preventDefault () в моем обработчике кликов, чтобы браузер не менял URL приложения.

  3. Используйте мою другую идею выше, но дополненную предложениями из ответа @naikus. Отправьте запрос AJAX с некоторым параметром, который позволяет веб-сервису узнать, что он вызывается с помощью вызова ajax. Если веб-служба вызывается из вызова ajax, просто верните JSON с URL-адресом сгенерированного ресурса. Если ресурс вызывается напрямую, верните фактический двоичный файл.

Чем больше я об этом думаю, тем больше мне нравится последний вариант. Таким образом, я могу получить информацию о запросе (время генерации, размер файла, сообщения об ошибках и т. Д.), И я могу обработать эту информацию перед началом загрузки. Недостатком является дополнительное управление файлами на сервере.

Есть ли другие способы сделать это? Какие-нибудь плюсы / минусы этих методов я должен знать?

Таурен
источник
возможный дубликат загрузки файла Handle с поста ajax
IsmailS
6
этот был "немного" раньше, так как это дубликат
GiM
1
url = 'http://localhost/file.php?file='+ $('input').val(); window.open(url); Простой способ получить те же результаты. Поместите заголовок в файл php. Не нужно отправлять ajax или получать запросы
abhishek bagul

Ответы:

171

Решение letronje работает только для очень простых страниц. document.body.innerHTML +=берет HTML-текст тела, добавляет HTML-код iframe и устанавливает innerHTML страницы в эту строку. Это, среди прочего, уничтожит любые привязки событий на вашей странице. Создайте элемент и используйте appendChildвместо него.

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

Или используя jQuery

$.post('/create_binary_file.php', postData, function(retData) {
  $("body").append("<iframe src='" + retData.url+ "' style='display: none;' ></iframe>");
}); 

Что это на самом деле делает: выполните запись в /create_binary_file.php с данными в переменной postData; если этот пост завершается успешно, добавьте новый iframe в тело страницы. Предполагается, что ответ из /create_binary_file.php будет содержать значение 'url', которое является URL-адресом, с которого можно загрузить сгенерированный файл PDF / XLS / etc. Добавление iframe на страницу, которая ссылается на этот URL, приведет к тому, что браузер предложит пользователю загрузить файл, предполагая, что веб-сервер имеет соответствующую конфигурацию MIME-типа.

SamStephens
источник
1
Я согласен, это лучше, чем использовать, +=и я обновлю свое приложение соответственно. Спасибо!
Таурен
3
Мне нравится эта концепция, однако Chrome имеет это сообщение в консоли: Resource interpreted as Document but transferred with MIME type application/pdf. Тогда это также предупреждает меня, что файл может быть опасным. Если я захожу на retData.url напрямую, никаких предупреждений или проблем.
Майкл Ирей
Оглядываясь в Интернете, можно подумать, что у вас могут возникнуть проблемы с PDF-файлами в iFrames, если ваш сервер неправильно настраивает Content-Type и Content-Disposition. На самом деле я сам не использовал это решение, просто уточнил предыдущее решение, поэтому, боюсь, у меня нет других советов.
SamStephens
1
@ Таурен: Если вы согласны с тем, что это лучший ответ, учтите, что вы можете в любой момент поменять принятый ответ или даже полностью убрать отметку о принятии. Просто отметьте новый ответ, и отметка о принятии будет переведена. (Старый вопрос, но он был недавно поднят во флаги.)
BoltClock
5
что такое retData.url?
Марк Тхиен
48

Я играл с другим вариантом, который использует капли. Мне удалось получить его для загрузки текстовых документов, и я загрузил PDF (однако они повреждены).

Используя API BLOB-объектов, вы сможете сделать следующее:

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

});

Это IE 10+, Chrome 8+, FF 4+. См. Https://developer.mozilla.org/en-US/docs/Web/API/URL.createObjectURL.

Он будет загружать только файлы в Chrome, Firefox и Opera. При этом используется атрибут загрузки тега привязки, чтобы браузер загружал его.

JoshBerke
источник
1
Отсутствует поддержка атрибута загрузки в IE и Safari :( ( caniuse.com/#feat=download ). Вы можете открыть новое окно и поместить туда содержимое, но не уверены насчет PDF или Excel ...
Marçal Juan
1
Вы должны освободить память браузера после звонка click:window.URL.revokeObjectURL(link.href);
Эдвард Брей
@JoshBerke Это старый, но он работает на моем конце. Как я могу получить его, чтобы сразу открыть мой каталог, чтобы сохранить, а не сохранить в моем браузере? Также есть ли способ определить имя входящего файла?
Джо Ко
2
Это повредит двоичные файлы, потому что они преобразуются в строки с использованием кодировки, и результат не очень близок к исходному двоичному
файлу
2
Может использовать mimetypes, чтобы не испортить информацию, используемую здесь с PDF-файлами: alexhadik.com/blog/2016/7/7/l8ztp8kr5lbctf5qns4l8t3646npqh
Матео Тибакира
18

Я знаю это старое, но я думаю, что нашел более элегантное решение. У меня была точно такая же проблема. Проблема с предложенными решениями заключалась в том, что все они требовали сохранения файла на сервере, но я не хотел сохранять файлы на сервере, потому что это вызывало другие проблемы (безопасность: доступ к файлу мог получить неаутентифицированные пользователи, очистка: как и когда вы избавляетесь от файлов). И, как и вы, мои данные были сложными, вложенными объектами JSON, которые было бы сложно поместить в форму.

Я создал две серверные функции. Первый подтвердил данные. Если произошла ошибка, она будет возвращена. Если это не было ошибкой, я вернул все параметры, сериализованные / закодированные в виде строки base64. Затем на клиенте у меня есть форма, в которой есть только один скрытый ввод и сообщения для второй функции сервера. Я установил скрытый ввод в строку base64 и отправил формат. Вторая серверная функция декодирует / десериализует параметры и генерирует файл. Форма может быть отправлена ​​в новом окне или в фрейме на странице, и файл откроется.

Требуется немного больше работы и, возможно, немного больше обработки, но в целом я почувствовал себя намного лучше с этим решением.

Код находится в 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);
    }

на клиенте

    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
    }
amersk
источник
1
Я не очень хорошо посмотрел на это решение, но стоит отметить, что ничто в решении, использующем create_binary_file.php, не требует сохранения файлов на диск. Было бы вполне возможно, чтобы create_binary_file.php генерировал двоичные файлы в памяти.
SamStephens
11

Есть более простой способ, создать форму и опубликовать ее, это рискует сбросить страницу, если возвращаемый тип MIME - это то, что откроет браузер, но для CSV и тому подобного он идеален.

Пример требует подчеркивания и 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();

Для таких вещей, как HTML, текст и тому подобное, убедитесь, что mimetype - это что-то вроде application / octet-stream.

PHP-код

<?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;
АКМ
источник
8

Короче говоря, нет более простого способа. Вам нужно сделать еще один запрос к серверу, чтобы показать файл PDF. Хотя есть несколько альтернатив, но они не идеальны и не будут работать во всех браузерах:

  1. Посмотрите на схему URI данных . Если двоичные данные невелики, вы можете использовать JavaScript для открытия окна, передающего данные в URI.
  2. Единственным решением для Windows / IE было бы иметь элемент управления .NET или FileSystemObject для сохранения данных в локальной файловой системе и открытия их оттуда.
VinayC
источник
Спасибо, я не знал о схеме URI данных. Похоже, что это может быть единственным способом сделать это в одном запросе. Но это не то направление, в котором я хочу идти.
Таурен
Из-за требований клиента я решил эту проблему, используя заголовки сервера в response ( Content-Disposition: attachment;filename="file.txt"), но я действительно хочу попробовать этот подход в следующий раз. Ранее я использовал URIданные для миниатюр, но мне никогда не приходило в голову использовать их для загрузки - спасибо, что поделились этим самородком.
бричины
8

Прошло много времени с тех пор, как этот вопрос был задан, но у меня была такая же проблема, и я хочу поделиться своим решением. Он использует элементы из других ответов, но я не смог найти его полностью. Он не использует форму или iframe, но требует пару запросов post / get. Вместо сохранения файла между запросами, он сохраняет данные поста. Это кажется простым и эффективным.

клиент

var apples = new Array(); 
// construct data - replace with your own
$.ajax({
   type: "POST",
   url: '/Home/Download',
   data: JSON.stringify(apples),
   contentType: "application/json",
   dataType: "text",

   success: function (data) {
      var url = '/Home/Download?id=' + data;
      window.location = url;
   });
});

сервер

[HttpPost]
// called first
public ActionResult Download(Apple[] apples)
{
   string json = new JavaScriptSerializer().Serialize(apples);
   string id = Guid.NewGuid().ToString();
   string path = Server.MapPath(string.Format("~/temp/{0}.json", id));
   System.IO.File.WriteAllText(path, json);

   return Content(id);
}

// called next
public ActionResult Download(string id)
{
   string path = Server.MapPath(string.Format("~/temp/{0}.json", id));
   string json = System.IO.File.ReadAllText(path);
   System.IO.File.Delete(path);
   Apple[] apples = new JavaScriptSerializer().Deserialize<Apple[]>(json);

   // work with apples to build your file in memory
   byte[] file = createPdf(apples); 

   Response.AddHeader("Content-Disposition", "attachment; filename=juicy.pdf");
   return File(file, "application/pdf");
}
Фрэнк Рем
источник
1
Я думаю, что мне нравится это решение больше всего. В моем случае, хотя я генерирую файл, я думаю, что я постараюсь сохранить файл в памяти до тех пор, пока он не будет доступен, а затем освободить память после его загрузки, чтобы мне никогда не приходилось записывать на диск.
BVernon
5
$scope.downloadSearchAsCSV = function(httpOptions) {
  var httpOptions = _.extend({
    method: 'POST',
    url:    '',
    data:   null
  }, httpOptions);
  $http(httpOptions).then(function(response) {
    if( response.status >= 400 ) {
      alert(response.status + " - Server Error \nUnable to download CSV from POST\n" + JSON.stringify(httpOptions.data));
    } else {
      $scope.downloadResponseAsCSVFile(response)
    }
  })
};
/**
 * @source: https://github.com/asafdav/ng-csv/blob/master/src/ng-csv/directives/ng-csv.js
 * @param response
 */
$scope.downloadResponseAsCSVFile = function(response) {
  var charset = "utf-8";
  var filename = "search_results.csv";
  var blob = new Blob([response.data], {
    type: "text/csv;charset="+ charset + ";"
  });

  if (window.navigator.msSaveOrOpenBlob) {
    navigator.msSaveBlob(blob, filename); // @untested
  } else {
    var downloadContainer = angular.element('<div data-tap-disabled="true"><a></a></div>');
    var downloadLink      = angular.element(downloadContainer.children()[0]);
    downloadLink.attr('href', window.URL.createObjectURL(blob));
    downloadLink.attr('download', "search_results.csv");
    downloadLink.attr('target', '_blank');

    $document.find('body').append(downloadContainer);

    $timeout(function() {
      downloadLink[0].click();
      downloadLink.remove();
    }, null);
  }

  //// Gets blocked by Chrome popup-blocker
  //var csv_window = window.open("","","");
  //csv_window.document.write('<meta name="content-type" content="text/csv">');
  //csv_window.document.write('<meta name="content-disposition" content="attachment;  filename=data.csv">  ');
  //csv_window.document.write(response.data);
};
Джеймс Макгиган
источник
3

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

JQuery на стороне клиента:

var download = function(resource, payload) {
     $("#downloadFormPoster").remove();
     $("<div id='downloadFormPoster' style='display: none;'><iframe name='downloadFormPosterIframe'></iframe></div>").appendTo('body');
     $("<form action='" + resource + "' target='downloadFormPosterIframe' method='post'>" +
      "<input type='hidden' name='jsonstring' value='" + JSON.stringify(payload) + "'/>" +
      "</form>")
      .appendTo("#downloadFormPoster")
      .submit();
}

..и затем декодируем json-строку на стороне сервера и устанавливаем заголовки для загрузки (пример PHP):

$request = json_decode($_POST['jsonstring']), true);
header('Content-Type: application/csv');
header('Content-Disposition: attachment; filename=export.csv');
header('Pragma: no-cache');
ralftar
источник
Мне нравится это решение. В вопросе ОП звучит так, будто запрашивающая сторона знает, ожидать ли загрузки файла или данных JSON, поэтому он может решить на стороне клиента и публиковать на другом URL, а не на стороне сервера.
Сам
2

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

Так что в зависимости от того, как сделан звонок. (будь то браузер или вызов веб-службы) вы можете использовать комбинацию этих двух способов: отправлять URL-адрес в браузер и отправлять необработанные данные любому другому клиенту веб-службы.

naikus
источник
Мой второй подход теперь выглядит более привлекательным для меня. Спасибо за подтверждение, что это стоящее решение. Вы предлагаете, чтобы я передал дополнительное значение в объекте json, которое указывает, что этот запрос выполняется из браузера как вызов ajax вместо вызова веб-службы? Я могу придумать несколько способов сделать это, но какую технику вы бы использовали?
Таурен
это не может быть определено http-заголовком User-Agent? Или любой другой заголовок http?
naikus
1

Я не спал уже два дня, пытаясь выяснить, как загрузить файл с помощью jquery с помощью вызова ajax. Вся поддержка, которую я получил, не могла помочь моей ситуации, пока я не попробую это.

Сторона клиента

function exportStaffCSV(t) {
   
    var postData = { checkOne: t };
    $.ajax({
        type: "POST",
        url: "/Admin/Staff/exportStaffAsCSV",
        data: postData,
        success: function (data) {
            SuccessMessage("file download will start in few second..");
            var url = '/Admin/Staff/DownloadCSV?data=' + data;
            window.location = url;
        },
       
        traditional: true,
        error: function (xhr, status, p3, p4) {
            var err = "Error " + " " + status + " " + p3 + " " + p4;
            if (xhr.responseText && xhr.responseText[0] == "{")
                err = JSON.parse(xhr.responseText).Message;
            ErrorMessage(err);
        }
    });

}

Сторона сервера

 [HttpPost]
    public string exportStaffAsCSV(IEnumerable<string> checkOne)
    {
        StringWriter sw = new StringWriter();
        try
        {
            var data = _db.staffInfoes.Where(t => checkOne.Contains(t.staffID)).ToList();
            sw.WriteLine("\"First Name\",\"Last Name\",\"Other Name\",\"Phone Number\",\"Email Address\",\"Contact Address\",\"Date of Joining\"");
            foreach (var item in data)
            {
                sw.WriteLine(string.Format("\"{0}\",\"{1}\",\"{2}\",\"{3}\",\"{4}\",\"{5}\",\"{6}\"",
                    item.firstName,
                    item.lastName,
                    item.otherName,
                    item.phone,
                    item.email,
                    item.contact_Address,
                    item.doj
                    ));
            }
        }
        catch (Exception e)
        {

        }
        return sw.ToString();

    }

    //On ajax success request, it will be redirected to this method as a Get verb request with the returned date(string)
    public FileContentResult DownloadCSV(string data)
    {
        return File(new System.Text.UTF8Encoding().GetBytes(data), System.Net.Mime.MediaTypeNames.Application.Octet, filename);
        //this method will now return the file for download or open.
    }

Удачи.

Отис-iDev
источник
2
Это будет работать для небольших файлов. Но если у вас большой файл, вы столкнетесь с проблемой слишком длинной ссылки. Попробуйте с 1000 записей, он сломается и экспортировать только часть данных.
Майкл Меррелл
1

Нашел его где-то давным-давно, и он работает отлично!

let payload = {
  key: "val",
  key2: "val2"
};

let url = "path/to/api.php";
let form = $('<form>', {'method': 'POST', 'action': url}).hide();
$.each(payload, (k, v) => form.append($('<input>', {'type': 'hidden', 'name': k, 'value': v})) );
$('body').append(form);
form.submit();
form.remove();
Ден Никитин
источник
0

Другой подход, вместо сохранения файла на сервере и его извлечения, заключается в использовании .NET 4.0+ ObjectCache с коротким сроком действия до второго действия (когда он может быть окончательно выгружен). Причина, по которой я хочу использовать JQuery Ajax для вызова, заключается в том, что он асинхронный. Создание моего динамического файла PDF занимает довольно много времени, и в течение этого времени я отображаю диалоговое окно с вращающимся вертелом (оно также позволяет выполнять другую работу). Подход с использованием данных, возвращаемых в «success:», для создания BLOB-объектов не работает надежно. Это зависит от содержимого файла PDF. Он легко искажается данными в ответе, если он не полностью текстовый, и это все, с чем может работать Ajax.

Рэй Смоллвуд
источник
0

Решение

Вложение Content-Disposition, кажется, работает для меня:

self.set_header("Content-Type", "application/json")
self.set_header("Content-Disposition", 'attachment; filename=learned_data.json')

Временное решение

Применение / октет-поток

У меня было нечто подобное, происходящее со мной с JSON, для меня на стороне сервера я устанавливал заголовок self.set_header ("Content-Type", " application / json "), однако, когда я изменил его на:

self.set_header("Content-Type", "application/octet-stream")

Это автоматически загрузило это.

Также знайте, что для того, чтобы файл все еще сохранял суффикс .json, он вам понадобится в заголовке файла:

self.set_header("Content-Disposition", 'filename=learned_data.json')
Mr-программа
источник
-1

С HTML5 вы можете просто создать якорь и нажать на него. Нет необходимости добавлять его в документ в детстве.

const a = document.createElement('a');
a.download = '';
a.href = urlForPdfFile;
a.click();

Все сделано.

Если вы хотите иметь специальное имя для загрузки, просто передайте его в downloadатрибуте:

const a = document.createElement('a');
a.download = 'my-special-name.pdf';
a.href = urlForPdfFile;
a.click();
переписан
источник
4
вопрос о методе POST
dovid