JavaScript / jQuery для завантаження файлу через POST з даними JSON


250

У мене є веб-сторінка на одній сторінці jQery. Він спілкується з веб-службою RESTful через дзвінки AJAX.

Я намагаюся досягти наступного:

  1. Надішліть POST, що містить дані JSON, на адресу REST.
  2. Якщо запит визначає відповідь JSON, тоді JSON повертається.
  3. Якщо запит вказує відповідь PDF / XLS / тощо, то повертається завантажуваний двійковий файл.

У мене зараз працюють 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;
}

Але це означає, що мені потрібно буде зробити більше, ніж один дзвінок на сервер, і мій сервер повинен будувати файли, що завантажуються, зберігати їх десь, а потім періодично очищати цю область зберігання.

Має бути простіший спосіб досягти цього. Ідеї?


EDIT: Переглянувши документи на $ .ajax, я бачу, що тип даних відповіді може бути лише одним 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-адресою на створений ресурс. Якщо ресурс викликається безпосередньо, поверніть фактичний бінарний файл.

Чим більше я думаю про це, тим більше мені подобається останній варіант. Таким чином я можу отримати інформацію про запит (час для генерації, розмір файлу, повідомлення про помилки тощо), і я можу діяти на цій інформації перед початком завантаження. Мінусом є додаткове управління файлами на сервері.

Будь-які інші способи досягти цього? Які плюси / мінуси цих методів я повинен знати?



6
цей був "трохи" раніше, тож як це дублікат
ГіМ

1
url = 'http://localhost/file.php?file='+ $('input').val(); window.open(url); Простий спосіб отримати однакові результати. Помістіть заголовок у файл php. Не потрібно надсилати жодних
аяксів

Відповіді:


171

Рішення letronje працює лише для дуже простих сторінок. document.body.innerHTML +=приймає текст тексту HTML, додає HTML для iframe і встановлює внутрішнійHTML сторінки на цей рядок. Це знищить будь-які події, пов’язані з вашою сторінкою, серед іншого. Створіть елемент і використовуйте 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 / тощо. Додавання на сторінку iframe, що посилається на цю URL-адресу, призведе до того, що браузер спонукає користувача до завантаження файлу, припускаючи, що веб-сервер має відповідну конфігурацію типу mime.


1
Я погоджуюся, це краще, ніж використовувати, +=і я відповідно оновлюю свою заявку. Дякую!
Таурен

3
Мені подобається ця концепція, однак Chrome має це повідомлення у консолі: Resource interpreted as Document but transferred with MIME type application/pdf. Потім він також попереджає мене, що файл може бути небезпечним. Якщо я відвідую retData.url безпосередньо, жодних попереджень чи проблем.
Майкл Ірі

Оглядаючи Інтернет, схоже, у вас можуть виникнути проблеми з PDF-файлами в iFrames, якщо ваш сервер не налаштовує тип вмісту та вміст-диспозицію належним чином. Я фактично не використовував це рішення самостійно, просто уточнив попереднє рішення, тому не маю жодних інших порад, боюся.
SamStephens

1
@Tauren: Якщо ви погоджуєтесь, що це краща відповідь, зауважте, що ви можете будь-коли змінити прийняту відповідь - або навіть повністю зняти знак прийняття. Просто перевірте нову відповідь, і позначка прийняття буде перенесена. (Старе запитання, але воно було
виведене

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. Для цього використовується атрибут завантаження в тезі прив’язки, щоб змусити браузер завантажувати його.


1
Немає підтримки для атрибута завантаження на IE та Safari :( ( caniuse.com/#feat=download ). Ви можете відкрити нове вікно та розмістити там вміст, але не впевнений у PDF чи Excel ...
Marçal Juan

1
Ви повинні звільнити пам’ять браузера після дзвінка click:window.URL.revokeObjectURL(link.href);
Едвард Брей

@JoshBerke Це старе, але воно працює на моєму кінці. Як я можу змусити його негайно відкрити каталог для збереження, а не для збереження у своєму браузері? Чи існує спосіб визначення імені вхідного файлу?
Джо Ко

2
Це може пошкодити бінарні файли, оскільки вони перетворюються на рядки за допомогою кодування, а результат не дуже близький до оригінального бінарного ...
Gobol

2
Може використовувати міметики, щоб не пошкодити інформацію, яку тут використовують у форматі
Mateo Tibaquira

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
    }

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, текст тощо, переконайтеся, що міметик - це щось таке, як 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 для збереження даних у локальній файловій системі та відкриття їх звідти.

Дякую, мені не було відомо про схему URI даних. Схоже, це може бути єдиний спосіб зробити це в одному запиті. Але це насправді не напрямок, у якому я хочу йти.
Таурен

Зважаючи на вимоги клієнта, я вирішив цю проблему, використовуючи заголовки сервера у відповіді ( Content-Disposition: attachment;filename="file.txt"), але мені дуже хочеться спробувати цей підхід наступного разу. Раніше я використовував URIдані для ескізів, але мені ніколи не спадало на думку використовувати їх для завантаження - дякую за те, що поділився цим самородом.
бричін

8

Минув такий час, як мені було задано це питання, але у мене був такий самий виклик і я хочу поділитися своїм рішенням. Він використовує елементи з інших відповідей, але я не зміг його знайти в повному обсязі. Він не використовує форму або iframe, але для цього потрібна пара запиту / отримання запиту. Замість того, щоб зберегти файл між запитами, він зберігає дані публікації. Це здається і простим, і ефективним.

клієнт

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');

Мені подобається це рішення. У запитанні ОП це звучить так, що запитувач знає, чи варто очікувати завантаження файлу або даних JSON, тож він міг вирішити в кінцевому підсумку і опублікувати іншу URL-адресу, а не приймати рішення про кінець сервера.
Сем

2

Я думаю, що найкращим підходом є використання комбінації. Ваш другий підхід, здається, є елегантним рішенням, де задіяні браузери.

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


Мій другий підхід виглядає і більш привабливим для мене. Дякуємо за підтвердження, що це гідне рішення. Ви припускаєте, що я передаю додаткове значення в об'єкт json, який вказує, що цей запит робиться з браузера як виклик ajax замість виклику веб-служби? Є кілька способів досягти цього, що я можу придумати, але яку техніку ви б використали?
Таурен

це не може бути визначено заголовком http-користувача? Або будь-який інший заголовок http?
naikus

1

Я вже два дні прокидаюся, намагаюся зрозуміти, як завантажити файл за допомогою jquery з ajax call. Вся підтримка, яку я отримав, не могла допомогти моїй ситуації, поки я не спробую це.

Сторона клієнта

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

Удачі.


2
Це буде працювати для невеликих файлів. Але якщо у вас є великий файл, ви зіткнетеся з проблемою URL-адреси занадто довго. Спробуйте з 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-файлу займає зовсім небагато часу, і я показую діалогове вікно діалогу зайнятості (він також дозволяє виконувати інші роботи). Підхід використання даних, повернутих у "успіху:" для створення Blob, не працює надійно. Це залежить від вмісту PDF-файлу. Дані у відповіді легко пошкоджуються, якщо вони не є повністю текстовими, і це все, з чим може працювати Ajax.


0

Рішення

Вкладення вмісту-диспозиції, здається, працює для мене:

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

Обхід

додаток / октет-потік

У мене щось подібне сталося з JSON, для мене на стороні сервера я встановлював заголовок на self.set_header ("Тип вмісту", " додаток / json "), проте коли я змінив його на:

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

Він автоматично завантажив його.

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

self.set_header("Content-Disposition", 'filename=learned_data.json')

-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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.