面试官:如何实现大文件切片上传?

公众号:程序员白特,关注我,每天进步一点点~

前端上传文件很大时,会出现各种问题,比如连接超时了,网断了,都会导致上传失败,这个时候就需要将文件切片上传,下面我们就来学习一下如何使用vue实现大文件切片上传吧

大文件为什么要切片上传

前端上传文件很大时,会出现各种问题,比如连接超时了,网断了,都会导致上传失败;

服务端限制了单次上传文件的大小;

项目实际场景

客户端需要上传一个算法包文件到服务器,这个算法包实测 3.7G

nginx配置文件 上传文件大小最大值为100M

切片上传原理

通过file.slice将大文件chunks切成许多个大小相等的chunk

将每个chunk上传到服务器

服务端接收到许多个chunk后,合并为chunks

第一版

先对文件按指定大小进行切片

/**
  * file: 需要切片的文件
  * chunkSize: 每片文件大小,1024*1024=1M
  */
chunkSlice(file, chunkSize) {
   const chunks = [],
       size = file.size,
       total = Math.ceil(size / chunkSize)
   for (let i = 0; i < size; i += chunkSize) {
       chunks.push({
           total,
           blob: file.slice(i, i + chunkSize),
       })
   }
   return chunks
}

处理切片后的文件,后端想要我传给他一个json对象,所以使用readAsDataURL读取文件

这里使用了一个插件spark-md5来生成每个切片的MD5

async handleFile(chunks) {
    const res = []
    for (const item of chunks) {
        const { bytes, md5 } = await this.addMark(item.blob)
        item.blob = bytes
        item.md5 = md5
        res.push(md5)
    }
    return res
},
// 使用FileReader读取每一片数据,并生成MD5编码
async addMark(chunk) {
    return new Promise((resolve, reject) => {
        const reader = new FileReader()
        const spark = new SparkMD5()

        reader.readAsDataURL(chunk)
        reader.onload = function (e) {
            const bytes = e.target.result
            spark.append(bytes)

            const md5 = spark.end()
            resolve({ bytes, md5 })
        }
    })
},

组装数据,包括每一片的排列顺序index,总共切了多少片total,文件IDfileID,每一片的md5编码md5,每一片数据fileData

mergeData(chunks) {
    const fileId = this.getUUID()
    const data = []
    for (let i = 0; i < chunks.length; i++) {
        const obj = {
            fileId,
            fileData: chunks[i].blob,//每片切片的数据
            fileIndex: i + 1,//每片数据索引
            fileTotal: chunks[i].total + '',
            md5: chunks[i].md5,
        }

        data.push(obj)
    }
    return { data, fileId }
},

上传文件,这里使用并发上传文件,提升文件上传速度

const chunks = chunkSlice(file,1024*1024)
this.handleFile(chunks)
const data = this.mergeData(chunks)

for(let i = 0; i < data.length; i++){
    this.uplload(data[i])
}

第一版遇到的问题

文件太大,切片太小,上传接口的timeout太短,并发请求时,全都在pendding,导致请求出错

第一版问题解决

对上传文件接口的timeout修改,调整时长,大一点

限制每次并发的数量,我用的是500个每次

第二版,切片 + web worker

为什么要使用web worker

在生成文件MD5编码时,需要读文件,是一个I/O操作,会阻塞页面,文件太大,导致页面卡死

将耗时操作转移到worker线程,主页面就不会卡住

vue2,使用worker

yarn add worker-loader

vue.config.js 配置

// vue.config.js
chainWebpack(config) {
    config.module.rule('worker')
        .test(/\.worker\.js$/)
        .use('worker-loader')
        .loader('worker-loader')
        // .options({ inline: 'fallback' })// 这个配置是个坑,不要加
},

新建file.worker.js

// file.worker.js
import SparkMD5 from 'spark-md5'

const chunkSlice = (file, chunkSize) => {
    const chunks = [],
        size = file.size,
        total = Math.ceil(size / chunkSize)
    for (let i = 0; i < size; i += chunkSize) {
        chunks.push({
            total,
            blob: file.slice(i, i + chunkSize),
        })
    }
    return chunks
}
const handleFile = async (chunks) => {
    const res = []
    for (const item of chunks) {
        const { bytes, md5 } = await addMark(item.blob)
        item.blob = bytes
        item.md5 = md5
        res.push(md5)
    }
    return res
}
const addMark = (chunk) => {
    return new Promise((resolve, reject) => {
        const reader = new FileReader()
        const spark = new SparkMD5()

        reader.readAsDataURL(chunk)
        reader.onload = function (e) {
            const bytes = e.target.result
            spark.append(bytes)

            const md5 = spark.end()
            resolve({ bytes, md5 })
        }
    })
}
const mergeData = (chunks, fileName, options) => {
    const fileId = getUUID() // 这里更好的方式是读整个文件的 MD5
    const data = []
    for (let i = 0; i < chunks.length; i++) {
        const obj = {
            ...options,
            suffix: '.tar.gz',
            fileId,
            fileName,
            fileData: chunks[i].blob,
            fileIndex: i + 1 + '',
            fileTotal: chunks[i].total + '',
            md5: chunks[i].md5,
        }

        data.push(obj)
    }
    return { data, fileId }
}
const getUUID = () => {
    return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) =>
        (c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16)
    )
}
const dataSlice = (data, step, fileId) => {
    const total = Math.ceil(data.length / step)
    let index = 1
    for (let i = 0; i < data.length; i += step) {
        const params = {
            type: 'workerFile',
            index,
            total,
            fileId,
            data: data.slice(i, i + step),
        }
        self.postMessage(params)
        index++
    }
}
self.addEventListener('error', (event) => {
    console.log('worker error', event)
})

