前端实现大文件分片下载
项目中经常遇到下文件下载的情况,网速差的情况下很影响用户体验,可以利用HTTP Renge将文件分若干份并发下载
·
第一步 利用node开发服务端接口
// 获取文件大小
app.get('/length', (req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.end('' + fs.statSync(`${__dirname}/lifivcyc.gif`).size);
});
// 下载
app.get('/', (req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.download('lifivcyc.gif', {
acceptRanges: true
});
});
服务端 配置请求头'Access-Control-Allow-Headers',
下载 接口 配置 'Range' acceptRanges: true
第二步:前端请求头配置
headers: {
Range: `bytes=${rangeStart}-${rangeEnd}`,
},
responseType: 'arraybuffer'
Range 是一个请求首部,告知服务器返回文件的哪一部分。在一个 Range 首部中,可以一次性请求多个部分,服务器会以 multipart 文件的形式将其返回。如果服务器返回的是范围响应,需要使用 206 Partial Content 状态码。假如所请求的范围不合法,那么服务器会返回 416 Range Not Satisfiable 状态码,表示客户端错误。服务器允许忽略 Range 首部,从而返回整个文件,状态码用 200 。
通过 Range 的 header 告诉服务端下载哪一部分内容
第三步 使用promise.all 发请求,合并请求结果
1 根据文件大小,每次请求大小,遍历生成多个请求
const downloadTask = [];
for (let i = 1; i <= chunkNum; i++) {
const rangeStart = chunkSize * (i - 1);
const rangeEnd = chunkSize * i - 1;
downloadTask.push(axios.get(path, {
headers: {
Range: `bytes=${rangeStart}-${rangeEnd}`,
},
responseType: 'arraybuffer'
}))
}
2 并发请求
// map 返回新数组
const arrayBuffers = await Promise.all(downloadTask.map(task => {
return task.then(res => res.data)
}))
return mergeArrayBuffer(arrayBuffers);
3 合并返回结果
function mergeArrayBuffer(arrays) {
let totalLen = 0;
for (let arr of arrays) {
totalLen += arr.byteLength;
}
let offset = 0
for (let arr of arrays) {
let uint8Arr = new Uint8Array(arr)
console.log(uint8Arr)
//在类型化数组中储存多个值,从特定数组中读取输入。
//移量参数 offset 指定从什么地方开始使用源数组 array 的值进行写入操作。
// ES2017 最新语法 创建初始化为 0 的,包含 length 个元素的无符号整型数组
let res = new Uint8Array(totalLen)
res.set(uint8Arr, offset)
offset += arr.byteLength
}
return res.buffer
}
4 转为blob数据
const blob = new Blob([res]);
// 拿到最终数据后,创建链接
const url = URL.createObjectURL(blob);
四 全部代码
1.服务端
const express = require('express');
const fs = require('fs');
const app = express();
// 获取文件大小
app.get('/length', (req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.end('' + fs.statSync(`${__dirname}/lifivcyc.gif`).size);
});
// 预检请求 options
app.options('/', (req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Headers', 'Range');
res.end('');
});
// 下载
app.get('/', (req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.download('lifivcyc.gif', {
acceptRanges: true
});
});
app.listen(3006, () => {
console.log(`server is running at port 3006`);
});
2.web端
<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://www.unpkg.com/axios@1.3.5/dist/axios.min.js"></script>
</head>
<body>
<img id="img" />
<script>
async function concurrencyDownload(path, size, chunkSize) {
let chunkNum = Math.ceil(size / chunkSize);
const downloadTask = [];
for (let i = 1; i <= chunkNum; i++) {
const rangeStart = chunkSize * (i - 1);
const rangeEnd = chunkSize * i - 1;
downloadTask.push(axios.get(path, {
headers: {
Range: `bytes=${rangeStart}-${rangeEnd}`,
},
responseType: 'arraybuffer'
}))
}
// map 返回新数组
const arrayBuffers = await Promise.all(downloadTask.map(task => {
return task.then(res => res.data)
}))
return mergeArrayBuffer(arrayBuffers);
}
function mergeArrayBuffer(arrays) {
let totalLen = 0;
for (let arr of arrays) {
totalLen += arr.byteLength;
}
let offset = 0
for (let arr of arrays) {
let uint8Arr = new Uint8Array(arr)
console.log(uint8Arr)
//在类型化数组中储存多个值,从特定数组中读取输入。
//移量参数 offset 指定从什么地方开始使用源数组 array 的值进行写入操作。
// ES2017 最新语法 创建初始化为 0 的,包含 length 个元素的无符号整型数组
let res = new Uint8Array(totalLen)
res.set(uint8Arr, offset)
offset += arr.byteLength
}
return res.buffer
}
(async function () {
// 请求文件大小
const { data: len } = await axios.get('http://localhost:3006/length');
// 下载文件
const res = await concurrencyDownload('http://localhost:3006', len, 300000);
// 转为blob数据
const blob = new Blob([res]);
// 拿到最终数据后,创建链接
const url = URL.createObjectURL(blob);
img.src = url;
})();
</script>
</body>
</html>
更多推荐
所有评论(0)