第二十章 Nest 大文件分片上传


在前端的文件上传功能中,只要请求头里定义 content-type 为 multipart/form-data,内容就会以下面形式传递到服务端,接着服务器再按照multipart/form-data的格式去提取数据 获取文件数据
1719751955317.png
但是当文件体积很大时 就会出现一个问题 文件越大 请求的时间会越长,会导致产品的体验很不好。
所以大文件上传时 我们要对齐进行优化 ,例如把1G的大文件 分割成10个100M的小文件,接着并行上传这些文件,服务端接收到10个文件之后再合并这10个小文件 成为一个大文件 这就是大文件分片上传的方案。

1、前端分片大文件方法

在游览器中 Blob 有着一个slice方法 可以截取特定范围的数据 File就是Blob中的一种,我们在选择文件之后可以通过slice 对 File 分片

地址:https://developer.mozilla.org/zh-CN/docs/Web/API/Blob/slice
1719752678591.png

2、后端合并分片文件的方法

在前端分片发送文件到后端全接收之后,在后端我们可以使用fs 的 createWriteStream 方法把每个分片按照不同位置写入文件

地址:https://nodejs.cn/api-v12/fs.html#fscreatewritestreampath-options
1719753140827.png

接下来我们创建项目来测试一下:

nest new large-file-sharding-upload

1719753353544.png
安装 multer 的 ts 类型的包

npm install -D @types/multer

在app.controller.ts中增加一个路由:

import { Body, Controller, Get, Post, UploadedFiles, UseInterceptors } from '@nestjs/common';
import { AppService } from './app.service';
import { FilesInterceptor } from '@nestjs/platform-express';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Post('upload')
  @UseInterceptors(FilesInterceptor('files', 20, {
    dest: 'uploads'
  }))
  uploadFiles(@UploadedFiles() files: Array<Express.Multer.File>, @Body() body) {
    console.log('body', body);
    console.log('files', files);
  }
}

接着启动服务:

pnpm run start:dev

可以看到根目录下,生成了uploads 目录:
1719753839814.png
然后我们在main.ts里面开启跨域支持:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.enableCors();
  await app.listen(3000);
}
bootstrap();

在根目录下新建index.html:(其中input 指定 multiple,可以选择多个文件)

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <!-- 引入axios库,用于发送HTTP请求 -->
    <script src="https://unpkg.com/axios@0.24.0/dist/axios.min.js"></script>
</head>

<body>
    <input id="fileInput" type="file" />
    <script>
        /* 获取文件输入元素 */
        const fileInput = document.querySelector('#fileInput');

        /* 定义每块的大小,单位为字节 */
        const chunkSize = 20 * 1024;

        /* 当文件输入改变时,处理文件分块上传 */
        fileInput.onchange = async function () {
            /* 获取选中的文件 */
            const file = fileInput.files[0];
            console.log(file);

            /* 分割文件为多个块 */
            const chunks = [];
            let startPos = 0;
            while (startPos < file.size) {
                chunks.push(file.slice(startPos, startPos + chunkSize));
                startPos += chunkSize;
            }

            /* 对每个分块创建表单数据并发送POST请求 */
            chunks.map((chunk, index) => {
                const data = new FormData();
                /* 为每个分块设置唯一的名称 */
                data.set('name', file.name + '-' + index);
                /* 添加分块到表单数据 */
                data.append('files', chunk);
                /* 使用axios发送POST请求上传分块 */
                axios.post('http://localhost:3000/upload', data);
            })
        }
    </script>
</body>

</html>

启动前端index.html

npx http-server

1719754178991.png
游览器访问:http://127.0.0.1:8080/
1719754275979.png
接着我们上传个文件 这里我上传的文件大小为20多kb 我的切片是20kb分片一个 所以一共会分片成2给
1719754989392.png
1719755058410.png
可以看到服务端接收了2给分片
1719755090845.png
接下来 我们在服务端实现接收到服务端的数据之后 实现将同一个文件的分片 放置到单独目录中

import { Body, Controller, Get, Post, UploadedFiles, UseInterceptors } from '@nestjs/common';
import { AppService } from './app.service';
import { FilesInterceptor } from '@nestjs/platform-express';
import * as fs from 'fs';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) { }

  @Post('upload')
  @UseInterceptors(FilesInterceptor('files', 20, {
    dest: 'uploads'
  }))
  uploadFiles(@UploadedFiles() files: Array<Express.Multer.File>, @Body() body) {
    console.log('body', body);
    console.log('files', files);

    const fileName = body.name.match(/(.+)\-\d+$/)[1];

    const chunkDir = 'uploads/chunks_' + fileName;

    if (!fs.existsSync(chunkDir)) {
      fs.mkdirSync(chunkDir);
    }
    fs.cpSync(files[0].path, chunkDir + '/' + body.name);
    fs.rmSync(files[0].path);
  }
}

再次上传文件 可以看到创建了目录 文件夹下有2个文件
1719755431335.png
我们在上传的时候 给文件名增加一个随机的字符串 防止重复

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <!-- 引入axios库,用于发送HTTP请求 -->
    <script src="https://unpkg.com/axios@0.24.0/dist/axios.min.js"></script>
</head>

