這是 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 中的範例以取得更多用法範例。
請在此處查看有關重大變更和發行說明的資訊。