前言

最近遇到一个需求,需要将批量选择的图片,批量一个个下载。

触发单个下载

在浏览器中触发下载,我们可以借用 a 元素来触发。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const downloadFile = async (url: string, name: string) => {
const res = await fetch(url);
const blob = await res.blob();

const strList = url.split('.');
const type = strList[strList.length - 1];

const downUrl = window.URL.createObjectURL(blob);
const downloadElement = document.createElement('a');
document.body.appendChild(downloadElement);
downloadElement.href = downUrl;
downloadElement.download = `${name}.${type}`;
downloadElement.click();
document.body.removeChild(downloadElement); // 下载完成移除元素
window.URL.revokeObjectURL(downUrl);
};

批量触发

单个实现了,批量就 promise 来批量执行就好了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
export const batchDownloadFile = async (files: { url: string; name: string; }[], showLoading = true) => {
if (showLoading) {
// 全局loading,可以忽略
loading.value = ElLoading.service({
lock: true,
text: '正在下载中...',
background: 'rgba(0, 0, 0, 0.7)',
});
}

const promiseList = files.map((file) => {
return new Promise(async (resolve, reject) => {
try {
downloadFile(file.url, file.name)
resolve(null);
} catch {
reject('下载错误');
}
});
});

const result = await Promise.allSettled(promiseList);

if (showLoading) {
loading.value?.close();
}

// 返回结果
return result.map((item, index) => {
return {
isSuccess: item.status === 'fulfilled',
name: files[index].name,
url: files[index].url,
};
});
};

使用 allSettled 而不用 all 是因为,Promise.all 是有一个被拒绝就会被拒绝,不符合部分成功的情况,所以这里使用的是 allSettled。

浏览器限制

对于一次性(即一次宏任务的 event loop)触发下载,浏览器会限制一次触发 a 元素下载最多 10 次。
对于下载选择的图片超过 10 张的情况下,要使用分批来下载。

使用 setTimeout 来绕过限制

既然一次宏任务限制 10 个,那么就借用 setTimeout 来分批下载即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
export const batchDownloadFile = async (files: { url: string; name: string; }[], showLoading = true) => {
const batchSize = 10;
const delay = 1000;

if (showLoading) {
loading.value = ElLoading.service({
lock: true,
text: '正在下载中...',
background: 'rgba(0, 0, 0, 0.7)',
});
}

const result: { isSuccess: boolean; name: string; url: string; }[] = [];

const downloadBatch = async (batch: { url: string; name: string; }[]) => {
const promiseList = batch.map((file) => {
return new Promise(async (resolve, reject) => {
try {
await downloadFile(file.url, file.name);
resolve(null);
} catch {
reject('下载错误');
}
});
});

const batchResult = await Promise.allSettled(promiseList);
batchResult.forEach((item, index) => {
result.push({
isSuccess: item.status === 'fulfilled',
name: batch[index].name,
url: batch[index].url,
});
});
};

const delayPromise = (ms: number) => new Promise((resolve) => { setTimeout(resolve, ms); });

// 这里使用递归去执行分批下载
const processBatches = async (remainingFiles: { url: string; name: string; }[]) => {
if (remainingFiles.length === 0) {
return;
}

const batch = remainingFiles.slice(0, batchSize);
const remaining = remainingFiles.slice(batchSize);

await downloadBatch(batch);
await delayPromise(delay);
await processBatches(remaining);
};

await processBatches(files);

if (showLoading) {
loading.value?.close();
}

return result;
};

改动后,顺利实现了批量下载超过 10 个文件。
result