在Web开发中,前端下载文件是一个常见的需求。根据文件来源和下载方式的不同,前端下载文件可以采用多种方案。下面将介绍几种常见的前端下载文件方案。

1、通过 window.open() 打开新页面下载文件

window.open(`url`, "_self");

使用场景:下载 excel 文件,后端提供接口,接口返回的是文件流,可以直接使用 window.open(),最简单的方式。

  • 优点:最简洁;
  • 弊端:当参数错误时,或其它原因导致接口请求失败,这时无法监听到接口返回的错误信息,需要保证请求必须是正确的且能正确返回数据流,不然打开页面会直接输出接口返回的错误信息,体验不好。

2、通过 a 标签打开新页面下载文件

export const exportFile = (url, fileName) => {
  const link = document.createElement("a");
  const body = document.querySelector("body");

  link.href = url;
  link.download = fileName;

  // fix Firefox
  link.style.display = "none";
  body.appendChild(link);

  link.click();
  body.removeChild(link);
};

通过 a 标签下载的方式,同 window.open()是一样的,唯一的优点是可以自定义下载后的文件名,a 标签里有 download 属性可以自定义文件名。

弊端:同 window.open()方式一样,无法监听错误信息。

问题:以上两种方式,当在下载.mp3 格式,或者视频文件时,浏览器会直接播放该文件,而达不到直接下载的功能,此时,当下载音视频文件时无法使用以上两种方式。

3、通过文件流的方式下载

为了解决.mp3 文件下载所带来的问题,通过 ajax 请求返回 Blob 对象,或者 ArrayBuffer 对象。

获取文件

如下:通过原生 ajax 请求返回 Blob 对象

const getBlob = (url) => {
  return new Promise((resolve) => {
    const xhr = new XMLHttpRequest();

    xhr.open("GET", url, true);
    xhr.responseType = "blob";
    xhr.onload = () => {
      if (xhr.status === 200) {
        resolve(xhr.response);
      }
    };

    xhr.send();
  });
};

同样,也可以通过 axios 返回 ArrayBuffer 对象来处理

import axios from "axios";
const getFile = (url) => {
  return new Promise((resolve, reject) => {
    axios({
      method: "get",
      url,
      responseType: "arraybuffer",
    })
      .then((data) => {
        resolve(data.data);
      })
      .catch((error) => {
        reject(error.toString());
      });
  });
};

ArrayBuffer(又称类型化数组)

ArrayBuffer 对象用来表示通用的、固定长度的原始二进制数据缓冲区。ArrayBuffer 不能直接操作,而是要通过类型数组对象或 DataView 对象来操作,它们会将缓冲区中的数据表示为特定的格式,并通过这些格式来读写缓冲区的内容。 Blob(Binary Large Object): 二进制大数据对象

Blob 对象表示一个不可变、原始数据的类文件对象。Blob 表示的不一定是 JavaScript 原生格式的数据。File 接口基于 Blob,继承了 blob 的功能并将其扩展使其支持用户系统上的文件。 注意:

如果下载文件是文本类型的(如: .txt, .js 之类的), 那么用 responseType: 'text'也可以, 但是如果下载的文件是图片, 视频之类的, 就得用 arraybuffer 或 blob,更多详情请查看 MDN 通过 ajax 请求的方式下载文件,可以解决第 1、2 中存在的弊端,当请求错误时或捕获到错误信息

保存文件

当获取到文件后,这时需要保存文件

const saveAs = (blob, filename) => {
  if (window.navigator.msSaveOrOpenBlob) {
    navigator.msSaveBlob(blob, filename);
  } else {
    const link = document.createElement("a");
    const body = document.querySelector("body");

    link.href = window.URL.createObjectURL(blob); // 创建对象url
    link.download = filename;

    // fix Firefox
    link.style.display = "none";
    body.appendChild(link);

    link.click();
    body.removeChild(link);

    window.URL.revokeObjectURL(link.href); // 通过调用 URL.createObjectURL() 创建的 URL 对象
  }
};

为了解决 IE(ie10 - 11)和 Edge 无法打开 Blob URL 链接的方法,微软自己有一套方法 window.navigator.msSaveOrOpenBlob(blob, filename),打开并保存文件,以上代码做了简单的兼容,navigator.msSaveBlob(blob, filename)是直接保存。注意,此为非标准功能,详情请查看相关文档。

