1.解决思路
利用head里面的range限制下载范围。把一个大文件,分多次小文件下载,最后进行合并。
HTTP 范围请求
HTTP 协议范围请求允许服务器只发送 HTTP 消息的一部分到客户端。范围请求在传送大的媒体文件,或者与文件下载的断点续传功能搭配使用时非常有用。如果在响应中存在 Accept-Ranges 首部(并且它的值不为 “none”),那么表示该服务器支持范围请求。
在一个 Range 首部中,可以一次性请求多个部分,服务器会以 multipart 文件的形式将其返回。如果服务器返回的是范围响应,需要使用 206 Partial Content 状态码。假如所请求的范围不合法,那么服务器会返回 416 Range Not Satisfiable 状态码,表示客户端错误。服务器允许忽略 Range 首部,从而返回整个文件,状态码用 200 。
Range 语法
Range: <unit>=<range-start>-
Range: <unit>=<range-start>-<range-end>
Range: <unit>=<range-start>-<range-end>, <range-start>-<range-end>, <range-start>-<range-end>
unit:范围请求所采用的单位,通常是字节(bytes)。<range-start>:一个整数,表示在特定单位下,范围的起始值。<range-end>:一个整数,表示在特定单位下,范围的结束值。这个值是可选的,如果不存在,表示此范围一直延伸到文档结束。
例子:
# 一次请求单个
curl http://i.imgur.com/z4d4kWk.jpg -i -H "Range: bytes=0-1023"
# 一次请求多个
curl http://www.example.com -i -H "Range: bytes=0-50, 100-150"
2.流程
- 请求后端,获取文件大小
- 根据放回的文件大小,计算文件分片
- 使用并发下载分片 (使用asyncpool并发控制)
- 分片下载后转出Uint8Array
- 执行合并
- 利用BolbURL保存图片
3.实现
//下载 主入口函数 url为下载的参数
function multiThreadedDownload(url) {
if (!url || !/https?/.test(url)) return;
console.log("多线程下载开始: " + +new Date());
download({
url,
chunkSize: 0.1 * 1024 * 1024,
poolLimit: 6,
}).then((buffers) => {
console.log("多线程下载结束: " + +new Date());
saveAs({ buffers, name: "我的压缩包", mime: "application/zip" });//6.利用BolbURL保存图片
});
}
//并发下载分片代码
async function download({ url, chunkSize, poolLimit = 1 }) {
const contentLength = await getContentLength(url);//1.先获取文件大小
const chunks = typeof chunkSize === "number" ? Math.ceil(contentLength / chunkSize) : 1;//2.根据放回的文件大小,计算文件分片
//3.使用并发下载分片
const results = await asyncPool(
poolLimit,
[...new Array(chunks).keys()],
(i) => {
let start = i * chunkSize;
let end = i + 1 == chunks ? contentLength - 1 : (i + 1) * chunkSize - 1;
return getBinaryContent(url, start, end, i);
}
);
const sortedBuffers = results
.map((item) => new Uint8Array(item.buffer));//4.分片下载后转出Uint8Array
return concatenate(sortedBuffers); //5.执行合并
}
//下载后,保存文件
function saveAs({ name, buffers, mime = "application/octet-stream" }) {
const blob = new Blob([buffers], { type: mime });
const blobUrl = URL.createObjectURL(blob);
const a = document.createElement("a");
a.download = name || Math.random();
a.href = blobUrl;
a.click();
URL.revokeObjectURL(blob);//Object URL 是一种伪协议 只能在本地识别
}
//`ArrayBuffer` 对象转换为 `Uint8Array` 对象
function concatenate(arrays) {
if (!arrays.length) return null;
let totalLength = arrays.reduce((acc, value) => acc + value.length, 0);
let result = new Uint8Array(totalLength); //需要先把 `ArrayBuffer` 对象转换为 `Uint8Array` 对象,才能操作
let length = 0;
for (let array of arrays) {
result.set(array, length);
length += array.length;
}
return result;
}
//根据参数 发起有范围的下载请求,一次一个
function getBinaryContent(url, start, end, i) {
return new Promise((resolve, reject) => {
try {
let xhr = new XMLHttpRequest();
xhr.open("GET", url, true);
xhr.setRequestHeader("range", `bytes=${start}-${end}`); // 请求头上设置范围请求信息
xhr.responseType = "arraybuffer"; // 设置返回的类型为arraybuffer ArrayBuffer 对象用来表示通用的、固定长度的原始二进制数据缓冲区
xhr.onload = function () {
resolve({
index: i, // 文件块的索引
buffer: xhr.response, // 范围请求对应的数据
});
};
xhr.send();
} catch (err) {
reject(new Error(err));
}
});
}
//并发控制
async function asyncPool(poolLimit, array, iteratorFn) {
const ret = []; // 存储所有的异步任务
const executing = []; // 存储正在执行的异步任务
for (const item of array) {
// 调用iteratorFn函数创建异步任务
const p = Promise.resolve().then(() => iteratorFn(item, array));
ret.push(p); // 保存新的异步任务
// 当poolLimit值小于或等于总任务个数时,进行并发控制
if (poolLimit <= array.length) {
// 当任务完成后,从正在执行的任务数组中移除已完成的任务
const e = p.then(() => executing.splice(executing.indexOf(e), 1));
executing.push(e); // 保存正在执行的异步任务
if (executing.length >= poolLimit) {
await Promise.race(executing); // 等待较快的任务执行完成
}
}
}
return Promise.all(ret);
}
//通过head方式 或者文件大小 Content-Length
function getContentLength(url) {
return new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest();
xhr.open("HEAD", url);
xhr.send();
xhr.onload = function () {
resolve(
xhr.getResponseHeader("Content-Length")
);
};
xhr.onerror = reject;
});
}