如何从 fetch 请求中的 readableStream 响应中获取可下载文件

Dav*_*mar 7 csv download stream rails-api reactjs

我有一个 React 应用程序,它向我的 Rails API 发送 POST 请求。我希望我的 API 端点生成一个 csv 文件,然后将该文件发送回 React 应用程序。我希望浏览器为最终用户下载 csv 文件。

这是端点的样子:

  def generate
      // next line builds the csv in tmp directory
      period_recap_csv = period_recap.build_csv
       // next line is supposed to send back the csv as response
      send_file Rails.root.join(period_recap.filepath), filename: period_recap.filename, type: 'text/csv'
    end
Run Code Online (Sandbox Code Playgroud)

在前端,我的请求如下所示:

export function generateCsvRequest(startDate, endDate) {
  fetch("http://localhost:3000/billing/finance-recaps/generate", {
    method: "post",
    headers: {
      Authorisation: `Token token=${authToken}`,
      'Accept': 'text/csv',
      'Content-Type': 'application/json',
      'X-Key-Inflection': 'camel',
    },
    //make sure to serialize your JSON body
    body: JSON.stringify({
      start_date: startDate,
      end_date: endDate
    })
  })
  .then( (response) => {
    console.log('resp', response);
    return response;
  }).then((data) => {
      // what should I do Here with the ReadableStream I get back ??
      console.log(data);
    }).catch(err => console.error(err));
}
Run Code Online (Sandbox Code Playgroud)

作为响应正文,我得到了一个 readableStream : 在此处输入图片说明

我现在应该如何使用 ReadableStream 对象在最终用户浏览器上启动该 CSV 文件的下载?

Dyl*_*211 8

当您使用 fetch API 时,您的响应对象有一堆方法来处理您获得的数据。最常用的是json()。如果您需要从服务器下载文件,您需要的是blob(),其工作方式与json().

response.blob().then(blob => download(blob))
Run Code Online (Sandbox Code Playgroud)

有很多 npm 包可以下载文件。文件保护程序就是其中之一。一种无需依赖即可工作的方法是使用a标签。类似的东西:

function download(blob, filename) {
  const url = window.URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.style.display = 'none';
  a.href = url;
  // the filename you want
  a.download = filename;
  document.body.appendChild(a);
  a.click();
  document.body.removeChild(a);
  window.URL.revokeObjectURL(url);
}
Run Code Online (Sandbox Code Playgroud)

无论如何,使用依赖项将涵盖更多边缘情况,并且通常更兼容。我希望这有用

如果您想在另一个选项卡中显示 pdf 而不是下载它,您可以使用window.open并传递由window.URL.createObjectURL.

function showInOtherTab(blob) {
  const url = window.URL.createObjectURL(blob);
  window.open(url);
}
Run Code Online (Sandbox Code Playgroud)


Dam*_*oux 6

如果端点是流,一种解决方案是检索整个文件然后保存它。

免责声明:我在下面提供的解决方案涉及将整个 pdf 文件加载到内存中,并避免打开新的窗口选项卡来下载文件(使用downloadjs:对于重命名和保存下载的数据非常有用)。如果有人找到了避免在保存之前将整个文件加载到内存中的解决方案,请随意分享:) :)

这是代码:

import download from 'downloadjs'

fetch(...)
  .then(response => {
    //buffer to fill with all data from server
    let pdfContentBuffer = new Int8Array();

    // response.body is a readableStream 
    const reader = response.body.getReader();

    //function to retreive the next chunk from the stream
    function handleChunk({ done, chunk })  {
      if (done) {
        //everything has been loaded, call `download()` to save gthe file as pdf and name it "my-file.pdf"
        download(pdfContentBuffer, `my-file.pdf`, 'application/pdf')
        return;
      }

      // concat already loaded data with the loaded chunk
      pdfContentBuffer = Int8Array.from([...pdfContentBuffer, ...chunk]);

      // retreive next chunk
      reader.read().then(handleChunk);
    }

    //retreive first chunk
    reader.read().then(handleChunk)
  })
  .catch(err => console.error(err))
Run Code Online (Sandbox Code Playgroud)

我分享它希望这能帮助其他人。