前端文件下载汇总「案例讲解」

本文汇总之前讲解的前端文件下载的知识点,包括下面的内容👇

  • 通过超链接下载文件
  • 通过 Blob 下载文件
  • 获取文件下载进度

本文会通过案例进行讲解,分篇讲解请导航到文末参考

案例环境

本文的案例都基于如下的环境:

  • MacBook Air - 芯片 Apple M1 (macOS Monterey 版本 12.4)
  • node version - v14.18.1
  • npm version - 6.14.5
  • Visual Studio Code - 版本 1.84.2 (安装插件 - Live Server)
  • Google Chrome - 版本 116.0.5845.187(正式版本) (arm64)
  • axios version - 1.6.2
  • koa version - ^2.14.2
  • Angular CLI version - 12.1.4

Live Server 插件用于启动静态资源。

通过超链接下载

超链接的文件下载考虑到超链接是同源或是跨域情况,读者可通过文章 【案例】同源策略 - CORS 处理熟悉同源策略。

同源链接

案例中,我们将开启一个服务器端渲染 Server-Side Rendering(SSR) 的项目:

bash 复制代码
ssr-app
├── public
│   └── test.txt
├── index.ejs
├── index.js
└── package.json

我们通过 Koa 开启一个 SSR 的应用:

Koa 是个用于构建 Web 应用的现代、轻量级的 Node.js 框架。

javascript 复制代码
const Koa = require('koa');
const Router = require('koa-router');
const ejs = require('ejs');
const fs = require('fs');
const path = require('path');
const static = require('koa-static');

const app = new Koa();
const router = new Router();

// 静态资源文件
const staticPath = path.join(__dirname, 'public');
app.use(static(staticPath));

// 模版文件
const template = fs.readFileSync('index.ejs', 'utf-8');

router.get('/', async (ctx) => {

  const fileName = 'test.txt';
  const fileUrl = path.join(fileName);

  // 模拟的模版文件数据
  const templateData = {
    title: 'SSR Page',
    content: 'Hello, Jimmy!',
    fileUrl: fileUrl
  };
  
  // 渲染模版
  const html = await ejs.render(template, templateData); 
  // 返回
  ctx.body = html;
});

app.use(router.routes());

// 端口监听
app.listen(3000, () => {
  console.log('Server is running on port 3000');
});

上面,我们开启了端口号为 3000 的服务,并且可通过路由 / 来获取 test.txt 文件。

下面,我们通过 纯 HTML 中 a 标签通过 JS 构建 a 标签 来获取文件。

纯 HTML 中 a 标签 :我们在 index.ejs 中添加 HTML 内容👇

html 复制代码
<!DOCTYPE html>
<html>
  <head>
    <title><%= title %></title>
  </head>
  <body>
    <h1 id="app"><%= content %></h1>
    <a href="<%= fileUrl %>" download="file.txt">Download File: <%= fileUrl %></a>
  </body>
</html>

上面,我们读取了模拟模版的数据 title, contentfileUrl 变量,用到的是 ejs 语法。

node index.js 开启服务后,整个页面渲染如下。

我们触发下 Download File: test.txt 超链接,test.txt 被下载。

是的,下载的文件名为 text.txt,我们在设定 a 标签的时候,使用了 download 属性并设定了值 file.txt。触发 a 标签,浏览器会自动下载文件。当然,我们不指定 download 属性值,文件则以默认的文件名 text.txt 来下载,如下👇

那么,我们是否可以通过 JavaScript 来完成上面的操作呢?

当然可以啦~

通过 JS 构建 a 标签 :我们更改下 index.ejs 中的 HTML 内容👇

html 复制代码
<!DOCTYPE html>
<html>
  <head>
    <title><%= title %></title>
  </head>
  <body>
    <h1 id="app"><%= content %></h1>
    <button id="download">Download File: <%= fileUrl %></button>
    <script>
      (function(){
        let downloadBtn = document.getElementById('download');
        downloadBtn.addEventListener('click', function() {
          const link = document.createElement('a'); // 创建一个 a 标签
          link.href = '<%= fileUrl %>'; // 设定 href 的链接
          link.setAttribute('download', 'file'); // 更改下载的文件名为 file,后缀名会自动添加
          document.body.appendChild(link); // 在 body 末尾追加生成的 a 标签
          link.click(); // 触发超链接 a 标签
          document.body.removeChild(link); // 移除创建的 a 标签 
        })
      })() // 闭包
    </script>
  </body>
