前端上传大文件怎么处理

前端上传大文件怎么处理

背景

当我们在做文件的导入功能的时候,如果导入的文件过大,可能会导所需要的时间够长,且失败后需要重新上传,我们需要前后端结合的方式解决这个问题

思路

我们需要做几件事情如下:

  • 对文件做切片,即将一个请求拆分成多个请求,每个请求的时间就会缩短,且如果某个请求失败,只需要重新发送这一次请求即可,无需从头开始
  • 通知服务器合并切片,在上传完切片后,前端通知服务器做合并切片操作
  • 控制多个请求的并发量,防止多个请求同时发送,造成浏览器内存溢出,导致页面卡死
  • 做断点续传,当多个请求中有请求发送失败,例如出现网络故障、页面关闭等,我们得对失败的请求做处理,让它们重复发送

实现

前端

示例代码仓库

仓库地址:
https://gitee.com/huangjinlin2022/resolve_upload_large_file

步骤1-切片,合并切片

在JavaScript中,文件FIle对象是Blob对象的子类,Blob对象包含一个重要的方法slice通过这个方法,我们就可以对二进制文件进行拆分,具体代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=s, initial-scale=1.0">
    <title>Document</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/axios/0.24.0/axios.min.js"></script>
</head>
<body>
    <input type="file" id="fileInput">
    <button id="uploadBtn">上传</button>
</body>
<script>
// 请求基准地址
axios.defaults.baseURL = 'http://localhost:3000'
// 选中的文件
var file = null
// 选择文件
document.getElementById('fileInput').onchange = function({target: {files}}){
    file = files[0] 
}
// 开始上传
document.getElementById('uploadBtn').onclick = async function(){
    if (!file) return
    // 创建切片   
    // let size = 1024 * 1024 * 10 //10MB 切片大小
    let size = 1024 * 50  //50KB 切片大小
    let fileChunks = []
    let index = 0 //切片序号
    for(let cur = 0; cur < file.size; cur += size){
        fileChunks.push({
            hash: index++,
            chunk: file.slice(cur, cur + size)
        })
    }
    // 上传切片
    const uploadList = fileChunks.map((item, index) => {
        let formData = new FormData()
        formData.append('filename', file.name)
        formData.append('hash', item.hash)
        formData.append('chunk', item.chunk)
        return axios({
            method: 'post',
            url: '/upload',
            data: formData
        })
    })
    await Promise.all(uploadList)
    // 合并切片
    await axios({
        method: 'get',
        url: '/merge',
        params: {
            filename: file.name
        }
    });
    console.log('上传完成')
}
</script>
</html>

步骤2-并发控制

结合Promise.race和异步函数实现,多个请求同时并发的数量,防止浏览器内存溢出,具体代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=s, initial-scale=1.0">
    <title>Document</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/axios/0.24.0/axios.min.js"></script>
</head>
<body>
    <input type="file" id="fileInput">
    <button id="uploadBtn">上传</button>
</body>
<script>
// 请求基准地址
axios.defaults.baseURL = 'http://localhost:3000'
// 选中的文件
var file = null
// 选择文件
document.getElementById('fileInput').onchange = function({target: {files}}){
    file = files[0] 
}
// 开始上传
document.getElementById('uploadBtn').onclick = async function(){
    if (!file) return
    // 创建切片   
    // let size = 1024 * 1024 * 10; //10MB 切片大小
    let size = 1024 * 50 //50KB 切片大小
    let fileChunks = []
    let index = 0 //切片序号
    for(let cur = 0; cur < file.size; cur += size){
        fileChunks.push({
            hash: index++,
            chunk: file.slice(cur, cur + size)
        });
    }
    // 控制并发
    let pool = []//并发池
    let max = 3 //最大并发量
    for(let i=0;i<fileChunks.length;i++){
        let item = fileChunks[i]
        let formData = new FormData()
        formData.append('filename', file.name)
        formData.append('hash', item.hash)
        formData.append('chunk', item.chunk)
        // 上传切片
        let task = axios({
            method: 'post',
            url: '/upload',
            data: formData
        })
        task.then((data)=>{
            //请求结束后将该Promise任务从并发池中移除
            let index = pool.findIndex(t=> t===task)
            pool.splice(index)
        })
        pool.push(task)
        if(pool.length === max){
            //每当并发池跑完一个任务,就再塞入一个任务
            await Promise.race(pool)
        }
    }
    //所有任务完成,合并切片
    await axios({
        method: 'get',
        url: '/merge',
        params: {
            filename: file.name
        }
    });
    console.log('上传完成')
}
</script>
</html>

