JSONデータを含むPOST経由でファイルをダウンロードするためのJavaScript / jQuery


250

jqueryベースの単一ページWebアプリケーションがあります。AJAX呼び出しを介してRESTful Webサービスと通信します。

私は次のことを達成しようとしています:

  1. JSONデータを含むPOSTをREST URLに送信します。
  2. リクエストがJSONレスポンスを指定している場合、JSONが返されます。
  3. リクエストがPDF / XLS / etc応答を指定している場合、ダウンロード可能なバイナリが返されます。

現在1と2が機能しており、クライアントのjqueryアプリは、JSONデータに基づいてDOM要素を作成することにより、返されたデータをWebページに表示します。また、#3はWebサービスの観点からも機能しています。つまり、正しいJSONパラメーターを指定すると、バイナリファイルが作成されて返されます。しかし、クライアントのJavaScriptコードで#3を処理する最良の方法がわかりません。

このような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を生成してサーバーに保存し、ファイルへのURLを含むJSONを返すことです。次に、ajax成功ハンドラーで別の呼び出しを発行して、次のようなことを行います。

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

しかし、それを行うには、サーバーに対して複数の呼び出しを行う必要があり、サーバーはダウンロード可能なファイルを作成してどこかに保存し、定期的にそのストレージ領域をクリーンアップする必要があります。

これを実現する簡単な方法がなければなりません。アイデア?


編集:$ .ajaxのドキュメントを確認した後、応答のdataTypeはの1つのみであることがわかります。そのxml, html, script, json, jsonp, textため、バイナリファイルをに埋め込んでいない限り、ajaxリクエストを使用してファイルを直接ダウンロードする方法はないと思います@VinayC回答で提案されているデータURIスキーム(これは私がやりたいことではありません)。

だから私は私の選択肢は次のとおりだと思います:

  1. ajaxを使用せず、代わりにフォーム投稿を送信して、JSONデータをフォーム値に埋め込みます。おそらく隠しiframeなどをいじる必要があるでしょう。

  2. ajaxを使用せず、代わりにJSONデータをクエリ文字列に変換して標準のGETリクエストを作成し、window.location.hrefをこのURLに設定します。ブラウザがアプリケーションURLから変更されないようにするために、クリックハンドラーでevent.preventDefault()を使用する必要があるかもしれません。

  3. 上記の他のアイデアを使用しますが、@ naikus回答からの提案で拡張されます。これがajax呼び出しによって呼び出されていることをWebサービスに通知するいくつかのパラメーターを指定してAJAX要求を送信します。Webサービスがajax呼び出しから呼び出された場合は、生成されたリソースへのURLとともにJSONを返すだけです。リソースが直接呼び出された場合は、実際のバイナリファイルを返します。

考えれば考えるほど、最後の選択肢が好きになります。このようにして、リクエストに関する情報(生成時間、ファイルのサイズ、エラーメッセージなど)を取得し、ダウンロードを開始する前にその情報に基づいて行動できます。欠点は、サーバー上の余分なファイル管理です。

これを達成する他の方法は?私が知っておくべきこれらの方法の長所/短所はありますか?



6
これは以前の「少し」だったので、どのように複製されますか
GiM

1
url = 'http://localhost/file.php?file='+ $('input').val(); window.open(url); 同じ結果を得る簡単な方法。ヘッダーをphpファイルに入れます。ajaxを送信したり、リクエストを取得したりする必要はありません
abhishek bagul

回答:


171

letronjeのソリューションは、非常に単純なページでのみ機能します。document.body.innerHTML +=本文のHTMLテキストを受け取り、iframe HTMLを追加し、ページの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>");
}); 

これが実際に行うこと:変数postDataのデータを使用して/create_binary_file.phpへの投稿を実行します。その投稿が正常に完了した場合は、ページの本文に新しいiframeを追加します。/create_binary_file.phpからの応答には、生成されたPDF / XLS / etcファイルをダウンロードできるURLである「url」という値が含まれることが前提となっています。そのURLを参照するページにiframeを追加すると、Webサーバーに適切なMIMEタイプの構成があると想定して、ブラウザーはユーザーにファイルのダウンロードを促します。


1
私は同意します。これは使用するよりも優れて+=おり、それに応じてアプリケーションを更新します。ありがとう!
Tauren

3
私はこのコンセプトが好きですが、Chromeではコンソールに次のメッセージが表示されます:Resource interpreted as Document but transferred with MIME type application/pdf。次に、ファイルが危険である可能性があることも警告します。retData.urlに直接アクセスしても、警告や問題はありません。
Michael Irey 2013

インターネットを見回すと、サーバーがContent-TypeとContent-Dispositionを適切に設定していない場合、iFrameでPDFに問題が発生する可能性があります。私は実際にこのソリューションを実際に使用したことはありません。以前のソリューションを明確にしただけなので、私が恐れている他のアドバイスはありません。
SamStephens 2013

1
@Tauren:これがより良い回答であることに同意する場合は、いつでも承認済みの回答を切り替えることができます。または、承認マークを完全に削除することもできます。新しい回答をチェックするだけで、承認マークが転送されます。(古い質問ですが、最近フラグで取り上げられました。)
BoltClock

