开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第14天,点击查看活动详情
class-transformer
星标5.4K
现在是
ES6
和Typescript
的时代了!现在我们每天都在和类打交道,class-transformer
可以帮助我们
- 实现字面量对象
plain object
和类实例对象instance of class
之间的转换- 基于一定条件序列化和反序列化对象(感觉已经不再使用了,都是过时的方法,也就不做深究了)
这个工具在前后端都非常有用
本文是写nestjs设置环境变量那一节遇到的一个小知识点,主要是一些api
的使用方法,在这里做一个简单小结
一、什么是class-transformer
首先我们来区分一下plain(literal) object
和class(constructor) objects
// literal
const literralObj = {
name: 'li',
age: 18
}
// class
class User {
constructor(public name: string, public age: number){
this.name = name
this.age = age
}
}
const user = new User('jing', 20)
plain(literal) object
是通过{ ... }
直接定义出来的class(constructor) objects
是通过类new
出来的
问题引出
我们在前端处理后端传送过来的数据时,经过反序列化转换的对象全是plain(literal) object
,如下:
{
"id": 1,
"firstName": "Johny",
"lastName": "Cage",
"age": 27
},
我们在前端定义了User
类
export class User {
id: number;
firstName: string;
lastName: string;
age: number;
getName() {
return this.firstName + ' ' + this.lastName;
}
isAdult() {
return this.age > 36 && this.age < 60;
}
}
我们在前端接收数据的时候,可以使用ts
语法显示标注接收对象的类型,可以给我们属性提示,但是我们没法使用User
类给我们提供的方法!
fetch('user.json').then((user: User) => {
// you can use user here, and type hinting also will be available to you,
// but user is not actually instance of User class
// this means that you can't use methods of User class
// error!
user.getName()
user.isAdult()
});
如果我们想要相关的方法,那必须重建整个对象,复制对应的属性值,这对于一个复杂对象来说很麻烦,且是一件很容易出错的事,如果使用class-transform
可以直接将收到的user
字面量对象转换为User
实例对象,进而可以使用其中的方法。
fetch('user.json').then((user: Object) => {
const realUsers = plainToClass(Use, user);
// now user is an instance of User class
});
二、安装
1. Node.js
npm install class-transformer --save
npm install reflect-metadata --save
需要保证reflect-metadata
在全局引入,例如在app.ts
中
import 'reflect-metadata';
2. Browser
npm install class-transformer --save
npm install reflect-metadata --save
官方说需要在index.html
中插入这个script
,不过我尝试的时候没做这一步也运行正常.
<html>
<head>
<!-- ... -->
<script src="node_modules/reflect-metadata/Reflect.js"></script>
</head>
<!-- ... -->
</html>
三、简单实操验证
- 前端代码
import ddRequest from '@/service'
import { plainToClass } from 'class-transformer'
export class User {
public username: string
public password: string
public age: number
constructor(username: string, password: string, age: number) {
this.username = username
this.password = password
this.age = age
}
getName() {
return this.username
}
isAdult() {
return this.age > 36 && this.age < 60
}
}
console.log('test.ts')
ddRequest
.get({
baseURL: 'http://localhost:4000/user'
})
.then((user: User) => {
// 书写代码有提示
console.log(user)
console.log(user.username)
// 有提示,但是会报错
console.log(user.getName())
console.log(user.isAdult())
})
- 后端代码
@Get()
findAll() {
const user = new User('coder', '12345', 18);
return user;
}
可以看到上面的前端代码中还没有使用class-transformer
,运行后会报错
如果使用上class-transformer
,稍作改造如下:
ddRequest
.get({
baseURL: 'http://localhost:4000/user'
})
.then((user: User) => {
const userRes = plainToClass(User, user)
console.log(userRes.username)
console.log(userRes.getName())
console.log(userRes.isAdult())
})
立马见效了,不仅有提示,还可以正确调用类里面的方法
四、方法介绍
我看了一下源码,很多github readme
中的示例方法都过时了,带class
的都不用了,换成了带instance
的,然后序列化和反序列化的方法也都过时了
// 实验数据
const plainUser = {
username: 'coder',
password: '123',
age: 18
}
const plainUsers = [
{
username: 'coder1',
password: '123',
age: 18
},
{
username: 'coder2',
password: '123',
age: 30
}
]
为了方便,就不模拟前后端交互了,仅对方法做一些测试和验证
1. plainToInstance
将字面量转为类实例,支持数组转换
/**
* Converts plain (literal) object to class (constructor) object. Also works with arrays.
*/
export declare function plainToInstance<T, V>(cls: ClassConstructor<T>, plain: V[], options?: ClassTransformOptions): T[];
export declare function plainToInstance<T, V>(cls: ClassConstructor<T>, plain: V, options?: ClassTransformOptions): T;
const user: User = plainToInstance(User, plainUser)
console.log(user.isAdult()) // false
const userList: User[] = plainToInstance(User, plainUsers)
console.log(userList)
2. InstanceToPlain
将类实例转为字面量对象,支持数组
/**
* Converts class (constructor) object to plain (literal) object. Also works with arrays.
*/
export declare function instanceToPlain<T>(object: T, options?: ClassTransformOptions): Record<string, any>;
export declare function instanceToPlain<T>(object: T[], options?: ClassTransformOptions): Record<string, any>[];
const instanceUser = new User('coder3', '123', 33)
const plain = instanceToPlain(instanceUser)
3. instanceToInstance
将一个类实例转换到另一个类实例,可以看做是做一次深拷贝
/**
* Converts class (constructor) object to new class (constructor) object. Also works with arrays.
*/
export declare function instanceToInstance<T>(object: T, options?: ClassTransformOptions): T;
export declare function instanceToInstance<T>(object: T[], options?: ClassTransformOptions): T[];
const instanceUser2 = new User('coder4', '123', 33)
const instanceUser3 = instanceToInstance(instanceUser2)
console.log(instanceUser3.isAdult())
4. serialize
序列化对象,原来是serialize
,现在建议使用JSON.stringify(instanceToPlain(object, options))
5. deserialize
和deserializeArray
反序列化,现在建议分别使用如下替代
instanceToClass(cls, JSON.parse(json), options)
JSON.parse(json).map(value => instanceToClass(cls, value, options))
关于序列化和反序列化我们一般也不用这两种方式,主要掌握一下上面对象之间转换的方法
五、方法可选配置参数
对于转换方法,我们能看到其实每一个方法都有一个配置选项,它的类型是ClassTransformOptions
,我们看一下它的源码
import { TargetMap } from './target-map.interface';
export interface ClassTransformOptions {
strategy?: 'excludeAll' | 'exposeAll';
excludeExtraneousValues?: boolean;
groups?: string[];
/**
* Only properties with "since" > version < "until" gonna be transformed.
*/
version?: number;
/**
* Excludes properties with the given prefixes. For example, if you mark your private properties with "_" and "__". you can set this option's value to ["_", "__"] and all private properties will be skipped.
* This works only for "exposeAll" strategy.
*/
excludePrefixes?: string[];
/**
* If set to true then class transformer will ignore the effect of all @Expose and @Exclude decorators. This option is useful if you want to kinda clone your object but do not apply decorators affects.
* __NOTE:__ You may still have to add the decorators to make other options work.
*/
ignoreDecorators?: boolean;
/**
* Target maps allows to set a Types of the transforming object without using @Type decorator. This is useful when you are transforming external classes, or if you already have type metadata for objects and you don't want to set it up again.
*/
targetMaps?: TargetMap[];
/**
* If set to true then class transformer will perform a circular check. (circular check is turned off by default). This option is useful when you know for sure that your types might have a circular dependency.
*/
enableCircularCheck?: boolean;
/**
* If set to true then class transformer will try to convert properties implicitly to their target type based on their typing information.
* DEFAULT: `false`
*/
enableImplicitConversion?: boolean;
/**
* If set to true then class transformer will take default values for unprovided fields. This is useful when you convert a plain object to a class and have an optional field with a default value.
*/
exposeDefaultValues?: boolean;
/**
* When set to true, fields with `undefined` as value will be included in class to plain transformation. Otherwise those fields will be omitted from the result.
* DEFAULT: `true`
*/
exposeUnsetFields?: boolean;
}
配置项比较多,下面仅看几个示例,其它的大家可以自己探索一下。
1. strategy
exposeAll
:转换所有excludeAll
:都不转换
const plainUser = {
username: 'coder',
password: '123',
age: 18,
height: 1.78
}
const user: User = plainToInstance(User, plainUser, {
strategy: 'exposeAll'
})
// User类中没有height属性,但是依然会被转换出来
console.log(user) // {username: 'coder', password: '123', age: 18, height: 1.78}
const user1: User = plainToInstance(User, plainUser, {
strategy: 'excludeAll'
})
// 任何属性都不转换
console.log(user1) // {username: undefined, password: undefined, age: undefined}
2. excludeExtraneousValues
当从一个plain
对象转类实例的时候,用来标识无关的属性(即plain
上有的属性在类中没有)要不要排除在外,需要在类的属性上加@Expose()
或@Exclude()
装饰器
可以看到装饰器是一个实验特性,需要进行设置,在tsconfig.json
中添加如下配置即可
"ExperimentalDecorators": true,
export class User {
@Expose()
public username: string
@Exclude()
public password: string
@Expose()
public age: number
}
const user: User = plainToInstance(User, plainUser, {
strategy: 'exposeAll',
excludeExtraneousValues: true
})
console.log(user) // {username: 'coder', password: undefined, age: 18}
默认情况下类定义中什么装饰器都不加相当于
@Exclude()
,相当于该属性被排除在外了,不做转换
@Expose()
还可以设置属性名对应关系,如下:
// 代表的是将plain对象中的name属性值转换到类实例的username属性下
@Expose({ name: 'name' })
public username: string
3. groups
只有在groups
数组里的才会被转换,这样说很多人可能会误解,其使用需要配合@Expose({groups:[]})
使用:
export class User {
@Expose({ name: 'name' })
public username: string
@Expose({ groups: ['admins'] })
public password: string
@Expose({ groups: ['users', 'admins'] })
public age: number
}
const user: User = plainToInstance(User, plainUser, {
excludeExtraneousValues: true,
groups: ['users']
})
// user1: User {username: 'coder', password: undefined, age: 18}
console.log('user1:', user)
const user2: User = plainToInstance(User, plainUser, {
excludeExtraneousValues: true,
groups: ['admins']
})
// user2: User {username: 'coder', password: '123', age: 18}
console.log('user2:', user2)
这个可以用来做鉴权方向的事情,如上,在类的定义中,我们可以在属性上定义相应的groups
,用来标识哪些用户角色可以访问,同时在转换配置中通过groups
来标识该转换的对象可以提供给哪些角色使用,可以看到上面的示例,password
属性值只暴露给admins
。
4. enableImplicitConversion
强制类型转换,这个在环境变量的验证那一章节也用到过,可以将plain
对象中的属性强制转换为类里对应属性的类型,能转则转不能转则直接报错!注意这个属性默认为false
,此时即使两边对应属性类型不一致也会转过去,但是如果设置了这个配置项为true
则会强制转,可能产生错误,用的时候需谨慎。
// class 中:public age: number
const plainUser = {
username: 'hello',
name: 'coder',
password: '123',
// age是一个字符串
age: 'hello',
height: 1.78
}
const user1: User = plainToInstance(User, plainUser, {
excludeExtraneousValues: true,
})
//user1: User {username: 'coder', password: undefined, age: 'hello'}
console.log('user1:', user1)
设置了enableImplicitConversion: true
后:直接报错了,因为hello
不能转为数字类型!
六、几个装饰器
@Exclude()
可以作用于类上,表示所有属性都被排除,也可以作用于具体某个属性上
export interface ExcludeOptions {
toClassOnly?: boolean;
toPlainOnly?: boolean;
}
@Expose()
标识需要转换,也可以作用于类和属性,与上面刚好相反
可以有以下配置项:
export interface ExposeOptions {
name?: string;
since?: number;
until?: number;
groups?: string[];
toClassOnly?: boolean;
toPlainOnly?: boolean;
}
@Type()
这里还没有完全搞明白,用的时候一直报错,不加@Type工作好好的,一加了就报错!希望有用过的大佬指点指点。
export declare function Type(typeFunction?: (type?: TypeHelpOptions) => Function, options?: TypeOptions): PropertyDecorator;
官方的案例如下:(自己没有成功)
import { Type } from 'class-transformer';
export class User {
id: number;
email: string;
password: string;
@Type(() => Date)
registrationDate: Date;
}
@Transform()
更加自由化的转换,可以在里面拿到转换前的数据,根据需要进行相应的转换
export declare function Transform(transformFn: (params: TransformFnParams) => any, options?: TransformOptions): PropertyDecorator;
export interface TransformFnParams {
// plain对象中对应的属性值,转换前
value: any;
// 该属性的属性名
key: string;
// 整个plain对象
obj: any;
//
type: TransformationType;
// 可选配置
options: ClassTransformOptions;
}
/**
* Possible transformation options for the @Transform decorator.
*/
export interface TransformOptions {
since?: number;
until?: number;
groups?: string[];
toClassOnly?: boolean;
toPlainOnly?: boolean;
}
很多东西都需要自己去尝试才行,用到了再回来看看吧!