前端小王hs:
清华大学出版社《后台管理实践——Vue.js+Express.js》作者
网络工程师 前端工程师 项目经理 阿里云社区博客专家
email: 337674757@qq.com
vx: 文章最下方有vx链接
资料/交流群: vx备注前端
面对需要上传文件的需求,首先会考虑到直接上传
直接上传带来的潜在问题就是一次性把这么多的内容存到内存,会造成内存溢出,直接的感受就是网页卡了,甚至自动关闭浏览器
一般一个标签页被分配的内存也就几百MB
,即使JavaScript
有内存回收机制,但上传一个多达几个G
的文件就显得捉襟见肘了
文件大,上传慢
所以下一个思路就是——分片上传
分片之前,要想每片多大?然后是一共多少片?所以对应下面两行代码:
const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB
const chunkCount = Math.ceil(file.size / CHUNK_SIZE);
接着就是进行切片了,从哪里开始切?切到哪里?怎么切?(file
的slice
方法)核心代码就是三行:
const start = index * chunkSize;
const end = Math.min(start + chunkSize, file.size);
const blob = file.slice(start, end);
然后就是切完返回(不考虑上传),代码如下:
const promises = [];
for (let i = 0; i < chunkCount; i++) {
promises.push(uploadChunk(file, i, CHUNK_SIZE, filename));
}
这样就得到了总的切片数量
如果手写,那么需要对file
对象的方法slice
有所了解,其实前端发展到现在,基本上常见的需求都有对应的方法,只是如果没了解,就容易没有方向,好在AI
出现了,不会的问一下就知道了
分片上传的核心在于,并不一次性把文件都存到内存,这样也仅仅是避免了内存溢出的问题,核心在于分片能够充分利用带宽,例如10
个分片,通过结合异步,就可以发起10
个http
请求,如下图所示:
但如果是串行执行,可以看到时间是一条直线的,如下图所示:
对应代码是:
const promises = [];
for (let i = 0; i < chunkCount; i++) {
promises.push(await uploadChunk(file, i, CHUNK_SIZE, filename));
}
原因就是执行完一次uploadChunk
才能执行下一次uploadChunk
所以这里要做一个优化,并行发起请求,代码如下所示:
const promises = [];
for (let i = 0; i < chunkCount; i++) {
// 没await
promises.push(uploadChunk(file, i, CHUNK_SIZE, filename));
}
try {
const results = await Promise.all(promises); // 统一返回结果
} catch (err) {
console.error("Some chunks failed to upload:", err);
}
但其实可以发现,整个时间还是非常慢的,如下图所示:
所以问题不在串行上传又或是并行上传,在于CPU
计算切片的时间太长了
这里有个问题,JavaScript
是单线程,是怎么看似"同时"发起这么多网络请求的?因为网络请求是由浏览器发出的,跟JavaScript
没多大关系,另外,异步,就是发起了之后JavaScript
不管了,直到有结果即计算完或者传输完,然后再通知主线程
关于异步,很多同学学了promise
也好,学了async await
也好,不知道什么时候用,其实当你觉得你这个逻辑,会阻塞到主线程了,那么就可以用了,那另外一个角度就是,当你发现有A
、B
两个代码块,B
的重要性大于A
,但是A
在B
之前执行,你发现执行起来非常慢,那就让异步去处理A
,主线程继续执行B
,当然这只是一个例子,事实上B
比A
重要完全可以放在A
之前是吧
await
的作用就是等待一个promise
的结果后,才会继续执行下面的逻辑
那么下一个需要解决的问题就是怎么减少计算切片的时间,切片不和网络请求一样,切片是在JavaScript
主线程进行的。可以在timeEnd
下加多一个代码,如下所示:
console.timeEnd('cutFile');
console.log('done');
上传后可以发现log
是在上传后才输出的,符合主线程的流程,如下图所示:
所以现在的思路就是,那一个线程这么慢,能否利用其他的线程?答案就是worker
了,将切片的逻辑交给worker
,多线程同时切片,所以时间就降下来了
最后就是上传,这里不再叙述
然后是后端,后端的主要问题就是接收之后怎么拼接的问题
逻辑比前端简单,拿到分片后,给分片标记好,即如下代码:
const chunkFilename = `${filename}.part${chunkIndex}`; // 构造分片文件名
const destPath = path.join(CHUNKS_DIR, filename); // 目标路径(按文件名分类)
写入存放分片的目录,即:
// 确保目标目录存在
fs.ensureDirSync(destPath);
// 写入分片到磁盘
fs.writeFileSync(path.join(destPath, chunkFilename), req.file.buffer);
然后合并,逻辑就是mergeFile
里的了,读取分片,写入一个目标文件,代码如下:
// 循环读取并写入所有分片
for (let i = 0; i < totalChunks; i++) {
const chunkPath = path.join(chunkFolder, `${filename}.part${i}`); // 分片路径
const data = await fs.readFile(chunkPath); // 读取分片内容
writeStream.write(data); // 写入到目标文件
}
结束后把原来的目录删掉,这里不再叙述
如果说手写的关键是什么,是对API
的掌握,不管是file
、还是blob
,后端的fs
等等,都是考验基础能力
这就是分片上传的一个基本的思路
欢迎关注csdn前端领域博主: 前端小王hs,喜欢可以点个赞!您的支持是我不断更新的动力!🔥🔥🔥
前端小王hs:
清华大学出版社《后台管理实践——Vue.js+Express.js》作者
网络工程师 前端工程师 项目经理 阿里云社区博客专家
email: 337674757@qq.com
vx: 文章最下方有vx链接
资料/交流群: vx备注前端