上节我们学了用 class-transformer + ClassSerializerInterceptor 来序列化 entity 对象,可以替代 vo。
这节我们自己来实现一下 ClassSerializerInterceptor,深入理解它的实现原理。
nest new serializer-interceptor-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);
}
}
改下 UserService:
import { 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 = [
new User({ id: 1, username: 'xxx', password: 'xxx', email: 'xxx@xx.com'}),
new User({ id: 2, username: 'yyy', password: 'yyy', email: 'yyy@yy.com'})
];
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`;
}
}
内置两条数据,这样就不用每次调用接口创建了。
然后安装 class-transformer 包:
npm install --save class-transformer
在 entity 上加一下 class-transformer 的装饰器:
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);
}
}
然后我们自己来实现 ClassSerializerInterceptor,还有一个自定义装饰器 SerializeOptions:
先来写这个自定义装饰器,它比较简单:
nest g decorator serialize-options --flat
import { SetMetadata } from '@nestjs/common';
import { ClassTransformOptions } from 'class-transformer';
export const CLASS_SERIALIZER_OPTIONS = 'class_serializer:options';
export const SerializeOptions = (options: ClassTransformOptions) =>
SetMetadata(CLASS_SERIALIZER_OPTIONS, options);
它做的事情就是往 class 或者 method 上加一个 metadata。
然后 interceptor 取出这个 metadata 的 options 给 class-transfromer 用。
所以这个 options 的类型就是 ClassTransformOptions。
是不是第一次见这样设置参数?
确实挺巧妙的。
然后来写 interceptor:
nest g interceptor class-serializer --flat --no-spec
import { CallHandler, ExecutionContext, Inject, Injectable, NestInterceptor } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ClassTransformOptions } from 'class-transformer';
import { Observable } from 'rxjs';
import { CLASS_SERIALIZER_OPTIONS } from './serialize-options.decorator';
@Injectable()
export class ClassSerializerInterceptor implements NestInterceptor {
@Inject(Reflector)
protected readonly reflector: Reflector;
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const contextOptions = this.getContextOptions(context);
return next.handle();
}
protected getContextOptions(
context: ExecutionContext,
): ClassTransformOptions | undefined {
return this.reflector.getAllAndOverride(CLASS_SERIALIZER_OPTIONS, [
context.getHandler(),
context.getClass(),
]);
}
}
注入 Reflector 包,用它的 getAllAndOverride 方法拿到 class 或者 handler 上的 metadata。
打印下看看。
我们把它加到 handler 上:
加一个调试配置:
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "debug nest",
"runtimeExecutable": "npm",
"args": [
"run",
"start:dev",
],
"skipFiles": [
"<node_internals>/**"
],
"console": "integratedTerminal",
}
]
}
打个断点,然后点击调试启动:
现在是 undefined:
加一下 @SerializeOptions 装饰器,用我们刚才写的那个。然后点击 restart:
这时候就拿到 options 了:
然后我们继续写:
import { CallHandler, ExecutionContext, Inject, Injectable, NestInterceptor, StreamableFile } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ClassTransformOptions } from 'class-transformer';
import { Observable, map } from 'rxjs';
import { CLASS_SERIALIZER_OPTIONS } from './serialize-options.decorator';
import * as classTransformer from 'class-transformer';
function isObject(value) {
return value !== null && typeof value === 'object'
}
@Injectable()
export class ClassSerializerInterceptor implements NestInterceptor {
@Inject(Reflector)
protected readonly reflector: Reflector;
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const contextOptions = this.getContextOptions(context);
return next
.handle()
.pipe(
map((res) =>
this.serialize(res, contextOptions),
),
);
}
serialize(
response: Record<string, any> | Array<Record<string, any>>,
options: ClassTransformOptions
){
if (!isObject (response) || response instanceof StreamableFile) {
return response;
}
return Array.isArray(response)
? response.map(item => this.transformToNewPlain(item, options))
: this.transformToNewPlain(response, options);
}
transformToNewPlain(
palin: any,
options: ClassTransformOptions,
) {
if (!palin) {
return palin;
}
return classTransformer.instanceToPlain(palin, options);
}
protected getContextOptions(
context: ExecutionContext,
): ClassTransformOptions | undefined {
return this.reflector.getAllAndOverride(CLASS_SERIALIZER_OPTIONS, [
context.getHandler(),
context.getClass(),
]);
}
}
在 interceptor 里用 map operator 对返回的数据做修改。
在 serialize 方法里根据响应是数组还是对象分别做处理,调用 transformToNewPlain 做转换。
这里排除了 response 不是对象的情况和返回的是文件流的情况:
transformToNewPlain 就是用 class-transformer 包的 instanceToPlain 根据对象的 class 上的装饰器来创建新对象:
有同学说不是 classToPlain 么?
那个 api 过时了,用 instanceToPlain 代替。
打个断点测试下:
最开始的响应数据是 user 对象:
转换完之后就是新的对象了:
这样我们就实现了 ClassSerializerInterceptor 拦截器的功能。
案例代码在小册仓库
总结
上节学了用 entity 结合 class-transfomer 的装饰器和 ClassSerializerInterceptor 拦截器实现复用 entity 做 vo 的功能。
这节我们自己实现了下。
首先是 @SerializeOptions 装饰器,它就是在 class 或者 handler 上加一个 metadata,存放 class-transformer 的 options。
在 ClassSerializerInterceptor 里用 reflector 把它取出来。
然后拦截响应,用 map oprator对响应做变换,调用 classTransformer 包的 instanceToPlain 方法进行转换。
自己实现一遍之后,对它的理解就更深了。