<body>
    <input id="fileInput" type="file" />
    <script>
        /* 获取文件输入元素 */
        const fileInput = document.querySelector('#fileInput');

        /* 定义每块的大小,单位为字节 */
        const chunkSize = 20 * 1024;

        /* 当文件输入改变时,处理文件分块上传 */
        fileInput.onchange = async function () {
            /* 获取选中的文件 */
            const file = fileInput.files[0];
            console.log(file);

            /* 分割文件为多个块 */
            const chunks = [];
            let startPos = 0;
            while (startPos < file.size) {
                chunks.push(file.slice(startPos, startPos + chunkSize));
                startPos += chunkSize;
            }

            const randomStr = Math.random().toString().slice(2, 8);

            /* 对每个分块创建表单数据并发送POST请求 */
            chunks.map((chunk, index) => {
                const data = new FormData();
                /* 为每个分块设置唯一的名称 */
                data.set('name', randomStr + '_' + file.name + '-' + index);
                /* 添加分块到表单数据 */
                data.append('files', chunk);
                /* 使用axios发送POST请求上传分块 */
                axios.post('http://localhost:3000/upload', data);
            })
        }
    </script>
</body>

</html>

最后就是合并分片请求:




  /**
   * 合并上传的文件片段。
   * 
   * 该方法用于处理文件分片上传后的合并操作。通过读取指定目录下的所有文件片段,
   * 将它们按顺序拼接成完整的文件,并删除已经合并的文件片段。
   * 
   * @param name 文件名,用于定位和识别待合并的文件片段。
   */
  @Get('merge')
  merge(@Query('name') name: string) {
    // 构建存储文件片段的目录路径
    const chunkDir = 'uploads/chunks_' + name;

    // 读取文件片段目录中的所有文件
    const files = fs.readdirSync(chunkDir);

    // 初始化用于跟踪已合并文件数量的变量
    let startPos = 0;
    let count = 0;

    // 遍历文件片段,逐个进行合并
    files.map(file => {
      // 构建当前文件片段的完整路径
      const filePath = chunkDir + '/' + file;
      // 创建读取当前文件片段的流
      const stream = fs.createReadStream(filePath);
      // 将文件片段流式传输到目标文件,从上一个片段的结束位置开始写入
      stream.pipe(fs.createWriteStream('uploads/' + name, {
        start: startPos
      })).on('finish', () => {
        // 合并完成一个文件片段后,增加计数
        count++;
        // 如果所有文件片段都已合并完成,删除已合并的文件片段目录
        if (count === files.length) {
          fs.rmSync(chunkDir, { recursive: true });
        }
      })

      // 更新下一个文件片段的写入起始位置
      startPos += fs.statSync(filePath).size;
    })
  }

然后在前端代码里,当分片全部上传完之后,调用 merge 接口:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <!-- 引入axios库,用于发送HTTP请求 -->
    <script src="https://unpkg.com/axios@0.24.0/dist/axios.min.js"></script>
</head>

<body>
    <input id="fileInput" type="file" />
    <script>
        /* 获取文件输入元素 */
        const fileInput = document.querySelector('#fileInput');

        /* 定义每块的大小,单位为字节 */
        const chunkSize = 20 * 1024;

        /* 当文件输入改变时,处理文件分块上传 */
        fileInput.onchange = async function () {
            /* 获取选中的文件 */
            const file = fileInput.files[0];
            console.log(file);

            /* 分割文件为多个块 */
            const chunks = [];
            let startPos = 0;
            while (startPos < file.size) {
                chunks.push(file.slice(startPos, startPos + chunkSize));
                startPos += chunkSize;
            }

            const randomStr = Math.random().toString().slice(2, 8);

            let tasks = []

            /* 对每个分块创建表单数据并发送POST请求 */
            chunks.map((chunk, index) => {
                const data = new FormData();
                /* 为每个分块设置唯一的名称 */
                data.set('name', randomStr + '_' + file.name + '-' + index);
                /* 添加分块到表单数据 */
                data.append('files', chunk);
                tasks.push(axios.post('http://localhost:3000/upload', data));

            })

            await Promise.all(tasks)

            axios.get('http://localhost:3000/merge?name=' + randomStr + '_' + file.name)
        }
    </script>
</body>

</html>

1719757051775.png
可以看到文件分片上传之后 会执行 merge 接口 合并生成了下面文件
1719757116857.png

相关推荐

  1. 文件分块

    2024-07-17 10:24:03       29 阅读
  2. 文件分片和断点

    2024-07-17 10:24:03       36 阅读
  3. nestjs文件

    2024-07-17 10:24:03       63 阅读
  4. NodeJs 第 文件

    2024-07-17 10:24:03       53 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-07-17 10:24:03       70 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-07-17 10:24:03       74 阅读
  3. 在Django里面运行非项目文件

    2024-07-17 10:24:03       62 阅读
  4. Python语言-面向对象

    2024-07-17 10:24:03       72 阅读

热门阅读

  1. 初识Flutter问答&学习步骤

    2024-07-17 10:24:03       22 阅读
  2. golang mux组件兼容转移url

    2024-07-17 10:24:03       23 阅读
  3. 用户excel对CAN进行图形化展示

    2024-07-17 10:24:03       24 阅读
  4. SpringBoot如何使用Kafka来优化接口请求的并发

    2024-07-17 10:24:03       26 阅读
  5. 力扣---46.全排列

    2024-07-17 10:24:03       24 阅读
  6. PFA、PEEK和PP的材质、特点及用途

    2024-07-17 10:24:03       23 阅读
  7. 2024.07.16 oracle函数练习

    2024-07-17 10:24:03       31 阅读
  8. 第一章 Typescript小白快速入门

    2024-07-17 10:24:03       20 阅读
  9. webpack生产环境下的配置

    2024-07-17 10:24:03       27 阅读
  10. Matlab学习笔记01 - 基本数据类型

    2024-07-17 10:24:03       27 阅读
  11. spring-boot2.x整合Kafka步骤

    2024-07-17 10:24:03       19 阅读
  12. 武汉大学学报哲学社会科学版

    2024-07-17 10:24:03       22 阅读