如何使用 JavaScript 下载文件
Published: 2023-01-05 10:50:14

下载文件是上网的一个重要方面。每天从 Internet 下载大量文件，从二进制文件（如应用程序、图像、视频和音频）到纯文本文件。
Web 开发人员想将此功能添加到应用程序中，那么可以按照以下方法进行操作。

我们将检查 3 种不同的方法：

- 仅使用 HTML 元素的基本模式；
- 使用带有 Fetch API 和 HTML 元素的 JavaScript；
- 使用 XMLHttpRequest 和 HTML 元素，但在更复杂的场景中，我们实施一个系统来衡量进度；

## 方法一
第一种也是最简单的方法意味着创建一个具有 download 属性的 a 标签。
根据定义，download 属性指定当用户单击超链接时将下载目标（href 属性中指定的文件）。

此外，使用此下载属性，我们可以在下载文件后指定文件的新名称。因此，如果我们想下载具有特定名称的文件，我们可以使用此属性来控制它。但是，当本机下载窗口出现时，用户仍然可以更改名称，但我们提供的名称将是默认名称。

如果省略该值，则使用原始文件名。

```html
<a href="https://t-bucket.sfo3.digitaloceanspaces.com/100mb.txt" download>Download 100mb.txt</a>
```
当我们不需要根据这个下载过程做任何操作时，这个方法就很棒。同时，即使我们无法使用 JavaScript 呈现 a 标签，我们也可以使用此方法：
```js
function downloadUsingAnchorElement() {
  const anchor = document.createElement('a');
  anchor.href = IMG_URL;
  anchor.download = FILE_NAME;
  
  document.body.appendChild(anchor);
  anchor.click();
  document.body.removeChild(anchor);
}
```
上面的函数做同样的事情，只是我们动态创建 a 标签，仅用于此下载操作，然后我们将其删除。

- IMG_URL 是我们要下载的图片的URL；

- FILE_NAME 为文件下载后的新名称；

此方法的局限性在于它必须遵循同源策略，因此该属性适用于同源 URL。

一个常见的场景是，当你想从另一台服务器下载图像时，浏览器没有下载它，而是在新选项卡中打开它。

此方法的关键方面是下载过程将自动开始，并且可以在浏览器中本地查看。

## 方法二
第二种方法和第三种方法使用与 a 标签相同的技术，但我们不提供图像 URL，而是将文件的内容转换为 Blob，然后使用 createObjectURL 方法从中创建 DOMString。
```js
async function downloadUsingFetch() {
  const image = await fetch(IMG_URL);
  const imageBlog = await image.blob();
  const imageURL = URL.createObjectURL(imageBlog);

  const anchor = document.createElement('a');
  anchor.href = imageURL;
  anchor.download = FILE_NAME;

  document.body.appendChild(anchor);
  anchor.click();
  document.body.removeChild(anchor);

  URL.revokeObjectURL(imageURL);
}
```
请注意，最后我们使用了 URL.revokeObjectURL ，这在内存管理方面很重要。使用 URL.createObjectURL 时会创建一个新的对象 URL，即使它是使用相同的 blob 对象调用的。

无论何时创建对象 URL，它都会在创建它的文档的整个生命周期内保持不变。浏览器将在卸载文档时释放所有对象 URL。但是，重要的是在不再需要对象 URL 时释放它们，以提高性能并最大限度地减少内存使用。

此方法的关键方面是下载过程将自动开始，但在我们的应用程序内，只有在下载完成后才会传递给浏览器。

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2ao3q7udwy5tceqjeb1m.gif)
请注意，在上面的 GIF 中，一旦我们单击“下载”按钮，似乎什么也没有发生，因为下载在我们的应用程序中作为异步任务发生，一旦完成，它将传递给浏览器。

一旦该浏览器窗口出现并且我们单击保存，该文件将自动保存在我们的计算机上。

使用这种方法，我们现在可以下载任何类型的文件，而不管源服务器是什么。然而，问题是因为下载发生在我们的应用程序内部，用户可能认为他点击时什么也没发生，因此我们需要通过实施进度测量来管理大文件下载。

同时，当我们需要在文件下载完成后在应用程序中执行某些操作时，此方法很有用。显示一条消息，向后端发送请求渲染一个新页面，等等......

## 方法三
第三种方法与第二种方法类似，我们仍将使用 Blob 和 createObjectURL，但我们将使用 XMLHttpRequest 而不是使用 Fetch API。

我们使用 XMLHttpRequest 而不是 Fetch，因为目前 Fetch API 不提供进度测量接口，而 XMLHttpRequest 提供。

```js
function downloadWithProgress() {
  const startTime = new Date().getTime();

  request = new XMLHttpRequest();

  request.responseType = "blob";
  request.open("get", IMG_URL, true);
  request.send();

  request.onreadystatechange = function () {
    if (this.readyState === 4 && this.status === 200) {
      const imageURL = window.URL.createObjectURL(this.response);

      const anchor = document.createElement("a");
      anchor.href = imageURL;
      anchor.download = FILE_NAME;
      document.body.appendChild(anchor);
      anchor.click();
    }
  };

  request.onprogress = function (e) {
    const percent_complete = Math.floor((e.loaded / e.total) * 100);

    const duration = (new Date().getTime() - startTime) / 1000;
    const bps = e.loaded / duration;

    const kbps = Math.floor(bps / 1024);

    const time = (e.total - e.loaded) / bps;
    const seconds = Math.floor(time % 60);
    const minutes = Math.floor(time / 60);
    console.log(
      `${percent_complete}% - ${kbps} Kbps - ${minutes} min ${seconds} sec remaining`
    );
  }
}
```
开始和 onreadystatechange 块类似于第二个函数。将响应下载为 Blob 对象，创建 DOMString，并使用锚元素下载文件。

Insideonprogress 我们使用 e.loaded 和 e.total 值来计算进度的百分比和经过的时间，以及下载速度和剩余时间。


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wpgebuekokbqif0u4apd.gif)

请注意，在上面的 GIF 中，我们的行为与第二种方法相同，只是现在我们可以监控进度。文件下载完成后，将被发送到浏览器，然后立即保存到磁盘。

## 总结

上述每个方法都代表了对前一个方法的更新。

第一种方法是最简单的。在此，我们只是将下载过程转发给浏览器以在本地进行管理。当应用程序不必根据加载状态执行某些操作时，此方法是首选方法。

在第二种情况下，我们在内部管理下载并仅在下载完成后才将其发送到浏览器。通过这种方式，我们可以控制应用程序内部的下载，我们可以根据其状态做出反应。此方法适用于快速下载的小文件，但当文件太大时，如果 UI 上没有任何提示用户正在下载，用户可能会认为应用程序有问题。

在最后一个方法中，我们实现了自己的进度测量，这与浏览器中的类似。