背景
随着 Web 应用的不断发展,文件下载功能成为许多应用中不可或缺的一部分。无论是文档、图片还是其他类型的文件,能够方便地从服务器下载文件对于提升用户体验至关重要。
原理
文件下载功能的基本原理是通过 HTTP 请求从服务器获取文件数据,然后在客户端触发文件下载。在 Vue 中,可以通过以下几种方式实现文件下载:
-
直接链接下载:通过
<a>
标签的 href 属性指向文件的 URL,用户点击链接即可下载文件。 -
后端 API 下载:通过 AJAX 请求从后端 API 获取文件数据,使用
Blob
对象创建一个临时的 URL,再通过<a>
标签触发下载。
示例
1. 直接链接下载
介绍
直接链接下载是最简单的方法,适用于文件已经存在于服务器上的情况。用户点击链接后,浏览器会自动发起请求并下载文件。
<template>
<div>
<h1>直接链接下载</h1>
<a href="/path/to/file.pdf" download="file.pdf">下载文件</a>
</div>
</template>
<script>
export default {
name: "DirectDownload"
};
</script>
2. 后端 API 下载
介绍
后端 API 下载适用于需要通过后端逻辑处理文件的情况。前端通过 AJAX 请求从后端 API 获取文件数据,动态创建 <a>
标签触发下载。
前端代码示例:
<template>
<div>
<h1>后端 API 下载</h1>
<button @click="downloadFile">下载文件</button>
</div>
</template>
<script>
import axios from 'axios';
export default {
name: "ApiDownload",
data() {
return {
fileName: "大文件.docx"
}
},
methods: {
async downloadFile() {
try {
const response = await axios.get(`/preview?fileName=${
this.fileName}`, {
responseType: 'blob'
});
// 从响应头里面获取文件名
let fileName = this.getFileName(response);
const url = window.URL.createObjectURL(new Blob([response.data]));
const a = document.createElement('a');
a.href = url;
a.download = fileName;
a.click();
window.URL.revokeObjectURL(url);
} catch (error) {
console.error('下载失败:', error);
}
},
getFileName(res) {
// 获取响应头里的 content-disposition 值 例: attachment; filename=%E5%A4%A7%E6%96%87%E4%BB%B6.docx
const contentDisposition = res.headers.get('content-disposition');
if (!contentDisposition) return null;
// 获取编码后的文件名
let fileName = contentDisposition.split('attachment; filename=')[1]
// 解码文件名
return decodeURIComponent(fileName)
},
}
};
</script>
node 服务端代码:
页面通过在请求中传递 fileName 查询参数来下载指定文件,服务端会首先检查文件是否存在,然后设置正确的 MIME 类型和 Content-Disposition 头部,以确保文件能够正确下载。
const express = require('express');
const multer = require('multer');
const fs = require('fs');
const path = require('path');
const app = express();
// 定义文件夹路径
const mergedDir = path.join(__dirname, 'merged');
// 文件下载接口
app.get('/download', (req, res) => {
const {
fileName } = req.query;
const filePath = path.join(mergedDir, fileName);
fs.access(filePath, fs.constants.F_OK, (err) => {
if (err) {
return res.status(404).json({
error: '文件不存在' });
}
const stats = fs.statSync(filePath);
if (stats.isFile()) {
const contentType = getContentType(fileName);
res.setHeader('Content-Type', contentType);
// 对 fileName 进行编码
const encodedFileName = encodeURIComponent(fileName);
res.setHeader('Content-Disposition', `attachment; filename=${
encodedFileName}`);
fs.createReadStream(filePath).pipe(res);
} else {
res.status(400).json({
error: '不是一个文件' });
}
});
});
// 获取文件的 MIME 类型
function getContentType(fileName) {
const ext = path.extname(fileName).toLowerCase();
switch (ext) {
case '.js':
return 'application/javascript';
case '.json':
return 'application/json';
case '.html':
return 'text/html';
case '.css':
return 'text/css';
case '.txt':
return 'text/plain';
case '.png':
return 'image/png';
case '.jpg':
case '.jpeg':
return 'image/jpeg';
case '.gif':
return 'image/gif';
case '.pdf':
return 'application/pdf';
case '.doc':
return 'application/msword';
case '.docx':
return 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
case '.ppt':
return 'application/vnd.ms-powerpoint';
case '.pptx':
return 'application/vnd.openxmlformats-officedocument.presentationml.presentation';
case '.xlsx':
return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
default:
return 'application/octet-stream';
}
}
// 启动服务器
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server is running on port ${
PORT}`);
});
注意
Content-Disposition
头部用于指示浏览器如何处理接收到的内容,它可以帮助浏览器决定是将文件显示在浏览器中(内联显示)还是提示用户下载文件。
-
inline
:内联显示文件// 对 fileName 进行编码 const encodedFileName = encodeURIComponent(fileName); res.setHeader('Content-Disposition', `inline; filename=${ encodedFileName}`);
-
attachment
:指示浏览器提示用户下载文件。// 对 fileName 进行编码 const encodedFileName = encodeURIComponent(fileName); res.setHeader('Content-Disposition', `attachment; filename=${ encodedFileName}`);
设置 Content-Disposition
时,拼接的文件名需使用 encodeURIComponent
编码,解决特殊字符或中文在 HTTP 头部中可能会引起解析错误或安全问题。
总结
在 Web 应用中实现文件下载功能的两种常见方法:直接链接下载和后端 API 下载。直接链接下载适用于文件已存在于服务器上的简单场景,而后端 API 下载则适用于需要通过后端逻辑处理文件的复杂场景。通过设置 Content-Disposition
头部,可以控制文件的显示或下载行为,并使用 encodeURIComponent
编码文件名以确保特殊字符和中文字符在 HTTP 头部中正确解析,提升系统的安全性和兼容性。