HOOOS

巧用 Service Worker,轻松实现断点续传和多线程下载?这几个技巧你得知道!

0 4 技术小能手 Service Worker断点续传多线程下载
Apple

Service Worker,这个听起来有点神秘的技术,其实离我们的生活并不遥远。很多网页应用之所以能像原生App一样流畅,甚至在离线状态下也能使用,Service Worker 功不可没。今天,咱们就来聊聊如何利用 Service Worker 实现一个强大的文件下载管理功能,让你的网页应用也能拥有断点续传、多线程下载的能力!

为啥要用 Service Worker 做下载?

传统的浏览器下载方式,用户体验上总有些欠缺。比如,一旦网络中断,下载就得重头开始;大文件下载速度慢,只能干等着。Service Worker 的出现,为我们提供了更多可能性:

  • 断点续传:即使网络中断,下次也能从上次停止的地方继续下载,省时省力。
  • 多线程下载:将文件分割成多个部分,同时下载,显著提升下载速度。
  • 离线支持:即使在离线状态下,也能继续管理已下载的文件,或者显示下载进度。
  • 后台下载:即使关闭网页,下载任务也能在后台继续进行。

核心技术点:Streams API + Range 请求

要实现这些功能,我们需要用到两个关键技术:

  • Streams API:用于处理文件流,可以方便地读取、写入和转换数据。
  • Range 请求:HTTP 协议中的一个特性,允许客户端请求资源的某个部分,用于实现断点续传。

实战演练:一步步实现下载管理

接下来,咱们就一步步地来实现一个基于 Service Worker 的文件下载管理功能。

1. 注册 Service Worker

首先,我们需要在网页中注册 Service Worker。在你的主 JavaScript 文件中,添加以下代码:

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js')
    .then(registration => {
      console.log('Service Worker 注册成功:', registration);
    })
    .catch(error => {
      console.log('Service Worker 注册失败:', error);
    });
}

这段代码会检查浏览器是否支持 Service Worker,如果支持,就注册名为 /sw.js 的 Service Worker 文件。

2. 编写 Service Worker 代码 (sw.js)

接下来,我们需要编写 Service Worker 的代码,处理下载请求。以下是一个简单的示例:

self.addEventListener('fetch', event => {
  // 拦截下载请求
  if (event.request.url.includes('/download')) {
    event.respondWith(handleDownload(event.request));
  }
});

async function handleDownload(request) {
  // 获取文件 URL
  const url = new URL(request.url);
  const fileUrl = url.searchParams.get('url');

  // 获取 Range 请求头
  const range = request.headers.get('range');
  let start = 0;
  if (range) {
    start = Number(range.replace(/bytes=/, '').split('-')[0]);
  }

  // 发起请求,获取文件信息
  const response = await fetch(fileUrl, { headers: { range: `bytes=${start}-` } });

  // 获取文件总大小
  const contentLength = response.headers.get('content-length');
  const total = Number(contentLength);

  // 创建 ReadableStream
  const stream = new ReadableStream({
    async start(controller) {
      const reader = response.body.getReader();
      let downloaded = start;

      try {
        while (true) {
          const { done, value } = await reader.read();
          if (done) {
            break;
          }

          // 将数据写入 Stream
          controller.enqueue(value);
          downloaded += value.length;

          // 发送下载进度
          self.clients.matchAll().then(clients => {
            clients.forEach(client => {
              client.postMessage({ type: 'downloadProgress', url: fileUrl, downloaded, total });
            });
          });
        }

        controller.close();
      } catch (error) {
        console.error('下载出错:', error);
        controller.error(error);
      } finally {
        reader.releaseLock();
      }
    }
  });

  // 返回 Response
  return new Response(stream, {
    headers: {
      'Content-Type': 'application/octet-stream',
      'Content-Disposition': `attachment; filename="${getFileName(fileUrl)}"`,
      'Accept-Ranges': 'bytes',
      'Content-Range': `bytes ${start}-${total - 1}/${total}`
    },
    status: 206, // Partial Content
    statusText: 'Partial Content'
  });
}

function getFileName(url) {
  return url.substring(url.lastIndexOf('/') + 1);
}

这段代码做了以下几件事:

  • 拦截 /download 请求:当网页发起包含 /download 的请求时,Service Worker 会拦截该请求,并调用 handleDownload 函数进行处理。
  • 获取文件 URL 和 Range:从请求中获取文件 URL 和 Range 请求头,确定下载的起始位置。
  • 发起 Range 请求:使用 Range 请求头向服务器请求文件的指定部分。
  • 创建 ReadableStream:使用 Streams API 创建一个 ReadableStream,用于读取文件流。
  • 发送下载进度:通过 postMessage API 向网页发送下载进度信息。
  • 返回 Response:返回一个包含文件流的 Response,设置正确的 Content-Type、Content-Disposition 和 Content-Range 头。

3. 网页端处理下载请求和进度

在网页端,我们需要创建一个下载链接,并监听 Service Worker 发送的下载进度信息。

