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

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

时间:2023-10-27 13:36:21浏览次数:34  
标签:文件 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/前端大文件上传、文件切片、断点续传/


 

 

标签:文件,const,断点续传,res,切片,return,上传
From: https://blog.51cto.com/u_14023400/8053010

相关文章

  • Java大文件上传(秒传、分片上传、断点续传)
    一、秒传秒传就是不传,实现逻辑就是看数据库或者缓存里是否已经有这个文件了,有了,直接从已有的文件去拿就可以了(返回文件地址)。这里判断是否是相同文件,要用到信息摘要算法,详情可以参考:一文读懂当前常用的加密技术体系。信息摘要算法常常被用来保证信息的完整性,防止信息在传输过程中被......
  • Java实战:大文件分片上传与断点续传策略及其实际应用
    在许多应用场景中,处理大型文件上传可能成为开发人员面临的一项挑战。在网络环境不稳定,或者文件体积过大的情况下,传统的文件上传方式可能会出现问题。这时,文件分片上传和断点续传技术就显得至关重要。本文将向您展示如何使用Java实现这两种技术,并探讨其主要应用场景。文件分片上传是......
  • Vue项目中大文件切片上传实现秒传、断点续传的详细实现教程
    一、考察点在Vue项目中,大图片和多数据Excel等大文件的上传是一个非常常见的需求。然而,由于文件大小较大,上传速度很慢,传输中断等问题也难以避免。因此,为了提高上传效率和成功率,我们需要使用切片上传的方式,实现文件秒传、断点续传、错误重试、控制并发等功能,并绘制进度条。在本文中,我......
  • 如何实现大文件上传:秒传、断点续传、分片上传
    前言文件上传是一个老生常谈的话题了,在文件相对比较小的情况下,可以直接把文件转化为字节流上传到服务器,但在文件比较大的情况下,用普通的方式进行上传,这可不是一个好的办法,毕竟很少有人会忍受,当文件上传到一半中断后,继续上传却只能重头开始上传,这种让人不爽的体验。那有没有比较好的......
  • 写一个cmd脚本,列出指定目录下的所有子目录和文件,限制层数
    在Windows的CMDshell中,tree命令并不直接支持指定层数。你可以编写CMD脚本达到相同目标。@echooffsetlocalset"root=%~1"set"maxdepth=%~2"set"curdepth=0"set"indent=":looppushd"%root%"for/d%%Din(*)do(echo%indent%......
  • FastAPI学习-17.其它响应html,文件,视频或其它
    前言通过我们返回JSON类型的接口会比较多,除了返回JSON格式,还可以响应其它格式的内容JSONResponseContent-Type 会被设置成 application/jsonHTMLResponseContent-Type 会被设置成 text/htmlPlainTextResponse Content-Type 会被设置成text/plainORJSONResponse......
  • .net 上传附件错误
     错误net::ERR_CONNECTION_ABORTED 导致这种错误的主要原因是上传的文件太大,服务器不能继续读取请求而过早中断链接Failedtoloadresource:theserverrespondedwithastatusof413()开发环境(IISExpress)1073741824=1GB根目录下创建 web.config 文件,内容如下<......
  • php结合webuploader断点续传的实现
    最近公司项目需要用到断点续传,所以记录一下其中的坑使用到的主要技术webuploaderthinkphp5断点续传的思路:客户端:   1.获取文件md5(MD5是文件唯一标识,用来判断是否存在此文件,并且用作分片的文件夹名)   2.将文件分片   3.验证分片是否上传过,上传过直接跳......
  • 罗列大地址下文件名在A列
    importosimportopenpyxldeffind_image_folders(root_folder):"""返回所有找到的包含图片的文件夹路径"""image_folders=set()forroot,dirs,filesinos.walk(root_folder):forfileinfiles:iffile.lower().endswith......
  • 每日一题:吃透大文件上传问题(附可运行的前后端源码)
    大文件上传前言在日常开发中,文件上传是常见的操作之一。文件上传技术使得用户可以方便地将本地文件上传到Web服务器上,这在许多场景下都是必需的,比如网盘上传、头像上传等。但是当我们需要上传比较大的文件的时候,容易碰到以下问题:上传时间比较久中间一旦出错就需要重新上传一般服务......