</html>

闭包, Closure 指的是在一个函数内部定义的函数,这个内部函数可以访问到其外部函数的变量。

如上代码,代码即文档。在上面代码中,点击 Download File: test.txt 按钮,通过 JavaScript 创建一个 a 标签,然后设定该标签的 hrefdownload 的值。如果你不想更改下载的文件名,设定 link.setAttribute('download', '') 即可。

跨域链接

上面同源策略中两种方法- 通过 纯 HTML 中 a 标签通过 JS 构建 a 标签 来获取文件,是否可以在跨域链接中使用呢?

下面我们来尝试下。

案例中,我们将新建一个前后端分离的项目:

bash 复制代码
# 前端
client
└── index.html

# 后端
server
├── public
│   ├── test.txt.zip # test.txt.zip 是对 test.txt 文件的压缩
│   └── test.txt
├── index.js
└── package.json

我们添加服务的入口文件 index.js

javascript 复制代码
const Koa = require('koa');
const Router = require('koa-router');
const fs = require('fs');
const path = require('path');
const static = require('koa-static');

const app = new Koa();
const router = new Router();

// 静态文件
const staticPath = path.join(__dirname, 'public');
app.use(static(staticPath));

router.get('/', async (ctx) => {

  const fileName = 'test.txt';
  const fileUrl = path.join(fileName);

  // 模拟的数据
  const data = {
    title: 'Hello, Jimmy!',
    fileUrl: fileUrl
  };
  // 返回
  ctx.body = {
    data
  };
});

app.use(router.routes());

// 监听服务的端口号为 3000
app.listen(3000, () => {
  console.log('Server is running on port 3000');
});

node index.js 启动服务,我们访问 http://localhost:3000/test.txt 路径,我们会看到页面直接展示了 test.txt 的文本内容出来了。

为了方便看到掉起浏览器的下载文件功能,我们采用 test.txt.zip 压缩文件,更改下 index.js 内容。

javascript 复制代码
// const fileName = 'test.txt'; // -
const fileName = 'test.txt.zip'; // +

我们开始一个个验证。

纯 HTML 中 a 标签 :我们在 client 端,添加 index.html 文件。

html 复制代码
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Download Client</title>
</head>
<body>
  <!-- a 标签上 href 是跨域链接 -->
  <a href="http://localhost:3000/test.txt.zip">Download File</a>
</body>
</html>

我们使用 Live Server 启动下静态文件服务器,默认开启 5500 端口号。我们触发下 Download File 超链接。可以吊起浏览器下载文件。那么,我们可以更改文件名下载?

我们来添加 download 属性值为 download='custom':

html 复制代码
<!-- <a href="http://localhost:3000/test.txt.zip">Download File</a> --> <!-- - -->
<a href="http://localhost:3000/test.txt.zip" download='custom'>Download File</a> <!-- + -->

发现并不能更改文件名。

那么,跨域中 通过 JS 构建 a 标签 来更改文件名,是否可行呢?也是不能的,因为都是通过操作 a 标签。

通过 JS 构建 a 标签 :我们更改下前端的 index.html 文件内容👇

html 复制代码
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Download Client</title>
</head>
<body>
  <button id="download">Download File</button>
  <script>
    (function(){
      let downloadBtn = document.getElementById('download');
      downloadBtn.addEventListener('click', function() {
        const link = document.createElement('a'); // 创建 a 标签
        link.href = 'http://localhost:3000/test.txt.zip'; // 跨域链接
        link.setAttribute('download', 'custom');  // 设定下载文件的名称
        document.body.appendChild(link); // 在 body 标签内追加 a 标签
        link.click(); // 点击 a 标签
        document.body.removeChild(link); // 移除创建的 a 标签元素 
      })
    })()
  </script>