self.addEventListener('message', async (event) => {
    // 确保接受的是我想要的消息  
    if (!event.data.type) return
    if (event.data.type != 'file') return
    console.log('worker success', event)

    const { file, chunkSize } = event.data
    const chunks = chunkSlice(file, chunkSize)
    const allMD5 = await handleFile(chunks)
    console.log(allMD5)
    // 此处 allMD5 可用来做后续的断点续传
    const { data, fileId } = mergeData(chunks, file.name)
    
    // 这里对处理好的数据进行切片,分片传递给主线程,是由于 Web Worker 试图将大量数据复制到主线程中,会导致内存溢出。
    dataSlice(data, 100, fileId)

})

这个报错一般是在使用 JavaScript Web Worker 时出现的,通常是由于 Web Worker 试图将大量数据复制到主线程中,导致内存溢出所引起的。

主进程使用

// xxx.vue文件
import Worker from '@/utils/worker/file.worker.js'


const worker = new Worker()
worker.postMessage({ type: 'file', file: this.curFile, chunkSize: 1024 * 1024 })

worker.onerror = (error) => {
    console.log('main error', error)
    worker.terminate()
}

const finalData = []
worker.onmessage = async (event) => {
    console.log('main success', event)
    if (event.data.type != 'workerFile') return

    const fileId = mergeWorkerData(finalData, event.data)
    if (fileId) {
        worker.terminate()

        const status = await stepLoad(finalData, 500)

        if (!status) {
            this.$message.error('文件上传失败')
        } else {
            this.$message.success('文件上传成功')

        }

    }
}

mergeWorkerData = (res, params) => {
    res.push(...params.data)
    return params.index == params.total ? params.fileId : false
}

const stepLoad = async (data, step) => {
    const res = []
    for (let i = 0; i < data.length; i += step) {
        res.push(data.slice(i, i + step))
    }
    for (const item of res) {
        const chunkRes = await Promise.all(item.map((v) => this.$api.upload(v)))
        if (chunkRes.some((v) => v.httpCode != 0)) {
            return false
        }

        const isEnd = chunkRes.filter((v) => v.finish)
        if (isEnd.length) {
            return true
        }
    }
}

总结

worker引入脚本或三方库可以使用importScript(),但是我没弄成功,一使用importScript()就会报错,Renference: importScript() xxxxxxxxxxxx,如果你们弄出来了,或者知道为什么,可以在下面留言~

