web 端如何监听下载进度百分比?

如果需要下载视频,可以通过 xhr 或者是 fetch 来下载,xhr 自带了 api 来监听下载的进度, fetch 也可以使用流式的接口来监听下载进度。

但是这两种方式都有一个问题,就是下载的内容会先保存在内存中,然后再触发下载来保存在磁盘中,这样一来效率比较低,而且文件如果很大那根本就无法实现。

如果是使用传统的下载方式,那么下载进度条只能在浏览器的下载管理中查看。

那怎么可以做到既不先把数据保存在内存中,也能在页面上显示下载进度

阅读 1.7k
avatarAI BotBETA
### 回答

要在 web 端监听下载进度百分比而不先将数据保存在内存中,你可以使用 Service Workers 结合 `fetch` API 的流式下载。Service Workers 在后台运行脚本,独立于网页,能够处理网络请求,包括下载文件。

#### 步骤概述

1. **注册 Service Worker**:
   在你的网页中注册一个 Service Worker,它将处理下载请求。

2. **在 Service Worker 中处理下载**:
   使用 `fetch` API 发起下载请求,并通过 `ReadableStream` 接口处理响应数据。同时,你可以通过 `postMessage` 方法将下载进度信息发送回主线程。

3. **在主线程中更新进度**:
   监听来自 Service Worker 的消息,并更新页面上的下载进度条。

#### 示例代码

**主线程(HTML + JavaScript)**:

<!DOCTYPE html>
<html lang="en">
<head>

<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Download Progress</title>

</head>
<body>

<button id="downloadBtn">Download Video</button>
<progress id="progressBar" value="0" max="100" style="width: 100%;"></progress>

<script>
    if ('serviceWorker' in navigator) {
        navigator.serviceWorker.register('/service-worker.js').then(function(registration) {
            console.log('Service Worker registered with scope:', registration.scope);
        }).catch(function(error) {
            console.log('Service Worker registration failed:', error);
        });
    }

    document.getElementById('downloadBtn').addEventListener('click', function() {
        navigator.serviceWorker.controller.postMessage({ action: 'download', url: 'path/to/video.mp4' });
    });

    navigator.serviceWorker.controller.addEventListener('message', function(event) {
        if (event.data.action === 'progress') {
            document.getElementById('progressBar').value = event.data.progress;
        } else if (event.data.action === 'done') {
            alert('Download complete!');
        }
    });
</script>

</body>
</html>


**Service Worker(service-worker.js)**:

self.addEventListener('message', function(event) {

if (event.data.action === 'download') {
    const url = event.data.url;
    const responseType = 'blob';

    fetch(url)
        .then(response => {
            if (!response.ok) {
                throw new Error('Network response was not ok');
            }
            return response.body.getReader();
        })
        .then(reader => {
            const totalSize = response.headers.get('Content-Length') || 1; // Fallback to 1 to avoid division by zero
            let loaded = 0;

            function read() {
                reader.read().then(({ done, value }) => {
                    if (done) {
                        self.postMessage({ action: 'done' });
                        return;
                    }

                    loaded += value.byteLength;
                    const progress = Math.round((loaded / totalSize) * 100);
                    self.postMessage({ action: 'progress', progress });

                    read(); // Recursive call to read the next chunk
                }).catch(error => {
                    console.error('Error reading stream:', error);
                });
            }

            read();
        })
        .catch(error => {
            console.error('Error downloading file:', error);
        });
}

});


#### 说明

- **Service Worker 注册**:在网页中注册 Service Worker,以便它可以处理后台任务。
- **消息传递**:通过 `postMessage` 方法在 Service Worker 和主线程之间传递消息,包括下载请求和进度更新。
- **流式下载**:使用 `fetch` API 和 `ReadableStream` 接口进行流式下载,避免将整个文件加载到内存中。
- **进度更新**:在 Service Worker 中读取文件流时,计算并发送下载进度到主线程,以便更新页面上的进度条。

这种方法允许你在不将下载内容首先保存在内存中的情况下,在页面上显示下载进度。
1 个回答

