掘金 后端 ( ) • 2024-05-14 16:53

文件上传

为了处理文件上传,Nest 为 Express 提供了一个基于 multer 中间件包的内置模块。Multer 处理以 multipart/form-data 格式发布的数据,该格式主要用于通过 HTTP POST 请求上传文件。该模块是完全可配置的,可以根据应用要求调整其行为

警告:Multer 无法处理不支持的多部分格式 (multipart/form-data) 的数据。另请注意,此包与 FastifyAdapter 不兼容

为了更好的类型安全,安装 Multer typings 包:

$ npm i -D @types/multer

现在可以使用 Express.Multer.File 类型(可以按如下方式导入此类型:import { Express } from 'express'

基本示例

要上传单个文件,只需将 FileInterceptor() 拦截器绑定到路由处理程序并使用 @UploadedFile() 装饰器从 request 中提取 file

@Post('upload')
@UseInterceptors(FileInterceptor('file'))
uploadFile(@UploadedFile() file: Express.Multer.File) {
  console.log(file);
}

提示:FileInterceptor( ) 装饰器是从 @nestjs/platform-express 包导出的。 @UploadedFile() 装饰器是从 @nestjs/common 导出的

FileInterceptor() 装饰器有两个参数:

  • fieldName:提供包含文件的 HTML 表单中的字段名称的字符串
  • options:MulterOptions 类型的可选对象。这与 multer 构造函数使用的对象相同

警告:FileInterceptor() 可能与 Google Firebase 等第三方云提供商不兼容

文件校验

通常,验证传入的文件元数据很有用,例如文件大小或文件 mime 类型。为此,可以创建自己的 管道 并将其绑定到使用 UploadedFile 装饰器注释的参数。下面的示例演示了如何实现基本的文件大小验证器管道:

import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common';

@Injectable()
export class FileSizeValidationPipe implements PipeTransform {
  transform(value: any, metadata: ArgumentMetadata) {
    // "value" is an object containing the file's attributes and metadata
    const oneKb = 1000;
    return value.size < oneKb;
  }
}

Nest 提供了一个内置的管道来处理常见的用例并促进/标准化新的添加。该管道称为 ParseFilePipe,可以按如下方式使用它:

@Post('file')
uploadFileAndPassValidation(
  @Body() body: SampleDto,
  @UploadedFile(
    new ParseFilePipe({
      validators: [
        // ... Set of file validator instances here
      ]
    })
  )
  file: Express.Multer.File,
) {
  return {
    body,
    file: file.buffer.toString(),
  };
}

需要指定一组将由 ParseFilePipe 执行的文件验证器。值得一提的是这个管道还有两个额外的可选选项:

errorHttpStatusCode any 验证器失败时抛出的 HTTP 状态代码。默认为 XSPACE400(错误请求) exceptionFactory 接收错误消息并返回错误的工厂

回到 FileValidator 。要将验证器与此管道集成,必须使用内置实现或提供自定义 FileValidator。请参见下面的示例:

export abstract class FileValidator<TValidationOptions = Record<string, any>> {
  constructor(protected readonly validationOptions: TValidationOptions) {}
  /**
   * 根据在构造函数中传递的选项,表示该文件是否应该被视为有效
   * @param File请求对象中的文件
   */
  abstract isValid(file?: any): boolean | Promise<boolean>;

  /**
   * 在验证失败的情况下构建错误消息
   * @param File请求对象中的文件
   */
  abstract buildErrorMessage(file: any): string;
}

提示:FileValidator 接口通过其 isValid 函数支持异步验证。要利用类型安全性,还可以将 file 参数键入为 Express.Multer.File,以防使用 express(默认)作为驱动程序

FileValidator 是一个常规类,可以访问文件对象并根据客户端提供的选项对其进行验证。Nest 有两个内置的 FileValidator 实现,可以在项目中使用:

  • MaxFileSizeValidator:检查给定文件的大小是否小于提供的值(以 bytes 衡量)
  • FileTypeValidator:检查给定文件的 mime 类型是否与给定值匹配

警告:为了验证文件类型,文件类型验证器 类使用 multer 检测到的类型。默认情况下,multer 从用户设备上的文件扩展名派生文件类型。但是,它不会检查实际的文件内容。由于文件可以重命名为任意扩展名,如果应用需要更安全的解决方案,请考虑使用自定义实现(例如检查文件的 幻数

这里修改上一个示例的代码片段:

@UploadedFile(
  new ParseFilePipe({
    validators: [
      new MaxFileSizeValidator({ maxSize: 1000 }),
      new FileTypeValidator({ fileType: 'image/jpeg' }),
    ],
  }),
)
file: Express.Multer.File,

提示:如果验证器的数量大幅增加或者它们的选项使文件变得混乱,可以在单独的文件中定义该数组,并将其作为命名常量(如 fileValidators)导入此处

最后,可以使用特殊的 ParseFilePipeBuilder 类来组合和构建验证器。通过如下所示使用它,可以避免手动实例化每个验证器并直接传递它们的选项:

@UploadedFile(
  new ParseFilePipeBuilder()
    .addFileTypeValidator({
      fileType: 'jpeg',
    })
    .addMaxSizeValidator({
      maxSize: 1000
    })
    .build({
      errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY
    }),
)
file: Express.Multer.File,

提示:默认情况下需要文件存在,但可以通过在 build 函数选项中添加 fileIsRequired:false 参数(与 errorHttpStatusCode 处于同一级别)将其设置为可选

文件数组

要上传文件数组(用单个字段名称标识),请使用 FilesInterceptor() 装饰器(注意装饰器名称中的复数 Files)。这个装饰器接受三个参数:

  • fieldName:文件名字
  • maxCount:定义接受的最大文件数的可选数字
  • options:可选的 MulterOptions 对象,如上所述

使用 FilesInterceptor() 时,使用 @UploadedFiles() 装饰器从 request 中提取文件

@Post('upload')
@UseInterceptors(FilesInterceptor('files'))
uploadFile(@UploadedFiles() files: Array<Express.Multer.File>) {
  console.log(files);
}

提示:FilesInterceptor() 装饰器是从 @nestjs/platform-express 包导出的。 @UploadedFiles() 装饰器是从 @nestjs/common 导出的

多个文件

要上传多个文件(所有文件都具有不同的字段名称键),请使用 FileFieldsInterceptor() 装饰器。这个装饰器有两个参数:

  • uploadedFields:一个对象数组,其中每个对象指定一个必需的 name 属性,其中一个字符串值指定一个字段名称,如上所述,以及一个可选的 maxCount 属性,如上所述
  • options:可选的 MulterOptions 对象,如上所述

使用 FileFieldsInterceptor() 时,使用 @UploadedFiles() 装饰器从 request 中提取文件

@Post('upload')
@UseInterceptors(FileFieldsInterceptor([
  { name: 'avatar', maxCount: 1 },
  { name: 'background', maxCount: 1 },
]))
uploadFile(@UploadedFiles() files: { avatar?: Express.Multer.File[], background?: Express.Multer.File[] }) {
  console.log(files);
}

接收任何文件

要上传具有任意字段名称键的所有字段,使用 AnyFilesInterceptor() 装饰器。这个装饰器可以接受一个可选的 options 对象,如上所述

使用 AnyFilesInterceptor() 时,使用 @UploadedFiles() 装饰器从 request 中提取文件

@Post('upload')
@UseInterceptors(AnyFilesInterceptor())
uploadFile(@UploadedFiles() files: Array<Express.Multer.File>) {
  console.log(files);
}

禁用文件上传

要接受 multipart/form-data 但不允许上传任何文件,请使用 NoFilesInterceptor。这将多部分数据设置为请求正文上的属性。随请求发送的任何文件都会抛出 BadRequestException

@Post('upload')
@UseInterceptors(NoFilesInterceptor())
handleMultiPartData(@Body() body) {
  console.log(body)
}

默认选项

可以如上所述在文件拦截器中指定多个选项。要设置默认选项,可以在导入 MulterModule 时调用静态 register() 方法,传入支持的选项。可以使用 此处 列出的所有选项

MulterModule.register({
  dest: './upload',
});

提示:MulterModule 类是从 @nestjs/platform-express 包中导出的

异步配置

当需要异步而不是静态设置 MulterModule 选项时,请使用 registerAsync() 方法。与大多数动态模块一样,Nest 提供了几种处理异步配置的技术

一种技术是使用工厂方法:

MulterModule.registerAsync({
  useFactory: () => ({
    dest: './upload',
  }),
});

工厂方法可以是 async,可以通过 inject 注入依赖

MulterModule.registerAsync({
  imports: [ConfigModule],
  useFactory: async (configService: ConfigService) => ({
    dest: configService.get<string>('MULTER_DEST'),
  }),
  inject: [ConfigService],
});

或者,可以使用类而不是工厂方法来配置 MulterModule,如下所示:

MulterModule.registerAsync({
  useClass: MulterConfigService,
});

上面的构造在 MulterModule 中实例化了 MulterConfigService,使用它来创建所需的选项对象。请注意,在此示例中,MulterConfigService 必须实现 MulterOptionsFactory 接口,如下所示。MulterModule 将在所提供类的实例化对象上调用 createMulterOptions() 方法

@Injectable()
class MulterConfigService implements MulterOptionsFactory {
  createMulterOptions(): MulterModuleOptions {
    return {
      dest: './upload',
    };
  }
}

如果要重用现有选项的 provider 而不是在 MulterModule 中创建私有副本,请使用 useExisting 语法

MulterModule.registerAsync({
  imports: [ConfigModule],
  useExisting: ConfigService,
});

流式文件

有时可能希望将文件从 REST API 发送回客户端。要使用 Nest 做到这一点,通常需要执行以下操作:

@Controller('file')
  export class FileController {
    @Get()
    getFile(@Res() res: Response) {
      const file = createReadStream(join(process.cwd(), 'package.json'));
      file.pipe(res);
    }
  }

但这样做最终会失去对后控制器拦截器逻辑的访问权限。要处理此问题,可以返回一个 StreamableFile 实例,在后台,框架将负责管道响应

流式文件类

StreamableFile 是一个保留要返回的流的类。要创建新的 StreamableFile,可以将 BufferStream 传递给 StreamableFile 构造函数

HINTStreamableFile 类可以从 @nestjs/common 导入

跨平台支持

默认情况下,Fastify 可以支持发送文件而无需调用 stream.pipe(res),所以根本不需要使用 StreamableFile 类。然而,Nest 支持在两种平台类型中使用 StreamableFile,所以如果在 ExpressFastify 之间切换,则无需担心这两种引擎之间的兼容性

示例

下面这个实例会将 package.json 作为文件而不是 JSON 返回,除了 JSON 文件,还可以扩展到图片、文档和任何其他文件类型

import { Controller, Get, StreamableFile } from '@nestjs/common';
import { createReadStream } from 'fs';
import { join } from 'path';

@Controller('file')
  export class FileController {
    @Get()
    getFile(): StreamableFile {
      const file = createReadStream(join(process.cwd(), 'package.json'));
      return new StreamableFile(file);
    }
  }

默认的内容类型是 application/octet-stream,如果需要自定义响应可以使用 res.set 方法或者 @Header() 装饰器,像这样:

import { Controller, Get, StreamableFile, Res } from '@nestjs/common';
import { createReadStream } from 'fs';
import { join } from 'path';
import type { Response } from 'express';

@Controller('file')
  export class FileController {
    @Get()
    getFile(@Res({ passthrough: true }) res: Response): StreamableFile {
      const file = createReadStream(join(process.cwd(), 'package.json'));
      res.set({
        'Content-Type': 'application/json',
        'Content-Disposition': 'attachment; filename="package.json"',
      });
      return new StreamableFile(file);
    }

    // Or even:
    @Get()
    @Header('Content-Type', 'application/json')
    @Header('Content-Disposition', 'attachment; filename="package.json"')
    getStaticFile(): StreamableFile {
      const file = createReadStream(join(process.cwd(), 'package.json'));
      return new StreamableFile(file);
    }  
  }

HTTP 模块

Axios 是一个功能丰富、应用广泛的 HTTP 客户端包。Nest 封装了 Axios 并通过内置的 HttpModule 将其公开。HttpModule 导出 HttpService 类,它提供了基于 Axios 的方法来执行 HTTP 请求。该库还将生成的 HTTP 响应转换为 Observables

提示:还可以直接使用任何通用 Node.js HTTP 客户端库,包括 gotundici

安装

安装所需的依赖

$ npm i --save @nestjs/axios axios

入门

安装过程完成后,要使用 HttpService,请先导入 HttpModule

@Module({
  imports: [HttpModule],
  providers: [CatsService],
})
export class CatsModule {}

使用正常的构造函数注入 HttpService

提示:HttpModuleHttpService 是从 @nestjs/axios 包导入的

@Injectable()
export class CatsService {
  constructor(private readonly httpService: HttpService) {}

  findAll(): Observable<AxiosResponse<Cat[]>> {
    return this.httpService.get('http://localhost:3000/cats');
  }
}

提示:AxiosResponse 是从 axios 包( npm i axios)导出的接口

所有 HttpService 方法都返回封装在 Observable 对象中的 AxiosResponse

配置

Axios 可以配置多种选项来自定义 HttpService 的行为。要配置底层的 Axios 实例,在导入它时将一个可选的选项对象传递给 HttpModule 的 register() 方法。这个选项对象将直接传递给底层的 Axios 构造函数

@Module({
  imports: [
    HttpModule.register({
      timeout: 5000,
      maxRedirects: 5,
    }),
  ],
  providers: [CatsService],
})
export class CatsModule {}

异步配置

当需要异步而不是静态地传递模块选项时,请使用 registerAsync() 方法。与大多数动态模块一样,Nest 提供了几种处理异步配置的技术

一种技术是使用工厂方法:

HttpModule.registerAsync({
  useFactory: () => ({
    timeout: 5000,
    maxRedirects: 5,
  }),
});

工厂方法可以是 async,可以通过 inject 注入依赖

HttpModule.registerAsync({
  imports: [ConfigModule],
  useFactory: async (configService: ConfigService) => ({
    timeout: configService.get('HTTP_TIMEOUT'),
    maxRedirects: configService.get('HTTP_MAX_REDIRECTS'),
  }),
  inject: [ConfigService],
});

或者,可以使用类而不是工厂来配置 HttpModule,如下所示

HttpModule.registerAsync({
  useClass: HttpConfigService,
});

上面的构造在 HttpModule 中实例化了 HttpConfigService,用它来创建一个选项对象。请注意,在此示例中,HttpConfigService 必须实现 HttpModuleOptionsFactory 接口,如下所示。HttpModule 将在所提供类的实例化对象上调用 createHttpOptions() 方法

@Injectable()
class HttpConfigService implements HttpModuleOptionsFactory {
  createHttpOptions(): HttpModuleOptions {
    return {
      timeout: 5000,
      maxRedirects: 5,
    };
  }
}

如果要重用现有选项 provider 而不是在 HttpModule 中创建私有副本,请使用 useExisting 语法

HttpModule.registerAsync({
  imports: [ConfigModule],
  useExisting: HttpConfigService,
});

直接使用 Axios

如果觉得 HttpModule.register 的选项不够用,或者只是想访问 @nestjs/axios 创建的底层 axios 实例,可以通过 HttpService#axiosRef 访问,如下:

@Injectable()
export class CatsService {
  constructor(private readonly httpService: HttpService) {}

  findAll(): Promise<AxiosResponse<Cat[]>> {
    return this.httpService.axiosRef.get('http://localhost:3000/cats');
    //                      ^ AxiosInstance interface
  }
}

完整示例

由于 HttpService ****方法的返回值是一个 Observable,可以使用 rxjs - firstValueFromlastValueFrompromise 的形式检索请求的数据

import { catchError, firstValueFrom } from 'rxjs';

@Injectable()
export class CatsService {
  private readonly logger = new Logger(CatsService.name);
  constructor(private readonly httpService: HttpService) {}

  async findAll(): Promise<Cat[]> {
    const { data } = await firstValueFrom(
      this.httpService.get<Cat[]>('http://localhost:3000/cats').pipe(
        catchError((error: AxiosError) => {
          this.logger.error(error.response.data);
          throw 'An error happened!';
        }),
      ),
    );
    return data;
  }
}

提示:访问 RxJS 关于 firstValueFromlastValueFrom 的文档以了解它们之间的差异

会话

HTTP 会话提供了一种跨多个请求存储有关用户的信息的方法,这对于 MVC 应用特别有用

与 Express 一起使用(默认)

安装依赖(及其类型,供 TypeScript 用户使用):

$ npm i express-session
$ npm i -D @types/express-session

安装完成后,将 express-session 中间件应用为全局中间件(例如,在 main.ts 文件中)

import * as session from 'express-session';
// somewhere in your initialization file
app.use(
  session({
    secret: 'my-secret',
    resave: false,
    saveUninitialized: false,
  }),
);

注意:默认的服务器端会话存储不是专门为生产环境设计的。它在大多数情况下会泄漏内存,不会扩展到单个进程,并且用于调试和开发

secret 用于签署会话 ID cookie。这可以是单个秘钥的字符串,也可以是多个秘钥的数组。如果提供了一组秘钥,则只有第一个元素将用于签署会话 ID cookie,而在验证请求中的签名时将考虑所有元素。秘钥本身应该不容易被人类解析,最好是一组随机字符

启用 resave 选项会强制将会话保存回会话存储,即使会话在请求期间从未被修改。默认值为 true,但不推荐使用默认值,因为默认值将来会更改

同样,启用 saveUninitialized 选项会强制将 uninitialized 的会话保存到存储中。当会话是新的但未修改时,它是未初始化的。选择 false 对于实现登录会话、减少服务器存储使用或遵守在设置 cookie 之前需要许可的法律很有用。选择 false 还有助于解决客户端在没有会话的情况下发送多个并行请求的竞争条件 (source)

可以将其他几个选项传递给 session 中间件,在 API 文档 中阅读更多关于它们的信息

提示:请注意,secure: true 是推荐选项。但是,它需要启用 https 的网站,即 HTTPS 是安全 cookie 所必需的。如果设置了安全,并且通过 HTTP 访问你的站点,则不会设置 cookie。如果 node.js 在代理后面并且使用 secure: true,需要在 express 中设置 trust proxy

现在可以在路由处理程序中设置和读取会话值,如下所示:

@Get()
findAll(@Req() request: Request) {
  request.session.visits = request.session.visits ? request.session.visits + 1 : 1;
}

提示: @Req() 装饰器是从 @nestjs/common 包导入的,而 Request 装饰器是从 express 包导入的

或者,可以使用 @Session() 装饰器从请求中提取会话对象,如下所示:

@Get()
findAll(@Session() session: Record<string, any>) {
  session.visits = session.visits ? session.visits + 1 : 1;
}

提示; @Session() 装饰器是从 @nestjs/common 包导入的