<a id="downloadLink" href="#">下载文件</a>
<progress id="progressBar" value="0" max="100"></progress>
<script>
  const downloadLink = document.getElementById('downloadLink');
  const progressBar = document.getElementById('progressBar');

  downloadLink.addEventListener('click', (event) => {
    event.preventDefault();
    const fileUrl = '你的文件URL'; // 替换为实际的文件 URL
    const downloadUrl = `/download?url=${encodeURIComponent(fileUrl)}`;
    window.location.href = downloadUrl;
  });

  navigator.serviceWorker.addEventListener('message', event => {
    if (event.data.type === 'downloadProgress') {
      const { url, downloaded, total } = event.data;
      // 更新进度条
      if (url === '你的文件URL') { // 替换为实际的文件 URL
        const progress = Math.floor((downloaded / total) * 100);
        progressBar.value = progress;
      }
    }
  });
</script>

这段代码做了以下几件事:

  • 创建下载链接:创建一个 <a> 标签作为下载链接,点击时会触发下载。
  • 监听点击事件:监听下载链接的点击事件,阻止默认行为,并创建一个包含 /download 的 URL,触发 Service Worker 的拦截。
  • 监听下载进度:监听 Service Worker 发送的 downloadProgress 消息,更新进度条的显示。

4. 实现断点续传

要实现断点续传,我们需要在网页端保存已下载的文件大小,并在下次下载时,将该大小作为 Range 请求头的起始位置发送给服务器。可以使用 localStorageIndexedDB 来保存已下载的大小。

以下是一个使用 localStorage 实现断点续传的示例:

// 在网页端保存已下载的大小
function saveDownloadedSize(url, downloaded) {
  localStorage.setItem(`downloadedSize_${url}`, downloaded);
}

// 在网页端获取已下载的大小
function getDownloadedSize(url) {
  const downloadedSize = localStorage.getItem(`downloadedSize_${url}`);
  return downloadedSize ? Number(downloadedSize) : 0;
}

// 修改 Service Worker 代码,在发送请求前获取已下载的大小
async function handleDownload(request) {
  // 获取文件 URL
  const url = new URL(request.url);
  const fileUrl = url.searchParams.get('url');

  // 获取已下载的大小
  const downloadedSize = getDownloadedSize(fileUrl);

  // 获取 Range 请求头
  const range = request.headers.get('range');
  let start = downloadedSize; // 使用已下载的大小作为起始位置
  if (range) {
    start = Number(range.replace(/bytes=/, '').split('-')[0]);
  }

  // ... 其他代码不变 ...

  // 在下载过程中,保存已下载的大小
  self.clients.matchAll().then(clients => {
    clients.forEach(client => {
      client.postMessage({ type: 'downloadProgress', url: fileUrl, downloaded, total });
      saveDownloadedSize(fileUrl, downloaded); // 保存已下载的大小
    });
  });

  // ... 其他代码不变 ...
}

5. 实现多线程下载

要实现多线程下载,我们需要将文件分割成多个部分,同时向服务器请求这些部分。可以使用 Promise.all 来并发请求多个文件块。

以下是一个简单的实现多线程下载的示例:

async function handleDownload(request) {
  // 获取文件 URL
  const url = new URL(request.url);
  const fileUrl = url.searchParams.get('url');

  // 获取文件信息
  const response = await fetch(fileUrl, { method: 'HEAD' });
  const contentLength = response.headers.get('content-length');
  const total = Number(contentLength);

  // 设置线程数
  const threads = 4;

  // 计算每个线程下载的大小
  const chunkSize = Math.ceil(total / threads);

  // 创建 Promise 数组
  const promises = [];

  // 循环创建线程
  for (let i = 0; i < threads; i++) {
    const start = i * chunkSize;
    const end = Math.min((i + 1) * chunkSize - 1, total - 1);

    // 创建请求
    const promise = fetch(fileUrl, { headers: { range: `bytes=${start}-${end}` } })
      .then(response => response.arrayBuffer())
      .then(buffer => ({ start, buffer }));

    promises.push(promise);
  }

  // 并发请求多个文件块
  const results = await Promise.all(promises);

  // 合并文件块
  const mergedBuffer = new Uint8Array(total);
  results.forEach(({ start, buffer }) => {
    mergedBuffer.set(new Uint8Array(buffer), start);
  });

  // 创建 Response
  return new Response(mergedBuffer, {
    headers: {
      'Content-Type': 'application/octet-stream',
      'Content-Disposition': `attachment; filename="${getFileName(fileUrl)}"`,
      'Accept-Ranges': 'bytes'
    }
  });
}

优化和注意事项

  • 错误处理:在下载过程中,可能会出现各种错误,例如网络中断、服务器错误等。我们需要添加适当的错误处理机制,例如重试、暂停、取消等。
  • 缓存管理:Service Worker 具有缓存功能,可以缓存下载的文件。我们需要合理地管理缓存,避免缓存过期或占用过多空间。
  • 用户体验:在下载过程中,我们需要提供良好的用户体验,例如显示下载进度、速度、剩余时间等。可以使用第三方库,例如 progressbar.js 来创建漂亮的进度条。
  • 安全性:Service Worker 运行在 HTTPS 环境下,可以保证下载的安全性。但是,我们需要注意防止恶意代码注入和跨站脚本攻击。

总结

Service Worker 为我们提供了强大的文件下载管理能力,可以实现断点续传、多线程下载等功能,提升用户体验。虽然实现起来稍微复杂一些,但只要掌握了 Streams API 和 Range 请求等核心技术,就能轻松应对各种下载场景。希望这篇文章能帮助你更好地理解和使用 Service Worker,打造更强大的网页应用!

点评评价

captcha
健康