The 2nd learning decorator record contains practical examples
Use a decorator
tsc --target ES5 --experimentalDecorators
tsconfig.json
{ "compilerOptions": { "target": "ES5", "experimentalDecorators": true } }
1. Class decorator
Class decorators are declared before the class declaration (immediately after the class declaration). Class decorators are applied to class constructors and can be used to monitor, modify or replace class definitions. Class decorators cannot be used in declaration files ( .d.ts) nor in any external context (such as declare d classes).
A class decorator expression is called at runtime as a function with the class's constructor as its only parameter.
If the class decorator returns a value, it replaces the class declaration with the provided constructor.
Basic usage
function classDecorator<T extends { new(...args: any[]): {} }>(constructor: T) { return class extends constructor { //Inherit the decorated class The methods and properties defined here will be added to it name = 'custom value' //custom add method test() { return 'custom added methods and' + this.name } } } @classDecorator class Test { } console.log(new Test().test());// Custom added methods and custom values
Example of use
import axios, { AxiosRequestConfig } from "axios"; // class decorator const requestDecorator = (baseURL) => <T extends { new(...args: any[]): {} }>(constructor: T) => { return class extends constructor { //post request encapsulation post(url, data = {}, config?: AxiosRequestConfig) { return axios({ method: 'POST', url: url, data: data, baseURL: baseURL, ...config, }) } //get request wrapper get(url, data = {}, config?: AxiosRequestConfig) { return axios({ method: 'GET', url: url, params: data, baseURL: baseURL, ...config, }) } //Inherit the decorated class The methods and properties defined here will be added to it } } @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. Method decorator
Method decorators are declared before (immediately) a method declaration. It is applied to the method's property descriptor and can be used to monitor, modify or replace the method definition. Method decorators cannot be used in declaration files ( .d.ts), in overloads or in any external context (such as declare d classes).
The method decorator expression will be called at runtime as a function, passing in the following 3 arguments:
1.For static members, it is the class constructor, and for instance members, it is the class's prototype object. 2.member's name. 3.The attribute descriptor of the member.
If the method decorator returns a value, it will be used as the method's property descriptor.
Notice If the code output target version is less than ES5 The return value is ignored.
Basic usage
const methodsDecorator = (name: string) => (target: any, methodsName: string, desc: PropertyDescriptor) => { const _value = desc.value; //function coverage desc.value = function () { //Here you can define the action you want _value.apply(this, arguments) } } class Test { @methodsDecorator() test(num) { } } console.log(new Test().test(123))
Error catch example
const error = (params = `function name error\n Parameter array: parms\n error message:error\n`) => (_, methodsName: any, desc: PropertyDescriptor) => { const newValue = function (...parms: any[]) { // Override the wrapper function and add a try catch try { return desc.value.apply(this, parms) // Borrow the encapsulated function and return the result, remember to write 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) // Error in function test //Parameter array: ["parameter 1", "parameter 2"] //Error message:{"message":"Error capture example","name":"Error"} } } return { value: newValue } } class Test { @error() test() { throw new Error('Error catch example') } } new Test().test('parameter 1','parameter 2')
3. Accessor Decorator
An accessor decorator declaration precedes an accessor declaration (immediately after the accessor declaration). Accessor decorators are applied to an accessor's property descriptor and can be used to monitor, modify or replace an accessor's definition. Accessor decorators cannot be used in declaration files (.d.ts), or in any external context (such as declare classes).
Notice TypeScript It is not allowed to decorate a member at the same time get and set accessor. Instead, all decorations of a member must be applied to the first accessor in document order. This is because, when the decorator is applied to a property descriptor, it combines get and set accessors, rather than being declared separately.
The accessor decorator expression will be called at runtime as a function, passing in the following 3 arguments:
1.For static members, it is the class constructor, and for instance members, it is the class's prototype object. 2.member's name. 3.The attribute descriptor of the member.
If the accessor decorator returns a value, it is used as the method's property descriptor.
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; } //set to not configurable @configurable(false) get x() { return this._x; } @configurable(false) get y() { return this._y; } } let res = new Point(1,2) res.x = 12 // You are not allowed to change this property
4. Attribute Decorator
A property decorator declaration precedes (immediately) a property declaration. Property decorators cannot be used in declaration files (.d.ts), or in any external context (such as declare classes).
Property decorator expressions are called at runtime as functions, passing in the following 2 arguments:
1.For static members, it is the class constructor, and for instance members, it is the class's prototype object. 2.member's name.
1. The return value is ignored.
2. Therefore, the property descriptor can only be used to monitor whether a property of a certain name is declared in the class.
Basic usage
//property decorator const attributeDecorator = () => (target: any, key: string) => { //Here you can do a very complex set of calculations and set default properties target[key] = 'default properties' } class Point { @attributeDecorator() private x: number; } console.log(new Point().x)
5. Parameter decorator
A parameter decorator declaration precedes (immediately) a parameter declaration. Argument decorators are applied to class constructors or method declarations. Parameter decorators cannot be used in declaration files (.d.ts), overloads or other external contexts (such as declare classes).
The parameter decorator expression will be called at runtime as a function, passing in the following 3 parameters:
1.For static members, it is the class constructor, and for instance members, it is the class's prototype object. 2.member's name. 3.The index of the parameter in the function parameter list.
The return value of the parameter decorator is ignored.
Parameter decorators must be used in conjunction with method decorators to make sense
Effect
class Test { @testMethods() test(@func(Number) num, @func(String) str) { console.log(typeof num) //Convert parameter to number console.log(typeof str) //Convert parameter to string } } new Test().test('123', 22222)
Decorator Definition
import "reflect-metadata"; //parameter decorator 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) } //method decorator 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) } }
metadata
1. Metadata is mainly implemented using the reflect-metadata library
2. The main function is to pass data in the class to different decorators for use
npm i reflect-metadata --save
tsconfig.json:
{ "compilerOptions": { "target": "ES5", "experimentalDecorators": true, "emitDecoratorMetadata": true } }
Introducing reflect-metadata in the global js will mount Reflect to the global
import "reflect-metadata";
1.Reflect.getMetadata(“design:paramtypes”, target, key) //Get the function parameter type
2.Reflect.getMetadata("design:returntype", target, key) //return value type
3.Reflect.getMetadata(“design:type”, target, key) // type
Example
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; }
customize
In addition to obtaining type information, it is often used to customize metadataKey and obtain its value at the right time. Examples are as follows:
function classDecorator(): ClassDecorator { return target => { // Define metadata on the class, the key is `classMetaData`, the value is `a` Reflect.defineMetadata('classMetaData', 'a', target); }; } function methodDecorator(): MethodDecorator { return (target, key, descriptor) => { // Define metadata on the prototype property 'someMethod' of the class, key is `methodMetaData`, value is `b` Reflect.defineMetadata('methodMetaData', 'b', target, key); }; } @classDecorator() class SomeClass { @methodDecorator() someMethod() {} } Reflect.getMetadata('classMetaData', SomeClass); // 'a' Reflect.getMetadata('methodMetaData', new SomeClass(), 'someMethod'); // 'b'
Example: Inversion of Control and Dependency Injection
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 => { // Get all injected services const providers = Reflect.getMetadata('design:paramtypes', target); // [OtherService] const args = providers.map((provider: Constructor) => new provider()); return new target(...args); }; Factory(TestService).testMethod(); // 1
Implementation of Controller and Get
If you are using TypeScript to develop Node applications, I believe you are no stranger to Controller, Get, POST decorators:
@Controller('/test') class SomeClass { @Get('/a') someGetMethod() { return 'hello world'; } @Post('/b') somePostMethod() {} }
These Decorator s are also implemented based on Reflect Metadata. This time, we define metadataKey on the value of descriptor:
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');
Next, create a function that maps out the route:
function mapRoute(instance: Object) { const prototype = Object.getPrototypeOf(instance); // Filter out the methodName of the class const methodsNames = Object.getOwnPropertyNames(prototype) .filter(item => !isConstructor(item) && isFunction(prototype[item])); return methodsNames.map(methodName => { const fn = prototype[methodName]; // Take out the defined metadata const route = Reflect.getMetadata(PATH_METADATA, fn); const method = Reflect.getMetadata(METHOD_METADATA, fn); return { route, method, fn, methodName } }) };
So we can get some useful information:
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' * }] * */
Finally, just bind the route related information to express or koa and it's ok.