使用分片下载结合Service Worker

主线程 (index.js)

负责启动下载过程,处理从Service Worker接收的消息,并更新下载进度或处理下载完成的逻辑。

// 记录已下载的字节数,用于断点续传
let downloadedSize = 0;

// 开始下载函数
function startDownload(url) {
    // 确保 Service Worker 已经注册并准备好
    navigator.serviceWorker.ready.then(registration => {
        // 向 Service Worker 发送消息,包含下载 URL 和分片大小
        registration.active.postMessage({ url, chunkSize: 2 * 1024 * 1024, downloadedSize });
    });
}

// 监听来自 Service Worker 的消息
navigator.serviceWorker.addEventListener('message', event => {
    if (event.data.type === 'progress') {
        // 更新下载进度
        console.log(`Download progress: ${event.data.progress.toFixed(2)}%`);
        document.getElementById('progress').innerText = `Download progress: ${event.data.progress.toFixed(2)}%`;
    } else if (event.data.type === 'complete') {
        // 下载完成,创建下载链接并触发下载
        const link = document.createElement('a');
        link.href = event.data.url;
        link.download = 'video.mp4';
        document.body.appendChild(link);
        link.click();
        document.body.removeChild(link);
    } else if (event.data.type === 'resume') {
        // 更新已下载的字节数,用于断点续传
        downloadedSize = event.data.downloadedSize;
    }
});

// 开始下载
startDownload('video_url');

Service Worker (sw.js)

负责分片下载文件,记录已下载的进度,并将下载进度和完成消息传回主线程。

// 监听来自主线程的消息
self.addEventListener('message', async (event) => {
    const { url, chunkSize, downloadedSize } = event.data;
    // 获取文件总大小
    const response = await fetch(url, { method: 'HEAD' });
    const totalSize = parseInt(response.headers.get('Content-Length'), 10);
    let currentDownloadedSize = downloadedSize;
    const chunks = [];

    // 分片下载文件
    for (let start = downloadedSize; start < totalSize; start += chunkSize) {
        const end = Math.min(start + chunkSize - 1, totalSize - 1);
        const chunk = await fetch(url, {
            headers: { 'Range': `bytes=${start}-${end}` }
        }).then(res => res.blob());
        chunks.push(chunk);
        currentDownloadedSize += chunk.size;

        // 向主线程发送下载进度
        self.clients.matchAll().then(clients => {
            clients.forEach(client => client.postMessage({
                type: 'progress',
                progress: (currentDownloadedSize / totalSize) * 100,
                downloadedSize: currentDownloadedSize
            }));
        });
    }

    // 合并所有分片并创建 Blob
    const blob = new Blob(chunks);
    const link = self.registration.scope + 'video.mp4';
    const cache = await caches.open('video-cache');
    await cache.put(link, new Response(blob));

    // 向主线程发送下载完成消息
    self.clients.matchAll().then(clients => {
        clients.forEach(client => client.postMessage({
            type: 'complete',
            url: link
        }));
    });
});

HTML

负责显示下载进度,并包含主线程的JavaScript代码。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Download Progress</title>
</head>
<body>
    <!-- 显示下载进度 -->
    <div id="progress">Download progress: 0%</div>
    <script src="index.js"></script>
</body>
</html>

功能逻辑说明

  1. 主线程 (index.js)

    • 记录已下载的字节数,用于断点续传。
    • 注册并准备Service Worker。
    • 向Service Worker发送下载请求,包含下载URL和分片大小。
    • 监听来自Service Worker的消息,更新下载进度或处理下载完成。
  2. Service Worker (sw.js)

    • 监听来自主线程的消息,获取下载URL和分片大小。
    • 获取文件总大小,并初始化已下载字节数。
    • 分片下载文件,每次下载一个小块,并记录已下载的字节数。
    • 向主线程发送下载进度消息。
    • 合并所有分片并创建Blob,缓存文件。
    • 向主线程发送下载完成消息。
  3. HTML

    • 显示下载进度。
撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题