前后端分离以后,对于纯前端来说后端的就是个黑盒,尤其文件的分段下载,我们的大脑在想他到底是咋实现的?如何一个视频,下载多少播放多少的?面试的时候也是说的云里雾里,面试官听的也是云里雾里,算了,不琢磨了,自己实现一下看看不就知道了么,是不是?
(声明:本人的测试代码很粗鄙,我就要用简单粗暴的东西,把原来看着很难得东西展示给大家,降低大家得学习成本,如果你们喜欢学习高大上的代码,请移步就好!因为本人深知高大上的弊端就是看不懂,浪费时间!)
第一步,初始化一个项目,并且简单实现前后端分离
1.创建一个文件夹,进去以后执行 npm init -y, 在项目里建立两个文件夹:server,src,server写node,src写前端。目的就是自己启个服务自己请求,自己用,行不行?
此时你开启了本地的前端和后端,不信就访问看看
简单吗?用最简单粗暴的代码实现了前后端分离。最好自己动手试试看,在看下面的代码。
第二步,单片下载
能不能基于上次下载的地方接着下载,也就是断点续传呢?可以的,HTTP 里有这部分协议,就是 range 相关的 header。就是说你可以通过 Range 的 header 告诉服务端下载哪一部分内容。
比如:Range: bytes=200-1000 下载200-1000 字节的内容, 之后它就会返回 206 的状态码,等待下次继续下载。直到出现Range: bytes=200- 表示下载结束了,一般情况下我们会这样设置:
Range: bytes=200-1000, 2000-6576, 19000-
测试如下:我们只下载 test.txt 文件的前5个字节,如下
不想下载axios就用这个:www.unpkg.com/axios@1.3.5… 都可以的,但是我劝大家下载下,因为大多数时候,我们都是在react,vue的框架下面编码的,而且 webpack这样的打包工具帮我们自动处理了文件的引用,导致很多前端编码五六年,离开框架和打包工具,都不知道如何写前端代码了,这就是个弊端。
道理就不讲了,测试看看:
这次并没有把整个test.txt 文件下载下来,而是下载了5个字节,对不对?
我们下载5以后的所有字符看看
查看结果,下图对应上面三个请求
第三步,多片下载
试试:Range 多段设置 Range: 'bytes=0-4,5-20,21-',刷新页面,居然报错了,是个跨域错误。
刚才都没有报错,现在居然报错了?为啥?什么鬼啊,头顶上方飘出十万个草泥马在奔跑。
烦死了,最讨厌找各种莫名其妙无法理解的错误,刚才3002的端口也跨域了,怎么没有说我不能跨域?现在报,我咋知道咋办?设置下CORS?
想想一下为啥之前不报跨域问题。现在报了吧。原因如下:一般情况下,如果非同源地址发送请求的时候,有个预请求过程,预检请求是 options 请求。一般浏览器会在三种情况下发送预检(preflight)请求:
·用到了非 GET、POST 的请求方法,比如 PUT、DELETE 等,会发预检请求看看服务端是否支持
·用到了一些非常规请求头,比如用到了 Content-Type,会发预检请求看看服务端是否支持
·用到了自定义 header,会发预检请求
上面的三种情况也不符合我们的状况呀,我们用的是 Range: 'bytes=0-4, 5-20, 20-',仔细研究你发现多Range的时候,浏览器把我们的Content-Type改变了,原来默认是:text/plain; charset=UTF-8 , 后面变成了:multipart/byteranges,所以就有了跨域提示,那就cors支持下:
app.options('/', (req, res, next) => { res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Headers', 'Range') res.end(''); });
看了下express的文档,发现他居然不支持多段下载。什么鬼!
他觉得你可以通过多发几个请求获得,没有必要搞个多段的,无语。。。。。。。。。。。。。。。
那就研究下分片下载,我们重头戏来了
用 range 来实现下文件的分片下载,最终合并成一个文件的功能。是不是就是平常我们大文件的下载模式?
下载个美女图片看看
启动index.html以后,点击按钮你就发现它请求了两次,第一次居然加载了一般图片
别急你离看到美女已经很近了,接下来我们要做的事将分好的两块图片合在一起,显示出来,不就成一个图片了么,我们可以用 Promise.all 来并行请求,但是如果想要拼接的话,响应值应该是二进制结果才行是不是,不然你请求到刚才的半个图片,怎么合并?
responseType: 'arraybuffer'
这个响应配置出来就是个二进制结果,试试看
行不行?接下来他们怎么合并呀?每个 arraybuffer 都创建一个对应的 Uint8Array 对象,我们是不是可以创建一个很大的 Uint8Array对象,然后把他们俩合并到这个大对象里面去,是不是就可以拼接成功,出来一个大美女了?
代码如下:
解释下,就是发送2次请求,按照reponseType为arraybuffer的格式返回数据,返回的数据是一个对象,它里面包含 Uint8Array格式的数据,所以我们用获取它就好了,其他数据不要了。现在获取到了2个Uint8Array数组,按照这2个Uint8Array的大小之和创建一个大的空Uint8Array数组,然后用set方法,把他们塞进去,一张大图片就搞好了。美女来袭!
第四步 优化
再粗鄙的测试代码,主要目标是为了在正式开发的时候能用到它,是不?咱们使用的时候,可以各自优化。比如这样:
index.html:
<!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'
}))
}
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 res = new Uint8Array(totalLen)
let offset = 0
for (let arr of arrays) {
let uint8Arr = new Uint8Array(arr)
res.set(uint8Arr, offset)
offset += arr.byteLength
}
return res.buffer
}
(async function() {
const { data: len } = await axios.get('http://localhost:3000/length');
const res = await concurrencyDownload('http://localhost:3000', len, 300000);
console.log(res)
const blob = new Blob([res]);
const url = URL.createObjectURL(blob);
img.src =url;
})();
</script>
</body>
</html>
server/index.js
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('./guangguang.png').size);
})
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('guangguang.png', {
acceptRanges: true
})
})
app.listen(3000, () => {
console.log(`server is running at port 3000`)
})
就是我通过/length的接口拿到这个文件总Range,你想把它分成几个文件就除几,最后得出一个近似相等的片段出来,然后分别获取数据,最后用createObjectURL合并下。
有没有发现,如果我一开始就把第四步的优化代码抛出来,你能明白我在搞啥吗?代码简单易懂,目的是为了从根本上学会它,不是停留在花里胡哨的封装上。