我有一个javascript应用程序,发送ajax POST请求到某个URL。Response可以是JSON字符串,也可以是文件(作为附件)。我可以很容易地在ajax调用中检测到Content-Type和Content-Disposition,但是一旦我检测到响应包含一个文件,我如何让客户端下载它呢?我在这里读过一些类似的帖子,但没有一个能提供我想要的答案。

请,请,请不要发布建议我不应该使用ajax或我应该重定向浏览器的答案,因为这些都不是一个选项。使用普通的HTML表单也是不可行的。我所需要的是向客户端显示一个下载对话框。这能做到吗?如何做到?


当前回答

我想指出在接受的答案中使用这种技术时出现的一些困难,即使用表单post:

You can't set headers on the request. If your authentication schema involves headers, a Json-Web-Token passed in the Authorization header, you'll have to find other way to send it, for example as a query parameter. You can't really tell when the request has finished. Well, you can use a cookie that gets set on response, as done by jquery.fileDownload, but it's FAR from perfect. It won't work for concurrent requests and it will break if a response never arrives. If the server responds with a error, the user will be redirected to the error page. You can only use the content types supported by a form. Which means you can't use JSON.

我最终使用在S3上保存文件并发送预签名URL来获取文件的方法。

其他回答

我需要一个类似的解决方案@alain-cruz的一个,但在nuxt/vue与多次下载。我知道浏览器阻止多个文件下载,我也有API返回一组csv格式的数据。我打算先使用JSZip,但我需要IE支持,所以这是我的解决方案。如果有人能帮助我改善这将是伟大的,但它为我工作到目前为止。

火了:

data : {
  body: {
    fileOne: ""col1", "col2", "datarow1.1", "datarow1.2"...so on",
    fileTwo: ""col1", "col2"..."
  }
}

page.vue:

<template>
  <b-link @click.prevent="handleFileExport">Export<b-link>
</template>

export default = {
   data() {
     return {
       fileNames: ['fileOne', 'fileTwo'],
     }
   },
  computed: {
    ...mapState({
       fileOne: (state) => state.exportFile.fileOne,
       fileTwo: (state) => state.exportFile.fileTwo,
    }),
  },
  method: {
    handleExport() {
      //exportFileAction in store/exportFile needs to return promise
      this.$store.dispatch('exportFile/exportFileAction', paramsToSend)
        .then(async (response) => {
           const downloadPrep = this.fileNames.map(async (fileName) => {
           // using lodash to get computed data by the file name
           const currentData = await _.get(this, `${fileName}`);
           const currentFileName = fileName;
           return { currentData, currentFileName };
         });
         const response = await Promise.all(downloadPrep);
         return response;
       })
       .then(async (data) => {
         data.forEach(({ currentData, currentFileName }) => {
           this.forceFileDownload(currentData, currentFileName);
         });
       })
       .catch(console.error);
    },
    forceFileDownload(data, fileName) {
     const url = window.URL
         .createObjectURL(new Blob([data], { type: 'text/csv;charset=utf-8;' }));
     const link = document.createElement('a');
     link.href = url;
     link.setAttribute('download', `${fileName}.csv`);
     document.body.appendChild(link);
     link.click();
   },
}

我也遇到过同样的问题,并成功地解决了它。我的用例是这样的。

“将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));
});

上面的代码片段只是执行以下操作

使用XMLHttpRequest将数组作为JSON发送到服务器。 在获取内容作为一个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调整为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")

我认为对于大多数复杂的情况,必须对数据进行正确的处理。在底层,filesver .js实现了与Jonathan Amend的答案相同的方法。

以下是我从不同渠道收集的解决方案: 服务器端实现:

    String contentType = MediaType.APPLICATION_OCTET_STREAM_VALUE;
    // Set headers
    response.setHeader("content-disposition", "attachment; filename =" + fileName);
    response.setContentType(contentType);
    // Copy file to output stream
    ServletOutputStream servletOutputStream = response.getOutputStream();
    try (InputStream inputStream = new FileInputStream(file)) {
        IOUtils.copy(inputStream, servletOutputStream);
    } finally {
        servletOutputStream.flush();
        Utils.closeQuitely(servletOutputStream);
        fileToDownload = null;
    }

客户端实现(使用jquery):

$.ajax({
type: 'POST',
contentType: 'application/json',
    url: <download file url>,
    data: JSON.stringify(postObject),
    error: function(XMLHttpRequest, textStatus, errorThrown) {
        alert(errorThrown);
    },
    success: function(message, textStatus, response) {
       var header = response.getResponseHeader('Content-Disposition');
       var fileName = header.split("=")[1];
       var blob = new Blob([message]);
       var link = document.createElement('a');
       link.href = window.URL.createObjectURL(blob);
       link.download = fileName;
       link.click();
    }
});   

下面是我使用临时隐藏表单的解决方案。

//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做同样的事情。