Html + Express 实现大文件分片上传、断点续传、秒传

在日常的网页开发中,文件上传是一项常见操作。通过文件上传技术,用户可以将本地文件方便地传输到Web服务器上。这种功能在许多场景下都是必不可少的,比如上传文件到网盘或上传用户头像等。

然而,当需要上传大型文件时,可能会遇到以下问题:

1. 长时间上传:由于文件大小较大,上传过程可能会耗费较长时间。

2. 上传中断重新上传:如果在上传过程中出现意外情况导致上传中断,用户需要重新开始整个上传过程,这会增加用户的不便。

3. 服务端限制:通常,服务端会对上传的文件大小进行限制,这可能导致无法上传大型文件。

为了解决这些问题,可以采用分片上传的方式:

分片上传即将大文件分割成小块,然后分块上传到服务器。通过分片上传,可以实现以下优势:

快速上传:由于每个小块的大小相对较小,上传时间大大缩短。

断点续传:如果上传过程中出现中断,只需重新上传中断的部分,而不需要重新上传整个文件,提高了用户体验。

避免大小限制:分片上传可以避免由于文件大小限制而无法上传大文件的问题。

通过采用分片上传技术,可以提升用户体验,加快大文件上传速度,并确保上传过程的稳定性和可靠性。

原理:


分片上传的概念类似于将一个大文件分割成多个小块,然后分别上传这些小块到服务器上。
首先,将待上传的大文件划分为固定大小的小块,比如每块大小为1MB。然后逐个上传这些小块到服务器。在上传过程中,可以同时处理多个小块的上传,也可以按顺序逐一上传小块。每个小块上传完成后,服务器会妥善保存这些小块,并记录它们的顺序和位置信息。
当所有小块都上传完成后,服务器会按照预先记录的顺序和位置信息,将这些小块组合成完整的大文件。最终,整个大文件就成功地被分片上传并合并完成了。这种分片上传的方式能够有效地提升大文件上传的效率和稳定性,确保文件上传过程更加可靠和高效。

前端代码

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/spark-md5@3.0.2/spark-md5.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/axios@1.4.0/dist/axios.min.js"></script>
</head>

<body>
    <input type="file" />
    <script>
        const CHUNK_SIZE = 1024 * 1024
        let hashName = ''
        let fileName = ''

        $('input').change(async (e) => {
            const file = e.target.files[0]
            const chunks = shardingChunks(file) // 分片
            fileName = file.name
            hashName = await shardingHash(file) // 获取文件hash值

            const { data: { existFile, existChunks } } = await axios.post('http://localhost:3000/uploader/verify', { fileHash: hashName, fileName });
            if (existFile) return; // 如果该hash值 && file.name 存在说明该文件已经在服务器上了
            uploader(chunks, existChunks)
        })

        //  分片
        const shardingChunks = (file) => {
            let start = 0
            const chunks = []
            while (start < file.size) {
                chunks.push(file.slice(start, start + CHUNK_SIZE))
                start += CHUNK_SIZE
            }
            return chunks
        }

        // 获取文件hash值
        const shardingHash = (file) => {
            return new Promise((resolve) => {
                const fileReader = new FileReader()
                fileReader.readAsArrayBuffer(file)
                fileReader.onload = (e) => {
                    const spark = new SparkMD5.ArrayBuffer()
                    spark.append(e.target.result)
                    resolve(spark.end())
                }
            })
        }

        // 分片上传
        const uploader = async (chunks, existChunks) => {
            const chunksArr = chunks.map((chunk, index) => ({
                fileHash: hashName,
                chunkHash: hashName + '-' + index,
                chunk
            }))
            const formDatas = chunksArr.map(item => {
                const formData = new FormData();
                formData.append("fileHash", item.fileHash);
                formData.append("chunkHash", item.chunkHash);
                formData.append("chunk", item.chunk);
                return formData;
            })
            let flagArr = []
            formDatas.forEach(async (item) => {
                const res = await axios.post('http://localhost:3000/uploader/upload', item, {
                    headers: {
                        'Content-Type': 'multipart/form-data'
                    }
                })
                flagArr.push(res.data.success)
                if (flagArr.length == formDatas.length && flagArr.every(item => item == true)) {
                    mergeFile() // 合并文件
                    flagArr = []
                }
            })
        }

        const mergeFile = async () => {
            const res = await axios.post('http://localhost:3000/uploader/merge',
                {
                    fileHash: hashName,
                    fileName: fileName
                })
            if (res.data.success) return alert('上传成功')
        }
    </script>
