前端分片上传文件

介绍

前端上传文件是项目中经常遇见的一个功能,但是如果文件太大了,我们就需要分片上传了,简单的说就是把文件切割成n个小片段,依次上传到服务器,最后再把这些片段拼接起来,组成完整的文件。

这里我不过多介绍服务端的逻辑,因为大部分上传文件的场景中后端是 oss 处理的,不需要我们写后端代码,大家大概知道个流程就好了。你需要掌握的基础知识包括, File 对象,Blob 对象, node express, stream 。

实现一个简单的上传文件的功能

首先,我们使用 node 写一个简单的后端服务,实现一个简单的文件上传功能。

js 复制代码
// 普通的单文件上传
const express = require('express')
const multer = require('multer')
const path = require('path')
const fs = require('fs')
const cors = require('cors')

const app = express()

const PORT = 3000

app.use(cors())
app.use(express.static('public'))
app.use(express.json())
app.use(express.urlencoded({ extended: true }))

const storage = multer.diskStorage({
    destination: function (req, file, cb) {
        cb(null, './uploads') // 项目的根目录需要 uploads 文件夹,不然会报错
    },
    filename: function (req, file, cb) {
        // 解决中文乱码问题
        file.originalname = Buffer.from(file.originalname, "latin1").toString(
            "utf8"
        );
        cb(null, file.originalname)
    }
})

const upload = multer({ storage: storage })

app.post('/upload', upload.single('file'), (req, res) => {
    res.send('File uploaded successfully')
})

app.listen(PORT, () => {
    console.log(`Server is running on port ${PORT}`);
})

这里使用 express 框架构架了一个简单的服务,需要注意的是代码中注释的内容,因为在我写测试代码的时候发现上传文件名称包含中文的文件时,到服务器端保存后文件名称会出现乱码。

谷歌一下之后发现是 Multer 这个库的问题,具体不多说明了,按照备注的代码操作就可以了。

前端代码实现:

html 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>File Upload</title>
</head>

<body>
    <p>upload single file</p>
    <input type="file" id="single-file">
    
    <script>
        // 单文件上传
        document.getElementById('single-file').addEventListener('change', function (event) {
            const file = event.target.files[0];

            console.log(file);

            const formData = new FormData()
            formData.append('file', file)
            fetch('http://localhost:3000/upload', {
                method: 'POST',
                body: formData
            }).then(res => {
                console.log(res);
            })
        })
    </script>
</body>

</html>

前后端的代码实现都非常的简单,接下来我们来实现分片上传。

分片上传文件

前端需要把文件切割成多个小片段,当前片段数小于总片段数就递归上传。input 标签选中的 File 文件实现了 Blob 的接口,也就是可以使用 slice 方法切割二进制数据。File 对象的 size 属性则可以实现切割计算总的片段数。

前端代码如下:

html 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>File Upload</title>
</head>

<body>
    <p>upload file chunks</p>
    <input type="file" id="fileInput">

    <script>
        // 分片上传
        document.getElementById('fileInput').addEventListener('change', function (event) {
            const file = event.target.files[0];
            if (file) {
                uploadFile(file);
            }
        });

        function uploadFile(file) {
            const chunkSize = 1 * 1024; // 1KB
            const totalChunks = Math.ceil(file.size / chunkSize);
            let currentChunk = 0;

            let progress = 0;

            function uploadChunk(start) {
                const end = Math.min(start + chunkSize, file.size);
                const chunk = file.slice(start, end);
                const formData = new FormData();
                formData.append('file', chunk);
                formData.append('filename', file.name);
                formData.append('chunkNumber', currentChunk);
                formData.append('totalChunks', totalChunks);

                fetch('http://localhost:9000/upload', {
                    method: 'POST',
                    body: formData
                }).then(response => {
                    if (response.ok) {
                        currentChunk++;

                        progress = ((currentChunk / totalChunks) * 100).toFixed(2) + '%';
                        console.log('progress:',progress);

                        if (currentChunk < totalChunks) {
                            uploadChunk(currentChunk * chunkSize);
                        } else {
                            console.log('Upload complete');
                        }
                    } else {
                        console.error('Upload failed');
                    }
                }).catch(error => {
                    console.error('Upload error', error);
                });
            }

            uploadChunk(0);
        }
    </script>
