使用React和Axios从Express API下载文件

时间:2019-10-30 17:33:29

标签: javascript reactjs express axios

将React客户端与Express API一起使用时,React客户端如何下载Express API发送的文件?

问题:

  • 如果我在浏览器栏中键入URL,然后按Enter键,则文件下载成功。
  • 但是,如果我使用Axios在React应用程序中调用相同的URL,则文件不会下载。

Express服务器

// Route handler for /api/files/testfile
const getFile = async (req, res, next) => {

    // File
    const fileName = 'file.csv';
    const filePath = path.join(__dirname, '/../../public/', fileName);

    // File options
     const options = {
        headers: {
            'x-timestamp': Date.now(),
            'x-sent': true,
            'content-disposition': "attachment; filename=" + fileName, // gets ignored
            'content-type': "text/csv"
        }
    }

    try {
        res.download(
            filePath,
            fileName,
            options
        );
        console.log("File sent successfully!");
    }
    catch (error) {
        console.error("File could not be sent!");
        next(error);
    }
});

反应客户

// When the user clicks the "Download as CSV" button
handleDownloadFile = () => {
    axios
        .get(
            `/api/files/testfile`, {
                responseType: 'blob',
                headers: {
                    'Content-Type': 'text/csv',
                }
            }
        )
        .then(response => {
            console.log(response.headers); // does not include content-disposition
            console.log("File downloading successfully!");
        })
        .catch( (error) => {
            console.error("File could not be downloaded:", error);
        });
}

我读到这可能与content-disposition标头有关。我尝试设置(请参见上面的代码),但标头未发送到客户端。


不良的“解决方案”:

  • 在React应用程序中:创建一个新的a元素,设置其href属性并通过JavaScript触发一个click。我正在寻找不需要此JS hack的解决方案。

  • 在React应用程序中:将atarget="_blank"一起使用,而不是Axios。但是,这不适合我,因为它会绕过我的axios配置设置(API URL,身份验证令牌等)

3 个答案:

答案 0 :(得分:1)

不幸的是,没有可靠的,跨平台的方法可以触发正常网页的浏览器下载行为,这在这里很合适。由于您无法在普通的DOM锚标记上使用带有内容处置,重定向或数据URI的纯URL,因此,在不创建隐藏的a并单击的情况下,看不到其他导致下载的方法它。但是,这似乎很好用(确实是filesaver.js之类的流行实用程序使用的机制)

构建一个简单的DownloadButton组件来在React中做到这一点非常简单。 Here's a working codepen模拟Axios响应,否则将开始结束,除非您要进行任何重构。我出于自身的理智/明朗起见,使用了钩子和async/await,但两者都不是绝对必要的。它确实在锚标记上使用了download attribute,在现代浏览器中都具有很好的支持。

function getFileNameFromContentDisposition(contentDisposition) {
  if (!contentDisposition) return null;

  const match = contentDisposition.match(/filename="?([^"]+)"?/);

  return match ? match[1] : null;
}

const DownloadButton = ({ children, fileName, loadingText }) => {
  const [loading, setLoading] = React.useState(false);
  const [error, setError] = React.useState(null);

  const handleClick = async () => {
    setLoading(true);
    setError(null);

    let res = null;

    try {
      // add any additional headers, such as authorization, as the second parameter to get below
      // also, remember to use responseType: 'blob' if working with blobs instead, and use res.blob() instead of res.data below
      res = await axios.get(`/api/files/${fileName}`);
      setLoading(false);
    } catch (err) {
      setLoading(false);
      setError(err);
      return;
    }

    const data = res.data; // or res.blob() if using blob responses

    const url = window.URL.createObjectURL(
      new Blob([data], {
        type: res.headers["content-type"]
      })
    );

    const actualFileName = getFileNameFromContentDisposition(
      res.headers["content-disposition"]
    );

    // uses the download attribute on a temporary anchor to trigger the browser
    // download behavior. if you need wider compatibility, you can replace this
    // part with a library such as filesaver.js
    const link = document.createElement("a");
    link.href = url;
    link.setAttribute("download", actualFileName);
    document.body.appendChild(link);
    link.click();
    link.parentNode.removeChild(link);
  };

  if (error) {
    return (<div>Unable to download file: {error.message}</div>);
  }

  return (
    <button onClick={handleClick} disabled={loading}>
      {loading ? loadingText || "Please wait..." : children}
    </button>
  );
};

对于content-disposition没有显示在ExpressJS的响应标题中,我不确定是什么问题。但是,根据ExpressJS docs,第二个参数是文件名,该文件名将作为content-disposition标头自动发送,因此您无需自己在options参数中指定它。是否显示其他参数?如果是这样,在重新定义options时可能会有冲突。但是,当使用与您的路径相似的本地路径运行示例时,我都不会遇到任何麻烦。

  

res.download(路径[,文件名] [,选项] [,fn])

     

Express v4.16.0及更高版本支持可选options参数。

     

将路径中的文件作为“附件”传输。通常,浏览器   将提示用户下载。默认情况下,内容处置   标头“ filename =”参数是路径(通常显示在   浏览器对话框)。使用filename参数覆盖此默认设置。

     

发生错误或传输完成时,该方法将调用   可选的回调函数fn。此方法使用res.sendFile()来   传输文件。

     

可选的options参数传递给底层   res.sendFile()调用,并采用完全相同的参数。

答案 1 :(得分:1)

似乎您必须根据此示例告诉axios文件直接在哪里:

axios({
  url: 'http://localhost:5000/static/example.pdf',
  method: 'GET',
  responseType: 'blob', // important
}).then((response) => {
  const url = window.URL.createObjectURL(new Blob([response.data]));
  const link = document.createElement('a');
  link.href = url;
  link.setAttribute('download', 'file.pdf');
  document.body.appendChild(link);
  link.click();
});

我假设您可以简单地更改api上的响应,以使用文件的新Blob返回Blob。但是似乎需要的主要部分是axios get呼叫上的.then响应。这样,您仍然可以使用jwt验证用户的状态并适当保护文件。

答案 2 :(得分:0)

您必须使用以下命令在反应中安装“ js-file-download”库

npm install --save js-file-download

使用axios在react文件中的代码如下:

 import download from 'js-file-download';
 downloadFile = () => {
   axios.get("localhost:3000/route/path/url")
     .then(resp => {
            download(resp.data, fileName);
     });
}