Nest使用multer实现文件上传,并实现大文件分片上传(下)

上节我们学了在 Express 里用 multer 包处理 multipart/form-data 类型的请求中的 file

单个、多个字段的单个、多个 file 都能轻松取出来。

接下来我们就来学习一下在Nest 里使用multer。

一,Nest如何使用multer实现文件上传

首先我们先创建一个Nest项目:

nest new nest-multer-upload -p npm

还需要安装下 multer 的 ts 类型的包:

npm install -D @types/multer

我们在AppController 添加这样一个 handler:

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

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}
  @Post('file')
  @UseInterceptors(FileInterceptor('file',{
    dest:'uploads'
  }))
  uploadFile(@UploadedFile() file:Express.Multer.File,@Body() body){
    console.log('body', body);
    console.log('file', file);
  }
}

使用 FileInterceptor 来提取 file 字段,然后通过 UploadedFile 装饰器把它作为参数传入。

然后 npm run start:dev 把服务跑起来,一保存,就可以看到这个目录被创建了:

然后我们来写前端代码,让 nest 服务支持静态文件的访问,然后让 nest 服务支持跨域,再单独跑个 http-server 来提供静态服务。

在根目录创建 index.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" multiple/>
    <script>
        const fileInput = document.querySelector('#fileInput');

        async function formData() {
            const data = new FormData();
            data.set('name','张三');
            data.set('age', 24);
            data.set('file', fileInput.files[0]);

            const res = await axios.post('http://localhost:3000/file', data);
            console.log(res);
        }

        fileInput.onchange = formData;
    </script>
</body>
</html>

先单独跑个 http-server 来提供静态服务:

npx http-server

接下来我们在页面选择一个文件上传:

服务端就打印了file对象并存到uploads文件夹:

 再来试下多文件上传:

// 多文件上传
  @Post('files')
  @UseInterceptors(FilesInterceptor('files',3,{
    dest:'uploads'
  }))
  uploadFiles(@UploadedFiles() files:Array<Express.Multer.File>,@Body() body) {
    console.log('body', body);
    console.log('files', files);
  }
//把 FileInterceptor 换成 FilesInterceptor,把 UploadedFile 换成 UploadedFiles,都是多加一个 s。

 前端代码:

<body>
  <input id="fileInput" type="file" multiple />
  <script>
    const fileInput = document.querySelector('#fileInput');

    async function formData() {
      const data = new FormData();
      data.set('name', '张三');
      data.set('age', 24);
      [...fileInput.files].forEach(item => {
        data.append('files', item)
      })

      const res = await axios.post('http://localhost:3000/files', data, {
        headers: { 'content-type': 'multipart/form-data' }
      });
      console.log(res);
    }

    fileInput.onchange = formData;
  </script>
</body>

这样就可以上传多文件了:

 

如果有多个文件的字段:

@Post('filesA')
  @UseInterceptors(FileFieldsInterceptor([
    { name: 'file1', maxCount: 2 },
    { name: 'file2', maxCount: 3 }
  ], {
    dest: 'uploads'
  }))
  uploadFileFields(@UploadedFiles() files: { file1?: Express.Multer.File[], file2?: Express.Multer.File[] }, @Body() body) {
    console.log('body', body);
    console.log('files', files);
  }

前端代码和之前都差不多,只是字段名和接口不一样,在这里就不一一赘述了

如果并不知道有哪些字段是 file :

@Post('filesB')
  @UseInterceptors(AnyFilesInterceptor({
      dest: 'uploads'
  }))
  uploadAnyFiles(@UploadedFiles() files: Array<Express.Multer.File>, @Body() body) {
      console.log('body', body);
      console.log('files', files);
  }

文件的校验:

像文件大小、类型的校验这种逻辑太过常见,Nest 给封装好了,可以直接用:

@Post('filesC')
  @UseInterceptors(FileInterceptor('file', {
    dest: 'uploads'
  }))
  uploadFile3(@UploadedFile(new ParseFilePipe({
    validators: [
      new MaxFileSizeValidator({ maxSize: 1000 }),
      new FileTypeValidator({ fileType: 'image/jpeg' }),
    ],
  })) file: Express.Multer.File, @Body() body) {
    console.log('body', body);
    console.log('file', file);
  }
  //ParseFilePipe:它的作用是调用传入的 validator 来对文件做校验
  //比如 MaxFileSizeValidator 是校验文件大小、FileTypeValidator 是校验文件类型

 我们来试试:

可以看到,返回的也是 400 响应,并且 message 说明了具体的错误信息

而且这个错误信息可以自己修改:

@Post('filesC')
  @UseInterceptors(FileInterceptor('file', {
    dest: 'uploads'
  }))
  uploadFile3(@UploadedFile(new ParseFilePipe({
    exceptionFactory:err => {
      throw new HttpException('错误信息:' + err ,400)
    },
    validators: [
      new MaxFileSizeValidator({ maxSize: 1000 }),
      new FileTypeValidator({ fileType: 'image/jpeg' }),
    ],
  })) file: Express.Multer.File, @Body() body) {
    console.log('body', body);
    console.log('file', file);
  }

看看效果:

 二,大文件分片上传

当文件很大的时候,上传就会变得比较慢。

假设传一个 100M 的文件需要 3 分钟,那传一个 1G 的文件就需要 30 分钟。

这样是能完成功能,但是产品的体验会很不好。

所以大文件上传的场景,需要做专门的优化。

把 1G 的大文件分割成 10 个 100M 的小文件,然后这些文件并行上传,不就快了?