5
retData.urlはどうなると思いますか?
マークティエン、2015

48

blobを使用する別のオプションを試してみました。私はなんとかテキストドキュメントをダウンロードすることができ、PDFもダウンロードしました(ただし、それらは破損しています)。

blob APIを使用すると、次のことが可能になります。

$.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çalJuan

1
呼び出し後にブラウザのメモリを解放する必要がありますclickwindow.URL.revokeObjectURL(link.href);
Edward Brey

@JoshBerkeこれは古いですが、私の側で動作します。ブラウザに保存するのではなく、すぐにディレクトリを開いて保存するにはどうすればよいですか?また、着信ファイルの名前を決定する方法はありますか?
Jo Ko

2
エンコードを使用して文字列に変換されるため、バイナリファイルが破損し、結果は元のバイナリにあまり近づきません...
Gobol

2
PDFでここで使用されているように、mimetypeを使用して情報を破損しない場合があります:alexhadik.com/blog/2016/7/7/l8ztp8kr5lbctf5qns4l8t3646npqh
Mateo Tibaquira

18

私はこの種の古いことを知っていますが、もっとエレガントな解決策を考え出したと思います。私はまったく同じ問題を抱えていました。提案されたソリューションで私が抱えていた問題は、すべてサーバーにファイルを保存する必要があるということでしたが、他の問題が発生したため、サーバーにファイルを保存したくありませんでした(セキュリティ:ファイルにアクセスできるため)認証されていないユーザー、クリーンアップ:ファイルを削除する方法と時期)そして、あなたのように、私のデータは複雑でネストされたJSONオブジェクトであり、フォームに入力するのが難しいでしょう。

2つのサーバー関数を作成しました。最初はデータを検証しました。エラーがあった場合、それが返されます。エラーではなかった場合、base64文字列としてシリアル化/エンコードされたすべてのパラメーターを返しました。次に、クライアントで、非表示の入力が1つだけあり、2番目のサーバー関数にポストするフォームがあります。非表示の入力をbase64文字列に設定し、フォーマットを送信します。2番目のサーバー関数は、パラメーターをデコード/デシリアライズしてファイルを生成します。フォームは新しいウィンドウまたはページのiframeに送信でき、ファイルが開きます。

少し作業が増え、おそらく少し処理が増えたようですが、全体として、このソリューションの方がはるかに良いと感じました。

コードは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 2013

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を使用して、ローカルファイルシステムにデータを保存し、そこから開くことです。

おかげで、私はデータURIスキームを知りませんでした。単一のリクエストでそれを行う唯一の方法である可能性があります。しかし、それは本当に私が行きたい方向ではありません。
Tauren

クライアントの要件により、応答でサーバーヘッダーを使用してこの問題を解決することになりました(Content-Disposition: attachment;filename="file.txt")が、次回はこのアプローチを本当に試したいと思います。URI以前はサムネイルにデータを使用していましたが、ダウンロードに使用することは思いもよらなかった-このナゲットを共有してくれてありがとう。
brichins 2013

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 2015

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-stringをデコードし、ダウンロード用のヘッダーを設定します(PHPの例):

$request = json_decode($_POST['jsonstring']), true);
header('Content-Type: application/csv');
header('Content-Disposition: attachment; filename=export.csv');
header('Pragma: no-cache');

私はこのソリューションが好きです。OPの質問では、リクエスターはファイルのダウンロードとJSONデータのどちらを期待するかを知っているようです。そのため、サーバー側ではなく、クライアント側で決定して別のURLに投稿できます。
サム・

2

私は最良のアプローチは組み合わせを使用することだと思います、あなたの2番目のアプローチはブラウザが関与するエレガントなソリューションのようです。

したがって、呼び出し方法によって異なります。(ブラウザであれWebサービス呼び出しであれ)2つの組み合わせを使用できます。URLをブラウザに送信し、生データを他のWebサービスクライアントに送信します。


私の2番目のアプローチは、今でも私にとってより魅力的に見えます。それが価値のある解決策であることを確認していただきありがとうございます。このリクエストがブラウザーからWebサービスコールではなくajaxコールとして行われていることを示す追加の値をjsonオブジェクトに渡すことを提案していますか?これを実現する方法はいくつか考えられますが、どのような手法を使用しますか?
Tauren

User-Agent httpヘッダーでそれを判断できないのですか?または他のhttpヘッダー?
naikus 2010

1

私は2日間目が覚めていて、ajax呼び出しでjqueryを使用してファイルをダウンロードする方法を理解しようとしています。私が得たすべてのサポートは、これを試すまで私の状況を助けられませんでした。

クライアント側

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が長すぎるという問題が発生します。1,000レコードで試してみてください。壊れてしまい、データの一部しかエクスポートされません。
マイケルメレル2015年

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

ファイルをサーバーに保存して取得する代わりの別のアプローチは、2番目のアクション(最終的にはダンプできる)までの短い有効期限で.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')

-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
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.