当前版本仅支持 stage1 阶段的装饰器提案,若您想支持 stage3,请将 class-formatter 升级到 5.0.0 及以上的版本
一套装饰器风格的数据格式化方法。
根据模板针对数据的每个字段进行格式化。
在一些复杂场景中(例如:大表单数据提交),会遇到层层嵌套的复杂数据结构,且不同字段之间拥有复杂的联动关系。
class-formatter
可以通过装饰器简化这种格式化过程,减轻心智负担,将格式化从业务逻辑中抽离出来
npm install class-formatter
tsconfig.json 需进行如下配置:
{
"compilerOptions": {
...
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
...
},
...
}
- 模板: 类即是模板。
-
指令: 模板中属性或方法的
class-formatter
装饰器,一个装饰器即为一个指令。 - 源数据: 被转换的数据。
-
转换: 调用
executeTransform
或executeTransArray
函数对源数据进行格式化的行为。
例如:
class User {
@toString()
name!: string;
@toNumber()
age!: number;
}
const user = {
name: '张三',
age: 18
};
const result = executeTransform(User, user);
- 模板:上述示例中,类
User
即为模板,也是转换函数(executeTransform
)的第一个参数。 - 指令:上述示例中,
@toString()
与@toNumber()
即为指令。 - 源数据:上述示例中,对象
user
即为源数据。
class User {
@toString()
name!: string;
@toNumber()
age!: number;
}
const user = {
name: '张三',
age: '18'
};
const formatUser = executeTransform(User, user);
// => { name: '张三', age: 18 }
在上述示例中,formatUser
一定拥有 字符串类型 属性 name
,数字类型 属性 age
。
class User {
@toString()
name!: string;
@toNumber()
age!: number;
}
const users = [{
name: '张三',
age: '18'
}];
const formatUsers = executeTransArray(User, users);
若源数据为数组,则可以使用 executeTransArray
进行格式化。
class User {
@toString()
name: string = '张三';
@toNumber(1)
age!: number;
}
- 若源数据中不存在属性,则会根据默认值生成该属性。
- 例如根据上述模板,若源数据不存在
name
属性,则转换结果一定拥有字符串属性name
,且name
属性值为 '张三'。
- 例如根据上述模板,若源数据不存在
- 默认值有两种传入方式,模板传入与指令传入,其中模板传入优先级 高于 指令传入。
- 上述模板中,
name
为模板传入,age
为指令传入。
- 上述模板中,
属性装饰器 | 说明 | 类型 | 默认值 |
---|---|---|---|
toNumber | 若属性为非数字类型,则将属性转换为 number 类型。autoTrans 为 true 时会自动将字符串转换为数字。 |
(value?: NumberConfig | number) => Decorator | defaultValue: 0 autoTrans: true |
toString | 若属性为非字符串类型,则将属性转换为 string 类型。autoTrans 为 true 时会自动将数字转换为字符串。 |
(value?: StringConfig | string) => Decorator | defaultValue: '' autoTrans: true |
toBoolean | 若属性为非布尔类型,则将属性转换为 boolean 类型 |
(value?: BooleanConfig | boolean) => Decorator | defaultValue: false |
toSymbol | 若属性为非 symbol 类型,则将属性转换为 symbol 类型 |
(value?: SymbolConfig | symbol) => Decorator | defaultValue: Symbol() |
toRegExp | 若属性为非正则类型,则将属性转换为正则类型 | (value?: RegConfig | RegExp | string) => Decorator | defaultValue: new RegExp('') |
toType | 若属性为非对象类型,则将属性转换为对象。 若指定了 Type ,则可以将类型转换为 Type 的类型。 |
(value?: ObjectConfig | Type) => Decorator | defaultValue: {} |
toArray | 若属性为非数组类型,则将属性转换为数组。 若指定了 Type ,则可以将数组内所有数据转换为 Type 的类型。 |
(value?: ArrayConfig | Type) => Decorator | defaultValue: [] |
toKeep | 保持源数据引用 | keys?: ModelKey | ModelKey[]) => Decorator | -- |
Remove | 移除属性 | (value?: RemoveConfig | RemoveCallback | ModelKey | ModelKey[]) => Decorator | -- |
Format | 对属性进行自定义格式化。 注意:Format 会在所有内置校验结束后执行,且不限制返回值类型,使用时请格外注意 |
(callback: FormatCallback, keys?: ModelKey | ModelKey[]) => Decorator | -- |
Rename | 对属性重命名。 | (name: string, keys?: ModelKey | ModelKey[]) => Decorator | -- |
方法装饰器 | 说明 | 类型 | 默认值 |
---|---|---|---|
ExtendMethod | 在结果中继承被装饰的方法或访问器 | (keys?: ModelKey | ModelKey[]) => MethodDecorator | -- |
类装饰器 | 说明 | 类型 | 默认值 |
---|---|---|---|
Extend | 继承父类的全部装饰器 | (parent: Type) => ClassDecorator | -- |
Mixins | 混入,同时继承全部类的装饰器 | (...parents: Type[]) => ClassDecorator | -- |
executeTransform
参数名称 | 说明 | 类型 |
---|---|---|
ClassType | 模板类 | Type |
values | 被格式化对象 | any |
options | 配置项 | Omit<FormatOptions, 'map'> |
executeTransArray
参数名称 | 说明 | 类型 |
---|---|---|
ClassType | 模板类 | Type |
values | 被格式化对象 | any[] |
options | 配置项 | FormatOptions |
type Decorator = (target: any, propertyKey: string) => void;
type NumberConfig = {
defaultValue?: number;
autoTrans?: boolean;
keys?: ModelKey | ModelKey[];
};
type StringConfig = {
defaultValue?: string;
autoTrans?: boolean;
keys?: ModelKey | ModelKey[];
};
type BooleanConfig = {
defaultValue?: boolean;
keys?: ModelKey | ModelKey[];
};
type SymbolConfig = {
defaultValue?: symbol;
keys?: ModelKey | ModelKey[];
};
type RegConfig = {
defaultValue?: Regexp | string;
keys?: ModelKey | ModelKey[];
};
type ObjectConfig<T = any> = {
defaultValue?: Partial<T>;
ClassType?: Type<T>;
keys?: ModelKey | ModelKey[];
};
type ArrayConfig<T = any> = {
defaultValue?: Partial<T>[];
ClassType?: Type<T>;
keys?: ModelKey | ModelKey[];
map?: (value: T, index: number, array: T[]) => T;
};
type FormatCallback = (item, values) => any;
名称 | 说明 |
---|---|
item | 属性被转换后的值 |
values | 源数据 注意:values 源数据的直接引用,请勿在转换过程中对其进行修改 |
shareValue | 共享数据 注意:shareValue 为共享数据的直接引用,请勿在转换过程中对其进行修改 |
type RemoveCallback = (value: any, target: Readonly<any>, shareValue?: any) => boolean;
type RemoveConfig = {
beforeRemove?: RemoveCallback;
keys?: ModelKey | ModelKey[];
};
interface Type<T = any> extends Function {
new(...args: any[]): T;
}
type ModelKey = string | number;
type FormatOptions<T> = {
mergeSource?: boolean;
key?: ModelKey;
shareValue?: any;
deep?: number;
map?: (value: T, index: number, array: T[]) => T;
}
名称 | 说明 | 类型 |
---|---|---|
mergeSource | 是否将 源数据 合并到转换结果中 | boolean |
key | 执行键 | ModelKey |
shareValue | 共享数据。可在自定义装饰器与 Format 中获取的额外数据 |
any |
deep | 转换深度限制。详情 | boolean |
map | 原生数组的 map 方法。仅在 executeTransArray 中生效 |
(value: T, index: number, array: T[]) => T |
const CustomDecorator = createFormatDecorator((values, shareValue, ...args) => {
// ...Do something
return values.name;
});
// type CustomeDecorator = (...args) => DecoratorFun
class User {
@CustomDecorator('Hello')
name!: string;
}
createFormatDecorator
属性 | 说明 | 类型 |
---|---|---|
callback | 装饰器执行回调 | (values, shareValue, ...args) => any |
keys | 可选参数 执行键 | ModelKey | ModelKey[] |
callback
属性 | 说明 | 类型 |
---|---|---|
values | 源数据 注意:values 为源数据的直接引用,请勿在转换过程中对其进行修改 |
any |
shareValue | 共享数据 注意:shareValue 为共享数据的直接引用,请勿在转换过程中对其进行修改 |
any |
args | 在生成的装饰器中传入的参数 | any[] |
所有指令丛上到下依次执行,指令格式化的结果会被传递给下一个指令。
const toAge = createFormatDecorator((values: Test) => {
return 9;
});
class Test {
@toAge() // 9
@toBoolean() // false
@toString('7') // '7'
@toNumber(5) // 7
@Format(v => v + 1) // 8
@Format(v => v - 7) // 1
age!: number;
}
const res = executeTransform(Test, {});
console.log(res); // { age: 1 }
executeTransform
的 options
属性中提供了 key
属性,以下称为 rootKey
。
在所有指令中均提供了 keys
属性的入口,以下称为 propertyKeys
。
- 若
rootKey
不存在,则会执行所有不存在propertyKeys
的指令。 - 若
rootKey
与propertyKeys
同时存在,仅有propertyKeys
包含rootKey
的指令会被执行。 - 不存在
propertyKeys
的指令会被无条件执行。
class User {
@toString({ keys: 'submit' })
name!: string;
@toNumber()
age!: number;
}
const user = {
name: '张三',
age: '18'
};
const formatUser = executeTransform(User, user, {
key: 'submit'
});
上述示例中:
-
toNumber
指令会无条件执行。 - 若
executeTransform
中传入的key
为'submit'
,则toString
指令会被执行,否则将忽略name
属性。
由于执行键的存在,我们可以方便的在同一个模板上定制多套格式化方法。如下:
class User {
@toString()
@toString({ defaultValue: '张三', keys: '1' })
@toString({ defaultValue: '李四', keys: '2' })
@toString({ defaultValue: '王五', keys: '3' })
name!: string;
@toNumber()
age!: number;
}
注意:当一个属性拥有多个装饰器时,模板的可读性下降,且难以迭代。因此建议优先考虑模板继承策略进行个性化复用。如无必要请尽量避免使用执行键。
class-formatter 提供了 createBatchDecorators
方法用于对多装饰器进行管理。
createBatchDecorators
属性 | 说明 | 类型 |
---|---|---|
...decorators | 需要统一管理的装饰器 | PropertyDecorator[] |
通过 createBatchDecorators
方法,我们可以对上述案例进行管理:
const NameManage = createBatchDecorators(
toString({ defaultValue: '张三', keys: '1' }),
toString({ defaultValue: '李四', keys: '2' }),
toString({ defaultValue: '王五', keys: '3' })
);
class User {
@toString()
@NameManage()
name!: string;
@toNumber()
age!: number;
}
如上将所有拥有执行键的装饰器封装成 NameManage
装饰器,toString
作为默认格式化指令,NameManage
则根据执行键分发指令。
- 若多个模板间存在循环引用,则子模板中引用的父模板失效(
typescript
自身限制)。 - 若模板自身循环引用,则默认可转化深度为 50,超过该深度的转换会被忽略。
- 若被转换对象存在循环引用,则忽略循环属性。
class Person {
@toString()
name!: string;
@toType(Person)
child!: Person;
}
const target = {
name: '父亲',
child: {
name: 1
}
}
// target.child 会被忽略
executeTransform(Person, {});
模板自循环理论上允许存在,但可能导致死循环。
为防止这种情况,且保证格式化顺利进行,class-formatter
限制了执行嵌套深度,默认 50。超过深度的数据会停止转换。
可以通过 options.deep
自行调整深度。
死循环例:
class Person {
@toString()
name!: string;
@toArray(Person)
childs: Person = [{}];
}
executeTransArray(Person, [{}], {
deep: 50
});
为实现更加灵活的模板组合方案,class-formatter
提供 mixins
方法实现多模板组合。
例如:
class A {
@toString()
a!: string;
}
class B {
@toString()
b!: string;
}
class C implements A, B {
a!: string;
b!: string;
@toString()
c!: string;
}
mixins(C, [A, B]);
如此 C
便即继承了 A
、B
的全部指令。
同时提供了 Mixins
类装饰器来简化混入。
class A {
@toString('A')
name!: string;
}
class B {
@toString('B')
name!: string;
}
@Mixins(A, B)
class C implements A, B {
@toString()
c!: string;
name!: string;
}
const res = executeTransform(C, {});
// res => { name: 'A', c: '' }
Mixins
传参拥有优先级,当多个模板中存在相同的属性时,后面参数的指令将会 完全覆盖 前一个参数的指令。
上述示例中,A 模板的 name
属性的指令将会被 B 模板的 name
完全覆盖。
- 所有转化规则均依赖指令,所有拥有指令的属性、方法、访问器均会被转换,其余属性、方法、访问器会被忽略。
- 指令可以通过
Extend
在多个模板间继承。 - 多个模板可通过
Mixins
组合成一个大模板,同时共享全部指令。 - 子模板中声名的同名属性若拥有指令,则子模板的指令将会 完全覆盖 继承的指令。(即重写指令)
- 子模板的 模板默认值 会覆盖父模板的 模板默认值。