vue 大文件切片上传

大文件为什么要切片上传

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

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

项目实际场景

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

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

切片上传原理

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

  • 将每个chunk上传到服务器

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

第一版

  • 先对文件按指定大小进行切片
js 复制代码
 /**
   * 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

js 复制代码
        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
js 复制代码
        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 }
        },
  • 上传文件,这里使用并发上传文件,提升文件上传速度
js 复制代码
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 配置
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
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 试图将大量数据复制到主线程中,导致内存溢出所引起的。

  • 主进程使用
js 复制代码
// 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,如果你们弄出来了,或者知道为什么,可以在下面留言

文章参考

-# 使用 Web Workers

-# C V大法:让你用最简单的方式使用Vue2 + Web Worker + js-xlsx 解析excel数据

-# vue项目中worker的使用及worker内引入第三方库

相关推荐
excel2 分钟前
webpack 核心编译器 十四 节
前端
excel9 分钟前
webpack 核心编译器 十三 节
前端
腾讯TNTWeb前端团队7 小时前
helux v5 发布了,像pinia一样优雅地管理你的react状态吧
前端·javascript·react.js
范文杰10 小时前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪11 小时前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪11 小时前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy11 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom12 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom12 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom12 小时前
React与Next.js:基础知识及应用场景
前端·面试·github