使用 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 函数将所有片段合并成一个完整的文件。合并完成后,删除临时文件。整个过程包括文件切片、上传、保存、合并和清理,确保了大文件的高效传输和处理。

相关推荐
wwangxu几秒前
Java 面向对象基础
java·开发语言
Monly212 分钟前
JS:JSON操作
前端·javascript·json
wdxylb15 分钟前
Linux下编写第一个bash脚本
开发语言·chrome·bash
幽兰的天空18 分钟前
Python实现的简单时钟
开发语言·python
YesPMP2524 分钟前
短剧小程序,打造专属短剧观看平台
小程序·app·html5·平台·短剧·影视
这题怎么做?!?26 分钟前
模板方法模式
开发语言·c++·算法
小何学计算机1 小时前
Nginx 配置基于主机名的 Web 服务器
服务器·前端·nginx
幽兰的天空1 小时前
简单的Python爬虫实例
开发语言·爬虫·python
web_code1 小时前
vite依赖预构建(源码分析)
前端·面试·vite
觉醒法师1 小时前
HarmonyOS开发 - 本地持久化之实现LocalStorage支持多实例
前端·javascript·华为·typescript·harmonyos