#前端##机械只有转码才有出路吗?##软件开发薪资爆料##2022届毕业生现状#
全部评论
打个压缩包,然后git拉可以吗?(不懂开发
点赞 回复 分享
发布于 06-05 20:34 辽宁
这里还能遇到白特大佬😍
点赞 回复 分享
发布于 06-24 11:57 江苏

相关推荐

360服务端&nbsp;面经360服务器开发—golang为主&nbsp;&nbsp;一面&nbsp;&nbsp;7.5&nbsp;&nbsp;全程45min1.自我介绍 2.上段实习(ps:上段实习也是网安行业的,问的比较细)3.golang并发优势4.gmp和gc(ps:回答较为全面)😬😬😬gmp从单进程讲到多进程/多线程,gmp的设计原理和优势,为什么等等gc从算法到gc阶段到写屏障等等5.服务器优化方案ps:当时简单回答了看CPU使用率,看MySQL慢查询日志,用普罗米修斯去监控数据库6.接口性能优化ps:好多方案,大概举例了11种,等我整理链接放在评论区7.es为什么那么快(es问的比较深,好久没看了)答:倒排索引,然后举例讲了一下怎么倒排的,内存数据库,举例高度分页会导致查询速度变低,json数据格式,数据存储小,不确定对不对8.高必发场景下数据如何平滑写入es9.MySQL索引等10.redis数据结构和场景11.redis,hll用过吗答:用过,是一种概率基数统计算法,统计网站PV和UV,同一个ip下注册用户数量等。360集团2025届校招内推【内推码】ES3C3K安全、算法、开发、大数据、运营、职能等十类方向,百余种岗位!&nbsp;北京&nbsp;/上海/深圳等多座城市任米哈游你选择早投早offer!【内推码】ES3C3K【内推码】ES3C3K【内推码】ES3C3K【内推网申链接】https://360campus.zhiye.com/campus/jobs?shareId=92cfb7e8-2ae3-49dc-b960-cf7ce3c1a6c7&amp;shareSource=2投递后查询阶段状态:https://neitui.italent.cn/360campus/candidate【福利待遇】&nbsp;部门团建:每月可享受&nbsp;150&nbsp;元&nbsp;/人的团队活动基金&nbsp;免费班车&nbsp;带薪病假:&nbsp;每月可享受一天带薪病假,当月有效不累计&nbsp;带薪年假:新入职员工即有每年10天的年假&nbsp;餐费补贴:&nbsp;每天可享有35元餐费补贴或者在食堂就餐(不分休息日和节假日均可就餐哦)&nbsp;六险一金:&nbsp;公积金12%顶格缴纳,补充商业保险&nbsp;免费健身:&nbsp;有免费的健身房和浴室&nbsp;免费按摩:&nbsp;有免费的按摩室,按摩师傅给你按摩、艾灸、电疗、拔罐&nbsp;年度体检:&nbsp;每年一次免费参加身体健康体检&nbsp;大家投递完可以在评论区打上姓名缩写+岗位,我来确认有没有内推成功喽
360集团
|
校招
|
26个岗位
点赞 评论 收藏
分享
11-13 15:41
已编辑
南京信息工程大学 Java
一面(主管面)1.&nbsp;自我介绍2.&nbsp;讲实习项目3.&nbsp;java方法传参是值拷贝还是引用拷贝4.&nbsp;String为什么不可变&nbsp;5.&nbsp;ArrayList和LinkedList使用场景6.&nbsp;final修饰的方法影响重载吗7.&nbsp;HashMap做本地缓存需要考虑什么&nbsp;8.&nbsp;ThreadLocal底层原理9.&nbsp;项目中具体怎么用的threadlocal10.&nbsp;讲讲jvm内存结构11.&nbsp;电商网站选用什么垃圾回收器比较好12.&nbsp;索引失效有哪些情况13.&nbsp;什么是回表14.&nbsp;慢SQL可能的原因,如何排查15.&nbsp;left&nbsp;join和right&nbsp;join区别16.&nbsp;kafka的消息丢失和重复消费问题17.&nbsp;xxljob相比timer的优势18.&nbsp;Nacos在项目中怎么用的19.&nbsp;讲下手写的rpc20.&nbsp;如何确保消息安全性21.&nbsp;服务端解析出请求交给线程池还是直接执行22.&nbsp;反问:校招流程以及中国制造网业务二面(技术面)1.&nbsp;期望薪资多少,有其他offer吗2.&nbsp;找工作看哪些城市3.&nbsp;什么是动态规划4.&nbsp;动态规划的优势是什么5.&nbsp;动态规划和分治法相同点和区别6.&nbsp;为什么不找AI相关的工作&nbsp;7.&nbsp;你觉得研究生期间AI的学习经历对你有帮助吗8.&nbsp;可以来提前实习吗9.&nbsp;了解敏捷开发吗10.&nbsp;单元测试用过JUnit吗11.&nbsp;了解设计模式和面向对象吗12.&nbsp;看java代码片段,说结果和时间复杂度(考点java值拷贝)13.&nbsp;算法题,说思路(一个滑动窗口、一个双指针)14.&nbsp;你觉得这些算法在开发中有用吗(我说我没遇到过,埋下隐患)15.&nbsp;电商场景题(需要用到动态规划算法解决)16.&nbsp;反问:校招名额17.&nbsp;时长:38min三面(CTO面)1.&nbsp;自我介绍2.&nbsp;高考怎么报的志愿3.&nbsp;考研怎么选的学校4.&nbsp;自定义分布式全局ID生成器怎么实现的5.&nbsp;缓存架构怎么设计的6.&nbsp;json和protobuf的理解&nbsp;7.&nbsp;对java的理解8.&nbsp;未来职业规划9.&nbsp;期望薪资10.&nbsp;什么时候可以实习11.&nbsp;对我的建议:说技术可以更全面些;做选择可以多问问同龄人或学长12.&nbsp;CTO面总结:聊天为主,交流了很多对技术的理解13.&nbsp;时长:50min11.5感谢信。应该是薪资要高了😅除了二面都是线下,0offer选手发面经攒人品 #面经#
查看99道真题和解析
点赞 评论 收藏
分享
7 19 评论
分享
牛客网
牛客企业服务