likes
comments
collection
share

📢 自定义下载文件进度条

作者站长头像
站长
· 阅读数 45

产品经理:浏览器下载的进度条,用户体验一点也不好,实现一个自定义下载进度条吧。

还给我列举了几条好处:

  1. 增强用户体验:自定义进度条可以让用户更直观地了解下载或上传的进度,从而增强用户的体验。

  2. 增加网站吸引力:在一些需要等待较长时间的操作中,如视频播放、文件下载等,添加一个美观的进度条可以增加网站的吸引力和用户留存率。

  3. 当看到下载进度条逐渐填满时,用户会感到一种成就感和满足感,这会让我们更加愿意继续使用这个服务。

哎,干不过人家,赶紧想想怎么实现,那么它就来了

先来一个下载按钮再来一个进度条

<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
评论
请登录