大文件分片上传

大文件上传介绍

  • 把大文件分成小文件,然后并行上传

如何拆分和合并

  • 通过 Blob的slice方法 对 File 分片,File是一种特殊的Blob
  • 合并:fs的createWriteStream方法支持指定 start,把每个分片按照不同位置写入文件。

Nestjs实现

html
<!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>
    <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);

            chunks.map((chunk, index) => {
                const data = new FormData();
                data.set('name', randomStr + '_' + file.name + '-' + index)
                data.append('files', chunk);
                axios.post('http://localhost:3006/upload', data);
            })
        
        }

    </script>
</body>
</html>

对拿到的文件进行分片,然后单独上传每个分片,分片名字为文件名 + index。 每 20k 一个分片,然后把它们移动到单独的目录:

typescript
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) { }

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }

  @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); // 删除临时文件
  }
}

在全部分片上传完之后,发送合并分片的请求

typescript
  @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.rm(chunkDir, {
            recursive: true
          }, () => { });
        }
      })

      startPos += fs.statSync(filePath).size;
    })
  }

当分片全部上传完之后,前端调用 merge 接口:

html
<!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>
    <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);
            const tasks = []
            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:3006/upload', data));
            })
            await Promise.all(tasks);
            axios.get('http://localhost:3006/merge?name=' + randomStr + '_' + file.name);
        }

    </script>
</body>
</html>

总结

当文件比较大的时候,文件上传会很慢,这时候一般我们会通过分片的方式来优化。

原理就是浏览器里通过 slice 来把文件分成多个分片,并发上传。

服务端把这些分片文件保存在一个目录下。

当所有分片传输完成时,发送一个合并请求,服务端通过 fs.createWriteStream 指定 start 位置,来把这些分片文件写入到同一个文件里,完成合并。

这样就实现了大文件分片上传。

手写常见js代码
欢迎来到前端练习生ZM的小站