代码之家  ›  专栏  ›  技术社区  ›  Tauren

JavaScript/jQuery通过POST下载包含JSON数据的文件

  •  229
  • Tauren  · 技术社区  · 15 年前

    我有一个基于jquery的单页webapp。它通过AJAX调用与RESTful web服务通信。

    我正在努力实现以下目标:

    1. 将包含JSON数据的帖子提交到REST url。
    2. 如果请求指定了JSON响应,则返回JSON。
    3. 如果请求指定了PDF/XLS/etc响应,则返回可下载的二进制文件。

    我有1&2现在开始工作,客户端jquery应用程序通过基于JSON数据创建DOM元素,在网页中显示返回的数据。我还从web服务的角度使用了#3,这意味着如果给定正确的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,我看到响应数据类型只能是 xml, html, script, json, jsonp, text ,所以我猜没有办法使用ajax请求直接下载文件,除非我按照@VinayC答案中的建议(我不想这么做)使用数据URI方案嵌入二进制文件。

    所以我想我的选择是:

    1. 不要使用ajax,而是提交表单帖子,并将我的JSON数据嵌入表单值中。可能需要搞乱隐藏的iFrame之类的东西。

    2. 不要使用ajax,而是将JSON数据转换为查询字符串,以构建标准的GET请求和set窗口。地方href指向此URL。可能需要使用事件。我的点击处理程序中的preventDefault(),以防止浏览器从应用程序URL更改。

    3. 使用我上面的另一个想法,但通过@naikus答案中的建议进行了增强。提交带有一些参数的AJAX请求,这些参数让web服务知道这是通过AJAX调用调用的。如果web服务是通过ajax调用调用的,只需返回JSON和一个指向生成资源的URL即可。如果直接调用资源,则返回实际的二进制文件。

    我想得越多,就越喜欢最后一个选项。通过这种方式,我可以获得有关请求的信息(生成时间、文件大小、错误消息等),并在开始下载之前根据这些信息采取行动。缺点是服务器上有额外的文件管理。

    还有其他方法可以做到这一点吗?我应该知道这些方法的优点/缺点吗?

    12 回复  |  直到 9 年前
        1
  •  58
  •   Jason Williams    9 年前

    我一直在玩另一个使用斑点的选项。我已经设法让它下载了文本文档,我还下载了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中下载该文件。这将使用锚标记上的下载属性强制浏览器下载。

        2
  •  9
  •   Frank Rem    11 年前

    问这个问题已经有一段时间了,但我也遇到了同样的挑战,我想分享我的解决方案。它使用了其他答案中的元素,但我无法完整地找到它。它不使用表单或iframe,但它确实需要post/get请求对。它不是在请求之间保存文件,而是保存post数据。它似乎既简单又有效。

    客户

    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");
    }
    
        3
  •  2
  •   Den Nikitin    6 年前

    在很久以前的某个地方找到的,它工作得很好!

    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();
    
        4
  •  4
  •   ralftar    10 年前

    这不完全是对原始帖子的回答,而是一个快速而肮脏的解决方案,用于将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');
    
        5
  •  170
  •   SamStephens SQL.injection    10 年前

    勒特朗耶 的解决方案只适用于非常简单的页面。 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>");
    }); 
    

    这实际上是做什么的:执行一个post to/create_binary_文件。在变量postData中包含数据的php;如果该帖子成功完成,请在页面正文中添加一个新的iframe。假设响应来自/create_binary_文件。php将包含一个值“url”,这是生成的PDF/XLS/etc文件可以从中下载的url。假设web服务器具有适当的mime类型配置,向引用该URL的页面添加iframe将导致浏览器促使用户下载该文件。

        6
  •  11
  •   aqm    12 年前

    有一种更简单的方法,创建一个表单并发布它,如果返回的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;
    
        7
  •  17
  •   amersk    13 年前

    我知道这种说法,但我想我已经想出了一个更优雅的解决方案。我也有同样的问题。我所建议的解决方案存在的问题是,它们都要求将文件保存在服务器上,但我不想将文件保存在服务器上,因为这会带来其他问题(安全性:未经身份验证的用户可能会访问该文件,清理:如何以及何时清除这些文件)。和你一样,我的数据是复杂的、嵌套的JSON对象,很难形成表单。

    我所做的是创建两个服务器函数。第一个验证了数据。如果有错误,它将被返回。如果不是错误,我返回了所有序列化/编码为base64字符串的参数。然后,在客户机上,我有一个表单,它只有一个隐藏的输入,并发布到第二个服务器函数。我将隐藏输入设置为base64字符串并提交格式。第二个服务器函数解码/反序列化参数并生成文件。表单可以提交到页面上的新窗口或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
        }
    
        8
  •  8
  •   VinayC    15 年前

    简而言之,没有比这更简单的方法了。您需要发出另一个服务器请求以显示PDF文件。尽管如此,几乎没有其他选择,但它们并不完美,也不能在所有浏览器上运行:

    1. 看看 data URI scheme .如果二进制数据很小,那么您可能可以使用javascript打开以URI传递数据的窗口。
    2. Windows/IE唯一的解决方案就是拥有。NET控件或FileSystemObject将数据保存在本地文件系统上并从那里打开。
        9
  •  5
  •   James McGuigan    10 年前
    $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);
    };
    
        10
  •  -2
  •   rewritten    8 年前

    有了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();
    
        11
  •  2
  •   naikus    15 年前

    我认为最好的方法是使用组合,第二种方法似乎是涉及浏览器的优雅解决方案。

    所以这取决于打电话的方式。(无论是浏览器还是web服务调用)您可以将两者结合使用,向浏览器发送URL,并向任何其他web服务客户端发送原始数据。

        12
  •  1
  •   Otis-iDev    10 年前

    我已经醒了两天了,现在正试图弄清楚如何使用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.
        }
    

    祝你好运

        13
  •  0
  •   Wray Smallwood    11 年前

    另一种方法不是将文件保存在服务器上并检索它,而是使用。NET 4.0+ObjectCache,在第二个操作之前有一个短暂的过期时间(此时它可以被最终转储)。我想使用jQueryAjax进行调用的原因是它是异步的。构建动态PDF文件需要相当长的时间,在此期间,我会显示一个繁忙的微调器对话框(它还允许完成其他工作)。使用“success:”中返回的数据创建Blob的方法不可靠。这取决于PDF文件的内容。它很容易被响应中的数据破坏,如果它不是完全文本的,这是Ajax所能处理的。

        14
  •  0
  •   Mr-Programs    6 年前

    解决方案

    内容处置附件 似乎对我有用:

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

    变通方法

    应用程序/八位字节流

    在JSON上,我也遇到了类似的事情 服务器端 我正在将标题设置为 自己设置标题(“内容类型” 应用程序/json ") 但是当我把它改成:

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

    它自动下载了它。

    也要知道,为了使文件仍能保存。json后缀您需要在文件名标题上添加它:

    self.set_header("Content-Disposition", 'filename=learned_data.json')
    
        15
  •  0
  •   Ryan Guild    5 年前

    制作自己的活动的问题

    本文提出的许多解决方案都让JavaScript异步运行,并创建一个链接元素,然后调用

    const a = documet.createElement("a") 
    a.click()
    

    或者创建鼠标事件

    new MouseEvent({/* ...some config */})
    

    这看起来不错吧?这有什么问题?

    什么是活动来源?

    事件源在计算领域有很多含义,比如基于云的体系结构中的发布系统,或者浏览器api EventSource .在浏览器的上下文中 所有事件都有一个源,并且该源具有隐藏属性,表明是谁发起了此事件(用户或站点)。

    知道了这一点,我们就可以开始理解为什么两个点击事件可能不会被同等对待

    user click*          new MouseEvent()
    -----------            -----------
    | Event 1 |            | Event 2 |
    -----------            -----------
         |                      |     
         |----------------------|
                     |
                     |
          ----------------------
          | Permissions Policy |    Available in chrome allows the server to control
          ----------------------    what features are going to be used by the JS
                     |
                     |
       ----------------------------
       | Browser Fraud Protection | The Browser REALLY doesnt like being told to pretend
       ---------------------------- to be a user. If you will remember back to the early
                     |              2000s when one click spun off 2000 pop ups. Well here
                     |              is where popups are blocked, fraudulent ad clicks are
                    \ /             thrown out, and most importantly for our case stops 
                     v              fishy downloads
          JavaScript Event Fires
    
    

    所以我不能下载一篇愚蠢的帖子

    不,你当然可以。你只需要给用户一个创建事件的机会。以下是一些模式,您可以使用它们来创建用户流,这些用户流是明显的、相互影响的,不会被标记为欺诈。(使用jsx sorry not sorry)

    表格 可用于通过post操作导航到url。

    const example = () => (
      <form
       method="POST"
       action="/super-api/stuff"
       onSubmit={(e) => {/* mutably change e form data but don't e.preventDetfault() */}}
      >
        {/* relevant input fields of your download */}
      </form>
    )
    

    预加载 如果您的下载是不可配置的,您可能需要考虑将下载预加载到 resp.blob() new Blob(resp) 这会告诉浏览器这是一个文件,我们不会对其进行任何字符串操作。和你可以使用的其他答案一样 window.URL.createObjectURL 没有提到的是

    createObjectURL会在JAVASCRIPT中造成内存泄漏 source

    如果你不想让C++恶霸来取笑你,你必须释放这个内存。啊,但我只是一个最爱捡垃圾的人。不用担心,这很简单,如果你在大多数框架中工作(对我来说),你只需要在你的组件和你的权利上注册某种清理效果。

    const preload = () => {
      const [payload, setPayload] = useState("")
      
      useEffect(() => {
        fetch("/super-api/stuff")
          .then((f) => f.blob())
          .then(window.URL.createObjectURL)
          .then(setPayload)
    
        return () => window.URL.revokeObjectURL(payload)
      }, [])
    
    
      return (<a href={payload} download disabled={payload === ""}>Download Me</a>)
    }
    
        16
  •  0
  •   Daniel    4 年前

    我想我已经接近了,但是有些东西正在破坏文件(图像),不管怎样,也许有人可以透露这种方法的问题

    $.ajax({
                url: '/GenerateImageFile',
                type: 'POST',
                cache: false,
                data: obj,
                dataType: "text",
                success: function (data, status, xhr) {
                    let blob = new Blob([data], { type: "image/jpeg" });
    
                    let a = document.createElement('a');
                    a.href = window.URL.createObjectURL(blob);
                    a.download = "test.jpg";
                    document.body.appendChild(a);
                    a.click();
                    document.body.removeChild(a);
                    window.URL.removeObjectURL(a.href);
                },
                complete: function () {
    
                },
                beforeSend: function () {
    
                }
            });
    
        17
  •  0
  •   RJohn    4 年前

    我喜欢弗兰克的想法,并决定自己改变主意。由于试图在一篇文章中完成这项工作非常复杂,我使用的是两篇文章的方法,但只需点击数据库一次,完成后无需保存文件或清理文件。

    首先,我运行ajax请求来检索数据,但不是从控制器返回数据,而是返回一个绑定到记录的TempData存储的GUID。

    $.get("RetrieveData", { name: "myParam"} , function(results){
        window.location = "downloadFile?id=" + results
    });
    
    public string RetrieveData(string name)
    {
        var data = repository.GetData(name);
        string id = Guid.NewGuid().ToString();
        var file = new KeyValuePair<string, MyDataModel>(name, data);
        TempData[id]=file;
        return id;
    }
    

    然后当我打开窗户。位置我将Guid传递给新方法,并从TempData获取数据。执行此方法后,TempData将被释放。

    public ActionResult DownloadFile(string id)
    {
       var file = (KeyValuePair<string,MyDataModel>)TempData[id];
       var filename = file.Key;
       var data = file.Value;
       var byteArray = Encoding.UTF8.GetBytes(data);
       ...
       return File(byteArray, "text/csv", "myFile.csv");
    }