面试官:你如何实现大文件上传

news2024/11/25 8:19:47

提到大文件上传,在脑海里最先想到的应该就是将图片保存在自己的服务器(如七牛云服务器),保存在数据库,不仅可以当做地址使用,还可以当做资源使用;或者将图片转换成base64,转换成buffer流,但是在javascript这门语言中不存在,但是这些只适用于一些小图片,对于大文件还是束手无策。

一、问题分析

如果将大文件一次性上传,会发生什么?想必都遇到过在一个大文件上传、转发等操作时,由于要上传大量的数据,导致整个上传过程耗时漫长,更有甚者,上传失败,让你重新上传!这个时候,我已经咬牙切齿了。先不说上传时间长久,毕竟上传大文件也没那么容易,要传输更多的报文,丢包也是常有的事,而且在这个时间段万不可以做什么其他会中断上传的操作;其次,前后端交互肯定是有时间限制的,肯定不允许无限制时间上传,大文件又更容易超时而失败....

一、解决方案

既然大文件上传不适合一次性上传,那么将文件分片散上传是不是就能减少性能消耗了。

没错,就是分片上传。分片上传就是将大文件分成一个个小文件(切片),将切片进行上传,等到后端接收到所有切片,再将切片合并成大文件。通过将大文件拆分成多个小文件进行上传,确实就是解决了大文件上传的问题。因为请求时可以并发执行的,这样的话每个请求时间就会缩短,如果某个请求发送失败,也不需要全部重新发送。

二、具体实现

1、前端

(1)读取文件

准备HTML结构,包括:读取本地文件(input类型为file)、上传文件按钮、上传进度。

<input type="file" id="input">
<button id="upload">上传</button>
<!-- 上传进度 -->
<div style="width: 300px" id="progress"></div>
复制代码

JS实现文件读取:

监听inputchange事件,当选取了本地文件后,打印事件源可得到文件的一些信息:

let input = document.getElementById('input')
let upload = document.getElementById('upload')
let files = {}//创建一个文件对象
let chunkList = []//存放切片的数组
​
// 读取文件
input.addEventListener('change', (e) => {
    files = e.target.files[0]
    console.log(files);
    
    //创建切片
    //上传切片
})
复制代码

观察控制台,打印读取的文件信息如下:

(2)创建切片

文件的信息包括文件的名字,文件的大小,文件的类型等信息,接下来可以根据文件的大小来进行切片,例如将文件按照1MB或者2MB等大小进行切片操作:

// 创建切片
function createChunk(file, size = 2 * 1024 * 1024) {//两个形参:file是大文件,size是切片的大小
    const chunkList = []
    let cur = 0
    while (cur < file.size) {
        chunkList.push({
                file: file.slice(cur, cur + size)//使用slice()进行切片
        })
        cur += size
    }
    return chunkList
}
复制代码

切片的核心思想是:创建一个空的切片列表数组chunkList,将大文件按照每个切片2MB进行切片操作,这里使用的是数组的Array.prototype.slice()方法,那么每个切片都应该在2MB大小左右,如上文件的大小是8359021,那么可得到4个切片,分别是[0,2MB]、[2MB,4MB]、[4MB,6MB]、[6MB,8MB]。调用createChunk函数,会返回一个切片列表数组,实际上,有几个切片就相当于有几个请求。

调用创建切片函数:

//注意调用位置,不是在全局,而是在读取文件的回调里调用
chunkList = createChunk(files)
console.log(chunkList);
复制代码

观察控制台打印的结果:

(3)上传切片

上传切片的个关键的操作:

第一、数据处理。需要将切片的数据进行维护成一个包括该文件,文件名,切片名的对象,所以采用FormData对象来进行整理数据。FormData 对象用以将数据编译成键值对,可用于发送带键数据,通过调用它的append()方法来添加字段,FormData.append()方法会将字段类型为数字类型的转换成字符串(字段类型可以是 Blob、File或者字符串:如果它的字段类型不是 Blob 也不是 File,则会被转换成字符串类

第二、并发请求。每一个切片都分别作为一个请求,只有当这4个切片都传输给后端了,即四个请求都成功发起,才上传成功,使用Promise.all()保证所有的切片都已经传输给后端。

//数据处理
async function uploadFile(list) {
    const requestList = list.map(({file,fileName,index,chunkName}) => {
        const formData = new FormData() // 创建表单类型数据
        formData.append('file', file)//该文件
        formData.append('fileName', fileName)//文件名
        formData.append('chunkName', chunkName)//切片名
        return {formData,index}
    })
        .map(({formData,index}) =>axiosRequest({
            method: 'post',
            url: 'http://localhost:3000/upload',//请求接口,要与后端一一一对应
            data: formData
        })
            .then(res => {
                console.log(res);
                //显示每个切片上传进度
                let p = document.createElement('p')
                p.innerHTML = `${list[index].chunkName}--${res.data.message}`
                document.getElementById('progress').appendChild(p)
            })
        )
        await Promise.all(requestList)//保证所有的切片都已经传输完毕
}
​
//请求函数
function axiosRequest({method = "post",url,data}) {
    return new Promise((resolve, reject) => {
        const config = {//设置请求头
            headers: 'Content-Type:application/x-www-form-urlencoded',
        }
        //默认是post请求,可更改
        axios[method](url,data,config).then((res) => {
            resolve(res)
        })
    })
}
​
// 文件上传
upload.addEventListener('click', () => {
    const uploadList = chunkList.map(({file}, index) => ({
        file,
        size: file.size,
        percent: 0,
        chunkName: `${files.name}-${index}`,
        fileName: files.name,
        index
    }))
    //发请求,调用函数
    uploadFile(uploadList)
​
})
复制代码

2、后端

(1)接收切片

主要工作:

第一:需要引入multiparty中间件,来解析前端传来的FormData对象数据;

第二:通过path.resolve()在根目录创建一个文件夹--qiepian,该文件夹将存放另一个文件夹(存放所有的切片)和合并后的文件;

第三:处理跨域问题。通过setHeader()方法设置所有的请求头和所有的请求源都允许;

第四:解析数据成功后,拿到文件相关信息,并且在qiepian文件夹创建一个新的文件夹${fileName}-chunks,用来存放接收到的所有切片;

第五:通过fse.move(filePath,fileName)将切片移入${fileName}-chunks文件夹,最后向前端返回上传成功的信息。

//app.js
const http = require('http')
const multiparty = require('multiparty')// 中间件,处理FormData对象的中间件
const path = require('path')
const fse = require('fs-extra')//文件处理模块
​
const server = http.createServer()
const UPLOAD_DIR = path.resolve(__dirname, '.', 'qiepian')// 读取根目录,创建一个文件夹qiepian存放切片
​
server.on('request', async (req, res) => {
    // 处理跨域问题,允许所有的请求头和请求源
    res.setHeader('Access-Control-Allow-Origin', '*')
    res.setHeader('Access-Control-Allow-Headers', '*')
​
    if (req.url === '/upload') { //前端访问的地址正确
        const multipart = new multiparty.Form() // 解析FormData对象
        multipart.parse(req, async (err, fields, files) => {
            if (err) { //解析失败
                return
            }
            console.log('fields=', fields);
            console.log('files=', files);
            
            const [file] = files.file
            const [fileName] = fields.fileName
            const [chunkName] = fields.chunkName
            
            const chunkDir = path.resolve(UPLOAD_DIR, `${fileName}-chunks`)//在qiepian文件夹创建一个新的文件夹,存放接收到的所有切片
            if (!fse.existsSync(chunkDir)) { //文件夹不存在,新建该文件夹
                await fse.mkdirs(chunkDir)
            }
​
            // 把切片移动进chunkDir
            await fse.move(file.path, `${chunkDir}/${chunkName}`)
            res.end(JSON.stringify({ //向前端输出
                code: 0,
                message: '切片上传成功'
            }))
        })
    }
})
​
server.listen(3000, () => {
    console.log('服务已启动');
})
复制代码

通过node app.js启动后端服务,可在控制台打印fields和files

(2)合并切片

第一:前端得到后端返回的上传成功信息后,通知后端合并切片:

// 通知后端去做切片合并
function merge(size, fileName) {
    axiosRequest({
        method: 'post',
        url: 'http://localhost:3000/merge',//后端合并请求
        data: JSON.stringify({
            size,
            fileName
        }),
    })
}
​
//调用函数,当所有切片上传成功之后,通知后端合并
await Promise.all(requestList)
merge(files.size, files.name)
复制代码

第二:后端接收到合并的数据,创建新的路由进行合并,合并的关键在于:前端通过POST请求向后端传递的合并数据是通过JSON.stringify()将数据转换成字符串,所以后端合并之前,需要进行以下操作:

  • 解析POST请求传递的参数,自定义函数resolvePost,目的是将每个切片请求传递的数据进行拼接,拼接后的数据仍然是字符串,然后通过JSON.parse()将字符串格式的数据转换为JSON对象;
  • 接下来该去合并了,拿到上个步骤解析成功后的数据进行解构,通过path.resolve获取每个切片所在的路径;
  • 自定义合并函数mergeFileChunk,只要传入切片路径,切片名字和切片大小,就真的将所有的切片进行合并。在此之前需要将每个切片转换成流stream对象的形式进行合并,自定义函数pipeStream,目的是将切片转换成流对象,在这个函数里面创建可读流,读取所有的切片,监听end事件,所有的切片读取完毕后,销毁其对应的路径,保证每个切片只被读取一次,不重复读取,最后将汇聚所有切片的可读流汇入可写流;
  • 最后,切片被读取成流对象,可读流被汇入可写流,那么在指定的位置通过createWriteStream创建可写流,同样使用Promise.all()的方法,保证所有切片都被读取,最后调用合并函数进行合并。
if (req.url === '/merge') { // 该去合并切片了
        const data = await resolvePost(req)
        const {
            fileName,
            size
        } = data
        const filePath = path.resolve(UPLOAD_DIR, fileName)//获取切片路径
        await mergeFileChunk(filePath, fileName, size)
        res.end(JSON.stringify({
            code: 0,
            message: '文件合并成功'
        }))
}
​
// 合并
async function mergeFileChunk(filePath, fileName, size) {
    const chunkDir = path.resolve(UPLOAD_DIR, `${fileName}-chunks`)
​
    let chunkPaths = await fse.readdir(chunkDir)
    chunkPaths.sort((a, b) => a.split('-')[1] - b.split('-')[1])
​
    const arr = chunkPaths.map((chunkPath, index) => {
        return pipeStream(
            path.resolve(chunkDir, chunkPath),
            // 在指定的位置创建可写流
            fse.createWriteStream(filePath, {
                start: index * size,
                end: (index + 1) * size
            })
        )
    })
    await Promise.all(arr)//保证所有的切片都被读取
}
​
// 将切片转换成流进行合并
function pipeStream(path, writeStream) {
    return new Promise(resolve => {
        // 创建可读流,读取所有切片
        const readStream = fse.createReadStream(path)
        readStream.on('end', () => {
            fse.unlinkSync(path)// 读取完毕后,删除已经读取过的切片路径
            resolve()
        })
        readStream.pipe(writeStream)//将可读流流入可写流
    })
}
​
// 解析POST请求传递的参数
function resolvePost(req) {
    // 解析参数
    return new Promise(resolve => {
        let chunk = ''
        req.on('data', data => { //req接收到了前端的数据
            chunk += data //将接收到的所有参数进行拼接
        })
        req.on('end', () => {
            resolve(JSON.parse(chunk))//将字符串转为JSON对象
        })
    })
}
复制代码

还未合并前,文件夹如下图所示:

合并后,文件夹新增了合并后的文件:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/90641.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

怎么复制网页上不能复制的文字(付费文档免费复制),一招搞定

好多小伙伴上网查资料的时候&#xff0c;想要复制网页内容&#xff0c;但是提示付费复制或者不允许复制&#xff0c;遇到这种情况怎么办呢&#xff1f;下面就是小编分享的一招搞定无法复制网页内容文字的方法。 怎么复制网页上不能复制的文字 借助360安全浏览器/360极速浏览器…

Minecraft 1.19.2 Forge模组开发 06.建筑生成

1.12.2自定义建筑生成 1.16.5自定义建筑生成 1.18.2自定义建筑生成 我们本次尝试在主世界生成一个自定义的建筑。 效果展示效果展示效果展示 由于版本更新缘故&#xff0c;1.19的建筑生成将不涉及任何Java包的代码编写&#xff0c;只需要在数据包中对建筑生成进行自定义。 …

基于粒子群优化算法的BP神经网络预测模型(Matlab代码实现)

目录 1 概述 2 粒子群优化算法 3 BP神经网络 4 PSO优化 BP网络算法 5 运行结果 6 参考文献 7 Matlab代码实现 1 概述 在工程应用中经常会遇到一些复杂的非线性系统,这些系统的状态方程复杂,难以准确的用数学方法建模,而BP神经网络实质上实现了一个从输入到输出的映射功…

【k8s】Kubernetes 基础组件详解

一、k8s简介 Kubernetes 是容器集群管理系统工具&#xff0c;是一个开源平台&#xff0c;可实现容器集群的自动化部署、自动扩缩容、维护等功能。Kubernetesk8s是Kubernetes的缩写&#xff0c;Google 于 2014 年开源了 Kubernetes 项目&#xff0c;Kubernetes的名字来自希腊语&…

无延时直播/超低延时直播实际测试延时效果(项目实测组图)

阿酷TONY / 2022-11-30 / 长沙 / 超多组图 无延时直播/超低延时直播&#xff0c;主要只测试延时情况&#xff0c;没有涉及直播产品的功能、使用操作界面&#xff0c;有兴趣的朋友可以加联系我实际测试哦~~~ 1.无延时直播应用场景 无延时直播/超低延时常见应用场景&#…

近90天互动量破百万,「围炉煮茶」究竟做对了什么?

今年秋冬&#xff0c;“围炉煮茶”爆红网络。小红书相关笔记数量突破8万&#xff0c;累计话题浏览量1200万次&#xff0c;近90天互动量破百万&#xff01; 茶&#xff0c;从老一辈的茶杯茶盘里&#xff0c;通过创新再造&#xff0c;成为年轻人的社交“新”头好。高流量曝光、高…

Go语言性能剖析利器--pprof实战

作者&#xff1a;耿宗杰 前言 关于pprof的文章在网上已是汗牛充栋&#xff0c;却是千篇一律的命令介绍&#xff0c;鲜有真正实操的&#xff0c;本文将参考Go社区资料&#xff0c;结合自己的经验&#xff0c;实战Go程序的性能分析与优化过程。 优化思路 首先说一下性能优化的…

什么是文档管理?

什么是文档管理&#xff1f; 文档管理是指在公司或组织内部组织、存储和检索文档以及自动化工作流程所需的流程和资源。文档可以是硬拷贝或数字格式&#xff0c;包括合同、表单、发票、工作申请、福利登记信息和其他记录。 强大的文档管理的重要性 平均而言&#xff0c;由于…

Web实时预览 界面组件Telerik——提高开发者工作效率的完美组合

Telerik DevCraft包含一个完整的产品栈来构建您下一个Web、移动和桌面应用程序。它使用HTML和每个.NET平台的UI库&#xff0c;加快开发速度。Telerik DevCraft提供最完整的工具箱&#xff0c;用于构建现代和面向未来的业务应用程序。 Telerik ASP.NET AJAX (Web Forms) 组件在…

SAP ABAP OAOR SBDSV1新建BDS类/DOI大数据量输出EXCEL时错误空白或不完整

SAP ABAP OAOR SBDSV1新建BDS类/ DOI大数据量输出 EXCEL时错误空白或不完整 引言&#xff1a; 今日回顾 DOI 使用过程中的两个问题&#xff0c;第一个是管理性问题&#xff08;新建 BDS 类&#xff09;&#xff0c;第二个是技术性问题&#xff08; DOI 大数据量输出 EXCEL 时错…

数据对接:从Notion Database到低代码平台app

前言 Notion简介 近几年&#xff0c;有一款叫Notion的产品异常火爆&#xff0c;它是集笔记、任务管理、Wiki、数据管理为一体的产品&#xff0c;他主打两个理念「模块化」和「All-in-one」&#xff0c;Notion最有魅力的还是引进了Database和双向链的理念 Notion也算是一个渐…

美丽田园通过聆讯:上半年期内利润下降近80% 演员刘涛代言

雷递网 雷建平 12月13日由演员刘涛代言的美丽田园医疗健康产业有限公司&#xff08;简称&#xff1a;“美丽田园”&#xff0c;Beauty Farm&#xff09;日前通过聆讯&#xff0c;准备在港交所上市。上半年期内利润下降近80%美丽田园首家门店1993年成立&#xff0c;至今美丽田园…

C++ Reference: Standard C++ Library reference: Containers: map: map: operators

C官网参考链接&#xff1a;https://cplusplus.com/reference/map/map/operators/ 函数 <map> std::relational operators (map) (1) template <class Key, class T, class Compare, class Alloc> bool operator ( const map<Key,T,Compare,Alloc>&…

记cf一些可能被hack的写法

1. unordered_map 现象 之前经常当O(1)用&#xff0c;作为一些卡常题的必备杀器&#xff0c; 而且&#xff0c;有的hdu/lc题不用umap的话&#xff0c;就只能手写哈希 但是&#xff0c;上次cf里写了个umap被卡了&#xff0c; 这才意识到之前看到的博客不是无稽之谈 其实我…

面试官:单体架构怎么向分布式微服务架构演变的?(8000字)

随着网站规模越来越大&#xff0c;单体应用往往很难再满足要求&#xff0c;就需要向分布式&#xff0c;微服务架构演变。 那么这个演变过程是怎么样的呢&#xff1f;都涉及到哪些组件&#xff0c;会遇到哪些问题&#xff0c;以及相应的解决方案都是什么&#xff0c;本篇文章就来…

吹爆,阿里MySQL优化笔记有多强?才在GitHub被BATJ联手封杀

公司来了个新同事&#xff0c;一看是个二本生&#xff0c;还是非科班出身&#xff0c;刚30岁。。。 结果没想到人家上来就把现有项目的性能优化了一遍&#xff0c;直接给公司节省了一半的成本&#xff0c;这种“王炸”打法&#xff0c;直接给所有人整懵了。 一问情况&#xff…

达梦:[-2420]:服务器未重启

在项目上&#xff0c;使用DMRMAN 备份数据库时&#xff0c;报[-2420]:服务器未重启的错。 因为是从实际生产的主备集群主节点拷贝的全备增备归档还原恢复到指定时间点还原恢复到指定时间点后&#xff0c;准备在测试环境搭建集群。 测试环境的主节点&#xff0c;还原恢复后&…

[附源码]Python计算机毕业设计高校勤工助学管理系统Django(程序+LW)

该项目含有源码、文档、程序、数据库、配套开发软件、软件安装教程 项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等…

Word控件Spire.Doc 【超链接】教程(6):在C#去除word文档中的超链接

Spire.Doc for .NET是一款专门对 Word 文档进行操作的 .NET 类库。在于帮助开发人员无需安装 Microsoft Word情况下&#xff0c;轻松快捷高效地创建、编辑、转换和打印 Microsoft Word 文档。拥有近10年专业开发经验Spire系列办公文档开发工具&#xff0c;专注于创建、编辑、转…

Jina AI创始人肖涵博士分享软件框架开发的3个常见错误及解决方法

回到三个月前一个晨跑的上午&#xff0c;你突然萌生了一个绝妙的想法在&#xff1a;“我要建立一个 AI 框架&#xff01;”你的大脑飞速运转&#xff0c;你的指尖在键盘上飞速敲打&#xff0c;快速地记下此刻的想法和灵感。你梦想着潜在的投资者&#xff0c;全新的应用程序&…