</body>
</html>

我们设定了自定义的文件名 link.setAttribute('download', 'custom');,但是没有效果。

小结

本小节演示了通过 a 标签元素的方法来下载超链接文件 。介绍了通过 纯 HTML 中 a 标签通过 JS 构建 a 标签 来获取文件的方式。它们有些异同:

  • 同源和跨域下,都可以使用 a 标签对超链接文件进行预览或者下载
  • 同源下,超链接文件 可以通过 a 标签 download 属性值更改下载文件名;跨域下,超链接文件不能被更改文件名
  • 超链接文件 ,通过a 标签,调起浏览器默认下载,可以在浏览器上看到自带的下载进度。页面上监听不到下载的进度。

通过 Blob 下载

上面我们讲解完了通过超链接下载文件,本小节我们讲讲如何将文件内容转成 Blob 文件。

Blod 对象表示一个不可变、原始数据的类文件对象。它的数据可以按文本或二进制的格式进行读取,也可以转换成 ReadableStream 用来操作数据。

因为已经将文件转为 Blob 了,不受同源策略的限制,这里可以忽略跨域请求。我们直接在同源下进行案例演示。

本案例中,我们将开启一个 SSR 项目:

bash 复制代码
ssr-app
├── public
│   └── test.txt.zip
├── index.ejs
├── index.js
└── package.json

我们通过 Koa 开启一个 SSR 的应用:

javascript 复制代码
const Koa = require('koa');
const Router = require('koa-router');
const views = require('koa-views');
const path = require('path');
const fs = require('fs');

const app = new Koa();
const router = new Router();


// 模版后缀
app.use(
  views(path.join(__dirname, 'views'), {
    extension: 'ejs'
  })
);

// 模版渲染的路径 /
router.get('/', async (ctx) => {
  await ctx.render('index'); // 然后模版文件 index.ejs
});

// 文件下载的路径 /download/file
router.get('/download/file', async (ctx) => {
  const filePath = path.join(__dirname, 'public', 'test.txt.zip');

  ctx.attachment(filePath);
  ctx.type = 'application/octet-stream';
  ctx.body = fs.createReadStream(filePath); // 创建可读流
});

app.use(router.routes());

// 服务监听的端口 3000
app.listen(3000, () => {
  console.log('Server is running on http://localhost:3000');
});

我们将 test.txt.zip 文件作为下载文件。在路径 / 中渲染了模版文件,然后在路径 /download/file 中,将文件 test.txt.zip 转为可读流返回。

然后,我们在 index.ejs 渲染模版文件中,添加内容 HTML 内容:

html 复制代码
<!DOCTYPE html>
<html>
<head>
  <title>SSR Download File</title>
</head>
<body>
  <h1>Hello, Jimmy!</h1>
  <button id="download">Download File</button>
  <script>
    (function(){
      let downloadBtn = document.getElementById("download");
      downloadBtn.addEventListener('click', function(){
        fetch('/download/file')
          .then(response => response.blob())
          .then(blobData => {
            const downloadLink = document.createElement('a'); // 创建 a 标签元素
            // createObjectURL 转换 blobData 为 url 链接
            downloadLink.href = URL.createObjectURL(blobData); 
            // 需要添加文件,包含后缀名
            downloadLink.download = 'demo.txt.zip'; 
            downloadLink.click(); // 触发 a 标签下载
            URL.revokeObjectURL(downloadLink.href); // 撤销 href
          })
      })
    })()
  </script>
</body>
</html>

Fetch 是一种在 JavaScript 中进行网络请求的现代 API,现代浏览器(包括谷歌浏览器)的内置功能。

在模版文件 index.ejs 中,我们请求了文件接口 http://localhost:3000/download/file,并获取到了返回的内容。然后通过 .then(response => response.blob()) 将响应的数据转化为 Blob 对象。之后配合 createObjectURL 方法将数据对象转化成为一个 url,最后通过 a 标签进行下载。