步骤3-断点续传

在单个请求失败后,触发catch的方法的时候,讲当前请求放到失败列表中,在本轮请求完成后,重复对失败请求做处理,具体代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=s, initial-scale=1.0">
    <title>Document</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/axios/0.24.0/axios.min.js"></script>
</head>
<body>
    <input type="file" id="fileInput">
    <button id="uploadBtn">上传</button>
</body>
<script>
// 请求基准地址
axios.defaults.baseURL = 'http://localhost:3000'
// 选中的文件
var file = null
// 选择文件
document.getElementById('fileInput').onchange = function({target: {files}}){
    file = files[0] 
}
// 开始上传
document.getElementById('uploadBtn').onclick = function(){
    if (!file) return;
    // 创建切片   
    // let size = 1024 * 1024 * 10; //10MB 切片大小
    let size = 1024 * 50; //50KB 切片大小
    let fileChunks = [];
    let index = 0 //切片序号
    for(let cur = 0; cur < file.size; cur += size){
        fileChunks.push({
            hash: index++,
            chunk: file.slice(cur, cur + size)
        })
    }
    // 控制并发和断点续传
    const uploadFileChunks = async function(list){
        if(list.length === 0){
            //所有任务完成,合并切片
            await axios({
                method: 'get',
                url: '/merge',
                params: {
                    filename: file.name
                }
            });
            console.log('上传完成')
            return
        }
        let pool = []//并发池
        let max = 3 //最大并发量
        let finish = 0//完成的数量
        let failList = []//失败的列表
        for(let i=0;i<list.length;i++){
            let item = list[i]
            let formData = new FormData()
            formData.append('filename', file.name)
            formData.append('hash', item.hash)
            formData.append('chunk', item.chunk)
            // 上传切片
            let task = axios({
                method: 'post',
                url: '/upload',
                data: formData
            })
            task.then((data)=>{
                //请求结束后将该Promise任务从并发池中移除
                let index = pool.findIndex(t=> t===task)
                pool.splice(index)
            }).catch(()=>{
                failList.push(item)
            }).finally(()=>{
                finish++
                //所有请求都请求完成
                if(finish===list.length){
                    uploadFileChunks(failList)
                }
            })
            pool.push(task)
            if(pool.length === max){
                //每当并发池跑完一个任务,就再塞入一个任务
                await Promise.race(pool)
            }
        }
    }
    uploadFileChunks(fileChunks)

}
</script>
</html>

后端

步骤1.安装依赖

npm i express@4.17.2
npm i multiparty@4.2.2

步骤2.接口实现

