📢 自定义下载文件进度条
产品经理:浏览器下载的进度条,用户体验一点也不好,实现一个自定义下载进度条吧。
还给我列举了几条好处:
-
增强用户体验:自定义进度条可以让用户更直观地了解下载或上传的进度,从而增强用户的体验。
-
增加网站吸引力:在一些需要等待较长时间的操作中,如视频播放、文件下载等,添加一个美观的进度条可以增加网站的吸引力和用户留存率。
-
当看到下载进度条逐渐填满时,用户会感到一种成就感和满足感,这会让我们更加愿意继续使用这个服务。
哎,干不过人家,赶紧想想怎么实现,那么它就来了
先来一个下载按钮再来一个进度条
<button id="downloadBtn">下载</button>
<progress id="progressBar" value="0" max="100"></progress>
再随便来点样式吧!!!
progress {
width: 100%;
height: 20px;
border: 1px solid #ddd;
border-radius: 8px;
background-color: #f5f5f5;
}
progress::-webkit-progress-bar {
background-color: #f5f5f5;
border-radius: 8px;
}
progress::-webkit-progress-value {
background-color: #0077ff;
border-radius: 8px;
}
最后长这样 ↓
原理
其实就是让请求返回二进制数据,监听progress进度事件,计算出已加载数据的百分比,展示下载过程,最后在调用浏览器的下载。
实现方式都有注释,就不在赘述啦。
XMLHttpRequest
使用 XMLHttpRequest 进行文件下载并显示自定义进度条的示例代码:
const downloadBtn = document.getElementById('downloadBtn');
const progressBar = document.getElementById('progressBar');
downloadBtn.addEventListener('click', () => {
downloadFile();
});
function downloadFile() {
let downloadURL = "../../file/dy.mp4"; // 下载链接
let xhr = new XMLHttpRequest(); // 创建 XMLHttpRequest 对象
xhr.open("GET", downloadURL, true); // 打开请求链接
xhr.responseType = 'blob'; // 设置响应类型为二进制数据
xhr.onload = function() {
if (xhr.status === 200) {
console.log(xhr.response);
let blob = xhr.response; // 获取响应数据
let link = document.createElement('a'); // 创建一个 a 标签
link.href = window.URL.createObjectURL(blob); // 设置下载链接
link.download = 'dy.mp4'; // 设置下载文件名
link.click(); // 模拟用户单击下载链接
}
};
xhr.onprogress = function(event) {
console.log(event);
// lengthComputable 表示ProgressEvent 所关联的资源是否具有可以计算的长度
if (event.lengthComputable) {
// 计算下载进度
const percent = (event.loaded / event.total) * 100;
// 更新进度条
document.getElementById("progressBar").value = percent;
}
};
xhr.send(); // 发送请求
}
fetch API
下面是使用 fetch API 进行文件下载并显示自定义进度条的示例代码:
const downloadBtn = document.getElementById('downloadBtn');
const progressBar = document.getElementById('progressBar');
const fileUrl = '../../file/dy.mp4';
downloadBtn.addEventListener('click', () => {
downloadFile(fileUrl, (percent) => {
// 更新进度条
document.getElementById("progressBar").value = percent;
}).then((blob) => {
// 保存文件
const fileName = 'dy.mp4';
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = fileName;
link.click();
}).catch((err) => {
console.error(err);
});
});
function downloadFile(url, onProgress) {
return new Promise((resolve, reject) => {
// https://developer.mozilla.org/zh-CN/docs/Web/API/Request
const req = new Request(url, {
method: 'GET',
mode: 'cors'
});
// https://developer.mozilla.org/zh-CN/docs/Web/API/Response
fetch(req).then((response) => {
if (response.status !== 200) {
reject(new Error(`下载文件失败。状态: ${response.status}`));
}
const contentLength = response.headers.get('Content-Length');
if (!contentLength) {
reject(new Error('无法确定文件的内容长度'));
}
const totalBytes = parseInt(contentLength, 10);
// https://developer.mozilla.org/zh-CN/docs/Web/API/Response/body
const reader = response.body.getReader();
let bytesReceived = 0;
let chunks = [];
reader.read().then(function processResult(result) {
if (result.done) {
const blob = new Blob(chunks);
resolve(blob);
return;
}
chunks.push(result.value);
bytesReceived += result.value.length;
const percent = (bytesReceived / totalBytes) * 100;
onProgress(percent);
// 不停的调用获取流数据,详见上方MDN
return reader.read().then(processResult);
});
}).catch((err) => {
reject(err);
});
});
} }).catch((err) => {
console.error(err);
});
}
上面都例子使用的都是本地文件返回的Content-Type都是文件类型(如下图)Content-Length也存在所有可以计算出进度,如果返回的是Content-Type: application/octet-stream二进制流,Content-Length不一定存在,所以需要下载的时候知道文件大小size,才能计算进度。
Content-Type: application/octet-stream 表示该响应的实体内容是一个未知的二进制流,通常用于没有明确的 MIME 类型或是不关心 MIME 类型的情况。这种类型的响应一般无法被浏览器直接展示,需要通过相关的应用程序进行解析和处理。
对于 Content-Type: application/octet-stream 的响应,可能会有 Content-Length 字段来表示实体内容的长度,也可以使用 Transfer-Encoding: chunked 方式来传输实体内容,这两种方式都可以实现传输。
需要注意的是,在使用 Transfer-Encoding: chunked 的情况下,Content-Length 字段可能会被省略,因为实体内容的长度是通过分块传输实现的,具体是否需要 Content-Length 字段取决于服务器端的实现。
性能
使用上面这种方式下载大文件时可能会长时间占用主线程,导致页面响应变慢甚至无响应。
那么有没有一种方式像浏览器默认下载那样不占用我们的主线程呢?
当然有WebWorker,为了解决这个问题,可以使用Web Worker在后台线程上执行文件下载,并通过postMessage API与主线程通信更新进度条。这样可以避免在主线程上执行长时间操作,提高页面的响应性能。
WebWorker
需要注意的是,由于 Web Workers 是运行在后台线程中的独立 JavaScript 代码片段,所以 Web Workers 中的代码对于浏览器中的资源访问权限是受限的。出于安全考虑,浏览器会通过 CORS
等机制来限制 Web Workers 对于跨域资源的访问。如果要发起跨域请求,需要对服务器的响应进行相应的处理,并且需要在服务器允许的情况下执行跨域请求。
在主线程中,我们可以监听 Web Workers 的消息,并更新下载进度条。
主线程JavaScript代码:
const downloadBtn = document.getElementById('downloadBtn');
const progressBar = document.getElementById('progressBar');
downloadBtn.addEventListener('click', () => {
// 文件下载链接
const downloadUrl = '../../file/dy.mp4';
// 创建Web Worker
// const worker = new Worker('./worker-XMLHttpRequest.js');
const worker = new Worker('./worker-fetch.js');
//发送消息 触发下载
worker.postMessage(downloadUrl);
worker.onmessage = (event) => {
if (event.data.type === 'progress') {
// 更新进度条宽度
progressBar.value = event.data.percent;
} else if (event.data.type === 'done') {
// 创建下载链接
const downloadLink = document.createElement('a');
downloadLink.href = window.URL.createObjectURL(event.data.blob);
downloadLink.download = 'dy.mp4';
document.body.appendChild(downloadLink);
// 触发下载
downloadLink.click();
}
};
});
在上述代码中,我们首先创建了一个 Web Workers,然后通过 addEventListener
方法监听消息,并在回调函数中更新下载进度条。如果收到了 done
消息,就表示文件下载完成。
worker-XMLHttpRequest.js
self.addEventListener("message", (event) => {
const downloadUrl = event.data;
const xhr = new XMLHttpRequest();
xhr.open("GET", downloadUrl, true);
xhr.responseType = "blob";
xhr.onprogress = (event) => {
console.log(event);
if (event.lengthComputable) {
// 计算进度百分比
const percent = (event.loaded / event.total) * 100;
// 发送进度消息给主线程
self.postMessage({ type: "progress", percent });
}
};
xhr.onload = (event) => {
if (xhr.status === 200) {
// 发送下载完成消息和文件Blob给主线程
self.postMessage({ type: "done", blob: xhr.response });
}
};
xhr.send();
});
worker-fetch.js
// 在 Web Workers 中下载文件并发送进度
self.addEventListener("message", function (e) {
const url = e.data;
fetch(url).then((response) => {
const contentLength = response.headers.get("Content-Length");
const totalBytes = parseInt(contentLength, 10);
const reader = response.body.getReader();
let bytesReceived = 0;
let chunks = [];
reader.read().then(function processResult(result) {
if (result.done) {
const blob = new Blob(chunks);
self.postMessage({ type: "done", blob: blob});
return;
}
chunks.push(result.value);
bytesReceived += result.value.length;
const percent = (bytesReceived / totalBytes) * 100;
self.postMessage({ type: "progress", percent, totalBytes });
return reader.read().then(processResult);
});
});
});
Fetch API 这个下载还是又必要在说明一下,在上述代码中,我们在 Web Workers 中监听 message
事件,并在事件处理函数中执行下载任务。首先通过 Fetch API 发起了一个 HTTP GET 请求,请求的 URL 是通过 postMessage
方法传递过来的参数。
随后,我们通过 response.headers.get('content-length')
获取了响应中的 content-length
头部信息,并使用 parseInt
方法将它转换成数字格式。通过这个方法,我们可以知道整个文件的大小,从而用于计算下载进度。
接着,我们通过 response.body.getReader()
获取了一个 ReadableStream 对象,然后通过 read()
方法开始读取数据。在 read()
方法的回调函数中,我们累加 result.value.length
,然后通过 postMessage
发送下载进度。最后,如果读取结束,我们就发送一个 done
消息。
需要注意的是,在实际的项目中,需要考虑更多的异常情况,例如网络错误、下载失败等。
使用其他http封装框原理都是一样的没有啥差别!!!
附:Angular实现
/**
* 显示进度条的下载
* @param url 下载链接
* @param name 文件名称
* @param size 文件大小
*/
downloadProgress(url: string = "", name: string = "", size: number = 1) {
let progress = 0; // 下载进度
this.http
.get(url, {
responseType: "blob",
observe: "events",
reportProgress: true,
})
.subscribe(
(event: HttpEvent <any> ) => {
console.log(event);
if (event.type === HttpEventType.DownloadProgress) {
// 兼容未返回total的oss不返回
const total = event.total ? event.total : size;
// 计算下载进度
progress = Math.round((100 / total) * event.loaded);
console.log("progress:", progress);
} else if (event.type === HttpEventType.Response) {
const blob = new Blob([event.body], {
type: "application/octet-stream",
}); // 获取响应数据
const objUrl = window.URL.createObjectURL(blob);
this.downloadDirectly(objUrl, name);
progress = 0; // 重置进度条
}
},
(error) => {
console.error("Download error:", error);
},
);
}
如果你觉得我的文章写得不好,欢迎指出来并提出建议。我会尽力改进文章质量,以便更好地为提升自己。当然,如果你只是想逗我开心,也欢迎继续吐槽,我会愉快地接受的!!!
转载自:https://juejin.cn/post/7239282069750202425