关于前端文件下载的一些方式

415 阅读2分钟

开发了这么久了,关于文件下载一直半懂不懂得,咦,为什么这个链接自动下载了,为什么这个链接会默认在浏览器预览展示,下载需要鉴权怎么办等等 花了半天时间整理了一下,方便日后复习巩固

后端部分

新建一个node服务,写上一些简单代码(直接GPT快速写的,ai时代,写这种基础功能真的很方便)

const express = require("express");
const path = require("path");
const fs = require("fs");
const app = express();
const PORT = 3000;

// 文件下载接口
app.get("/download", (req, res) => {
  const filename = "报告.pdf";

  const filePath = path.join(__dirname, "downloads", filename);

  if (fs.existsSync(filePath)) {
    const filestream = fs.createReadStream(filePath);
    filestream.pipe(res);
  } else {
    res.status(404).send("File not found.");
  }
});

// 启动服务器
app.listen(PORT, () => {
  console.log(`Server is running on http://localhost:${PORT}`);
});

好了,运行node, 直接打开 http://localhost:3000/download

image.png 结果是在网页内部显示pdf,这样嘛,如果换成xls,会怎么样

image.png 哦豁,居然是下载,但是呢,这个下载又有问题,1:没有文件名 2:格式也不识别 3:最主要的是,同一个接口,可能会出现有时候会进行下载,有时候又直接是在网页打开,这显然是不可接受的。好了,接下来一一解决下

//添加这两行代码
res.setHeader("Access-Control-Expose-Headers", "Content-Disposition");
res.setHeader(
    "Content-Disposition",
    `attachment; filename*=UTF-8''abc.xls`
);

加上这代码之后,嗯,刷新链接,所有链接都是直接下载了,这样符合我的期望 然后,解释一下Content-Disposition,

image.png 这是官方解答,大概意思就是一个标准协议,然后告知浏览器内容怎么展示,把里面“attachment”换成“inline”,刷新就发现,pdf的会默认在浏览器中展示,总结归纳以下我得见解,浏览器会自动识别文件内容,如果是inline属性,并且支持在线预览的,就会直接预览显示,反之,则下载,好叭,这个我个人推荐一直用attachment

噢,对了,上面abc.xls是写的固定的,因为刚测试下载发现中文命名会报错,先用英文演示一下,然后我们解决一下中文问题

中文报错 image.png 很简单,在node里面用encodeURIComponent转义一下即可

下面是关于

node的完整代码

const express = require("express");
const path = require("path");
const fs = require("fs");
const cors = require("cors");
const app = express();
const PORT = 3000;

// 使用cors中间件
app.use(
  cors({
    origin: "*", // 允许所有来源,注意生产环境中需要设置成特定的域名
    methods: ["GET", "POST"],
    allowedHeaders: ["Authorization", "Content-Type"],
    exposedHeaders: ["Content-Disposition"],
  })
);

// 普通文件下载
app.get("/download", (req, res) => {
  const filename = "报告.pdf";
  const filePath = path.join(__dirname, "downloads", filename);
  if (fs.existsSync(filePath)) {
    const encodedFileName = encodeURIComponent(path.basename(filePath), "utf-8");
    res.setHeader(
      "Content-Disposition",
      `attachment; filename*=UTF-8''${encodedFileName}`
    );
    getFile(res, filePath);
  } else {
    res.status(404).send("File not found.");
  }
});

// 鉴权文件下载
app.get("/downloadAndToken", (req, res) => {
  const authHeader = req.headers["authorization"];
  if (!authHeader || authHeader !== "123") {
    return res.status(403).send("Authorization failed");
  }

  const filename = "报告.pdf";
  const filePath = path.join(__dirname, "downloads", filename);
  if (fs.existsSync(filePath)) {
    const encodedFileName = encodeURIComponent(path.basename(filePath), "utf-8");
    res.setHeader(
      "Content-Disposition",
      `attachment; filename*=UTF-8''${encodedFileName}`
    );
    getFile(res, filePath);
  } else {
    res.status(404).send("File not found.");
  }
});

const getFile = (res, filePath) => {
  const filestream = fs.createReadStream(filePath);
  filestream.pipe(res);
}

// 启动服务器
app.listen(PORT, () => {
  console.log(`Server is running on http://localhost:${PORT}`);
});



理清楚了后端的逻辑,现在需要弄前端逻辑了

前端部分

方式一:

直接下载

a标签下载
<a href="http://localhost:3000/download" download>下载</a>
js下载
let a = document.createElement('a');
a.style = 'display: none';
a.href = 'http://localhost:3000/download';
document.body.appendChild(a);
document.body.removeChild(a);

方式二:

鉴权下载

  const response = await axios.get('http://localhost:3000/downloadAndToken', {
                responseType: 'blob',
                headers: {
                    'Authorization': `123`
                },
});
const objectUrl = URL.createObjectURL(response.data)
const a = document.createElement('a')
a.setAttribute('href', objectUrl)
a.setAttribute('download', '报告.pdf')
a.click()
URL.revokeObjectURL(objectUrl)

有一点说一下,关于下载,之前通过接口请求下载,文件名一直是前端自定义的,殊不知其实header里面content-disposition有带过来,好的,展示对应代码

const disposition = response.headers['content-disposition'];
const filename = disposition.match(/filename\*=UTF-8''(.+)/)[1];
return decodeURIComponent(filename);

以下是前端三种方式下载的

全部代码

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <a  download target="_blank">a标签下载</a>

    <button onclick="getInterface()">接口下载</button>
    <button onclick="getInterfaceAndToken()">鉴权接口下载</button>
    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
    <script>
        const BASE_URL = 'http://localhost:3000'
        const INTERFACE_URL = BASE_URL + '/download'
        const INTERFACE_TOKEN_URL = BASE_URL + '/downloadAndToken'
        document.querySelector('a').href = INTERFACE_URL

        const getFileName = (response) => {
            const disposition = response.headers['content-disposition'];
            const filename = disposition.match(/filename\*=UTF-8''(.+)/)[1];
            return decodeURIComponent(filename);
        }
        const downloadFile = (response) => {
            const objectUrl = URL.createObjectURL(response.data)
            const a = document.createElement('a')
            a.setAttribute('href', objectUrl)
            a.setAttribute('download', getFileName(response))
            a.click()
            URL.revokeObjectURL(objectUrl)
        }
        const getInterface = async () => {
            const response = await axios.get(INTERFACE_URL, {
                responseType: 'blob',
            });

            downloadFile(response)
        }
        const getInterfaceAndToken = async () => {
            const response = await axios.get(INTERFACE_TOKEN_URL, {
                responseType: 'blob',
                headers: {
                    'Authorization': `123`
                },
            });
            downloadFile(response)
        }

    </script>
</body>
</html>

补充说明,最好加上responseType为blob,不加或者其他类型,能生成,但是就比较麻烦点了

最后总结以下,虽然干了这么久前端,几乎没怎么写过文档,发现自己写文章的能力是真的不行, 还是得多练习练习文字组织能力,加油。