首页 > 其他分享 >前端大文件上传、文件切片、断点续传

前端大文件上传、文件切片、断点续传

时间:2023-09-22 09:23:26浏览次数:48  
标签:文件 const 断点续传 res 切片 return 上传

一、项目初始化
1、项目初始化
我们创建一个 big-file-upload 目录作为当前项目的根目录文件。

执行以下命令对当前项目进行初始化,生成 package.json 文件:

npm init -y
2、搭建项目结构
在项目根目录中创建 public 目录,作为前端静态资源目录。同时在 public 中创建 index.html 用于构建前端页面。

|--- chunks # 保存上传的文件切片
|--- public # 保存前端资源
|--- files # 存放上传成功的大文件
|--- js # 存放前端 JS 文件
| |--- axios.min.js # 发送请求
|--- index.html # 前端页面
|--- server.js # 后端服务器代码
3、下载服务端依赖包
下载项目所需依赖包:

npm i express fs-extra multiparty
插件说明:

express:搭建服务器

fs-extra:文件处理

multiparty:文件上传

二、前后端初始代码
1、前端页面
前端 index.html 页面初始代码如下:

<div>
<input type="file" id="chooseFile">
<button id="uploadFile">上传</button>
</div>
<div style="margin: 20px 0;">
<progress value="0" id="progress"></progress>
<span id="message"></span>
</div>
<div>
<button id="stopUpload">暂停</button>
<button id="keepUpload">续传</button>
</div>

<script>
const chooseFile = document.getElementById('chooseFile');
const uploadFile = document.getElementById('uploadFile');
const progress = document.getElementById('progress');
const message = document.getElementById('message');
const stopUpload = document.getElementById('stopUpload');
const keepUpload = document.getElementById('keepUpload');
</script>

2、后端服务器
后端 server.js 初始代码如下:

const express = require('express');
const bodyParser = require('body-parser');
const path = require('path');
const fse = require('fs-extra');
const multiparty = require('multiparty');

const app = express();
// 设置静态文件目录
app.use(express.static(__dirname + '/public'));
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());

// 后续的接口配置代码

app.listen(3000, () => console.log('3000 端口启动成功'));
3、启动项目
在终端中执行以下命令启动后端 express 服务器:

node server.js
如果安装了 nodemon 插件,也可以用插件启动服务器(服务器可自动重启):

nodemon server.js
服务器启动成功后,在浏览器中访问 http://localhost:3000 即可显示 index.html 页面。

三、分片上传(前端)
1、获取选中文件
let checkedFile = null; // 用户选中的文件
chooseFile.addEventListener('change', function (e) {
const [file] = e.target.files;
if(file) {
checkedFile = file;
}
})
2、文件切片
const chunkSize = 3 * 1024 * 1024; // 每一个分片的大小:3M
const chunksList = []; // 用户选中文件的切片文件

// 上传文件
uploadFile.addEventListener('click', function () {
// 如果没有选中的文件,直接结束
if (!checkedFile) return;
// 文件切片
createChunks();
})
// 创建文件切片
const createChunks = () => {
let start = 0; // 切片文件的起始位置
while (start < checkedFile.size) {
// 文件切片
const chunkItem = checkedFile.slice(start, start + chunkSize);
// 将切片保存到全局的切片数组中
chunksList.push(chunkItem);
start += chunkSize;
}
}

3、上传切片
我们通过 axios 来发送请求,所以需要在 index.html 中引入 axios,并配置基础路径:

<script src="./js/axios.min.js"></script>
<script>
axios.defaults.baseURL = 'http://localhost:3000';
// ... 其他代码
</script>
将切片文件数据通过 formData 发送给后端:

// 上传文件
uploadFile.addEventListener('click', function () {
// 如果没有选中的文件,直接结束
if (!checkedFile) return;
// 文件切片
const chunksList = createChunks();
// 上传切片
uploadChunks(chunksList);
})

// 上传切片
const uploadChunks = async () => {
// 处理切片数据格式
const formDataList = chunksList.map((chunkItem, index) => {
let formData = new FormData();
formData.append('file', chunkItem); // 切片文件信息
formData.append('fileName', checkedFile.name); // 完整文件名
formData.append('chunkName', index); // 切片名(将 index 作为每一个切片的名字)
return formData;
});
// 依次上传每一个切片文件
const requestList = formDataList.map(formData => {
return axios({
url: '/upload',
data: formData,
method: 'POST'
})
});
// 等待所有切片上传完成
await Promise.all(requestList);
}