为什么我们本小节开头说不受同源策略的限制 。因为 createObjectURL 转成的数据对象 url 是在当前域名下生成,这里是 http://localhost:3000/path/to,可以查看 downloadLink.href 的值。感兴趣读者可以在跨域下进行验证,比如: http://localhost:5500,生成的对象 url 将是 http://localhost:3000/path/to

触发下载按钮 Download File。我们将看到自动调起浏览器下载,文件被下载下来。

小结

本小节中,我们使用 BlobcreateObjectURL,并整合了 fetch 进行文件的下载。它有以下的特点:

  • 不受同源策略的限制 - 同源和跨域文件链接都可以
  • 需要设定 download 的名称,包含文件后缀,否则生成的文件没有后缀
  • 自动唤起浏览器的下载,下载进度由浏览器控制

获取文件下载进度

上面两小节通过超链接下载和[通过 Blob 下载](#通过 Blob 下载 "#%E9%80%9A%E8%BF%87Blob%E4%B8%8B%E8%BD%BD")都是自动调起浏览器下载,下载的进度浏览器进行反馈,文件小的时候浏览器会很快下载完并提示,但是文件很大的话,那么下载就很慢了,准确来说数据拉取很慢,点击之后页面很久才会响应。当然,我们可以加个 loading 转圈圈占位提示用户,但是不友好。是否让用户知道数据加载到哪里了呢,加载完后浏览器吊起下载?

需要解答上面这个问题,其实我们解决问题我们如何获取到文件加载的进度呢? 即可。

在开始之前,我们生成一个大文件,比如 1GBtest.zip 文件。

bash 复制代码
$ cd path/to/project/public
# 从 /dev/zero 中创建大小为 1GB 的 test.zip 空文件
$ dd if=/dev/zero of=test.zip bs=1m count=1024 

带着这个问题,我们采用两种实现方式:

  • 原生的 XMLHttpRequest
  • 结合 axios 使用
  • 结合 angular 使用

我们一一来讲解,step by step

原生的 XMLHttpRequest

我们先来认识 XMLHttpRequestXMLHttpRequest (XHR) 对象用来和服务端进行任何数据交互。我们先来了解 XHR 对象的关键属性和方法。

XMLHttpRequest 实例关键属性:

属性名 说明
readyState 「只读属性」表示接口请求的状态。有值:0 -> UNSENT 表示客户端已经创 XHR 对象,但是 open() 方法没有调用;1 -> OPENED 表示 open() 方法被调用;2 -> HEADERS_RECEIVED 表示 send() 方法被调用,此时可以获取到相应头 headers 的信息和响应状态 status3 -> LOADING 表示数据下载中,responseText 中保存部分数据;4 -> DONE 表示请求操作完成,可以获取响应数据。状态 4 常用
response 「只读属性」表示返回的数据。数据的类型可以是 ArrayBuffer, Blob, Document, JS 对象,字符串等,这取决于 responseType 设置什么值
responseType 指定响应的类型。
status 「只读属性」响应状态码
timeout 请求接口自动取消的时间设定(毫秒)
withCredentials 带凭证。跨域时候使用值 true

XMLHttpRequest 实例关键方法:

方法名 说明
open() 初始化一个请求。open(method, url, async「optional」, user「optional」, password「optional」)
send() 发送一个请求。send(body「optional」)
setRequestHeader() 设置请求头。setRequestHeader(header, value)
abort() 请求发送过程中,中断请求。

XMLHttpRequest 事件,这里我理解为钩子函数,关键的有:

钩子函数 说明
readystatechange / onreadystatechange readyState 值更改时触发
timeout / ontimeout 当接口请求超时情况触发
loadend / onloadend 当接口请求完成后触发,不管接口是成功请求还是失败请求
abort / onabort 当接口请求被中断时触发
progress / onprogress 当请求接收更多的数据时,定期触发。"定期触发" 的时间间隔是由浏览器决定的,并且取决于网络传输速度和其他因素。常常用来展示数据拉取进度

Ok,我们开始编写案例。在本案例中,我们将开启一个 SSR 项目:

bash 复制代码
ssr-app
├── public
│   └── test.zip
├── index.ejs
├── index.js
└── package.json

index.js 的入口文件内容如下👇

javascript 复制代码
const Koa = require('koa');
const Router = require('koa-router');
const views = require('koa-views');
const path = require('path');
const fs = require('fs');

const app = new Koa();
const router = new Router();


// 模版文件后缀
app.use(
  views(path.join(__dirname, 'views'), {
    extension: 'ejs'
  })
);


// 模版路径 /
router.get('/', async (ctx) => {
  await ctx.render('index'); // 渲染模版 index
});

// 下载文件路径 /download/file
router.get('/download/file', async (ctx) => {
  const filePath = path.join(__dirname, 'public', 'test.zip');

  const stats = fs.statSync(filePath);
  const fileSize = stats.size; // 文件大小
  ctx.set('Content-Length', fileSize.toString()); // 设置 Content-Length

  ctx.set('Content-disposition', 'attachment; filename=test.zip'); // disposition: attachment -> 文件直接下载
  ctx.set('Content-type', 'application/octet-stream');
  ctx.body = fs.createReadStream(filePath); // 创建可读流
});

app.use(router.routes());
// 监听服务端口 3000
app.listen(3000, () => {
  console.log('Server is running on http://localhost:3000');
});

上面,我们设定了文件下载的接口 /download/file⚠️请注意,为了能够触发 onprogress 事件 。服务器必须支持分块传输或者提供 Content-Length 头部信息。我们还设定了 Content-Disposition

Content-Disposition 内容配置有以下的值:

备注
attachment 控制文件下载 。告诉浏览器将响应体作为附件下载,而不是在浏览器中直接打开。同时,可以设置 filename 参数指定下载文件的名称,如上示例
inline 控制内联显示 。告诉浏览器在页面中直接内联现实响应体,而不是下载。一些图片,PDF 等文件的展示比较常用。

我们在模版文件 index.ejs 添加 HTML 内容:

html 复制代码
<!DOCTYPE html>
<html>
<head>
  <title>SSR Download File</title>
</head>
<body>
  <h1>Hello, Jimmy!</h1>
  <button id="download">Download File</button>
  <div style="display: flex; align-items: center;">
    <span>Downloading progress:</span>
    <progress id="progress" value="0" max="100"></progress>
    <span id="progressVal">0%</span>
  </div>
  <p id="hint"></p>
  <script>
    (function(){

      let downloadBtn = document.getElementById("download");
      let progressDom = document.getElementById("progress");
      let progressValDom = document.getElementById("progressVal");
      let hintDom = document.getElementById('hint');

      downloadBtn.addEventListener('click', function(){
        const startTime = new Date().getTime(); // 开始事件

        const request = new XMLHttpRequest();
        request.responseType = 'blob'; // 响应类型
        request.open('get', '/download/file');
        request.send();
        
        request.onreadystatechange = function() {
          if(this.readyState == 4 && this.status == 200) { // 是否已经下载成功
            const downloadLink = document.createElement('a');
            // createObjectURL 转换 blobData 为 url 链接
            downloadLink.href = URL.createObjectURL(this.response);
            downloadLink.download = 'demo.zip'; // 需要添加文件,包含后缀名
            downloadLink.click(); // 触发 a 标签下载
            URL.revokeObjectURL(downloadLink.href); // 撤销 href
          }
        }
        
        // 重点
        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; // bits per second 每秒位数
          const kbps = Math.floor(bps / 1024); // kilobits per second 千比特每秒

          const remain_time = Math.ceil((e.total - e.loaded) / bps); // 剩余的时间(秒)

          progressDom.value = percent_complete;
          progressVal.innerText = percent_complete + '%';
          hintDom.innerText = `${kbps} Kbps => ${remain_time} seconds remaining.`;
        }
      })
    })()
  </script>
