文件分片上传??拿捏

产品:什么???上传一张不到三十兆的图片要将近一分钟,不知道的还以为系统挂了呢,能不能搞,能不能优化。

前端:这个得问后端

后端:上传接口都给你们前端了,你们就不能做个分片上传啊

工作中偶尔遇到这种扯淡的后端,连文件分片上传和文件普通上传的实现逻辑都没搞清楚,就开始甩锅给前端,这个时候,前端就应该要了解分片上传和文件普通上传的区别,就能让你有理有据的去怼后端

前端:通用的上传接口怎么处理文件分片和文件合并,你不会连分片需要合并都不知道吧?

后端:我们用的是阿里云OSS,合并什么?

前端:不会吧,难道你连阿里云OSS的分片上传方案都不知道吗!!!回去多看看书吧!!!

文件分片上传与合并

xml 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <title>文件分片上传</title>
    <style>
      .file-info {
        margin: 10px 0;
      }
    </style>
  </head>
  <body>
    <input type="file" id="fileInput" />
    <button id="uploadBtn">上传</button>
    <div id="fileInfo" class="file-info"></div>
​
    <script>
      const fileInput = document.getElementById("fileInput");
      const uploadBtn = document.getElementById("uploadBtn");
      const fileInfo = document.getElementById("fileInfo");
​
      let file = null;
      let fileHash = "";
      let chunkSize = 1 * 1024 * 1024; // 1MB
      let chunks = [];
      let uploadedChunks = 0;
​
      // 选择文件
      fileInput.addEventListener("change", (e) => {
        file = e.target.files[0];
        if (!file) return;
​
        fileInfo.innerHTML = `
                <p>文件名: ${file.name}</p>
                <p>文件大小: ${(file.size / 1024 / 1024).toFixed(2)} MB</p>
                <p>分片大小: ${(chunkSize / 1024 / 1024).toFixed(2)} MB</p>
                <p>总分片数: ${Math.ceil(file.size / chunkSize)}</p>
            `;
​
        // 生成文件唯一标识
        generateFileHash(file).then((hash) => {
          fileHash = hash;
          console.log("文件hash:", fileHash);
        });
      });
​
      // 上传按钮
      uploadBtn.addEventListener("click", async () => {
        if (!file) {
          alert("请先选择文件");
          return;
        }
​
        // 分割文件
        chunks = createChunks(file, chunkSize);
​
        // 上传所有分片
        await uploadChunks(chunks);
​
        // 合并请求
        await mergeRequest();
​
        alert("上传完成!");
      });
​
      // 生成文件hash
      async function generateFileHash(file) {
        return new Promise((resolve) => {
          const spark = new SparkMD5.ArrayBuffer();
          const reader = new FileReader();
          const size = file.size;
          const offset = 2 * 1024 * 1024;
​
          // 读取文件前2M,中间2M,最后2M的内容来计算hash
          const chunks = [
            file.slice(0, offset),
            file.slice(size / 2 - offset / 2, size / 2 + offset / 2),
            file.slice(size - offset, size),
          ];
​
          let currentChunk = 0;
​
          reader.onload = (e) => {
            spark.append(e.target.result);
            currentChunk++;
​
            if (currentChunk < chunks.length) {
              reader.readAsArrayBuffer(chunks[currentChunk]);
            } else {
              resolve(spark.end());
            }
          };
​
          reader.readAsArrayBuffer(chunks[currentChunk]);
        });
      }
​
      // 分割文件
      function createChunks(file, chunkSize) {
        const chunks = [];
        let start = 0;
​
        while (start < file.size) {
          chunks.push({
            index: chunks.length,
            file: file.slice(start, start + chunkSize),
          });
          start += chunkSize;
        }
​
        return chunks;
      }
​
      // 上传分片
      async function uploadChunks(existingChunks = []) {
        const uploadedChunkIndexes = existingChunks.map((c) => c.index);
​
        for (let i = 0; i < chunks.length; i++) {
          const formData = new FormData();
          formData.append("file", chunks[i].file);
          const searchParams = new URLSearchParams();
          searchParams.append("index", chunks[i].index);
          searchParams.append("fileHash", fileHash);
​
          try {
            await fetch(
              "http://localhost:3000/upload?" + searchParams.toString(),
              {
                method: "POST",
                body: formData,
              }
            );
          } catch (err) {
            console.error("上传失败:", err);
            break;
          }
        }
      }
​
      // 合并请求
      async function mergeRequest() {
        try {
          await fetch("http://localhost:3000/upload/merge", {
            method: "POST",
            headers: {
              "Content-Type": "application/json",
            },
            body: JSON.stringify({
              fileHash: fileHash,
              fileName: file.name,
              total: chunks.length,
            }),
          });
        } catch (err) {
          console.error("合并失败:", err);
        }
      }
    </script>
​
    <!-- 用于生成文件hash的库 -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/spark-md5/3.0.2/spark-md5.min.js"></script>
  </body>
</html>

