使用 Axios 上传大文件分片上传

背景

在上传大文件时,分片上传是一种常见且有效的策略。由于大文件在上传过程中可能会遇到内存溢出、网络不稳定等问题,分片上传可以显著提高上传的可靠性和效率。通过将大文件分割成多个小分片,不仅可以减少单次上传的数据量,降低内存消耗,还能在遇到网络中断时仅需重传失败的分片,从而提高整体上传的成功率和用户体验。

步骤

安装 Axios

如果你还没有安装 Axios,可以通过 npm 或 yarn 来安装:

bash 复制代码
npm install axios
# 或者
yarn add axios

获取文件

点击按钮选择文件上传,通过 event 事件对象拿到文件。

javascript 复制代码
<template>
  <div>
    <input type="file" @change="uploadFile"></input>
  </div>
</template>
<script>
import axios from "axios";

export default {
	methods: {
    	uploadFile(event) {
	      const files = event.target.files || event.dataTransfer.files;
	      const file = files[0];
	      console.log('file::: ', file);
	      this.uploadChunks(file, file.name, progress => {
	        console.log(`Upload progress: ${progress * 100}%`);
      	  });
       },
    },
}
</script>

文件切片并使用 Axios 上传切片:

1. 文件切片:

  • 定义 chunkSize 每片大小为 1MB,计算文件需要分割成的总分块数 totalChunks

2. 循环分块上传:

  • 遍历每个分块,计算每个分块的起始位置 start 和结束位置 end

  • 使用 file.slice 方法创建 blob 对象表示当前分块。

  • 创建 FormData 对象,并添加分块数据及其他元数据(文件名、分块索引、总分块数)。

3. 循环分块上传:

  • 使用 axios.post 发送 POST 请求到 /upload 接口,携带分块数据。

  • 设置请求头 Content-Typemultipart/form-data

4. 循环分块上传:

  • 成功上传分块后,记录已上传的分块数量,并调用上传进度的回调函数 onProgress

  • 设如果上传失败,捕获并记录错误信息。

javascript 复制代码
async uploadChunks(file, fileName, onProgress) {
	const chunkSize = 1 * 1024 * 1024; // 1MB
    const totalChunks = Math.ceil(file.size / chunkSize);
    let uploadedChunks = 0;

    for (let i = 0; i < totalChunks; i++) {
        const start = i * chunkSize;
        const end = Math.min(start + chunkSize, file.size);
        const blob = file.slice(start, end);

        const formData = new FormData();
        formData.append('file', blob, `${fileName}_${i}`);
        formData.append('filename', fileName);
        formData.append('chunkIndex', i.toString());
        formData.append('totalChunks', totalChunks.toString());

        try {
			const response = await axios.post('/upload', formData, {
            	headers: {
              		'Content-Type': 'multipart/form-data',
            	},
          	});
			console.log(`Chunk ${i} uploaded successfully.`);
			uploadedChunks++;
	
			if (onProgress) {
				onProgress(uploadedChunks / totalChunks);
	        }
		} catch (error) {
			console.error(`Failed to upload chunk ${i}:`, error);
		}
      }
    }

完整代码

javascript 复制代码
<template>
  <div>
    <input type="file" @change="uploadFile"></input>
  </div>
</template>

<script>
import axios from "axios";

export default {
  data() {},
  methods: {
    uploadFile(event) {
      console.log('event::: ', event);
      // 获取文件对象
      const files = event.target.files || event.dataTransfer.files;
      console.log('files::: ', files);
      const file = files[0];
      this.uploadChunks(file, file.name, progress => {
        console.log(`Upload progress: ${progress * 100}%`);
      });
    },
   
    async uploadChunks(file, fileName, onProgress) {
      // 定义每个分片的大小为 1MB
      const chunkSize = 1 * 1024 * 1024; // 1MB
      // 计算总分片数
      const totalChunks = Math.ceil(file.size / chunkSize);
      let uploadedChunks = 0;
      
      // 遍历所有分片
      for (let i = 0; i < totalChunks; i++) {
        // 计算当前分片的起始位置
        const start = i * chunkSize;
        // 计算当前分片的结束位置
        const end = Math.min(start + chunkSize, file.size);
        // 创建当前分片的 Blob 对象
        const blob = file.slice(start, end);
		
		// 创建表单数据对象
        const formData = new FormData();
        // 添加当前分片的文件
        formData.append('file', blob, `${fileName}_${i}`);
        // 添加文件名
        formData.append('filename', fileName);
        // 添加分片索引
        formData.append('chunkIndex', i.toString());
        // 添加总分片数
        formData.append('totalChunks', totalChunks.toString());

        try {
          // 上传分片
          const response = await axios.post('/upload', formData, {
            headers: {
              'Content-Type': 'multipart/form-data',
            },
          });
          console.log(`Chunk ${i} uploaded successfully.`);
          uploadedChunks++;
		  // 上传进度
          if (onProgress) {
            onProgress(uploadedChunks / totalChunks);
          }
        } catch (error) {
          console.error(`Failed to upload chunk ${i}:`, error);
        }
      }
    }
  }
}
</script>

<style lang="scss" scoped></style>

注意:

  1. 使用 FormData 上传文件切片,确保文件部分是以二进制格式上传的。
  2. 设置 Content-Typemultipart/form-data

服务端合并切片

实现原理

1. 搭建服务

  • 服务搭建:引入 express 模块,创建了一个 express 应用实例 app

  • 设置端口号 PORT 并使用 app.listen() 启动 express 应用,使其监听指定的端口。

