前端分片上传文件

介绍

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

相关推荐
abc80021170348 分钟前
前端Bug 修复手册
前端·bug
Best_Liu~11 分钟前
el-table实现固定列,及解决固定列导致部分滚动条无法拖动的问题
前端·javascript·vue.js
_斯洛伐克1 小时前
下降npm版本
前端·vue.js
苏十八2 小时前
前端进阶:Vue.js
前端·javascript·vue.js·前端框架·npm·node.js·ecmascript
st紫月3 小时前
用MySQL+node+vue做一个学生信息管理系统(四):制作增加、删除、修改的组件和对应的路由
前端·vue.js·mysql
乐容3 小时前
vue3使用pinia中的actions,需要调用接口的话
前端·javascript·vue.js
似水明俊德4 小时前
ASP.NET Core Blazor 5:Blazor表单和数据
java·前端·javascript·html·asp.net
至天5 小时前
UniApp 中 Web/H5 正确使用反向代理解决跨域问题
前端·uni-app·vue3·vue2·vite·反向代理
与墨学长5 小时前
Rust破界:前端革新与Vite重构的深度透视(中)
开发语言·前端·rust·前端框架·wasm
H-J-L5 小时前
Web基础与HTTP协议
前端·http·php