Nuxt.js 从静态部署到 SSR 部署中 File-Saver 插件的问题及解决方案

372 阅读2分钟

wallhaven-1kld7w.png 在某个项目初期,我们选择了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;
      }
    },