2. 接受并存储切片

  • 接收切片:服务端定义了一个 /upload 路由,使用 multer 中间件处理上传的文件切片。multer 会将上传的文件暂存到指定的目录(例如 uploads/)。

  • 保存切片:服务端根据 filenamechunkIndex 创建一个临时目录,并将上传的切片移动到该目录中。例如,切片路径可能为 uploads/filename/chunkIndex

  • 创建目录:如果临时目录不存在,服务端会使用 mkdir 方法递归创建目录。

3. 切片合并

  • 检测最后一个切片:当接收到的切片索引等于 totalChunks - 1 时,说明这是最后一个切片,触发切片合并操作。

  • 读取所有切片:在 mergeChunks 函数中,服务端遍历所有已上传的切片,按顺序读取每个切片的内容。

  • 合并切片:将所有切片的内容按顺序拼接成一个完整的文件。这里使用 Buffer.concat 方法将多个 Buffer 对象合并成一个。

  • 写入合并后的文件:将合并后的文件内容写入到目标目录(例如 merged/)。

  • 删除临时文件:合并完成后,删除所有临时切片文件,释放存储空间。

使用 node 示例

javascript 复制代码
const express = require('express');
const multer = require('multer');
const fs = require('fs');
const path = require('path');
const util = require('util');

const app = express();
const upload = multer({ dest: 'uploads/' });

// 设置静态文件夹
app.use(express.static('uploads'));

// 将 fs 方法转换为 Promise 版本
const mkdir = util.promisify(fs.mkdir);
const rename = util.promisify(fs.rename);
const unlink = util.promisify(fs.unlink);
const readFile = util.promisify(fs.readFile);
const writeFile = util.promisify(fs.writeFile);

// 文件合并函数
async function mergeChunks(filename, totalChunks) {
    // 定义存储切片临时文件夹路径
    const tempDir = `uploads/${filename}/`;
    // 定义最终合并文件的路径
    const outputFilePath = `merged/${filename}`;

    // 创建输出目录
    await mkdir(path.dirname(outputFilePath), { recursive: true });

    // 初始化一个空的 Buffer 用于存储合并后的数据
    let combinedData = Buffer.alloc(0);

    // 遍历所有切片文件并读取内容
    for (let i = 0; i < totalChunks; i++) {
        // 获取每个切片文件的路径
        const chunkPath = `${tempDir}${i}`;
        // 读取当前切片文件的内容
        const chunkData = await readFile(chunkPath);

        // 合并切片文件的内容追加到 combinedData 中
        combinedData = Buffer.concat([combinedData, chunkData]);
    }

     // 将合并后的数据写入最终的输出文件
    await writeFile(outputFilePath, combinedData);

    console.log('File merged successfully.');

    // 删除临时切片文件
    for (let i = 0; i < totalChunks; i++) {
        const chunkPath = `${tempDir}${i}`;
        try {
            await unlink(chunkPath);
        } catch (err) {
            console.error(`Error deleting chunk ${i}:`, err);
        }
    }

    // 删除临时文件夹
    try {
        await rmdir(tempDir, { recursive: true });
        console.log('Temporary directory deleted successfully.');
    } catch (err) {
        console.error('Error deleting temporary directory:', err);
    }
}


// 处理文件上传
app.post('/upload', upload.single('file'), async (req, res) => {
    const { filename, chunkIndex, totalChunks } = req.body;
    const chunkPath = `uploads/${filename}/${chunkIndex}`;

    try {
        // 创建文件切片目录
        await mkdir(path.dirname(chunkPath), { recursive: true });

        // 移动上传的文件到切片目录
        await rename(req.file.path, chunkPath);

        console.log(`Chunk ${chunkIndex} saved successfully`);

        // 如果这是最后一个切片,则合并所有切片
        if (parseInt(chunkIndex) === parseInt(totalChunks) - 1) {
            await mergeChunks(filename, totalChunks);
        }

        res.status(200).send('Chunk received');
    } catch (err) {
        console.error(`Error handling chunk ${chunkIndex}:`, err);
        res.status(500).send('Internal Server Error');
    }
});

// 启动服务器
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
    console.log(`Server is running on port ${PORT}`);
});

注意:

  1. fs 模块的方法转换为 Promise 版本,以便防止文件合并顺序错误而导致文件损坏。
  2. 在创建输出文件流时,设置 flags: 'w'encoding: null,确保以二进制格式写入文件。
  3. 在创建输入文件流时,设置 encoding: null,确保以二进制格式读取文件。

总结

前端:

点击按钮选取文件后,通过事件对象 event 拿到文件并按指定大小(如 1MB)进行分片,使用循环遍历每个分片,创建 blob 对象表示分片,将分片及其相关信息(文件名、分片索引、总分片数)封装到 FormData 对象中,最后使用 axios 发送 POST 请求上传每个分片。

服务端:

服务端通过 API 接口(如 /upload)接收前端上传的每个分片,解析请求中的 formData,提取分片数据、文件名、分片索引和总分片数,使用 expressmulter 接收这些片段,将其保存到临时目录,并在接收到最后一个片段时调用 mergeChunks 函数将所有片段合并成一个完整的文件。合并完成后,删除临时文件。整个过程包括文件切片、上传、保存、合并和清理,确保了大文件的高效传输和处理。

相关推荐
腾讯TNTWeb前端团队3 小时前
helux v5 发布了,像pinia一样优雅地管理你的react状态吧
前端·javascript·react.js
范文杰7 小时前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪7 小时前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪7 小时前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy8 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom8 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom8 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom8 小时前
React与Next.js:基础知识及应用场景
前端·面试·github
uhakadotcom8 小时前
Remix 框架:性能与易用性的完美结合
前端·javascript·面试
uhakadotcom9 小时前
Node.js 包管理器:npm vs pnpm
前端·javascript·面试