第2次学习装饰器记录内含实际案例
使用装饰器
tsc --target ES5 --experimentalDecorators
tsconfig.json
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true
}
}
1.类装饰器
类装饰器在类声明之前被声明(紧靠着类声明)。 类装饰器应用于类构造函数,可以用来监视,修改或替换类定义。 类装饰器不能用在声明文件中( .d.ts),也不能用在任何外部上下文中(比如declare的类)。
类装饰器表达式会在运行时当作函数被调用,类的构造函数作为其唯一的参数。
如果类装饰器返回一个值,它会使用提供的构造函数来替换类的声明。
基本用法
function classDecorator<T extends { new(...args: any[]): {} }>(constructor: T) {
return class extends constructor {
//继承装饰的class 这里定义的方法及属性会添加到下去
name = '自定义的值'
//自定义添加的方法
test() {
return '自定义添加的方法和' + this.name
}
}
}
@classDecorator
class Test {
}
console.log(new Test().test());// 自定义添加的方法和自定义的值
使用示例
import axios, { AxiosRequestConfig } from "axios";
// 类装饰器
const requestDecorator = (baseURL) => <T extends { new(...args: any[]): {} }>(constructor: T) => {
return class extends constructor {
//post 请求封装
post(url, data = {}, config?: AxiosRequestConfig) {
return axios({
method: 'POST',
url: url,
data: data,
baseURL: baseURL,
...config,
})
}
//get 请求封装
get(url, data = {}, config?: AxiosRequestConfig) {
return axios({
method: 'GET',
url: url,
params: data,
baseURL: baseURL,
...config,
})
}
//继承装饰的class 这里定义的方法及属性会添加到下去
}
}
@requestDecorator(' https://www.baidu.com/')
class ClassTest {
[x: string]: any;
async getData() {
let res = await this.get('/s', {
wd: 'axios'
})
console.log(res.statusText)//OK
return res
}
}
new ClassTest().getData()
2.方法装饰器
方法装饰器声明在一个方法的声明之前(紧靠着方法声明)。 它会被应用到方法的 属性描述符上,可以用来监视,修改或者替换方法定义。 方法装饰器不能用在声明文件( .d.ts),重载或者任何外部上下文(比如declare的类)中。
方法装饰器表达式会在运行时当作函数被调用,传入下列3个参数:
1.对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
2.成员的名字。
3.成员的属性描述符。
如果方法装饰器返回一个值,它会被用作方法的属性描述符。
注意 如果代码输出目标版本小于ES5返回值会被忽略。
基本用法
const methodsDecorator = (name: string) => (target: any, methodsName: string, desc: PropertyDescriptor) => {
const _value = desc.value;
//函数覆盖
desc.value = function () {
//在这里你可以定义你想要的操作
_value.apply(this, arguments)
}
}
class Test {
@methodsDecorator()
test(num) {
}
}
console.log(new Test().test(123))
错误捕获示例
const error =
(params = `函数name出错了\n参数数组:parms\n错误信息:error\n`) =>
(_, methodsName: any, desc: PropertyDescriptor) => {
const newValue = function (...parms: any[]) { // 覆盖封装函数并添加 try catch
try {
return desc.value.apply(this, parms) // 借用封装的函数并返回结果,记得写 return
} catch (error) {
const errorText = params
.replace('name', methodsName)
.replace('parms', JSON.stringify(parms))
.replace('error', JSON.stringify( error instanceof Error ? { message: error.message, name: error.name } : error ))
console.log(errorText)
// 函数test出错了
//参数数组:["参数1","参数2"]
//错误信息:{"message":"错误捕获示例","name":"Error"}
}
}
return {
value: newValue
}
}
class Test {
@error()
test() {
throw new Error('错误捕获示例')
}
}
new Test().test('参数1','参数2')
3.访问器装饰器
访问器装饰器声明在一个访问器的声明之前(紧靠着访问器声明)。 访问器装饰器应用于访问器的 属性描述符并且可以用来监视,修改或替换一个访问器的定义。 访问器装饰器不能用在声明文件中(.d.ts),或者任何外部上下文(比如 declare的类)里。
注意 TypeScript不允许同时装饰一个成员的get和set访问器。取而代之的是,一个成员的所有装饰的必须应用在文档顺序的第一个访问器上。这是因为,在装饰器应用于一个属性描述符时,它联合了get和set访问器,而不是分开声明的。
访问器装饰器表达式会在运行时当作函数被调用,传入下列3个参数:
1.对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
2.成员的名字。
3.成员的属性描述符。
如果访问器装饰器返回一个值,它会被用作方法的属性描述符。
function configurable(value: boolean) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.configurable = value;
};
}
class Point {
private _x: number;
private _y: number;
constructor(x: number, y: number) {
this._x = x;
this._y = y;
}
//设置成不可配置
@configurable(false)
get x() { return this._x; }
@configurable(false)
get y() { return this._y; }
}
let res = new Point(1,2)
res.x = 12 // 不允许你改这个属性
4.属性装饰器
属性装饰器声明在一个属性声明之前(紧靠着属性声明)。 属性装饰器不能用在声明文件中(.d.ts),或者任何外部上下文(比如 declare的类)里。
属性装饰器表达式会在运行时当作函数被调用,传入下列2个参数:
1.对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
2.成员的名字。
1.返回值被忽略。
2.因此,属性描述符只能用来监视类中是否声明了某个名字的属性。
基本用法
//属性装饰器
const attributeDecorator = () => (target: any, key: string) => {
//在这里你可以做一套非常复杂的计算 出默认属性并设置下去
target[key] = '默认属性'
}
class Point {
@attributeDecorator()
private x: number;
}
console.log(new Point().x)
5.参数装饰器
参数装饰器声明在一个参数声明之前(紧靠着参数声明)。 参数装饰器应用于类构造函数或方法声明。 参数装饰器不能用在声明文件(.d.ts),重载或其它外部上下文(比如 declare的类)里。
参数装饰器表达式会在运行时当作函数被调用,传入下列3个参数:
1.对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
2.成员的名字。
3.参数在函数参数列表中的索引。
参数装饰器的返回值会被忽略。
参数装饰器必须要和方法装饰器配合使用才有意义
效果
class Test {
@testMethods()
test(@func(Number) num, @func(String) str) {
console.log(typeof num) //参数转化成 number
console.log(typeof str) //参数转化成 string
}
}
new Test().test('123', 22222)
装饰器定义
import "reflect-metadata";
//参数装饰器
const func = (params: any) => (target: any, method: string, index: number) => {
let res = Reflect.getMetadata(method, target) || []
res.push({ name: params, index: index })
Reflect.defineMetadata(method, res, target)
}
//方法装饰器
const testMethods = (params?: any) => (target: any, methodsName: any, desc: any) => {
let _value = desc.value;
let list = Reflect.getMetadata(methodsName, target)
desc.value = function () {
let param = Array.from(arguments)
for (const { name, index } of list) {
if (typeof name === 'function') {
param[index] = name(param[index])
} else {
param[index] = name
}
}
_value.apply(this, param)
}
}
元数据
1.元数据 主要是使用 reflect-metadata 这个库来实现的
2.主要作用是在class 中传递数据给不同的装饰器使用
npm i reflect-metadata --save
tsconfig.json:
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
在全局js 中引入 reflect-metadata 会挂载 Reflect 到全局
import "reflect-metadata";
1.Reflect.getMetadata(“design:paramtypes”, target, key) //获取函数参数类型
2.Reflect.getMetadata(“design:returntype”, target, key) //返回值类型
3.Reflect.getMetadata(“design:type”, target, key) // 类型
示例
function Prop(): PropertyDecorator {
return (target, key: string) => {
const type = Reflect.getMetadata('design:type', target, key);
console.log(`${key} type: ${type.name}`);
// other...
};
}
class SomeClass {
@Prop()
public Aprop!: string;
}
自定义
除能获取类型信息外,常用于自定义 metadataKey,并在合适的时机获取它的值,示例如下:
function classDecorator(): ClassDecorator {
return target => {
// 在类上定义元数据,key 为 `classMetaData`,value 为 `a`
Reflect.defineMetadata('classMetaData', 'a', target);
};
}
function methodDecorator(): MethodDecorator {
return (target, key, descriptor) => {
// 在类的原型属性 'someMethod' 上定义元数据,key 为 `methodMetaData`,value 为 `b`
Reflect.defineMetadata('methodMetaData', 'b', target, key);
};
}
@classDecorator()
class SomeClass {
@methodDecorator()
someMethod() {}
}
Reflect.getMetadata('classMetaData', SomeClass); // 'a'
Reflect.getMetadata('methodMetaData', new SomeClass(), 'someMethod'); // 'b'
例子:控制反转和依赖注入
type Constructor<T = any> = new (...args: any[]) => T;
const Injectable = (): ClassDecorator => target => {};
class OtherService {
a = 1;
}
@Injectable()
class TestService {
constructor(public readonly otherService: OtherService) {}
testMethod() {
console.log(this.otherService.a);
}
}
const Factory = <T>(target: Constructor<T>): T => {
// 获取所有注入的服务
const providers = Reflect.getMetadata('design:paramtypes', target); // [OtherService]
const args = providers.map((provider: Constructor) => new provider());
return new target(...args);
};
Factory(TestService).testMethod(); // 1
Controller 与 Get 的实现
如果你在使用 TypeScript 开发 Node 应用,相信你对 Controller、Get、POST 这些 Decorator,并不陌生:
@Controller('/test')
class SomeClass {
@Get('/a')
someGetMethod() {
return 'hello world';
}
@Post('/b')
somePostMethod() {}
}
这些 Decorator 也是基于 Reflect Metadata 实现,这次,我们将 metadataKey 定义在 descriptor 的 value 上:
const METHOD_METADATA = 'method';
const PATH_METADATA = 'path';
const Controller = (path: string): ClassDecorator => {
return target => {
Reflect.defineMetadata(PATH_METADATA, path, target);
}
}
const createMappingDecorator = (method: string) => (path: string): MethodDecorator => {
return (target, key, descriptor) => {
Reflect.defineMetadata(PATH_METADATA, path, descriptor.value);
Reflect.defineMetadata(METHOD_METADATA, method, descriptor.value);
}
}
const Get = createMappingDecorator('GET');
const Post = createMappingDecorator('POST');
接着,创建一个函数,映射出 route:
function mapRoute(instance: Object) {
const prototype = Object.getPrototypeOf(instance);
// 筛选出类的 methodName
const methodsNames = Object.getOwnPropertyNames(prototype)
.filter(item => !isConstructor(item) && isFunction(prototype[item]));
return methodsNames.map(methodName => {
const fn = prototype[methodName];
// 取出定义的 metadata
const route = Reflect.getMetadata(PATH_METADATA, fn);
const method = Reflect.getMetadata(METHOD_METADATA, fn);
return {
route,
method,
fn,
methodName
}
})
};
因此,我们可以得到一些有用的信息:
Reflect.getMetadata(PATH_METADATA, SomeClass); // '/test'
mapRoute(new SomeClass());
/**
* [{
* route: '/a',
* method: 'GET',
* fn: someGetMethod() { ... },
* methodName: 'someGetMethod'
* },{
* route: '/b',
* method: 'POST',
* fn: somePostMethod() { ... },
* methodName: 'somePostMethod'
* }]
*
*/
最后,只需把 route 相关信息绑在 express 或者 koa 上就 ok 了。
参考资料
1.ts 官网
2.深入理解 TypeScript