</body>

</html>

这里需要注意就是 totalChunks 的计算需要使用 Math.ceil 向上舍入, end 的取值使用 Math.min, 这样就可以避免超过文件的长度。

服务端代码:

js 复制代码
// 断点续传的后端 node 实现
const express = require('express');
const multer = require('multer');
const cors = require('cors')

const fs = require('fs');
const path = require('path');

const app = express();

app.use(cors()) // 允许跨域

// 配置存储选项
const storage = multer.diskStorage({
    destination: (req, file, cb) => {
        // 如果没有 uplaods 文件夹,则创建文件夹
        const uploadDir = path.join(__dirname, 'uploads');
        if (!fs.existsSync(uploadDir)) {
            fs.mkdirSync(uploadDir, { recursive: true });
        }
        cb(null, uploadDir);
    }
});

const upload = multer({ storage });

// 解析 JSON 和 URL 编码数据
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// 处理文件上传
app.post('/upload', upload.single('file'), (req, res) => {
    const { filename, chunkNumber, totalChunks } = req.body;
    if (!filename || chunkNumber === undefined || totalChunks === undefined) {
        return res.status(400).send('Filename, chunkNumber or totalChunks is missing');
    }

    const tempFilePath = path.join(__dirname, 'uploads', `${filename}.part${chunkNumber}`);
     // 在将磁盘中上传的 原生 chunk 名称更改为 filename.part0
     // xxxx 变更为 xxx.part0
    fs.renameSync(req.file.path, tempFilePath);

    // 合并文件逻辑
    if (Number(chunkNumber) + 1 === Number(totalChunks)) {
        const finalFilePath = path.join(__dirname, 'uploads', filename);
        const writeStream = fs.createWriteStream(finalFilePath);
        for (let i = 0; i < totalChunks; i++) {
            const chunkPath = path.join(__dirname, 'uploads', `${filename}.part${i}`);
            const data = fs.readFileSync(chunkPath); // 读取 chunk 文件
            writeStream.write(data);
            fs.unlinkSync(chunkPath); // 逐一删除 chunk 文件
        }
        writeStream.end();
    }

    res.send('Chunk uploaded successfully');
});

const PORT = 9000
app.listen(PORT, () => {
    console.log(`Server is running on http://localhost:${PORT}`);
});

服务端主要是 createWriteStream 将文件片段拼接,然后删除片段文件。你可以通过断点调试理解这个步骤。

相关推荐
用户21816970493022 分钟前
Flutter(四)Dart语法 空安全 运算符 流程控制
前端
许彰午22 分钟前
政务督办的分合模式:主办协办的并发审批
前端·javascript·政务
用户693717500138422 分钟前
AI时代,程序员该往哪走?
前端·后端
陳陈陳23 分钟前
🚀 前端流式输出革命:SSE + BFF 架构从零到一实战指南
vue.js·架构·node.js
ttwuai27 分钟前
GoFrame 后台日志清空失败:无 WHERE 删除为什么被拦住
前端·golang
易筋紫容32 分钟前
创建型模式:对象的诞生艺术
开发语言·前端·javascript
Revolution611 小时前
第一次运行 Node.js:终端里的 JavaScript 怎样执行
后端·面试·node.js
半句唐诗1 小时前
我是如何通过 Access Token 成功发布第一个 npm 包的
前端·npm·node.js
程序员黑豆1 小时前
鸿蒙应用开发:@Provider 与 @Consumer 跨组件双向同步详解
前端·harmonyos
paopaokaka_luck1 小时前
基于springboot3+vue3的智能文库平台(AI智能搜索、AI智能汇总、实时在线状态展示、多格式文档预览与富文本编辑、Echarts图形化分析)
前端·网络·spring boot·网络协议·echarts