前端分片上传文件

介绍

前端上传文件是项目中经常遇见的一个功能,但是如果文件太大了,我们就需要分片上传了,简单的说就是把文件切割成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 将文件片段拼接,然后删除片段文件。你可以通过断点调试理解这个步骤。

相关推荐
恋猫de小郭1 小时前
Flutter Zero 是什么?它的出现有什么意义?为什么你需要了解下?
android·前端·flutter
崔庆才丨静觅7 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60618 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了8 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅8 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅9 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅9 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment9 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅9 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊9 小时前
jwt介绍
前端