使用NestJS的API-05

Nestjs

有时候,我们需要对输出的数据执行额外操作。我们可能不想暴露特定的属性或以某种其他方式修改响应。在这篇文章中,我们将探讨NestJS为我们提供的各种解决方案,以改变我们在响应中发送的数据。

你可以在这个系列的这个仓库中找到代码。

序列化

首先需要关注的是序列化。这是一个在返回给用户之前转换响应数据的过程。

在这个系列的前几部分中,我们在API的各个部分中移除了密码。更好的方法是使用class-transformer

// users/user.entity.ts

import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import { Exclude } from 'class-transformer';

@Entity()
class User {
  @PrimaryGeneratedColumn()
  public id?: number;

  @Column({ unique: true })
  public email: string;

  @Column()
  public name: string;

  @Column()
  @Exclude()
  public password: string;
}

export default User;

NestJS配备了ClassSerializerInterceptor,它在底层使用class-transformer。要应用上述转换,我们需要在我们的控制器中使用它:

@Controller('authentication')
@UseInterceptors(ClassSerializerInterceptor)
class AuthenticationController

如果我们发现自己在很多控制器中添加ClassSerializerInterceptor,我们可以改为全局配置它。

// main.ts

import { NestFactory, Reflector } from '@nestjs/core';
import { AppModule } from './app.module';
import * as cookieParser from 'cookie-parser';
import { ClassSerializerInterceptor, ValidationPipe } from '@nestjs/common';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe());
  app.useGlobalInterceptors(new ClassSerializerInterceptor(
    app.get(Reflector))
  );
  app.use(cookieParser());
  await app.listen(3000);
}
bootstrap();

ClassSerializerInterceptor初始化时需要Reflector

默认情况下,我们实体的所有属性都是暴露的。我们可以通过提供额外的选项给class-transformer来改变这个策略。为此,我们需要使用@SerializeOptions()装饰器。

@Controller('authentication')
@SerializeOptions({
  strategy: 'excludeAll'
})
export class AuthenticationController
// users/user.entity.ts

import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import { Expose } from 'class-transformer';

@Entity()
class User {
  @PrimaryGeneratedColumn()
  public id?: number;

  @Column({ unique: true })
  @Expose()
  public email: string;

  @Column()
  @Expose()
  public name: string;

  @Column()
  public password: string;
}

export default User;

@SerializeOptions()装饰器有更多你可能会发现有用的选项。它匹配你可以为class-transformer中的classToPlain方法提供的选项。

class-transformer库有相当多有用的特性。另一个值得注意的特性是转换值的能力。为了演示它,让我们创建一个可为空的列:

@Entity()
class Post {
  // ...

  @Column({ nullable: true })
  public category?: string;
}

由于category是一个可为空的列,它是可选的,它的值是null,直到我们设置它。这意味着在响应中发送null值:

上述行为可能被认为是不可取的,解决它最直接的方式是使用@Transform装饰器。如果值等于null,我们不想在响应中发送它。

@Column({ nullable: true })
@Transform(value => {
  if (value !== null) {
    return value;
  }
})
public category?: string;

使用@Res()装饰器的问题

在这个系列的前一部分中,

我们使用了@Res()装饰器来访问Express响应对象。借助它,我们能够将cookies附加到响应中。

@HttpCode(200)
@UseGuards(LocalAuthenticationGuard)
@Post('log-in')
async logIn(@Req() request: RequestWithUser, @Res() response: Response) {
  const {user} = request;
  const cookie = this.authenticationService.getCookieWithJwtToken(user.id);
  response.setHeader('Set-Cookie', cookie);
  user.password = undefined;
  return response.send(user);
}

使用@Res()装饰器让我们失去了使用NestJS的一些优势。不幸的是,它干扰了ClassSerializerInterceptor。为了避免这种情况,我们可以遵循NestJS创造者的一些建议。如果我们使用request.res对象而不是@Res()装饰器,我们就不会将NestJS置于特定于express的模式中。

@HttpCode(200)
@UseGuards(LocalAuthenticationGuard)
@Post('log-in')
async logIn(@Req() request: RequestWithUser) {
  const {user} = request;
  const cookie = this.authenticationService.getCookieWithJwtToken(user.id);
  request.res.setHeader('Set-Cookie', cookie);
  return user;
}

上述是一个巧妙的小技巧,我们利用它来同时利用NestJS内置的机制和直接访问响应对象。

自定义拦截器

上面我们使用@Transform装饰器来跳过单个属性,如果它等于null。对每个可为空的属性这样做看起来不是一个干净的方法。

幸运的是,除了使用ClassSerializerInterceptor,我们还可以创建我们自己的拦截器。拦截器可以服务于多种目的,其中之一是操作请求/响应流。

// utils/excludeNull.interceptor.ts

import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import recursivelyStripNullValues from './recursivelyStripNullValues';

@Injectable()
export class ExcludeNullInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next
      .handle()
      .pipe(map(value => recursivelyStripNullValues(value)));
  }
}

每个拦截器都需要实现NestInterceptor,因此也就是intercept方法。它接受两个参数:

  1. ExecutionContext - 它提供有关当前上下文的信息
  2. CallHandler - 它包含调用路由处理程序并返回一个RxJS Observable的handle方法

intercept方法包装了请求/响应流,我们可以在路由处理程序执行前后添加逻辑。在上述代码中,我们调用了路由处理程序并修改了响应。

由于NestJS框架中有不少地方使用了RxJS,官方的TypeScript启动器已经包含了它。

// utils/recursivelyStripNullValues.ts

function recursivelyStripNullValues(value: unknown): unknown {
  if (Array.isArray(value)) {
    return value.map(recursivelyStripNullValues);
  }
  if (value !== null && typeof value === 'object') {
    return Object.fromEntries(
      Object.entries(value).map(([key, value]) => [key, recursivelyStripNullValues(value)])
    );
  }
  if (value !== null) {
    return value;
  }
}

在上述函数中,我们递归遍历数据结构,并只保留不等于null的值。它对数组和普通对象都适用。

如果你想了解更多关于JavaScript中递归的信息,可以查看《使用递归遍历数据结构》。执行上下文和调用栈,同样每个递归函数都可以转换为迭代函数。

总结

在这篇中我们探讨了如何修改我们发送给用户的响应。虽然最直接的方法是使用ClassSerializerInterceptor来序列化响应,但我们也可以创建自己的拦截器。我们还探讨了如何绕过使用@Res()装饰器的问题。

图像格式:便携式网络图形(PNG)
每像素位数:24
颜色:真彩色
尺寸:1200 x 450
隔行扫描:是

图像格式:便携式网络图形(PNG)
每像素位数:24
颜色:真彩色
尺寸:570 x 423
隔行扫描:是

图像格式:便携式网络图形(PNG)
每像素位数:24
颜色:真彩色
尺寸:580 x 362
隔行扫描:是

发布于 2022-05-30,更新于 2024-11-27