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

Posted

技术标签:

【中文标题】使用 React 和 Axios 从 Express API 下载文件【英文标题】:Download file from Express API using React and Axios 【发布时间】:2020-02-26 01:50:40 【问题描述】:

在使用带有 Express API 的 React 客户端时,React 客户端如何下载由 Express API 发送的文件?

问题:

如果我在浏览器栏中输入 url 并按 enter 文件下载成功。 但如果我使用 Axios 在我的 React 应用程序中调用相同的 url,文件不会下载。

快递服务器

// 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、身份验证令牌等)

【问题讨论】:

【参考方案1】:

不幸的是,目前还没有可靠的跨平台方法来触发浏览器对普通网页的下载行为。由于您不能在普通的 DOM 锚标记上使用带有内容处置、重定向或数据 URI 的普通 URL,因此我看不到另一种在不创建隐藏的 a 并单击它的情况下导致下载的方法。然而,这似乎运作良好(并且确实是流行的实用程序使用的机制,如filesaver.js)

在 React 中构建一个粗略的 DownloadButton 组件来执行此操作非常简单。 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 及以后版本支持可选选项参数。

将路径中的文件作为“附件”传输。通常,浏览器 将提示用户下载。默认情况下,内容处置 header “filename=” 参数是路径(这通常出现在 浏览器对话框)。用文件名参数覆盖这个默认值。

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

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

【讨论】:

【参考方案2】:

看来你得直接根据这个例子告诉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。但它似乎需要的主要部分是 .then 对您的 axios get 调用的响应。这样您仍然可以使用 jwt 验证用户的状态并适当地保护您的文件。

【讨论】:

【参考方案3】:

您必须使用以下命令在 react 中安装“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);
     );

【讨论】:

它损坏了我下载的文件。 请参考这里的答案link@PrathameshMore

以上是关于使用 React 和 Axios 从 Express API 下载文件的主要内容,如果未能解决你的问题,请参考以下文章

Axios 和 Wufoo:从 React 发布时“不允许请求标头字段授权”

使用 Axios 将对象从 React.js 发布到 PHP

使用 Axios 从 React 向 Django 发送数据 - 发布请求为空

如何使用 React Router V4 从 axios 拦截器重定向?

为啥我的 axios 使用 React.useEffect 一遍又一遍地从 Rails 后端获取调用?

如何使用 Axios 将 CSRF Coo​​kie 从 React 发送到 Django Rest Framework