前言

在之前的文章中我们已经实现了格式化响应的数据结构,现在我们就来处理响应的具体数据,很常见的一个场景:我们查询了一条用户信息,这个信息可能包含了用户的账号、密码、昵称、头像,但是我们肯定不能把密码也返回给前端,所以我们就需要控制返回的数据。

有很多种做法,比如简单的就是通过js的delete操作符去删除对象上的属性。

如果是使用prisma的话,可以在查询的时候,配置select属性来限制返回的查询数据。

// 假设我们有一个User模型,带有id, name, email和password字段
// 我们想要返回用户的id和name,但不返回email和password

const user = await prisma.user.findUnique({
  where: { id: someUserId },
  select: {
    id: true,
    name: true,
    email: false, // 明确指出不选择email
    password: false, // 明确指出不选择password
  }
});

// 现在user对象将仅包含id和name字段
return user;

但是这种方式,每次都要去改动具体的服务,而且复用性也不佳,所以适合少量的地方使用。

在Nestjs中,官方有一篇Serialization的文章,通过序列化的方式,也可以实现这种功能,本篇着重讲解这个。

原理

序列化的前提是依赖了一个库:class-transformer,这个库有三样东西:

  1. Exclude,跳过特定的属性装饰器;
  2. Expose, 公开特定的属性装饰器;
  3. classToPlain,此方法将类对象转换回纯 javascript 对象;

三者相辅相成,装饰器给类对象增加了规则,classToPlain则会根据规则将类对象进行处理,最后返回一个符合规则的纯JavaScript对象。

在Nestjs中,有一个全局的序列化拦截器ClassSerializerInterceptor,它本质上就是classToPlain的封装,所以它理所当然的也会有classToPlain的配置项。

我们将其配置成全局后,就可以通过ExcludeExpose等装饰器,对一个实体类进行装饰,最后new出这个实体类,最终这个实体类对象被ClassSerializerInterceptor处理。

class-transformer这个库还有很多装饰器,比如Transform数据转换装饰器,大家可以自行查阅文档:文档地址

Nestjs序列化文档:Serialization

教程

首先我们先注册全局序列化拦截器:

app.module.ts

import { ClassSerializerInterceptor, Module } from "@nestjs/common";
import { APP_INTERCEPTOR } from "@nestjs/core";



@Module({
    imports: [],
    controllers: [],
    providers: [
        {
            provide: APP_INTERCEPTOR,
            useClass: ClassSerializerInterceptor
        }
    ]
})
export class AppModule {}

然后我们去创建一个实体类文件,一般会在对应的模块目录里创建entities目录存放这个模块的实体类文件。

实体类文件如果是通过cli命令resource创建的模块,它会在选择生成数据库的数据模型输入Y时,生成对应的entities目录。

我们在该目录创建一个文件upload.entity.ts,填入以下内容:

import { Exclude, Expose } from "class-transformer";

@Exclude()
export class ImageResult {
    /** 文件名 */
    @Expose()
    filename: string;
    /** 文件路径 */
    @Expose()
    path: string;
    /** 文件大小 */
    @Expose()
    size: number;
    /** 文件mime */
    @Expose()
    mimetype: string;

    constructor(data: Partial<Express.Multer.File>) {
        Object.assign(this, data);
    }
}

在整个类上面使用@Exclude(),表示这个类对象上的所有属性,都不允许发送给前端。

然后再需要发送的属性上使用@Expose()标记。

注意constructor的时候,我们是需要接收一个具体的参数的,这个参数可以是数据库查询后得到的对象,也可以是其他,我们通过将参数对象的属性全部挂载到类对象上,再通过拦截器序列化数据,从而实现功能。

在去控制器使用:

upload.controller.ts

import { Controller, Post, UploadedFile } from "@nestjs/common";
import { UploadFile } from "src/utils/decorators";
import { ImageResult } from "./entities/upload.entity";

@Controller("upload")
export class UploadController {
    /** 上传图片 */
    @Post("image")
    @UploadFile("file", "image")
    async uploadImage(@UploadedFile() file: Express.Multer.File) {
        return new ImageResult(file);
    }
}

return出实体类的实例对象即可。

此时前端再来访问这个接口,得到的对象就是处理后的了。

{
  "filename": "1704955381389-9296196086.jpg",
  "path": "uploads/1704955381389-9296196086.jpg",
  "size": 83019,
  "mimetype": "image/jpeg"
}

序列化传配置参数

如果看了classToPlain这个方法的文档,就能知道其实他是有配置选项的,而我们的拦截器也是基于这个做的封装,所以它也可以传递配置选项,只是传的方式不同了,我们需要通过一个装饰器,将配置作为元数据存在控制器类上。

import { Controller, Post, SerializeOptions, UploadedFile } from "@nestjs/common";
import { UploadFile } from "src/utils/decorators";
import { ImageResult } from "./entities/upload.entity";

@Controller("upload")
export class UploadController {
    /** 上传图片 */
    @Post("image")
    @UploadFile("file", "image")
    @SerializeOptions({ strategy: "excludeAll" })
    async uploadImage(@UploadedFile() file: Express.Multer.File) {
        return new ImageResult(file);
    }
}

需要注意的是,这种方式的配置优先级是大于在ImageResult实体类上的装饰器规则的。

由于配置了{ strategy: "excludeAll" }表示所有的属性都不能返回,哪怕我在ImageResult中@Expose()标记了属性,也是会忽略的,最终前端得到的是一个空对象。

除了这个还有其他配置,有兴趣可以自行查阅文档:文档

分类: Nest.js 标签: Nestjs序列化对象实体类

评论

暂无评论数据

暂无评论数据

目录