四、分片上传(后端)
配置上传的切片文件的存储路径:

// 上传的切片文件的存储路径
const ALL_CHUNKS_PATH = path.resolve(__dirname, 'chunks');
配置 /upload 接口:

app.post('/upload', (req, res) => {
const multipartyForm = new multiparty.Form();
multipartyForm.parse(req, async (err, fields, files) => {
if (err) {
console.log('文件切片上传失败:', err);
res.send({
code: 0,
message: '文件切片上传失败'
});
return;
}
// 前端发送的切片文件信息
const [file] = files.file;
// 前端发送的完整文件名 fileName 和分片名 chunkName
const { fileName: [fileName], chunkName: [chunkName] } = fields;
// 当前文件的切片存储路径(将文件名作为切片的目录名)
const chunksPath = path.resolve(ALL_CHUNKS_PATH, fileName);
// 判断当前文件的切片目录是否存在
if (!fse.existsSync(chunksPath)) {
// 创建切片目录
fse.mkdirSync(chunksPath);
}
// 将前端发送的切片文件移动到切片目录中
await fse.move(file.path, `${chunksPath}/${chunkName}`);
res.send({
code: 1,
message: '切片上传成功'
})
})
})

五、合并分片(前端)
等到所有的切片上传完成后,发送 /merge 请求合并切片:

// 合并切片
const mergeChunks = async () => {
const { data } = await axios({
url: '/merge',
method: 'POST',
data: {
chunkSize,
fileName: checkedFile.name
}
});
if(data.code) {
message.innerText = '文件上传成功';
}
}

// 上传切片
const uploadChunks = async (chunksList) => {
// ....
// 等待所有切片上传完成
await Promise.all(requestList);
mergeChunks();
}

六、合并分片(后端)
配置切片合并完成后的完成文件存储路径:

// 切片合并完成后的文件存储路径
const UPLOAD_FILE_PATH = path.resolve(__dirname, 'public/files');
配置合并切片接口 /merge 代码:

app.post('/merge', async (req, res) => {
// 获取前端发送的参数
const { chunkSize, fileName } = req.body;
// 当前文件切片合并成功后的文件存储路径
const uploadedFile = path.resolve(UPLOAD_FILE_PATH, fileName);
// 找到当前文件所有切片的存储目录路径
const chunksPath = path.resolve(ALL_CHUNKS_PATH, fileName);
// 读取所有的切片文件,获取到文件名
const chunksName = await fse.readdir(chunksPath);
// 对切片文件名按照数字大小排序
chunksName.sort((a, b) => (a - 0) - (b - 0));
// 合并切片
const unlinkResult = chunksName.map((name, index) => {
// 获取每一个切片路径
const chunkPath = path.resolve(chunksPath, name);
// 获取要读取切片文件内容
const readChunk = fse.createReadStream(chunkPath);
// 获取要写入切片文件配置
const writeChunk = fse.createWriteStream(uploadedFile, {
start: index * chunkSize,
end: (index + 1) * chunkSize
})
// 将读取到的 readChunk 内容写入到 writeChunk 对应位置
readChunk.pipe(writeChunk);

return new Promise((resolve) => {
// 文件读取结束后删除切片文件(必须要将文件全部删除后,才能才能外层文件夹)
readChunk.on('end', () => {
fse.unlinkSync(chunkPath);
resolve();
});
})
})
// 等到所有切片文件合并完成,且每一个切片文件都删除成功
await Promise.all(unlinkResult);
// 删除切片文件所在目录
fse.rmdirSync(chunksPath);
res.send({
code: 1,
message: '文件上传成功'
})
})

七、秒传(前端)
秒传,指的就是用户当前选择上传的文件,之前已经传递过,就可以直接提示上传成功。

前端发送 /verify 请求到后端,用于判断当前文件是否已经上传过:

// 上传文件
uploadFile.addEventListener('click', async function () {
// 如果没有选中的文件,直接结束
if (!checkedFile) return;

// 验证文件之前是否上传成功过
const res = await verifyFile();
if(res.code) {
message.innerText = "上传成功(秒传)";
return; // 终止后续代码的执行
}

// 文件切片
const chunksList = createChunks();
// 上传切片
uploadChunks(chunksList);
})

// 验证文件是否上传过
const verifyFile = async () => {
const { data } = await axios({
url: '/verify',
method: 'POST',
data: {
fileName: checkedFile.name
}
});
return data;
}

