本文汇总之前讲解的前端文件下载的知识点,包括下面的内容👇
- 通过超链接下载文件
- 通过
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
, content
和 fileUrl
变量,用到的是 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
标签,然后设定该标签的 href
和 download
的值。如果你不想更改下载的文件名,设定 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
。我们将看到自动调起浏览器下载,文件被下载下来。
小结
本小节中,我们使用 Blob
和 createObjectURL
,并整合了 fetch
进行文件的下载。它有以下的特点:
- 不受同源策略的限制 - 同源和跨域文件链接都可以
- 需要设定
download
的名称,包含文件后缀,否则生成的文件没有后缀 - 自动唤起浏览器的下载,下载进度由浏览器控制
获取文件下载进度
上面两小节通过超链接下载和[通过 Blob 下载](#通过 Blob 下载 "#%E9%80%9A%E8%BF%87Blob%E4%B8%8B%E8%BD%BD")都是自动调起浏览器下载,下载的进度浏览器进行反馈,文件小的时候浏览器会很快下载完并提示,但是文件很大的话,那么下载就很慢了,准确来说数据拉取很慢,点击之后页面很久才会响应。当然,我们可以加个 loading
转圈圈占位提示用户,但是不友好。是否让用户知道数据加载到哪里了呢,加载完后浏览器吊起下载?
需要解答上面这个问题,其实我们解决问题我们如何获取到文件加载的进度呢? 即可。
在开始之前,我们生成一个大文件,比如 1GB
的 test.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
我们先来认识 XMLHttpRequest。XMLHttpRequest (XHR) 对象用来和服务端进行任何数据
交互。我们先来了解 XHR
对象的关键属性和方法。
XMLHttpRequest 实例关键属性:
属性名 | 说明 |
---|---|
readyState |
「只读属性」表示接口请求的状态。有值:0 -> UNSENT 表示客户端已经创 XHR 对象,但是 open() 方法没有调用;1 -> OPENED 表示 open() 方法被调用;2 -> HEADERS_RECEIVED 表示 send() 方法被调用,此时可以获取到相应头 headers 的信息和响应状态 status ;3 -> 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.loaded
和 e.total
进行处理。计算出拉取文件的速度(千比特每秒)和剩余时间(秒),并在页面中展示出来。当文件流拉取完后,到了我们的老朋友 a
标签元素上场,处理该 blob
二进制对象数据,调起浏览器自动下载。
上面也提到了,e.total 需要后端服务配合
Content-Length
触发 Download File
按钮后的数据拉取的动图效果👇
XHR
能够直接获取到文件下载的进度,那么,我们为什么不对其进行封装呢?我们当然可以对原生进行封装,但是有现成成熟的库,我们为什么不用呢?
下面介绍两种使用方法👇
结合 axios
使用
axios 是很受欢迎的 JavaScript
库,是基于 promise
的 HTTP
客户端,适用于浏览器和 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
使用
axios
在 react
和 vue
框架开发的时,用的比较频繁。笔者使用的 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'
。不同的库和框架 react
和 vue
等大同小异,就看开发需要和团队要求来使用。
上面实现的效果如下动图👇
小节
本小节中,我们通过使用了原生的 XHR
来拉取数据,我们需要注意:
- 服务端要配合
Content-Length
- 客户端需要在钩子函数
onprogress
中处理数据 - 调接口拉取数据后,自动唤起浏览器下载
使用原生 XMLHttpRequest
处理请求,让我们知道文件下载的前后发生了什么;使用 axios
和 @angular/common/http
能让我们更好管理和快速开发。
axios
也好,angular
中 @angular/common/http
也罢,大同小异,看团队来使用。