然后等 10 个小文件都传完之后,再发一个请求把这 10 个小文件合并成原来的大文件。

这就是大文件分片上传的方案。

那如何拆分和合并呢?

浏览器里 Blob 有 slice 方法,可以截取某个范围的数据,而 File 就是一种 Blob。

所以可以在 input 里选择了 file 之后,通过 slice 对 File 分片。

那合并呢?

fs 的 createWriteStream 方法支持指定 start,也就是从什么位置开始写入。

这样把每个分片按照不同位置写入文件里,就完成合并了。

创建个 Nest 项目:

nest new large-file-sharding-upload

在 AppController 添加一个路由:

@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) 
  }

前端代码我们这样写:

<!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>
        /*
        对拿到的文件进行分片,然后单独上传每个分片,分片名称为文件名+index
        */
        const fileInput = document.querySelector('#fileInput');
        const chunkSize = 20 * 1024
        async function formData() {
           const file = fileInput.files[0]
           const chunks = []
           let startPos = 0
           while(startPos < file.size) {
                chunks.push(file.slice(startPos, startPos + chunkSize));
                startPos += chunkSize;
            }
            chunks.map((chunk, index) => {
                const data = new FormData();
                data.set('name', file.name + '-' + index)
                data.append('files', chunk);
                axios.post('http://localhost:3000/upload', data);
            })
            console.log(res);
        }
        fileInput.onchange = formData;
    </script>
</body>
</html>

接下来我们来测试一下,这里我测试用的图片是 40k:

 每 20k 一个分片,一共是 2 个分片,服务端接收到了这 2 个分片:

接下来我们来进行合并操作:

@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);
    // 然后我们来合并文件
    const chunkDirMerge = 'uploads/chunks_'+ fileName;
    const filesMerge = fs.readdirSync(chunkDirMerge);
    let count = 0;
    let startPos = 0;
    filesMerge.map(file => {
      const filePath = chunkDirMerge + '/' + file;
      const stream = fs.createReadStream(filePath);
      stream.pipe(fs.createWriteStream('uploads/' + fileName, {
        start: startPos
      })).on('finish', () => {
        count ++;
        // 然后我们在合并完成之后把 chunks 目录删掉。
        if(count === files.length) {
          fs.rm(chunkDir, {
            recursive: true
          }, () =>{}); 
        }
      })

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

 测试一下:

接收到的文件分片:

合并之后:

至此,大文件分片上传就完成了。

好的,针对你的第一个问题,我可以告诉你如何使用 JavaScript 实现 OSS 批量下载文件的功能。 首先,你需要使用阿里云提供的 OSS SDK for JavaScript,通过 npm 安装: ``` npm install ali-oss --save ``` 然后,你可以使用以下代码实现批量下载文件: ```javascript const OSS = require('ali-oss'); const co = require('co'); const fs = require('fs'); // 创建 OSS 客户端实例 const client = new OSS({ accessKeyId: '<AccessKeyId>', accessKeySecret: '<AccessKeySecret>', bucket: '<BucketName>', region: '<Region>', }); // 要下载的文件列表 const files = [ 'path/to/file1.txt', 'path/to/file2.txt', 'path/to/file3.txt', ]; // 下载文件 co(function* () { for (let file of files) { const result = yield client.get(file); fs.writeFileSync(`./${file}`, result.content); } }).catch(function (err) { console.log(err); }); ``` 这段代码首先创建了一个 OSS 客户端实例,然后定义了要下载的文件列表,最后使用 `co` 库实现异步流程控制,循环遍历文件列表,逐个下载文件并保存到本地。 至于你提到的第二个问题,我可以告诉你如何在 NestJS使用阿里云 OSS 进行文件上传。 首先,你需要安装 `@nest-modules/multer` 和 `ali-oss` 两个库: ``` npm install @nest-modules/multer ali-oss --save ``` 然后,你可以在 NestJS 中定义一个上传服务: ```typescript import { Injectable } from '@nestjs/common'; import { MulterModule } from '@nest-modules/multer'; import * as OSS from 'ali-oss'; @Injectable() export class UploadService { private client: OSS; constructor() { this.client = new OSS({ accessKeyId: '<AccessKeyId>', accessKeySecret: '<AccessKeySecret>', bucket: '<BucketName>', region: '<Region>', }); } async upload(file: Express.Multer.File): Promise<string> { const result = await this.client.put(file.originalname, file.buffer); return result.url; } } ``` 这段代码定义了一个 `UploadService`,通过 `ali-oss` 库创建了一个 OSS 客户端实例,在 `upload` 方法中实现文件上传功能,返回上传后的文件 URL。 最后,你可以在 NestJS 控制器中使用这个上传服务: ```typescript import { Controller, Post, UseInterceptors, UploadedFile } from '@nestjs/common'; import { FileInterceptor } from '@nestjs/platform-express'; import { UploadService } from './upload.service'; @Controller('upload') export class UploadController { constructor(private readonly uploadService: UploadService) {} @Post() @UseInterceptors(FileInterceptor('file')) async upload(@UploadedFile() file: Express.Multer.File): Promise<string> { return this.uploadService.upload(file); } } ``` 这段代码定义了一个 `UploadController` 控制器,使用 `@UseInterceptors` 装饰器启用 `FileInterceptor` 中间件,将上传文件传递给 `upload` 方法,调用上传服务上传文件并返回文件 URL。 希望这些代码能够帮助到你,如果你还有其他问题,可以随时问我。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

safe030

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值