class-transformer
这是ES6和TypeScript的时代。如今,您比以往任何时候都更多地使用类和构造函数对象。 Class-transformer允许您将普通对象转换为某个类的实例,反之亦然。 它还允许基于特定条件序列化/反序列化对象。 这个工具在前端和后端都非常有用。
在plunker中可以查看如何与Angular 2一起使用的示例。 源代码可在这里找到。
目录
- 什么是class-transformer
- 安装
- 方法
- 强制类型安全实例
- 处理嵌套对象
- 暴露getter和方法返回值
- 使用不同名称暴露属性
- 跳过特定属性
- 根据操作跳过
- 跳过类的所有属性
- 跳过私有属性或某些前缀的属性
- 使用分组控制排除的属性
- 使用版本控制来控制暴露和排除的属性
- 将日期字符串转换为Date对象
- 处理数组
- 额外的数据转换
- 其他装饰器
- 使用泛型
- 隐式类型转换
- 如何处理循环引用?
- Angular2示例
- 示例
- 发布说明
什么是class-transformer⬆
在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[]) => {
// 您可以在这里使用users,类型提示也会对您可用,
// 但users实际上并不是User类的实例
// 这意味着您不能使用User类的方法
});
在这段代码中,您可以使用users[0].id
,也可以使用users[0].firstName
和users[0].lastName
。
但是您不能使用users[0].getName()
或users[0].isAdult()
,因为"users"实际上是
普通JavaScript对象的数组,而不是User对象的实例。
当您说它是users: User[]
时,您实际上欺骗了编译器。
那么该怎么办呢?如何让users
数组成为User
对象的实例而不是普通JavaScript对象?
解决方案是创建User对象的新实例,并手动将所有属性复制到新对象中。
但是一旦您有更复杂的对象层次结构,事情可能很快就会出错。
有替代方案吗?是的,您可以使用class-transformer。这个库的目的是帮助您将普通JavaScript 对象映射到您拥有的类的实例。
这个库对于在API中暴露的模型也很棒, 因为它提供了很好的工具来控制您的模型在API中暴露的内容。 以下是它看起来的样子:
fetch('users.json').then((users: Object[]) => {
const realUsers = plainToInstance(User, users);
// 现在realUsers中的每个用户都是User类的实例
});
现在您可以使用users[0].getName()
和users[0].isAdult()
方法了。
安装⬆
Node.js⬆
-
安装模块:
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
在
index.html
的 head 中添加 reflect-metadata 的<script>
标签:<html> <head> <!-- ... --> <script src="node_modules/reflect-metadata/Reflect.js"></script> </head> <!-- ... --> </html>
如果你使用的是 Angular 2,应该已经安装了这个 shim。
-
如果你使用 system.js,你可能想在
map
和package
配置中添加以下内容:{ "map": { "class-transformer": "node_modules/class-transformer" }, "packages": { "class-transformer": { "main": "index.js", "defaultExtension": "js" } } }
方法⬆
plainToInstance⬆
此方法将普通的 JavaScript 对象转换为特定类的实例。
import { plainToInstance } from 'class-transformer';
let users = plainToInstance(User, userJson); // 将用户普通对象转换为单个用户实例。也支持数组
plainToClassFromExist⬆
此方法将普通对象转换为实例,使用已填充的目标类实例对象。
const defaultUser = new User();
defaultUser.role = 'user';
let mixedUser = plainToClassFromExist(defaultUser, user); // 当没有设置值时,mixedUser 应具有 role = user 的值,否则使用原值。
instanceToPlain⬆
此方法将类对象转换回普通的 JavaScript 对象,之后可以使用 JSON.stringify
。
import { instanceToPlain } from 'class-transformer';
let photo = instanceToPlain(photo);
instanceToInstance⬆
此方法将类对象转换为该类对象的新实例。 这可以被视为对象的深度克隆。
import { instanceToInstance } from 'class-transformer';
let photo = instanceToInstance(photo);
你还可以在转换选项中使用 ignoreDecorators
选项来忽略类中使用的所有装饰器。
serialize⬆
你可以使用 serialize
方法直接将模型序列化为 JSON:
import { serialize } from 'class-transformer';
let photo = serialize(photo);
serialize
适用于数组和非数组。
deserialize 和 deserializeArray⬆
你可以使用 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);
// 现在 album 是一个 Album 对象,其中包含 Photo 对象
提供多个类型选项⬆
如果嵌套对象可以是不同类型,你可以提供一个额外的选项对象,
该对象指定一个鉴别器。鉴别器选项必须定义一个 property
,用于保存对象的子类型名称,
以及嵌套对象可以转换为的可能 subTypes
。子类型有一个 value
,保存类型的构造函数,
还有一个 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;
}
让 album 等于使用 plainToInstance 将 albumJson 转换为 Album 实例。
// 现在 album 是一个 Album 对象,其中包含一个没有 __type
属性的 UnderWater 对象。
提示:这同样适用于具有不同子类型的数组。此外,你可以在选项中指定 `keepDiscriminatorProperty: true` 以在结果类中也保留判别器属性。
## 暴露 getter 和方法返回值
你可以通过在 getter 或方法上设置 `@Expose()` 装饰器来暴露它们的返回值:
```typescript
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;
}
现在当你转换 User 时,password
属性将被跳过,不会包含在转换结果中。
根据操作跳过
你可以控制在哪种操作中排除属性。使用 toClassOnly
或 toPlainOnly
选项:
import { Exclude } from 'class-transformer';
export class User {
id: number;
email: string;
@Exclude({ toPlainOnly: true })
password: string;
}
现在 password
属性只会在 instanceToPlain
操作中被排除。反之,使用 toClassOnly
选项。
跳过类的所有属性
你可以跳过类的所有属性,只暴露那些明确需要的属性:
import { Exclude, Expose } from 'class-transformer';
@Exclude()
export class User {
@Expose()
id: number;
@Expose()
email: string;
password: string;
}
现在 id
和 email
将被暴露,而 password 在转换过程中将被排除。
或者,你可以在转换过程中设置排除策略:
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: ['_'] });
// 这里 plainUser 将等于
// { id: 1, name: "Johny Cage" }
使用分组控制排除的属性
你可以使用分组来控制哪些数据将被暴露,哪些不会:
import { Exclude, Expose, instanceToPlain } from 'class-transformer';
export class User {
id: number;
name: string;
@Expose({ groups: ['user', 'admin'] }) // 这意味着这些数据只会暴露给用户和管理员
email: string;
@Expose({ groups: ['user'] }) // 这意味着这些数据只会暴露给用户
password: string;
}
let user1 = instanceToPlain(user, { groups: ['user'] }); // 将包含 id、name、email 和 password
let user2 = instanceToPlain(user, { groups: ['admin'] }); // 将包含 id、name 和 email
使用版本控制来控制暴露和排除的属性
如果你正在构建一个有不同版本的 API,class-transformer 为此提供了非常有用的工具。 你可以控制在哪个版本中应该暴露或排除模型的哪些属性。例如:
import { Exclude, Expose, instanceToPlain } from 'class-transformer';
export class User {
id: number;
name: string;
@Expose({ since: 0.7, until: 1 }) // 这意味着这个属性将从版本 0.7 开始暴露,直到版本 1
email: string;
@Expose({ since: 2.1 }) // 这意味着这个属性将从版本 2.1 开始暴露
password: string;
}
let user1 = instanceToPlain(user, { version: 0.5 }); // 将包含 id 和 name
let user2 = instanceToPlain(user, { version: 0.7 }); // 将包含 id、name 和 email
let user3 = instanceToPlain(user, { version: 1 }); // 将包含 id 和 name
let user4 = instanceToPlain(user, { version: 2 }); // 将包含 id 和 name
let user5 = instanceToPlain(user, { version: 2.1 }); // 将包含 id、name 和 password
将日期字符串转换为 Date 对象
有时你的普通 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> {
// 自定义数组函数 ...
}
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 对象的普通表示时,
它会将你的 photo 对象中的日期值转换为 moment 日期。
@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 属性,因为它们是暴露的变量。
email 属性也被暴露,因为我们提到了 "user.email" 分组。
处理泛型
由于 TypeScript 目前还没有很好的反射能力,泛型暂不支持。 一旦 TypeScript 团队为我们提供更好的运行时类型反射工具,泛型将会被实现。 不过,你可以使用一些技巧,也许能解决你的问题。 查看这个示例。
隐式类型转换
注意 如果你同时使用 class-validator 和 class-transformer,你可能不想启用此功能。
根据 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 将是 `{ prop: "1234" }` - 注意 prop 值已被转换为字符串。
* result2 将是 `{ prop: 1234 }` - 默认行为
*/
如何处理循环引用?
循环引用会被忽略。
例如,如果你正在转换包含 photos
属性(类型为 Photo
)的 User
类,
而 Photo
包含指向其父 User
的 user
链接,那么在转换过程中 user
将被忽略。
循环引用只在 instanceToInstance
操作中不被忽略。
Angular2 示例
假设你想下载用户并希望它们自动映射到 User
类的实例。
import { plainToInstance } from 'class-transformer';
this.http
.get('users.json')
.map(res => res.json())
.map(res => plainToInstance(User, res as Object[]))
.subscribe(users => {
// 现在 "users" 的类型是 User[],每个用户都可以使用 getName() 和 isAdult() 方法
console.log(users);
});
你也可以在 providers
中注入 ClassTransformer
类作为服务,并使用其方法。
在 plunker 中查看如何与 Angular 2 一起使用的示例。 源代码在这里。
示例
查看 ./sample 中的示例,了解更多使用示例。
发布说明
有关重大变更和发布说明的信息,请参阅此处。