</body>

</html>

后端代码(Node)

const express = require("express");
const cors = require("cors");
const bodyParser = require("body-parser");
const fse = require("fs-extra");
const path = require("path");
const multipart = require("connect-multiparty");
const multipartMiddleware = multipart();
 
const app = express();
 
app.use(cors());
app.use(bodyParser.json());
 
// 所有上传的文件存放在该目录下
const UPLOADS_DIR = path.resolve("uploads");
 
/**
 * 上传
 */
app.post("/upload", multipartMiddleware, (req, res) => {
  const { fileHash, chunkHash } = req.body;
 
  // 如果临时文件夹(用于保存分片)不存在,则创建
  const chunkDir = path.resolve(UPLOADS_DIR, fileHash);
  if (!fse.existsSync(chunkDir)) {
    fse.mkdirSync(chunkDir);
  }
 
  // 如果临时文件夹里不存在该分片,则将用户上传的分片移到临时文件夹里
  const chunkPath = path.resolve(chunkDir, chunkHash);
  if (!fse.existsSync(chunkPath)) {
    fse.moveSync(req.files.chunk.path, chunkPath);
  }
 
  res.send({
    success: true,
    msg: "上传成功",
  });
});
 
/**
 * 合并
 */
app.post("/merge", async (req, res) => {
  const { fileHash, fileName } = req.body;
 
  // 最终合并的文件路径
  const filePath = path.resolve(UPLOADS_DIR, fileHash + path.extname(fileName));
  // 临时文件夹路径
  const chunkDir = path.resolve(UPLOADS_DIR, fileHash);
 
  // 读取临时文件夹,获取该文件夹下“所有文件(分片)名称”的数组对象
  const chunkPaths = fse.readdirSync(chunkDir);
 
  // 读取临时文件夹获得的文件(分片)名称数组可能乱序,需要重新排序
  chunkPaths.sort((a, b) => a.split("-")[1] - b.split("-")[1]);
 
  // 遍历文件(分片)数组,将分片追加到文件中
  const pool = chunkPaths.map(
    (chunkName) =>
      new Promise((resolve) => {
        const chunkPath = path.resolve(chunkDir, chunkName);
        // 将分片追加到文件中
        fse.appendFileSync(filePath, fse.readFileSync(chunkPath));
        // 删除分片
        fse.unlinkSync(chunkPath);
        resolve();
      })
  );
  await Promise.all(pool);
  // 等待所有分片追加到文件后,删除临时文件夹
  fse.removeSync(chunkDir);
 
  res.send({
    success: true,
    msg: "合并成功",
  });
});
 
/**
 * 校验
 */
app.post("/verify", (req, res) => {
  const { fileHash, fileName } = req.body;
 
  // 判断服务器上是否存在该hash值的文件
  const filePath = path.resolve(UPLOADS_DIR, fileHash + path.extname(fileName));
  const existFile = fse.existsSync(filePath);
 
  // 获取已经上传到服务器的文件分片
  const chunkDir = path.resolve(UPLOADS_DIR, fileHash);
  const existChunks = [];
  if (fse.existsSync(chunkDir)) {
    existChunks.push(...fse.readdirSync(chunkDir));
  }
 
  res.send({
    success: true,
    msg: "校验文件",
    data: {
      existFile,
      existChunks,
    },
  });
});
 
const server = app.listen(3000, () => {
  console.log(`Example app listening on port ${server.address().port}`);
});

效果图

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值