在某个项目初期,我们选择了Nuxt.js进行开发,并采用了静态部署的方式。为了实现文件下载功能,我们选用了
file-saver这一流行的JavaScript库。在静态部署模式下,file-saver表现良好,能够正常地按需下载文件,并且支持自定义文件名。
随着项目需求的变化,我们决定将原有的静态部署改为服务器端渲染(SSR)模式,以提升首屏加载速度和SEO友好度。迁移后不久,我们收到了用户的反馈:在新版本中,下载的文件名不再是我们设定的名称,而是变成了文件的URL路径。经过排查,我们发现问题出在file-saver库上——它默认只在浏览器环境中工作,而在Node.js环境下(即服务端渲染时),它的行为会有所不同。
问题分析
file-saver是一个用于在浏览器中保存文件的JavaScript库。当在客户端(浏览器)环境中使用时,它可以正确地处理文件下载请求并允许开发者自定义文件名。但在服务端渲染的应用中,由于file-saver尝试在没有DOM环境的Node.js中运行,这导致了文件下载功能的行为异常,特别是文件名的问题。
解决方案
为了解决这个问题,我们需要确保在服务端渲染的应用中正确处理文件下载逻辑。以下基于vue模版的方案:
主要使用的是 HTTP 请求来获取文件内容,并将其转换为 Blob 对象,然后通过创建一个临时的 <a> 元素来触发文件下载。虽然这种方法在客户端看起来像是流式处理,但实际上它是在客户端接收到完整文件内容后才开始下载的。这种方法适用于大多数情况,但对于非常大的文件,建议结合后端的流式传输来优化性能。
// data数据
data() {
return {
showProgress: false,
downloadProgress: 0,
controller: null, // 用于取消下载
};
},
//函数
async handleDownload(path, fileName) {
try {
let filePath = path;
// 重置进度
this.downloadProgress = 0;
this.showProgress = true;
// 创建 AbortController 用于取消下载
this.controller = new AbortController();
const response = await this.$axios.get(filePath, {
responseType: "blob",
onDownloadProgress: (progressEvent) => {
const percentCompleted = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
);
this.downloadProgress = percentCompleted;
},
signal: this.controller.signal,
});
// 将返回的数据包装为 Blob 对象
const blob = new Blob([response.data], {
type: response.headers["content-type"],
});
// 创建一个 Blob URL
const url = window.URL.createObjectURL(blob);
// 创建一个隐藏的 <a> 元素
const link = document.createElement("a");
link.href = url;
// 设置下载文件的名称
link.download = fileName || "file.pdf";
// 触发点击
link.click();
// 释放内存中的 URL 对象
window.URL.revokeObjectURL(url);
// 下载完成后关闭进度条
setTimeout(() => {
this.showProgress = false;
this.downloadProgress = 0;
}, 1000);
} catch (error) {
if (error.name === "AbortError") {
this.$message.info(this.$t("product.download.cancelled"));
} else {
console.error("下载失败:", error);
this.$message.error(this.$t("product.download.failed"));
}
} finally {
this.controller = null;
}
},