文件分片上传??拿捏

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

前端:这个得问后端

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

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

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

后端:我们用的是阿里云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服务也是有提供对应的接口方案,但是,我们应该要了解分片是怎样进行存储,又是怎么合并成一个文件的。

最后

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

相关推荐
安心不心安31 分钟前
React hooks——useReducer
前端·javascript·react.js
像风一样自由202033 分钟前
原生前端JavaScript/CSS与现代框架(Vue、React)的联系与区别(详细版)
前端·javascript·css
啃火龙果的兔子34 分钟前
react19+nextjs+antd切换主题颜色
前端·javascript·react.js
_pengliang40 分钟前
小程序按住说话
开发语言·javascript·小程序
布兰妮甜41 分钟前
创建游戏或互动体验:从概念到实现的完整指南
javascript·游戏开发·游戏ai·互动体验·用户输入处理
paid槮44 分钟前
HTML5如何创建容器
前端·html·html5
ん贤1 小时前
如何加快golang编译速度
后端·golang·go
小飞悟1 小时前
一打开文章就弹登录框?我忍不了了!
前端·设计模式
烛阴1 小时前
Python模块热重载黑科技:告别重启,代码更新如丝般顺滑!
前端·python
吉吉612 小时前
Xss-labs攻关1-8
前端·xss