class-formatter
v3.3.1
Published
一套装饰器风格的数据格式化方法。
Downloads
56
Readme
class-formatter 使用文档
注意:
当前版本仅支持 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
为指令传入。
- 上述模板中,
API
|属性装饰器|说明|类型|默认值|
|---|---|---|---|
|toNumber|若属性为非数字类型,则将属性转换为 number
类型。autoTrans
为 true
时会自动将字符串转换为数字。|(value?: NumberConfig | number) => Decorator|defaultValue: 0autoTrans: 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|
Interface
Decorator
type Decorator = (target: any, propertyKey: string) => void;
NumberConfig
type NumberConfig = {
defaultValue?: number;
autoTrans?: boolean;
keys?: ModelKey | ModelKey[];
};
StringConfig
type StringConfig = {
defaultValue?: string;
autoTrans?: boolean;
keys?: ModelKey | ModelKey[];
};
BooleanConfig
type BooleanConfig = {
defaultValue?: boolean;
keys?: ModelKey | ModelKey[];
};
SymbolConfig
type SymbolConfig = {
defaultValue?: symbol;
keys?: ModelKey | ModelKey[];
};
RegConfig
type RegConfig = {
defaultValue?: Regexp | string;
keys?: ModelKey | ModelKey[];
};
ObjectConfig
type ObjectConfig<T = any> = {
defaultValue?: Partial<T>;
ClassType?: Type<T>;
keys?: ModelKey | ModelKey[];
};
ArrayConfig
type ArrayConfig<T = any> = {
defaultValue?: Partial<T>[];
ClassType?: Type<T>;
keys?: ModelKey | ModelKey[];
map?: (value: T, index: number, array: T[]) => T;
};
FormatCallback
type FormatCallback = (item, values) => any;
|名称|说明| |---|---| |item|属性被转换后的值| |values|源数据注意:values 源数据的直接引用,请勿在转换过程中对其进行修改| |shareValue|共享数据注意:shareValue 为共享数据的直接引用,请勿在转换过程中对其进行修改|
RemoveCallback
type RemoveCallback = (value: any, target: Readonly<any>, shareValue?: any) => boolean;
RemoveConfig
type RemoveConfig = {
beforeRemove?: RemoveCallback;
keys?: ModelKey | ModelKey[];
};
Type
interface Type<T = any> extends Function {
new(...args: any[]): T;
}
ModelKey 执行键类型
type ModelKey = string | number;
FormatOptions
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
组合成一个大模板,同时共享全部指令。 - 子模板中声名的同名属性若拥有指令,则子模板的指令将会 完全覆盖 继承的指令。(即重写指令)
- 子模板的 模板默认值 会覆盖父模板的 模板默认值。