以下为完整代码(原生版)

const getBlob = ({ method = "GET", url }) => {
  return new Promise((resolve) => {
    const xhr = new XMLHttpRequest();

    xhr.open(method, url, true);
    xhr.responseType = "blob";
    xhr.onload = () => {
      if (xhr.status === 200) {
        resolve(xhr.response);
      }
    };

    xhr.send();
  });
};

const saveAs = (blob, filename) => {
  if (window.navigator.msSaveOrOpenBlob) {
    navigator.msSaveBlob(blob, filename);
  } else {
    const link = document.createElement("a");
    const body = document.querySelector("body");

    link.href = window.URL.createObjectURL(blob); // 创建对象url
    link.download = filename;

    // fix Firefox
    link.style.display = "none";
    body.appendChild(link);

    link.click();
    body.removeChild(link);

    window.URL.revokeObjectURL(link.href); // 通过调用 URL.createObjectURL() 创建的 URL 对象
  }
};

export const download = (url, filename = "") => {
  getBlob({ url }).then((blob) => {
    saveAs(blob, filename);
  });
};

用后台的 filename 的话,可以从 header 的 content-disposition 中去获取。

4、如何实现批量下载,且打包文件

在第 3 点的基础上,如果要实现批量下载,那能做到的只是连续多次调用 download 方法,这样无法批量集中的下载文件。这个时候就需要能够对已获取到的文件流,进行一个打包的操作,然后一次下载完毕。

这时,需要用到两个库 jszip 和 file-saver

完整的思路:通过 ajax 获取文件,然后用 jszip 压缩文件, 再用 file-saver 生成文件

  • 获取文件 同上(第 3 点)
  • 打包文件
export const download = () => {
  const urls = ["url", "url"]; //需要下载的路径
  const zip = new JSZip();
  const cache = {};
  const promises = [];
  urls.forEach((item) => {
    const promise = getBlob(item).then((data) => {
      // 下载文件, 并存成ArrayBuffer对象
      zip.file("下载文件名", data, { binary: true }); // 逐个添加文件
      cache[item.fileName] = data;
    });
    promises.push(promise);
  });

  Promise.all(promises).then(() => {
    zip.generateAsync({ type: "blob" }).then((content) => {
      // 生成二进制流
      FileSaver.saveAs(content, `打包下载.zip`); // 利用file-saver保存文件
    });
  });
};

完整代码

/**
 * 获取文件
 * @param url
 * @returns {Promise<any>}
 */
const getBlob = (url) => {
  return new Promise((resolve) => {
    const xhr = new XMLHttpRequest();

    xhr.open("GET", url, true);
    xhr.responseType = "blob";
    xhr.onload = () => {
      if (xhr.status === 200) {
        resolve(xhr.response);
      }
    };

    xhr.send();
  });
};

/**
 * 批量打包zip包下载
 * @param urlArr Array [{url: 下载文件的路径, fileName: 下载文件名称}]
 * @param filename zip文件名
 */
export const download = (urlArr, filename = "打包下载") => {
  if (!urlArr.length > 0) return;
  const zip = new JSZip();
  const cache = {};
  const promises = [];
  urlArr.forEach((item) => {
    const promise = getBlob(item.url).then((data) => {
      // 下载文件, 并存成ArrayBuffer对象
      zip.file(item.fileName, data, { binary: true }); // 逐个添加文件
      cache[item.fileName] = data;
    });
    promises.push(promise);
  });

  Promise.all(promises).then(() => {
    zip.generateAsync({ type: "blob" }).then((content) => {
      // 生成二进制流
      FileSaver.saveAs(content, `${filename}.zip`); // 利用file-saver保存文件
    });
  });
};

注意:由于通过浏览器进行打包压缩,如果文件过大,或者下载的内容过多,可能导致浏览器崩溃。

总结

综上所述,前端下载文件的方案多种多样,开发者应根据具体需求和场景选择合适的方案。对于简单的静态文件下载,可以直接使用<a>标签;对于动态生成的文件或需要通过Ajax请求获取的文件,可以使用JavaScript或第三方库来实现下载功能。无论选择哪种方案,都应注意文件的安全性、兼容性和性能问题,以确保良好的用户体验。

本文由mdnice多平台发布


jywud
36 声望6 粉丝