</body>
</html>

模版页面初始化的效果👇

👌,我们如何获取到文件加载的进度呢? 可以解答了:

在上面,我们设置了request.responseType = 'blob' 的接口。触发按钮 Download File,发起接口请求,监听 onprogress 钩子事件 progress event,对返回的已加载数据 e.loadede.total 进行处理。计算出拉取文件的速度(千比特每秒)和剩余时间(秒),并在页面中展示出来。当文件流拉取完后,到了我们的老朋友 a 标签元素上场,处理该 blob 二进制对象数据,调起浏览器自动下载。

上面也提到了,e.total 需要后端服务配合 Content-Length

触发 Download File 按钮后的数据拉取的动图效果👇

XHR 能够直接获取到文件下载的进度,那么,我们为什么不对其进行封装呢?我们当然可以对原生进行封装,但是有现成成熟的库,我们为什么不用呢?

下面介绍两种使用方法👇

结合 axios 使用

axios 是很受欢迎的 JavaScript 库,是基于 promiseHTTP 客户端,适用于浏览器和 nodejs

我们的案例结构同 [原生的 XMLHttpRequest](#原生的 XMLHttpRequest "#%E5%8E%9F%E7%94%9F%E7%9A%84XMLHttpRequest")。在其基础上更改模版文件 index.ejs 内容为:

html 复制代码
<!DOCTYPE html>
<html>
<head>
  <title>SSR Download File</title>
  <!-- 引入 aixos -->
  <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</head>
<body>
  <h1>Hello, Jimmy!</h1>
  <button id="download">Download File</button>
  <div style="display: flex; align-items: center;">
    <span>Downloading progress:</span>
    <progress id="progress" value="0" max="100"></progress>
    <span id="progressVal">0%</span>
  </div>
  <script>
    (function(){

      let downloadBtn = document.getElementById("download");
      let progressDom = document.getElementById("progress");
      let progressValDom = document.getElementById("progressVal");
      let hintDom = document.getElementById('hint');

      downloadBtn.addEventListener('click', function(){
        axios({
          method: 'get',
          url: '/download/file',
          responseType: 'blob',  // 响应类型
          onDownloadProgress: function(progressEvent) { // 重点
            const percent_complete = ((progressEvent.loaded / progressEvent.total) * 100).toFixed(2);
            progressDom.value = percent_complete;
            progressVal.innerText = percent_complete + '%';
          }
        })
          .then(response => {
            const downloadLink = document.createElement('a'); // 创建 a 标签元素
            // createObjectURL 转换 blobData 为 url 链接
            downloadLink.href = URL.createObjectURL(response.data);
            downloadLink.download = 'demo.zip'; // 需要添加文件,包含后缀名
            downloadLink.click(); // 触发 a 标签下载
            URL.revokeObjectURL(downloadLink.href); // 撤销 href
          })
          .catch(error => {
            // 处理错误
            console.error(error);
          });
      })
    })()
  </script>
</body>
</html>

我们做了下面的更改:

  • header 中引入 axios
  • axios 调用替换原生的 XMLHttpRequest

上面的调用方式,中规中矩,多多少少看到原生调用的影子,比如 responseType: 'blob'onDownloadProgress

结合 angular 使用

axiosreactvue 框架开发的时,用的比较频繁。笔者使用的 angular 框架来开发,其中集成了 @angular/common/http 模块。那么,它又是如何像 axios 调用文件下载的呢?

本案例,假设我们已经编写好了前端分离的接口文件(接口跨域请求),案例服务端结构如[原生的 XMLHttpRequest](#原生的 XMLHttpRequest "#%E5%8E%9F%E7%94%9F%E7%9A%84XMLHttpRequest")。案例中前端的结构如下:

bash 复制代码
client
└── src
│    └── app
│    │    ├── demo
│    │    │    ├── demo.component.html
│    │    │    ├── demo.component.scss
│    │    │    ├── demo.component.ts
│    │    │    └── demo.component.spec.ts
│    │    ├── services
│    │    │    ├── demo.service.ts
│    │    │    └── demo.service.spec.ts
│    │    ├── ...
│    │    ├── app.module.ts
│    │    └── app.component.ts
│    ├── ...
│    └── index.html
├── ...
└── package.json

我们在 demo.component.html 中添加 HTML 内容:

html 复制代码
<button
    type="button"
    class="btn btn-primary"
    (click)="downloadDemo()">
  <fa-icon [icon]="faDownload"></fa-icon>
  <span>download demo</span>
</button>

上面页面,我们简单生成了一个带图标的 download demo 的按钮。

然后我们生成 demo 的服务类文件 demo.service.ts

typescript 复制代码
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable()
export class DemoService {
  constructor(
    protected http: HttpClient
  ){}
  
  public dowloadFile(url: string): Observable<any> {
    return this.http.get(
      url,
      {
        observe: 'events',
        reportProgress: true, // 触发进度
        responseType: 'blob' // 响应类型
      }
    );
  }
}

接着,我们在 demo.component.ts 中调用刚才生成的服务:

typescript 复制代码
import { Component, OnInit } from '@angular/core';
import { faDownload } from '@fortawesome/free-solid-svg-icons';
import { DemoService } from 'path/to/demo.service.ts';

@Component({
  selector: 'demo',
  templateUrl: './demo.component.html',
  styleUrls: ['./demo.component.css']
})
export class DemoComponent implements OnInit {
  public faDownload = faDownload;
  
  constructor(
    protected demoService: DemoService
  ){}
  
  ngOnInit(): void {
  }
  
  public downloadDemo(): void {
    let url = 'http://localhost:3000/download/file';
    this.demoService.downloadFile(url).subscribe({
      next: (event: any) => {
        if (event.type === HttpEventType.DownloadProgress) {
          // 进度 process 通知
          const percentDone = Math.round(100 * event.loaded / event.total);
          console.log(`File is ${percentDone}% downloaded.`);
        } else if (event.type === HttpEventType.Response) {
          // HTTP 相应完成
          const downloadLink = document.createElement('a'); // 创建 a 标签元素
          // createObjectURL 转换 blobData 为 url 链接
          downloadLink.href = URL.createObjectURL(event.body);
          downloadLink.download = 'demo.zip';  // 需要添加文件,包含后缀名
          downloadLink.click(); // 触发 a 标签下载
          URL.revokeObjectURL(downloadLink.href); // 撤销 href
        }
      }
    })
  }
}

同理,我们这里也设置了 responseType ,开启 progress -〉 reportProgress,并设定 responseType: 'blob'。不同的库和框架 reactvue 等大同小异,就看开发需要和团队要求来使用。

上面实现的效果如下动图👇

小节

本小节中,我们通过使用了原生的 XHR 来拉取数据,我们需要注意:

  • 服务端要配合 Content-Length
  • 客户端需要在钩子函数 onprogress 中处理数据
  • 调接口拉取数据后,自动唤起浏览器下载

使用原生 XMLHttpRequest 处理请求,让我们知道文件下载的前后发生了什么;使用 axios@angular/common/http 能让我们更好管理和快速开发。

axios 也好,angular@angular/common/http 也罢,大同小异,看团队来使用。

参考

相关推荐
大前端爱好者1 小时前
React 19 新特性详解
前端
Amagi.1 小时前
Spring中Bean的作用域
java·后端·spring
随云6321 小时前
WebGL编程指南之着色器语言GLSL ES(入门GLSL ES这篇就够了)
前端·webgl
2402_857589361 小时前
Spring Boot新闻推荐系统设计与实现
java·spring boot·后端
J老熊2 小时前
Spring Cloud Netflix Eureka 注册中心讲解和案例示范
java·后端·spring·spring cloud·面试·eureka·系统架构
Benaso2 小时前
Rust 快速入门(一)
开发语言·后端·rust
sco52822 小时前
SpringBoot 集成 Ehcache 实现本地缓存
java·spring boot·后端
原机小子2 小时前
在线教育的未来:SpringBoot技术实现
java·spring boot·后端
寻找09之夏2 小时前
【Vue3实战】:用导航守卫拦截未保存的编辑,提升用户体验
前端·vue.js
吾日三省吾码2 小时前
详解JVM类加载机制
后端