八、秒传(后端)
app.post('/verify', (req, res) => {
// 获取前端发送的文件名
const { fileName } = req.body;
// 获取当前文件路径(如果上传成功过的保存路径)
const filePath = path.resolve(UPLOAD_FILE_PATH, fileName);
// 判断文件是否存在
if (fse.existsSync(filePath)) {
res.send({
code: 1,
message: '文件已存在,不需要重新上传'
});
return;
}
res.send({
code: 0,
message: '文件未上传过'
})
})

九、暂停上传(前端)
暂停上传,是通过取消 axios “切片上传”请求的发送,来实现文件暂停上传:

const CancelToken = axios.CancelToken;
let source = CancelToken.source();

// 暂停上传
stopUpload.addEventListener('click', function () {
// 取消后续所有“切片上传”请求的发送
source.cancel('终止上传!');
// 重置 source
source = CancelToken.source();
message.innerText = "暂停上传";
})

// 上传切片
const uploadChunks = async (chunksList) => {
// ...
// 依次上传每一个切片文件
const requestList = formDataList.map(formData => {
return axios({
url: '/upload',
data: formData,
method: 'POST',
cancelToken: source.token, // 添加 cancelToken,用于后续取消请求发送
})
});
// ....
}

十、断点续传(后端)
断点续传,就是要判断当前文件是否上传完成,如果没有上传完成,就需要找到已上传的其中一部分。

因此,“断点续传”和“验证文件是否上传成功过”可以使用同一个后端接口:

app.post('/verify', async (req, res) => {
// 获取前端发送的文件名
const { fileName } = req.body;
// 获取当前文件路径(如果上传成功过的保存路径)
const filePath = path.resolve(UPLOAD_FILE_PATH, fileName);
// 判断文件是否存在
if (fse.existsSync(filePath)) {
res.send({
code: 1,
message: '文件已存在,不需要重新上传'
});
return;
}

// 断点续传:判断文件是否有上传的一部分切片内容
// 获取该文件的切片文件的存储目录
const chunksPath = path.resolve(ALL_CHUNKS_PATH, fileName);
// 判断该目录是否存在
if (fse.existsSync(chunksPath)) {
// 目录存在,则说明文件之前有上传过一部分,但是没有完整上传成功
// 读取之前已上传的所有切片文件名
const uploaded = await fse.readdir(chunksPath);
res.send({
code: 0,
message: '该文件有部分上传数据',
uploaded
});
return;
}

res.send({
code: 0,
message: '文件未上传过'
})
})

十一、断点续传(前端)
// 上传文件
uploadFile.addEventListener('click', async function () {
// ...
// 上传所有切片
uploadChunks(res.uploaded); // 将已上传的部分数据传递过去
})
// 点击续传按钮
keepUpload.addEventListener('click', async function () {
const res = await verifyFile();
if (!res.code) {
// 只要没有上传成功,不管是否有之前的上传记录,都需要继续上传
uploadChunks(res.uploaded); // 将已上传的部分数据传递过去
}
})

// 上传切片
const uploadChunks = async (uploaded = []) => {
// 处理切片数据格式
// ...
// 将处理好的文件切片 formData 数据中,还未上传的部分筛选出来
formDataList = formDataList.filter((_, index) => uploaded.indexOf(index + '') < 0);

// 依次上传每一个切片文件
// ...
}

十二、进度条
// 上传文件
uploadFile.addEventListener('click', async function () {
// ...

// 验证文件之前是否上传成功过
const res = await verifyFile();
if (res.code) {
message.innerText = "上传成功(秒传)";
progress.value = progress.max; // 将文件最大值设置为进度条的 value 值
return; // 终止后续代码的执行
}
// ...
})

// 上传切片
const uploadChunks = async (uploaded = []) => {
// 将之前已上传的切片数量设置为当前进度条的进度
progress.value = uploaded.length;
// ...
// 依次上传每一个切片文件
const requestList = formDataList.map(formData => {
return axios({
// ...
}).then(() => {
// 每一个切片上传成功后,进度条都 +1
progress.value++;
})
});
// 等待所有切片上传完成
// ...
}

// 创建文件切片
const createChunks = () => {
// ...

// 将文件切片的总数作为进度条的最大值
progress.max = chunksList.length;
}

