后端系统常见的对象有三种:
Entity:数据实体,和数据库表对应。
DTO: Data Transfer Object,用于封装请求参数。
VO:Value Object,用于封装返回的响应数据。
三者的关系如下:

但文档中并没有提到 VO 对象,这是为什么呢?
因为有替代方案。
我们来看一下:
nest new vo-test
生成一个 user 的 CRUD 模块:
nest g resource user --no-spec
在 entity 里加一些内容:
export class User {
    id: number;
    username: string;
    password: string;
    email: string;
    constructor(partial: Partial<User>) {
        Object.assign(this, partial);
    }
}Partial 是把 User 的属性变为可选:

可以传入部分属性,然后 Object.assign 赋值到 this。
然后 CreateUserDto 里包含这些属性:
export class CreateUserDto {
    username: string;
    password: string;
    email: string;
}实现下 UserService 的 create 和 find 的逻辑:

这里我们直接用数组模拟 database 来保存数据。
import { BadRequestException, Injectable } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { User } from './entities/user.entity';
const database = [];
let id = 0;
@Injectable()
export class UserService {
  create(createUserDto: CreateUserDto) {
    const user = new User(createUserDto);
    user.id = id++;
    database.push(user);
    return user;
  }
  findAll() {
    return database;
  }
  findOne(id: number) {
    return database.filter(item =>  item.id === id).at(0);
  }
  update(id: number, updateUserDto: UpdateUserDto) {
    return `This action updates a #${id} user`;
  }
  remove(id: number) {
    return `This action removes a #${id} user`;
  }
}把服务跑起来:
npm run start:dev
创建两个 user:


查一下:

可以看到,user 的 password 也被返回了。
而这个应该过滤掉。
一般这种情况,我们都会封装个 vo。
创建 vo/user.vo.ts:
export class UserVo {
    id: number;
    username: string;
    email: string;
    constructor(partial: Partial<UserVo>) {
        Object.assign(this, partial);
    }
}然后把数据封装成 vo 返回:
findAll() {
    return database.map(item => {
      return new UserVo({
        id: item.id,
        username: item.username,
        email: item.email
      });
    });
}
findOne(id: number) {
    return database.filter(item =>  item.id === id).map(item => {
      return new UserVo({
        id: item.id,
        username: item.username,
        email: item.email
      });
    }).at(0);
}试一下:

可以看到,这样就没有 password 了。
但你会发现 UserVo 和 User entity 很类似:


对于 dto 我们可以通过 PartialType、PickType、OmitType、IntersectionType 来组合已有 dto,避免重复。
那 vo 是不是也可以呢?
是的,nest 里可以直接复用 entity 作为 vo。
这里要用到 class-transformer 这个包:
npm install --save class-transformer
然后在 UserController 的查询方法上加上 ClassSerializerInterceptor 就好了:

代码恢复原样:

现在返回的数据就没有 password 字段了:


class-transformer 这个包我们用过,是用于根据 class 创建对应的对象的。
当时是 ValidationPipe 里用它来创建 dto class 对应的对象。
这里也是用它来创建 entity class 对应的对象。
简单看下 ClassSerializerInterceptor 的源码:


它是通过 map 对响应做转换,在 serialize 方法拿到响应的对象,如果是数组就拿到每个元素。
在 transformToPlain 方法里,调用 classToPlain 创建对象。
它会先拿到响应对象的 class、然后根据 class 上的装饰器来创建新的对象。
当然,装饰器不只有 @Exclude,还有几个有用的:
import { Exclude, Expose, Transform } from "class-transformer";
export class User {
    id: number;
    username: string;
    @Exclude()
    password: string;
    @Expose()
    get xxx(): string {
        return `${this.username} ${this.email}`;
    }
    @Transform(({value}) => '邮箱是:' + value)
    email: string;
    constructor(partial: Partial<User>) {
        Object.assign(this, partial);
    }
}@Expose 是添加一个导出的字段,这个字段是只读的。
@Transform 是对返回的字段值做一些转换。
测试下:

可以看到,返回的数据多了 xxx 字段,email 字段也做了修改:

这样基于 entity 直接创建 vo 确实方便多了。
此外,你可以可以通过 @SerializeOptions 装饰器加一些序列化参数:

strategy 默认值是 exposeAll,全部导出,除了有 @Exclude 装饰器的。
设置为 excludeAl 就是全部排除,除了有 @Expose 装饰器的。
当然,你可以 ClassSerializerInterceptor 和 SerializeOptions 加到 class 上:

这样,controller 所有的接口返回的对象都会做处理:



swagger 那节当返回对象的时候,我们都是创建了个 vo 的类,在 vo class 上加上 swagger 的装饰器:


其实没必要,完全可以直接用 entity。
安装 swagger 的包:
npm install --save @nestjs/swagger然后在 main.ts 添加 swagger 的入口代码:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  const config = new DocumentBuilder()
    .setTitle('Test example')
    .setDescription('The API description')
    .setVersion('1.0')
    .addTag('test')
    .build();
  const document = SwaggerModule.createDocument(app, config);
  SwaggerModule.setup('doc', app, document);
  await app.listen(3000);
}
bootstrap();现在 @apiResponse 里就可以直接指定 User 的 entity 了:

import { Controller, Get, Post, Body, Patch, Param, Delete, UseInterceptors, ClassSerializerInterceptor, SerializeOptions, HttpStatus } from '@nestjs/common';
import { UserService } from './user.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { ApiBody, ApiOperation, ApiResponse } from '@nestjs/swagger';
import { User } from './entities/user.entity';
@Controller('user')
@SerializeOptions({
  // strategy: 'excludeAll'
})
@UseInterceptors(ClassSerializerInterceptor)
export class UserController {
  constructor(private readonly userService: UserService) {}
  @Post()
  create(@Body() createUserDto: CreateUserDto) {
    return this.userService.create(createUserDto);
  }
  @ApiOperation({summary:'findAll'})
  @ApiResponse({
    status: HttpStatus.OK,
    description: 'ok',
    type: User
  })
  @Get()
  findAll() {
    return this.userService.findAll();
  }
  @Get(':id')
  findOne(@Param('id') id: string) {
    return this.userService.findOne(+id);
  }
  @Patch(':id')
  update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
    return this.userService.update(+id, updateUserDto);
  }
  @Delete(':id')
  remove(@Param('id') id: string) {
    return this.userService.remove(+id);
  }
}在 User 里加一下 swagger 的装饰器:

import { ApiHideProperty, ApiProperty } from "@nestjs/swagger";
import { Exclude, Expose, Transform } from "class-transformer";
export class User {
    @ApiProperty()
    id: number;
    @ApiProperty()
    username: string;
    @ApiHideProperty()
    @Exclude()
    password: string;
    @ApiProperty()
    @Expose()
    get xxx(): string {
        return `${this.username} ${this.email}`;
    }
    @ApiProperty()
    @Transform(({value}) => '邮箱是:' + value)
    email: string;
    constructor(partial: Partial<User>) {
        Object.assign(this, partial);
    }
}注意,这里要用 @ApiHideProperty 把 password 字段隐藏掉。

可以看到,现在的 swagger 文档是对的:

而且我们没有用 vo 对象。
这也是为什么 Nest 文档里没有提到 vo,因为完全可以用 entity 来替代。
案例代码在小册仓库
总结 
后端系统中常见 entity、vo、dto 三种对象,vo 是用来封装返回的响应数据的。
但是 Nest 文档里并没有提到 vo 对象,因为完全可以用 entity 来代替。
entity 里加上 @Exclude 可以排除某些字段、@Expose 可以增加一些派生字段、@Transform 可以对已有字段的序列化结果做修改。
然后在 cotnroller 上加上 ClassSerializerInterceptor 的 interceptor,还可以用 @SerializeOptions 来添加 options。
它的底层是基于 class-transfomer 包来实现的,拿到响应对象,plainToClass 拿到 class,然后根据 class 的装饰器再 classToPlain 创建序列化的对象。
swagger 的 @ApiResponse 也完全可以用 entity 来代替 vo,在想排除的字段加一下 @ApiHideProperty 就好了。
Nest 文档里并没有提到 vo 对象,因为完全没有必要,可以直接用序列化的 entity。