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 请求头的起始位置发送给服务器。可以使用 localStorage
或 IndexedDB
来保存已下载的大小。
以下是一个使用 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,打造更强大的网页应用!