前端分片上传文件

介绍

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

相关推荐
慧一居士34 分钟前
flex 布局完整功能介绍和示例演示
前端
DoraBigHead36 分钟前
小哆啦解题记——两数失踪事件
前端·算法·面试
一斤代码6 小时前
vue3 下载图片(标签内容可转图)
前端·javascript·vue
中微子6 小时前
React Router 源码深度剖析解决面试中的深层次问题
前端·react.js
光影少年6 小时前
从前端转go开发的学习路线
前端·学习·golang
中微子7 小时前
React Router 面试指南:从基础到实战
前端·react.js·前端框架
3Katrina7 小时前
深入理解 useLayoutEffect:解决 UI "闪烁"问题的利器
前端·javascript·面试
前端_学习之路8 小时前
React--Fiber 架构
前端·react.js·架构
coderlin_8 小时前
BI布局拖拽 (1) 深入react-gird-layout源码
android·javascript·react.js
甜瓜看代码8 小时前
1.
react.js·node.js·angular.js