前端主要逻辑:前端获取一个hash做为单次上传的标识(正常通过服务端获取,以确保唯一性),对文件进行分片并序列化,然后进行所有分片文件的上传,同时传递对应hash以及分片的序列号,最后,所有分片上传成功后调用文件合并接口。

服务端使用node实现:

ini 复制代码
const express = require("express");
const multer = require("multer");
const fs = require("fs");
const path = require("path");
const cors = require("cors");
​
const app = express();
app.use(cors());
app.use(express.json());
​
// 上传目录
const UPLOAD_DIR = path.resolve(__dirname, "upload");
​
// 确保上传目录存在
if (!fs.existsSync(UPLOAD_DIR)) {
  fs.mkdirSync(UPLOAD_DIR);
}
​
// 修改multer配置
const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    const fileHash = req.query.fileHash;
    const chunkDir = path.resolve(UPLOAD_DIR, fileHash);
    if (!fs.existsSync(chunkDir)) {
      fs.mkdirSync(chunkDir);
    }
    cb(null, chunkDir);
  },
  filename: (req, file, cb) => {
    let query = req.query;
    // 使用fileHash和index作为文件名  分片
    cb(null, `${query.fileHash}-${query.index}`);
  },
});
​
// 创建一个普通上传中间件来解析字段
const upload = multer({
  storage,
});
​
// 上传分片
app.post("/upload", upload.single("file"), (req, res) => {
  res.json({
    code: 200,
    success: true,
    message: "分片上传成功",
  });
});
​
// 合并分片
app.post("/upload/merge", async (req, res) => {
  const { fileHash, fileName, total } = req.body;
  const filePath = path.resolve(UPLOAD_DIR, `${fileHash}-${fileName}`);
  const chunkDir = path.resolve(UPLOAD_DIR, fileHash);
​
  try {
    // 检查分片是否完整
    let chunks = fs.readdirSync(chunkDir);
    if (chunks.length !== total) {
      return res.status(400).json({
        success: false,
        message: "分片数量不匹配",
      });
    }
​
    // 按照索引排序
    chunks = chunks.sort((a, b) => {
      const aIndex = parseInt(a.split("-").pop());
      const bIndex = parseInt(b.split("-").pop());
      return aIndex - bIndex;
    });
​
    let writeStream = fs.createWriteStream(filePath);
​
    // 合并文件
    await Promise.all(
      chunks.map((chunk, index) => {
        const chunkPath = path.resolve(chunkDir, chunk);
        return new Promise((resolve) => {
          writeStream.write(fs.readFileSync(chunkPath), () => {
            resolve();
          });
        });
      })
    );
    writeStream.end();
​
    // 删除临时目录
    // fs.rmdirSync(chunkDir);
​
    res.json({
      code: 200,
      success: true,
      message: "合并成功",
    });
  } catch (err) {
    res.status(500).json({
      code: 500,
      success: false,
      message: "合并失败",
    });
  }
});
​
​
// 启动服务器
const PORT = 3000;
app.listen(PORT, () => {
  console.log(`服务器已启动,访问 http://localhost:${PORT}`);
});
​

服务端主要逻辑:

  1. 上传分片接口:通过前端传递的参数和文件流,multer 插件将每一段分片存储在同一个hash目录下,并且每一段分片命名上通过 "-" 拼接序列号
  2. 合并文件接口:通过前端参数,对相应hash目录下的分片进行排序,创建一个文件流,并将分片按顺序写入到这个文件流中,最后返回文件的访问链接

简单分析上述逻辑

1、获取一个唯一hash,单次文件上传对应一个hash(通常后端接口获取)

2、通过slice 方法对File 对象进行分割(分片),设置一个size 大小,每段最多size大小

3、上传所有分片文件,并传递相应参数,如:hash、index(文件顺序)......

4、当所有分片上传成功,合并文件

实际场景

工作当中一般不会这样实现服务端,一般文件存储都会用流行的OSS 存储服务,并不会自己手动去实现这些功能,服务端只需要按要求调用OSS 服务提供的接口即可,即使是分片上传功能,OSS服务也是有提供对应的接口方案,但是,我们应该要了解分片是怎样进行存储,又是怎么合并成一个文件的。

最后

分片上传虽然能提升上传的速度,但也是受限于带宽的,分片处理方案只会更充分利用服务器的带宽,但如果服务器带宽就有限,那么即使是分片上传也提升不了太大上传速度。

相关推荐
恋猫de小郭1 小时前
Flutter Zero 是什么?它的出现有什么意义?为什么你需要了解下?
android·前端·flutter
牛奔2 小时前
Go 如何避免频繁抢占?
开发语言·后端·golang
想用offer打牌7 小时前
MCP (Model Context Protocol) 技术理解 - 第二篇
后端·aigc·mcp
崔庆才丨静觅7 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60618 小时前
完成前端时间处理的另一块版图
前端·github·web components
KYGALYX8 小时前
服务异步通信
开发语言·后端·微服务·ruby
掘了8 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅8 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅9 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
爬山算法9 小时前
Hibernate(90)如何在故障注入测试中使用Hibernate?
java·后端·hibernate