这是 ES6 和 Typescript 时代。如今,您比以往任何时候都更多地使用类和构造函数对象。类转换器允许您将普通对象转换为类的某些实例,反之亦然。它还允许根据条件序列化/反序列化对象。这个工具在前端和后端都非常有用。
示例如何在 plunker 中使用 Angular 2。源代码可在此处获取。
在 JavaScript 中有两种类型的对象:
普通对象是属于Object
类实例的对象。有时,当通过{}
表示法创建时,它们被称为文字对象。类对象是具有自己定义的构造函数、属性和方法的类的实例。通常您通过class
符号来定义它们。
那么,问题出在哪里呢?
有时你想将普通的 javascript 对象转换为你拥有的 ES6类。例如,如果您从后端、某些 api 或 json 文件加载 json,并且在JSON.parse
之后,您将获得一个普通的 javascript 对象,而不是您拥有的类的实例。
例如,您正在加载的users.json
中有一个用户列表:
[
{
"id" : 1 ,
"firstName" : " Johny " ,
"lastName" : " Cage " ,
"age" : 27
},
{
"id" : 2 ,
"firstName" : " Ismoil " ,
"lastName" : " Somoni " ,
"age" : 50
},
{
"id" : 3 ,
"firstName" : " Luke " ,
"lastName" : " Dacascos " ,
"age" : 12
}
]
你有一个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 ;
}
}
您假设您正在从users.json
文件下载User
类型的用户,并且可能需要编写以下代码:
fetch ( 'users.json' ) . then ( ( users : User [ ] ) => {
// you can use users here, and type hinting also will be available to you,
// but users are not actually instances of User class
// this means that you can't use methods of User class
} ) ;
在此代码中,您可以使用users[0].id
,也可以使用users[0].firstName
和users[0].lastName
。但是,您不能使用users[0].getName()
或users[0].isAdult()
因为“users”实际上是纯 JavaScript 对象的数组,而不是 User 对象的实例。当您说它的users: User[]
时,您实际上对编译器撒了谎。
那么该怎么办呢?如何制作User
对象实例的users
数组而不是普通的 javascript 对象?解决方案是创建 User 对象的新实例并手动将所有属性复制到新对象。但是,一旦您拥有更复杂的对象层次结构,事情可能会很快出错。
替代方案?是的,您可以使用类转换器。该库的目的是帮助您将纯 JavaScript 对象映射到您拥有的类的实例。
该库也非常适合在 API 中公开的模型,因为它提供了一个很好的工具来控制模型在 API 中公开的内容。这是一个示例:
fetch ( 'users.json' ) . then ( ( users : Object [ ] ) => {
const realUsers = plainToInstance ( User , users ) ;
// now each user in realUsers is an instance of User class
} ) ;
现在您可以使用users[0].getName()
和users[0].isAdult()
方法。
安装模块:
npm install class-transformer --save
需要reflect-metadata
shim,也安装它:
npm install reflect-metadata --save
并确保将其导入到全局位置,例如 app.ts:
import 'reflect-metadata' ;
使用ES6特性,如果您使用旧版本的node.js,您可能需要安装es6-shim:
npm install es6-shim --save
并将其导入到像 app.ts 这样的全局位置:
import 'es6-shim' ;
安装模块:
npm install class-transformer --save
需要reflect-metadata
shim,也安装它:
npm install reflect-metadata --save
将<script>
添加到index.html
头部的reflect-metadata:
< html >
< head >
<!-- ... -->
< script src =" node_modules/reflect-metadata/Reflect.js " > </ script >
</ head >
<!-- ... -->
</ html >
如果您使用的是 Angular 2,您应该已经安装了这个垫片。
如果您使用 system.js,您可能需要将其添加到map
和package
配置中:
{
"map" : {
"class-transformer" : " node_modules/class-transformer "
},
"packages" : {
"class-transformer" : { "main" : " index.js " , "defaultExtension" : " js " }
}
}
此方法将普通 JavaScript 对象转换为特定类的实例。
import { plainToInstance } from 'class-transformer' ;
let users = plainToInstance ( User , userJson ) ; // to convert user plain object a single user. also supports arrays
此方法使用已填充的对象(目标类的实例)将普通对象转换为实例。
const defaultUser = new User ( ) ;
defaultUser . role = 'user' ;
let mixedUser = plainToClassFromExist ( defaultUser , user ) ; // mixed user should have the value role = user when no value is set otherwise.
此方法将您的类对象转换回纯 JavaScript 对象,稍后可以是JSON.stringify
。
import { instanceToPlain } from 'class-transformer' ;
let photo = instanceToPlain ( photo ) ;
此方法将类对象转换为类对象的新实例。这可能被视为对象的深度克隆。
import { instanceToInstance } from 'class-transformer' ;
let photo = instanceToInstance ( photo ) ;
您还可以在转换选项中使用ignoreDecorators
选项来忽略您的类正在使用的所有装饰器。
您可以使用serialize
方法将模型序列化为 json:
import { serialize } from 'class-transformer' ;
let photo = serialize ( photo ) ;
serialize
适用于数组和非数组。
您可以使用deserialize
方法从 json 反序列化模型:
import { deserialize } from 'class-transformer' ;
let photo = deserialize ( Photo , photo ) ;
要使反序列化适用于数组,请使用deserializeArray
方法:
import { deserializeArray } from 'class-transformer' ;
let photos = deserializeArray ( Photo , photos ) ;
plainToInstance
方法的默认行为是设置普通对象的所有属性,甚至是类中未指定的属性。
import { plainToInstance } from 'class-transformer' ;
class User {
id : number ;
firstName : string ;
lastName : string ;
}
const fromPlainUser = {
unkownProp : 'hello there' ,
firstName : 'Umed' ,
lastName : 'Khudoiberdiev' ,
} ;
console . log ( plainToInstance ( User , fromPlainUser ) ) ;
// User {
// unkownProp: 'hello there',
// firstName: 'Umed',
// lastName: 'Khudoiberdiev',
// }
如果此行为不满足您的需求,您可以在plainToInstance
方法中使用excludeExtraneousValues
选项,同时根据要求公开所有类属性。
import { Expose , plainToInstance } from 'class-transformer' ;
class User {
@ Expose ( ) id : number ;
@ Expose ( ) firstName : string ;
@ Expose ( ) lastName : string ;
}
const fromPlainUser = {
unkownProp : 'hello there' ,
firstName : 'Umed' ,
lastName : 'Khudoiberdiev' ,
} ;
console . log ( plainToInstance ( User , fromPlainUser , { excludeExtraneousValues : true } ) ) ;
// User {
// id: undefined,
// firstName: 'Umed',
// lastName: 'Khudoiberdiev'
// }
当您尝试转换具有嵌套对象的对象时,需要知道您尝试转换的对象类型。由于 Typescript 尚不具备良好的反射能力,因此我们应该隐式指定每个属性包含的对象类型。这是使用@Type
装饰器完成的。
假设我们有一个包含照片的相册。我们正在尝试将专辑普通对象转换为类对象:
import { Type , plainToInstance } from 'class-transformer' ;
export class Album {
id : number ;
name : string ;
@ Type ( ( ) => Photo )
photos : Photo [ ] ;
}
export class Photo {
id : number ;
filename : string ;
}
let album = plainToInstance ( Album , albumJson ) ;
// now album is Album object with Photo objects inside
如果嵌套对象可以是不同类型,您可以提供一个附加选项对象,它指定一个鉴别器。鉴别器选项必须定义一个property
,该属性保存对象的子类型名称以及嵌套对象可以转换为的可能的subTypes
。子类型有一个value
,它保存 Type 的构造函数和name
,可以与鉴别器的property
相匹配。
假设我们有一个相册,里面有一张顶级照片。但这张照片可以有某些不同的类型。我们正在尝试将专辑普通对象转换为类对象。普通对象输入必须定义附加属性__type
。默认情况下,此属性在转换过程中被删除:
JSON 输入:
{
"id" : 1 ,
"name" : " foo " ,
"topPhoto" : {
"id" : 9 ,
"filename" : " cool_wale.jpg " ,
"depth" : 1245 ,
"__type" : " underwater "
}
}
import { Type , plainToInstance } from 'class-transformer' ;
export abstract class Photo {
id : number ;
filename : string ;
}
export class Landscape extends Photo {
panorama : boolean ;
}
export class Portrait extends Photo {
person : Person ;
}
export class UnderWater extends Photo {
depth : number ;
}
export class Album {
id : number ;
name : string ;
@ Type ( ( ) => Photo , {
discriminator : {
property : '__type' ,
subTypes : [
{ value : Landscape , name : 'landscape' } ,
{ value : Portrait , name : 'portrait' } ,
{ value : UnderWater , name : 'underwater' } ,
] ,
} ,
} )
topPhoto : Landscape | Portrait | UnderWater ;
}
let album = plainToInstance ( Album , albumJson ) ;
// now album is Album object with a UnderWater object without `__type` property.
提示:这同样适用于具有不同子类型的数组。此外,您可以在选项中指定keepDiscriminatorProperty: true
以将鉴别器属性保留在结果类中。
您可以通过为 getter 或方法设置@Expose()
装饰器来公开 getter 或方法返回的内容:
import { Expose } from 'class-transformer' ;
export class User {
id : number ;
firstName : string ;
lastName : string ;
password : string ;
@ Expose ( )
get name ( ) {
return this . firstName + ' ' + this . lastName ;
}
@ Expose ( )
getFullName ( ) {
return this . firstName + ' ' + this . lastName ;
}
}
如果您想使用不同的名称公开某些属性,可以通过为@Expose
装饰器指定name
选项来实现:
import { Expose } from 'class-transformer' ;
export class User {
@ Expose ( { name : 'uid' } )
id : number ;
firstName : string ;
lastName : string ;
@ Expose ( { name : 'secretKey' } )
password : string ;
@ Expose ( { name : 'fullName' } )
getFullName ( ) {
return this . firstName + ' ' + this . lastName ;
}
}
有时您想在转换过程中跳过某些属性。这可以使用@Exclude
装饰器来完成:
import { Exclude } from 'class-transformer' ;
export class User {
id : number ;
email : string ;
@ Exclude ( )
password : string ;
}
现在,当您转换用户时, password
属性将被跳过,并且不会包含在转换结果中。
您可以控制要排除属性的操作。使用toClassOnly
或toPlainOnly
选项:
import { Exclude } from 'class-transformer' ;
export class User {
id : number ;
email : string ;
@ Exclude ( { toPlainOnly : true } )
password : string ;
}
现在,仅在instanceToPlain
操作期间才会排除password
属性。反之亦然,请使用toClassOnly
选项。
您可以跳过该类的所有属性,并仅公开那些显式需要的属性:
import { Exclude , Expose } from 'class-transformer' ;
@ Exclude ( )
export class User {
@ Expose ( )
id : number ;
@ Expose ( )
email : string ;
password : string ;
}
现在id
和email
将被暴露,密码将在转换过程中被排除。或者,您可以在转换期间设置排除策略:
import { instanceToPlain } from 'class-transformer' ;
let photo = instanceToPlain ( photo , { strategy : 'excludeAll' } ) ;
在这种情况下,您不需要@Exclude()
整个类。
如果您使用前缀命名私有属性,例如使用_
,那么您也可以从转换中排除此类属性:
import { instanceToPlain } from 'class-transformer' ;
let photo = instanceToPlain ( photo , { excludePrefixes : [ '_' ] } ) ;
这将跳过所有以_
前缀开头的属性。您可以传递任意数量的前缀,并且以这些前缀开头的所有属性都将被忽略。例如:
import { Expose , instanceToPlain } from 'class-transformer' ;
export class User {
id : number ;
private _firstName : string ;
private _lastName : string ;
_password : string ;
setName ( firstName : string , lastName : string ) {
this . _firstName = firstName ;
this . _lastName = lastName ;
}
@ Expose ( )
get name ( ) {
return this . _firstName + ' ' + this . _lastName ;
}
}
const user = new User ( ) ;
user . id = 1 ;
user . setName ( 'Johny' , 'Cage' ) ;
user . _password = '123' ;
const plainUser = instanceToPlain ( user , { excludePrefixes : [ '_' ] } ) ;
// here plainUser will be equal to
// { id: 1, name: "Johny Cage" }
您可以使用组来控制将公开哪些数据以及不会公开哪些数据:
import { Exclude , Expose , instanceToPlain } from 'class-transformer' ;
export class User {
id : number ;
name : string ;
@ Expose ( { groups : [ 'user' , 'admin' ] } ) // this means that this data will be exposed only to users and admins
email : string ;
@ Expose ( { groups : [ 'user' ] } ) // this means that this data will be exposed only to users
password : string ;
}
let user1 = instanceToPlain ( user , { groups : [ 'user' ] } ) ; // will contain id, name, email and password
let user2 = instanceToPlain ( user , { groups : [ 'admin' ] } ) ; // will contain id, name and email
如果您正在构建具有不同版本的 API,则 class-transformer 拥有非常有用的工具。您可以控制模型的哪些属性应在哪个版本中公开或排除。例子:
import { Exclude , Expose , instanceToPlain } from 'class-transformer' ;
export class User {
id : number ;
name : string ;
@ Expose ( { since : 0.7 , until : 1 } ) // this means that this property will be exposed for version starting from 0.7 until 1
email : string ;
@ Expose ( { since : 2.1 } ) // this means that this property will be exposed for version starting from 2.1
password : string ;
}
let user1 = instanceToPlain ( user , { version : 0.5 } ) ; // will contain id and name
let user2 = instanceToPlain ( user , { version : 0.7 } ) ; // will contain id, name and email
let user3 = instanceToPlain ( user , { version : 1 } ) ; // will contain id and name
let user4 = instanceToPlain ( user , { version : 2 } ) ; // will contain id and name
let user5 = instanceToPlain ( user , { version : 2.1 } ) ; // will contain id, name and password
有时,您的纯 JavaScript 对象中有一个以字符串格式接收的日期。并且您想从中创建一个真正的 javascript Date 对象。您只需将 Date 对象传递给@Type
装饰器即可做到这一点:
import { Type } from 'class-transformer' ;
export class User {
id : number ;
email : string ;
password : string ;
@ Type ( ( ) => Date )
registrationDate : Date ;
}
当您想要将值转换为这些类型时,可以对Number
、 String
、 Boolean
基元类型使用相同的技术。
当您使用数组时,您必须提供数组包含的对象的类型。您可以在@Type()
装饰器中指定此类型:
import { Type } from 'class-transformer' ;
export class Photo {
id : number ;
name : string ;
@ Type ( ( ) => Album )
albums : Album [ ] ;
}
您还可以使用自定义数组类型:
import { Type } from 'class-transformer' ;
export class AlbumCollection extends Array < Album > {
// custom array functions ...
}
export class Photo {
id : number ;
name : string ;
@ Type ( ( ) => Album )
albums : AlbumCollection ;
}
库将自动处理适当的转换。
ES6 集合Set
和Map
也需要@Type
装饰器:
export class Skill {
name : string ;
}
export class Weapon {
name : string ;
range : number ;
}
export class Player {
name : string ;
@ Type ( ( ) => Skill )
skills : Set < Skill > ;
@ Type ( ( ) => Weapon )
weapons : Map < string , Weapon > ;
}
您可以使用@Transform
装饰器执行其他数据转换。例如,当您将对象从普通对象转换为类时,您希望使Date
对象成为moment
对象:
import { Transform } from 'class-transformer' ;
import * as moment from 'moment' ;
import { Moment } from 'moment' ;
export class Photo {
id : number ;
@ Type ( ( ) => Date )
@ Transform ( ( { value } ) => moment ( value ) , { toClassOnly : true } )
date : Moment ;
}
现在,当您调用plainToInstance
并发送 Photo 对象的简单表示形式时,它会将照片对象中的日期值转换为时刻日期。 @Transform
装饰器还支持组和版本控制。
@Transform
装饰器提供了更多参数,让您可以配置转换的完成方式。
@ Transform ( ( { value , key , obj , type } ) => value )
争论 | 描述 |
---|---|
value | 转换前的属性值。 |
key | 转换后的属性的名称。 |
obj | 变换源对象。 |
type | 变换类型。 |
options | 传递给转换方法的选项对象。 |
签名 | 例子 | 描述 |
---|---|---|
@TransformClassToPlain | @TransformClassToPlain({ groups: ["user"] }) | 使用instanceToPlain 转换方法返回并公开类上的属性。 |
@TransformClassToClass | @TransformClassToClass({ groups: ["user"] }) | 使用instanceToInstance 转换方法返回并公开类上的属性。 |
@TransformPlainToClass | @TransformPlainToClass(User, { groups: ["user"] }) | 使用 plainToInstance 转换方法返回并公开类上的属性。 |
上述装饰器接受一个可选参数: ClassTransformOptions - 转换选项,如组、版本、名称
一个例子:
@ Exclude ( )
class User {
id : number ;
@ Expose ( )
firstName : string ;
@ Expose ( )
lastName : string ;
@ Expose ( { groups : [ 'user.email' ] } )
email : string ;
password : string ;
}
class UserController {
@ TransformClassToPlain ( { groups : [ 'user.email' ] } )
getUser ( ) {
const user = new User ( ) ;
user . firstName = 'Snir' ;
user . lastName = 'Segal' ;
user . password = 'imnosuperman' ;
return user ;
}
}
const controller = new UserController ( ) ;
const user = controller . getUser ( ) ;
user
变量将仅包含firstName、lastName、email 属性,因为它们是公开的变量。由于我们提到了“user.email”组,因此电子邮件属性也被公开。
不支持泛型,因为 TypeScript 尚不具备良好的反射能力。一旦 TypeScript 团队为我们提供了更好的运行时类型反射工具,泛型就会被实现。但是您可以使用一些调整,也许可以解决您的问题。看看这个例子。
注意如果您将类验证器与类转换器一起使用,您可能不想启用此功能。
启用基于 Typescript 提供的类型信息的内置类型之间的自动转换。默认禁用。
import { IsString } from 'class-validator' ;
class MyPayload {
@ IsString ( )
prop : string ;
}
const result1 = plainToInstance ( MyPayload , { prop : 1234 } , { enableImplicitConversion : true } ) ;
const result2 = plainToInstance ( MyPayload , { prop : 1234 } , { enableImplicitConversion : false } ) ;
/**
* result1 will be `{ prop: "1234" }` - notice how the prop value has been converted to string.
* result2 will be `{ prop: 1234 }` - default behaviour
*/
循环引用将被忽略。例如,如果您要转换包含Photo
类型的属性photos
User
类,并且Photo
包含指向其父User
链接user
,则在转换过程中user
将被忽略。仅在instanceToInstance
操作期间不会忽略循环引用。
假设您想要下载用户并希望它们自动映射到User
类的实例。
import { plainToInstance } from 'class-transformer' ;
this . http
. get ( 'users.json' )
. map ( res => res . json ( ) )
. map ( res => plainToInstance ( User , res as Object [ ] ) )
. subscribe ( users => {
// now "users" is type of User[] and each user has getName() and isAdult() methods available
console . log ( users ) ;
} ) ;
您还可以将类ClassTransformer
作为服务注入providers
中,并使用它的方法。
示例如何在 plunker 中使用 Angular 2。源代码在这里。
查看 ./sample 中的示例以获取更多用法示例。
请在此处查看有关重大更改和发行说明的信息。