大文件分片上传

使用大文件分片技术提升性能和用户体验.


使用大文件分片技术可以改善前端应用程序处理大文件时的性能和用户体验,特别是在网络条件不佳的情况下。

文件切片

使用 JavaScript 将大文件分割成多个小块,通过使用 File Api 中的 Blob 对象来实现,Blob 对象表示一个不可变的、原始数据的类文件对象,可以通过 File 对象的 slice 方法来将文件切割成指定大小的块。

const createChunk = (file, index, chunkSize) => {
  return new Promise((resolve) => {
    const start = index * chunkSize;
    const end = start + chunkSize;
    const fileReader = new FileReader();
    const blob = file.slice(start, end);
    fileReader.onload = (e) => {
      resolve({
        start,
        end,
        index,
        blob,
      });
    };
    fileReader.readAsArrayBuffer(blob);
  });
};

上传分片

使用 Ajax 或者其它网络请求方法将这些分片上传到服务器。

const uploadChunk = async (chunk) => {
  try {
    const formData = new FormData();
    formData.append("file", chunk.blob);
    formData.append("start", chunk.start);
    formData.append("end", chunk.end);
    formData.append("index", chunk.index);

    const response = await fetch("http://localhost:3000/upload", {
      method: "POST",
      body: formData,
    });

    if (!response.ok) {
      throw new Error("上传块失败");
    }
  } catch (error) {
    console.log(error);
  }
};

服务器端重组

在服务器端,接收到文件分片后,需要将这些分片重新组合成原始文件,可以通过一些标记或索引来确定哪些分片属于同一个文件,并将它们合并成完整的文件。

const express = require("express");
const multer = require("multer");
const fs = require("fs");
const path = require("path");

const app = express();
const PORT = 3000;

app.use(function (req, res, next) {
  res.header("Access-Control-Allow-Origin", "*");
  res.header(
    "Access-Control-Allow-Headers",
    "Origin, X-Requested-With, Content-Type, Accept"
  );
  next();
});

// 创建一个 Multer 实例来处理文件上传
const upload = multer({ dest: "uploads/" });

// 存储分片信息的对象
const uploadedChunks = {};

// 处理客户端上传的文件分片
app.post("/upload", upload.single("file"), (req, res) => {
  const start = parseInt(req.body.start);
  const end = parseInt(req.body.end);
  const index = parseInt(req.body.index);
  const file = req.file;

  // 读取分片的内容并将其追加到已保存的文件末尾
  fs.readFile(file.path, (err, data) => {
    if (err) {
      console.error(err);
      return res.status(500).send("读取分片数据失败");
    }

    // 将接收到的分片追加到已保存的文件末尾
    const chunkFilePath = path.join(
      __dirname,
      `uploads/${file.originalname}.${index}`
    );
    fs.appendFileSync(chunkFilePath, data);

    console.log(`Chunk ${index} saved as ${chunkFilePath}`);

    // 将分片信息存储到 uploadedChunks 中
    uploadedChunks[index] = {
      start,
      end,
      path: chunkFilePath,
    };

    // 如果是最后一个分片,发送成功响应
    if (end >= file.size) {
      console.log("收到所以分片");
      return res.send("分片上传成功");
    }

    // 否则,继续等待下一个分片
    res.send("收到分片");
  });
});

// 合并文件分片
consat mergeChunks = (outputFilePath) => {
  // 创建可写流
  const writeStream = fs.createWriteStream(outputFilePath);

  // 按索引顺序合并分片
  Object.keys(uploadedChunks).sort((a, b) => parseInt(a) - parseInt(b)).forEach(key => {
    const chunkInfo = uploadedChunks[key];
    if (fs.existsSync(chunkInfo.path)) {
      const chunkData = fs.readFileSync(chunkInfo.path);
    writeStream.write(chunkData);
    // 删除已合并的分片文件
    fs.unlinkSync(chunkInfo.path);
    }
  });

  // 结束写入流
  writeStream.end();

  console.log('文件重组成功!');
}

app.listen(PORT, () => {
  console.log(`服务器正在运行 http://localhost:${PORT}`);
});

进度追踪

在上传或处理过程中,可以通过监听上传进度事件或其他相关事件来实现进度追踪,并将进度信息反馈给用户,让用户了解操作的进展情况。

// 计算并更新上传进度
const progress = Math.min((end / totalSize) * 100, 100);
document.getElementById("progress").innerText = `已上传: ${progress.toFixed(
  2
)}%`;
// 如果上传已经完成,执行额外的逻辑
if (end >= totalSize) {
  document.getElementById("progress").innerText = `上传完成!`;
  // 这里可以执行一些额外的逻辑,比如显示上传完成的消息或者重定向到其他页面
}

错误处理

在分片上传或者处理过程中可能会出现各种错误,例如网络中断、服务器错误等,需要适当的错误处理机制来处理这些情况,并且向用户提供反馈

<input type="file" id="fileInput" />
const fileInput = document.getElementById("fileInput");

const CHUNK_SIZE = 1024 * 1024 * 5;

fileInput.onchange = async (e) => {
  const file = e.target.files[0];
  const chunks = await cutFile(file);
  await uploadChunks(chunks);
};

const cutFile = async (file) => {
  const chunkCount = Math.ceil(file.size / CHUNK_SIZE);
  const result = [];
  for (let i = 0; i < chunkCount; i++) {
    const chunk = await createChunk(file, i, CHUNK_SIZE);
    result.push(chunk);
  }
  return result;
};

const createChunk = (file, index, chunkSize) => {
  return new Promise((resolve) => {
    const start = index * chunkSize;
    const end = start + chunkSize;
    const fileReader = new FileReader();
    const blob = file.slice(start, end);
    fileReader.onload = (e) => {
      resolve({
        start,
        end,
        index,
        blob,
      });
    };
    fileReader.readAsArrayBuffer(blob);
  });
};

const uploadChunks = async (chunks) => {
  for (const chunk of chunks) {
    await uploadChunk(chunk);
  }
};

const uploadChunk = async (chunk) => {
  try {
    const formData = new FormData();
    formData.append("file", chunk.blob);
    formData.append("start", chunk.start);
    formData.append("end", chunk.end);
    formData.append("index", chunk.index);

    const response = await fetch("http://localhost:3000/upload", {
      method: "POST",
      body: formData,
    });

    if (!response.ok) {
      throw new Error("上传块失败");
    }
  } catch (error) {
    console.log(error);
  }
};