前端干全栈实现大文件上传
前言
个人项目中加一个小需求,上传功能原来是只能上传图片,现在需要支持视频等其他类型文件上传,并且支持大文件上传,这里记录分享前端用原生js,后端用node的实现过程
思考
图片上传和其他类型文件上传以及大文件上传区别有什么?
我的理解是如果都是小文件,整个上传实现方面是前后端的技术实现方面没有区别的,可能有的会对文件类型做限制,例如必须是图片之类的;如果是大文件,使用分片上传和断点续传等技术,否则会导致 网络阻塞
浏览器卡顿
服务器压力大
等情况
实现思路
其他类型文件上传和普通的图片上传没区别,如果有图片限制只需要把图片类型的校验限制去掉就行
大文件上传需要前端先将上传的文件切片,然后再依次上传文件切片,上传完所有的切片后,发请求告诉服务端,把上传的所有切片按顺序合并,得到原文件
代码实现
前端
获取文件并切分
获取文件使用 input
标签设置 type=file
<input id="file" type="file" />
upload
按钮点击上传文件, 点击事件中获取input
中选中的文件,将文件按1M的大小使用file.slice()
进行切分,得到分块数量
let blockSize = 1024 * 1024;
let blockCount = Math.ceil(file.size / blockSize);
let block = file.slice(i * blockSize, (i + 1) * blockSize);
上传文件
上传函数使用js原生实现,这里使用 fetch
(也可以使用 XMLHttpRequest
对象)上传文件
注意! image
参数要和服务端的参数保持一致,否则上传失败
const formData = new FormData();
formData.append("image", block, fileName);
formData.append("block_index", i + 1);
fetch(ipAddress + "/upload_large_file", {
method: "post",
body: formData,
})
大文件被切分成了多个块,根据分块数量循环上传,为了避免一次性发送大量请求导致请求失败,使用async await
同步的方式一个个文件上传,并记录文件名
...
for (let i = 0; i < blockCount; i++) {
await new Promise((resolve, reject) => {
fetch()
...
前端把所有切块文件上传完成后,把记录的文件名数组发送给node服务端,node服务端根据文件数组合并后返回结果,到这里前端上传部分就完成了
前端完整代码
后端
接收切块文件
node后端部分实现的是两部分,先是接收上传的文件保存到服务器的指定位置
const upload = multer({
limits: { fileSize: 1024 * 1024 * 5 }
});
app.post("/upload_large_file", upload.single("image"), (req, res) => {
const des_file = __dirname + "/public/images/" + req.files[0].originalname;
fs.readFile(req.files[0].path, (err, data) => {
fs.writeFile(des_file, data, (err) => {
...
});
});
});
这里使用使用了 multer
中间件处理文件,使用 upload.single('image')
处理单个文件的上传(也能实现其他功能,例如对上传文件类型进行过滤),设置单个文件最大为5M,上传的文件读取后保存到 /public/images/
目录下面
注意! /public/images/
目录需要提前创建
合并切块文件
前端上传完所有切块文件后,发合并请求通知服务端,服务端根据前端传递的文件切块列表进行文件切块合并
...
const files = filePaths;
const targetFile = mergedFileName;
const writeStream = fs.createWriteStream(targetFile);
files.forEach((file, index) => {
const filePath = path.join(__dirname, file);
const readStream = fs.createReadStream(filePath);
readStream.pipe(writeStream);
});
...
files
为基于服务端拼接路径后的文件列表,这个列表记录了前端的切片名称以及切块文件顺序,mergedFileName
为合并后的文件名称(原文件名),这里流的方式进行文件合并,使用 fs
模块的 createWriteStream
创建一个可写流,然后将所有切块文件读取流 createReadStream
合并到可读流中,然后将可读流 pipe
写入到可写流中,最终将合并好的文件写入到磁盘,到这里服务端的部分就完成了
后端完整代码
奇怪的情况
使用一张大小12M的高清图片上传,发现node端合并后的文件打不开, 使用其他类型的大文件上传也同样无法正常打开
原图效果
上传到服务端后的效果
文件似乎损坏了,前端通过浏览器控制台输出查看整个过程都执行文件切块都上传成功了
通过输出的计算切块文件的大小和对比原文件大小,发现文件大小是一致的,都是 12948424
,检查服务端保存的切块文件发现只有第一张切块可以访问,其他切块图片都不能正常访问,切块文件和前端上传的一致
从后端进行排查,通过固定参数的方式,分别进行了 txt
文件和 png
图片文件,发现是可以正常合并的,两个txt
文件合并效果如下
使用图片合并后也能正常打开,虽然图片花了,上面两张为原图,下面为合并图
测试使用的浏览器版本和系统
- Google Chrome: 版本 114.0.5735.133(正式版本)(64 位)
- Microsoft Edge: 版本 114.0.1823.43 (正式版本) (64 位)
- Windows11
原因分析
通过静态参数在服务端合并排查的方式,可以排除node代码和文件格式异常的异常导致的无法打开的问题,问题可能出现在前端切块那里,查了MDN的文档以及Baidu,Google,ChatGPT等发现都是前端都是用的 file.slice()
进行切分的,也尝试了不同的切块逻辑,发现最后都是文件无法正常打开,社区的大佬们一起看看分析分析
有掘友遇到过类似问题吗?欢迎探讨交流
转载自:https://juejin.cn/post/7250025436016459837