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

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

  • 通过超链接下载文件
  • 通过 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 也罢,大同小异,看团队来使用。

参考

相关推荐
涔溪11 分钟前
css3弹性布局
前端·css·css3
Array[赵]24 分钟前
npm 最新国内淘宝镜像地址源 (旧版已不能用)
前端·npm·node.js
于慨25 分钟前
pnpm报错如Runing this command will add the dependency to the workspace root所示
前端·npm
李豆豆喵1 小时前
第29天:安全开发-JS应用&DOM树&加密编码库&断点调试&逆向分析&元素属性操作
开发语言·javascript·安全
田本初1 小时前
【NodeJS】Express写接口的整体流程
前端·javascript·node.js·express
知野小兔1 小时前
【Vue】Keep alive详解
前端·javascript·vue.js
好奇的菜鸟1 小时前
TypeScript中的接口(Interface):定义对象结构的强类型方式
前端·javascript·typescript
小胖霞1 小时前
monorepo和pnpm
前端
橘子味的冰淇淋~1 小时前
全面解析 Map、WeakMap、Set、WeakSet
前端·javascript·vue.js
小闫BI设源码1 小时前
uniapp中的事件:v-on
前端·vue.js·uni-app