const express = require('express')
const multiparty = require('multiparty')
const fs = require('fs')
const path = require('path')
const { Buffer } = require('buffer')
// 上传文件最终路径
const STATIC_FILES = path.join(__dirname, './static/files')
// 上传文件临时路径
const STATIC_TEMPORARY = path.join(__dirname, './static/temporary')
const server = express()
// 静态文件托管
server.use(express.static(path.join(__dirname, './dist')))
// 切片上传的接口
server.post('/upload', (req, res) => {
    const form = new multiparty.Form();
    form.parse(req, function(err, fields, files) {
        let filename = fields.filename[0]
        let hash = fields.hash[0]
        let chunk = files.chunk[0]
        let dir = `${STATIC_TEMPORARY}/${filename}`
        // console.log(filename, hash, chunk)
        try {
            if (!fs.existsSync(dir)) fs.mkdirSync(dir)
            const buffer = fs.readFileSync(chunk.path)
            const ws = fs.createWriteStream(`${dir}/${hash}`)
            ws.write(buffer)
            ws.close()
            res.send(`${filename}-${hash} 切片上传成功`)
        } catch (error) {
            console.error(error)
            res.status(500).send(`${filename}-${hash} 切片上传失败`)
        }
    })
})
//合并切片接口
server.get('/merge', async (req, res) => {
    const { filename } = req.query
    try {
        let len = 0
        const bufferList = fs.readdirSync(`${STATIC_TEMPORARY}/${filename}`).map(hash => {
            const buffer = fs.readFileSync(`${STATIC_TEMPORARY}/${filename}/${hash}`)
            len += buffer.length
            return buffer
        });
        //合并文件
        const buffer = Buffer.concat(bufferList, len);
        const ws = fs.createWriteStream(`${STATIC_FILES}/${filename}`)
        ws.write(buffer);
        ws.close();
        res.send(`切片合并完成`);
    } catch (error) {
        console.error(error);
    }
})

server.listen(3000, _ => {
    console.log('http://localhost:3000/')
})

其他实现

如果使用腾讯云或阿里云文件上传的服务,它们提供了npm库,例如腾讯云的cos-js-sdk-v5,它自身提供的切片相关的配置。

转载作品,原作者:https://juejin.cn/post/7053658552472174605,文章来源:

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022年1月17日 16:40
下一篇 2022年1月19日 16:07

相关推荐

  • 推荐一款免费的网页打印控件

    做过网页打印的应该都清楚,如果想使用js直接调用本地打印机进行打印任务都需要基于打印控件,因为js不能直接调用打印机,即使是google浏览器也是内置的打印控件。 网页打印控件比较…

    JavaScript 2021年12月16日
    01660
  • CSS终于在2024年增加了垂直居中功能align-content:center

    CSS终于在 2024 年增加了垂直居中功能align-content: center,CSS 对齐一般会使用 flexbox 或 grid 布局,因为 align-content 在默认的流式布局中不起作用。在 2024 年,浏览器实现了 align-content。

    2024年9月2日
    0890
  • 解决访问国外公共静态资源速度慢的问题

    由于众所周知的原因,jsdelivr国内被墙,部分地区可能无法正常访问,其他国外的公共静态资源也存在访问速度慢或无法正常访问的问题,为解决该问题,辉哥博客&蓝易云安全开发了国外静态资源js和css资源CDN替换国内的脚本,供所有用户免费使用。

    2022年6月29日
    01550
  • CSS代码判断实现PC端隐藏 手机端显示

    CSS代码判断实现PC端隐藏 手机端显示,有些内容部署在PC端但是有不适合在手机端显示(比如盒子过大,遮挡内容)或者手机端显示毫无意义等。我们可以使用下面的代码来实现:电脑端显示,手机端隐藏

    Html/CSS 2021年11月25日
    05000
  • vue3打印解决方案:Vue-Plugin-HiPrint

    Vue-Plugin-HiPrint 是一个Vue.js的插件,旨在提供一个简单而强大的打印解决方案。通过 Vue-Plugin-HiPrint,您可以轻松地在Vue.js应用程序中实现高度定制的打印功能。但是本文只简单介绍 Vue-Plugin-HiPrint 在vue3中如何使用固定模板打印的使用方法,以便可以快速集成并使用它。

    2023年12月15日
    04140
  • uniapp小程序自定义头部导航区域

    uniapp项目小程序中难免会有一些页面需要自定义头部导航区域,因为原生的头部太简陋了,本文记录下uniapp自定义小程序的头部导航。

    2024年8月6日 前端开发
    0730

发表回复

登录后才能评论
分享本页
返回顶部