参考文章:http://blog.ncmem.com/wordpress/2023/09/22/%e5%89%8d%e7%ab%af%e5%a4%a7%e6%96%87%e4%bb%b6%e4%b8%8a%e4%bc%a0%e3%80%81%e6%96%87%e4%bb%b6%e5%88%87%e7%89%87%e3%80%81%e6%96%ad%e7%82%b9%e7%bb%ad%e4%bc%a0/

欢迎入群一起讨论

 

 

标签:文件,const,断点续传,res,切片,return,上传
From: https://www.cnblogs.com/songsu/p/17721537.html

相关文章

  • 压缩和归档、文件搜索、文本过滤
    列表对比Linux系统下常用压缩与解压缩命令的区别压缩命令特点解压缩命令zip可压缩目录,不删除原文件unzipgzip删除原文件,可指定压缩比gunzipbzip2删除源文件bunzip2xz压缩比更大unxztar命令的语法与常用选项有哪些?各选项的作用是什么?语法:tar选项打包后的名字打包的文件或目录......
  • Python - 读取CSV文件发现有重复数据,如何清洗以及保存为CSV文件,这里有完整的过程!!!!
    语言:Python功能:1、清洗CSV文件中重复数据。2、保存为CSV文件大体流程:1、首先观察CSV文件中的数据布局格式如何?2、通过csv包读取数据。并根据规则使用continue,来跳过本次循环,并将所需数据保存到列表A中,当列表A中的数据变成len(列表A)==2时,将此数据保存到列......
  • 第二章 文件管理
    1.文件目录与路径1.1文件目录1.1.1  根目录:/1.1.2默认颜色:蓝色表示目录,绿色表示可执行文件,浅蓝色表示链接文件,红色表示压缩文件,黄色表示设备文件等。这些颜色是Linux系统默认的颜色,用户可以根据自己的喜好进行修改。1.1.3根目录下常见的目录:1.2文件路径1.2.1 绝对......
  • window和linux下有关xxx.dll和xxx.so动态库,可执行文件运行时的动态库检索路径文档
    没想到详细的内容都在库和命令的man手册中。ld.so动态库手册里有描述ELF可执行文件在运行时,都会在哪几个位置检索动态库。如果共享对象依赖项不包含斜杠,则它按以下顺序搜索:(1)使用二进制文件的DT_RPATH动态节属性中指定的目录(如果存在且DT_RUNPATH属性不存在)。不推荐......
  • 【C#】【Python】【实例】统计多个文件夹下的图片
    因工作需要繁琐的进行同一目录多个文件夹下的图片统计,便使用代码来解决。需求:统计的是多少个文件夹包含了图片,并非是统计有多少张图。 我们先用Python来创建一个现场环境(巩固巩固py知识):1importos23#根目录4root_path=r"C:\Users\Desktop\1111"56forit......
  • Linux文件管理笔记
     一、文件目录和路径在Linux系统中,文件和目录被组织成一个树状的结构,称为文件目录结构。根目录是整个文件目录结构的最顶层,表示为“/”。所有其他目录和文件都是从根目录开始的。文件路径是指从根目录到目标目录或文件的路径。路径可以是绝对路径或相对路径。-绝对路径:从根目录......
  • shell批量执行命令与文件传输脚本
    shell批量执行命令与文件传输脚本需求:对未进行主机信任操作的服务器进行批量操作实现:由于ssh只能在交互模式中输入服务器密码进行登录登操作,不便于进行大批量服务器进行巡检或日志采集。sshpass恰好又解决了这个问题,使用ssh-ppasswd可以实现命令行输入密码操作,便于进行规模......
  • 完整教程:使用SPRING BOOT实现大文件断点续传及文件校验
    一、简介随着互联网的快速发展,大文件的传输成为了互联网应用的重要组成部分。然而,由于网络不稳定等因素的影响,大文件的传输经常会出现中断的情况,这时需要重新传输,导致传输效率低下。为了解决这个问题,可以实现大文件的断点续传功能。断点续传功能可以在传输中断后继续传输,而不需......
  • python打包文件过大的解决方法
       开发完一个python客户端文件后,一般需要发布为exe文件以便用户下载安装。但很多初学者开发的一个仅有几M的小软件,打包的exe文件却高达几十M甚至几百M。这里根据工作实践总结了一个有效方法分享给大家。   首先,在选定路径下创建一个文件夹folder   其次,在新建文......
  • vs code 保存显示无法写入文件的解决方法
    右键文件夹点击属性选择安全把当前用户权限都勾选上就可以了......