一、装饰器简介
1.1 什么是装饰器
一句话:装饰器就是一个接收特定参数的函数,使用@函数名
可以对一些类,属性,方法等进行装饰来实现一些 运行时 的 hook 拦截机制。
代码示例如下:
less
@readonly
class Test {
@log
test() {}
}
对于java,装饰器可以理解成java中的注解,如@Override
,@Deprecated
等。
对于python,等同于python的装饰器,如@classmethod
、@staticmethod
等。
1.2 如何启用装饰器
Decorator 装饰器是 ES7 的一个新语法,目前仍处于提案中,因此你需要经过一些配置才能使用。
1.2.1 在javascript中使用
- 安装
css
npm install --save-dev @babel/plugin-proposal-decorators
- 配置 .babelrc
json
{ "plugins": [ ["@babel/plugin-proposal-decorators", { "legacy": true }], ]}
1.2.2 在typescript中使用
由于装饰器是一项实验性功能,可以通过命令行 或者 tsconfig.json 中增加配置来使用。
- 命令行启用
css
tsc --target ES5 --experimentalDecorators --emitDecoratorMetadata
- tsconfig.json
json
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
二、装饰器分类
1.1 类装饰器
typescript
declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;
类装饰器是接收一个参数的函数,该参数就是被装饰的类自己。
如下示例,通过Animal
装饰器给装饰的对象添加一个eat
方法
javascript
function Animal(target:any){
console.log(target);// target就是Dog
target.prototype.eat = function() {
console.log('start eating');
};
}
@Animal
class Dog{}
const dog =new Dog();
dog.eat(); // start eating
如果我们要给这个 Animal 指定吃什么怎么办?
javascript
function Animal(food:string){
return function(target:any){
target.prototype.eat = function() {
console.log('start eating '+food);
};
}
}
@Animal("shit")
class Dog{}
const dog =new Dog();
dog.eat(); // start eating shit
如上所示,如果装饰器要接收我们的自定义参数,可以定义个函数接收你的自定义参数,然后返回一个标准的装饰器即可,这就是装饰器工厂模式。
1.2 方法装饰器
typescript
declare type MethodDecorator = <T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void;
方法装饰器是接收三个参数的函数,可以返回一个属性描述符 PropertyDescriptor,也可以不返回。
- 参数一: 是类的原型(prototype),
- 参数二: 是表示方法名,
- 参数三: 表示被装饰参数的属性描述符 PropertyDescriptor
如下示例,通过Type
装饰器来让 Dog 吃指定的东西。
typescript
class Dog{
@Type("shit")
eat(){
console.log("i want to eat")
}
}
function Type(type:string){
return function(target:any,property:string,descriptor:PropertyDescriptor){
const oldCb=descriptor.value;// 该方法的原始值
descriptor.value=function(){ // 修改装饰的方法为一个新的方法console.log("having "+type);
oldCb.call(this); //调用原函数
console.log(type); //hook逻辑
}
}
}
const dog =new Dog();
dog.eat(); // 1.i want to eat 2.shit
1.3 属性装饰器
typescript
declare type PropertyDecorator = (target: Object, propertyKey: string | symbol) => void;
属性装饰器是接收二个参数的函数。
- 参数一: 是类的原型(prototype),
- 参数二: 是表示属性名,
如下示例,通过Readonly
让 Dog 的 name 属性不可变更(只读)
vbnet
class Dog{
@Readonly
name="傻狗";
}
function Readonly(target:any,property:string){
Object.defineProperty(target.constructor, property, { writable: false })
}
const dog =new Dog();
dog.name="傻猫"; //报错,name不可修改
1.4 参数装饰器
typescript
declare type ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number) => void;
参数装饰器是接收三个参数的函数。
- 参数一: 是类的原型(prototype)
- 参数二: 是表示属性/方法名,
- 参数三: 表示该参数的索引(第几个)
如下示例,通过Shit
装饰器来让 Dog 的 eat 方法的参数强行变成 shit
less
class Dog{
eat(@Shit some:any){
console.log("i want to eat " + some);
}
}
function Shit(target:any,property:string,parameterIndex:number){
const oldCb=target[property];// 获取该方法的原始值
target[property]=function(...args:any[]){ // 修改装饰的方法为一个新的方法
args[parameterIndex]="shit"; //把指定位置参数强行转成shit
return oldCb.apply(this,args); //调用原函数
}
}
const dog =new Dog();
dog.eat("meat"); // 1. i want to eat shit
三、装饰器执行顺序
3.1 装饰器叠加
我们先来看看多个装饰器叠加在一起时候的上下执行顺序。
javascript
function Thin() {
console.log("进入Thin装饰器")
return function (target: any) {
console.log("执行Thin装饰器")
}
}
function Beautiful() {
console.log("进入Beautiful装饰器")
return function (target: any) {
console.log("执行Beautiful装饰器")
}
}
@Thin()
@Beautiful()
class Girl{
}
打印结果
scss
进入Thin装饰器
进入Beautiful装饰器
执行Beautiful装饰器 //为什么Beautiful装饰器先执行?
执行Thin装饰器
我们来看看编译结果,再来解释为什么会出现这种结果?
scss
var Girl = /** @class */ (function () {
function Girl() {
}
Girl = __decorate([
Thin(),
Beautiful()
], Girl);
return Girl;
}());
从编译结果可以看出
- 实际在执行 __decorate 之前会先从上到下先执行 Thin()/Beautiful()装饰器工厂函数以获得装饰器函数。
- 但是最终执行装饰器函数时候是从下向上先执行 Beautiful 装饰器,再执行 Thin 装饰器。
我们再看下面一个例子
typescript
function Thin(target: any) {
console.log("执行Thin装饰器")
}
function Beautiful(target: any) {
console.log("执行Beautiful装饰器")
}
@Thin
@Beautiful
class Girl {
}
编译结果如下:
php
var Girl = /** @class */ (function () {
function Girl() {
}
Girl = __decorate([
Thin,
Beautiful
], Girl);
return Girl;
}());
打印结果
scss
执行Beautiful装饰器
执行Thin装饰器
结论
- 如果是装饰器工厂函数形式
@decorator()
,会从上向下依次对装饰器工厂函数求值获得装饰器函数,然后再会从下向上执行。 - 装饰器
@decorator
形式,会从下向上执行
3.2 不同类型装饰器
同样的,我们来看个代码示例
less
//类装饰器
function Thin(target: any) {
console.log("执行Thin装饰器")
}
//方法装饰器
function Dance(target: any, property: string, descriptor: PropertyDescriptor) {
console.log("执行Dance装饰器")
}
//属性装饰器
function Age(target: any, property: string) {
console.log("执行Age装饰器")
}
//参数装饰器
function Jazz(target: any, property: string, parameterIndex:number) {
console.log("执行Jazz方法参数装饰器")
}
@Thin
class Girl {
@Age
age=20;
@Dance
dance(@Jazz type:string) {
}
}
打印结果
scss
执行Age装饰器
执行Jazz方法参数装饰器
执行Dance装饰器
执行Thin装饰器
调整age属性和方法dance顺序
less
@Thin
class Girl {
@Dance
dance(@Jazz type:string) {
}
@Age
age=20;
}
打印结果
scss
执行Jazz方法参数装饰器
执行Dance装饰器
执行Age装饰器
执行Thin装饰器
结论
- 属性和方法会按照书写顺序从上向下先执行
- 如果是方法装饰器中有参数装饰器,会优先执行参数装饰器,然后再执行对应方法装饰器
- 最后执行类装饰器
四、reflect-metadata
4.1 介绍
Reflect Metadata 同样也是 ES7 的一个提案,用于在运行时访问和操作装饰器的元数据。它提供了一组对元数据进行修改、查询、定义的 API,可以读取和写入装饰器相关的元数据信息,比如方法的参数类型,参数个数等等,常用于解决控制反转,依赖注入。
- 安装
css
npm i reflect-metadata
- 使用
arduino
import "reflect-metadata";//需要在代码顶部导入使用
//...other code
4.1 获取元数据
Reflect.getMetadata("design:type",...)
获取属性类型Reflect.getMetadata("design:paramtypes",...)
获取方法参数列表和类型Reflect.getMetadata("design:returntype",...)
获取方法返回类型
代码示例如下:
typescript
//属性装饰器
function Age(target: any, property: string) {
const type = Reflect.getMetadata('design:type', target, property);
console.log(type);//[Function: Number]
}
//方法装饰器
function Dance(target: any, property: string, descriptor: PropertyDescriptor) {
const type = Reflect.getMetadata('design:type', target, property);
console.log(type);//[Function: Function]
const paramtypes = Reflect.getMetadata('design:paramtypes', target, property);
console.log(paramtypes);//[ [Function: String] ]
const returntype = Reflect.getMetadata('design:returntype', target, property);
console.log(returntype);//[Function: Boolean]
}
class Girl {
@Age
age:number=20;
@Dance
dance(type:string):boolean {
return true;
}
}
4.2 自定义元数据
Reflect.metadata
是一个装饰器,可以直接@Reflect.metadata(key, value)
形式来给对应的类/方法/属性添加一些自定义的元数据。
typescript
@Reflect.metadata('cls-meta', '刘亦菲')
class Girl {
@Reflect.metadata('method-meta', '跳舞')
public dance() {
return 'i am dancing';
}
}
console.log(Reflect.getMetadata('cls-meta', Test));//刘亦菲
console.log(Reflect.getMetadata('method-meta', new Girl(), 'dance')); //跳舞
也可以在函数中通 Reflect.defineMetadata
给对应的类/方法/属性添加一些自定义的元数据
typescript
//类装饰器
function Thin(target: any) {
Reflect.defineMetadata('cls-meta', 'i am thin', target);
}
//方法装饰器
function Dance(target: any, property: string, descriptor: PropertyDescriptor) {
Reflect.defineMetadata('method-meta', 'i can dance', target,property);
}
@Thin
class Girl {
@Dance
dance(type:string):boolean {
return true;
}
}
console.log(Reflect.getMetadata('cls-meta', Girl)); // 'i am thin'
五、nestjs 路由实践
我们根据上面的介绍来实现一个简单的装饰器路由框架,来了解类似 nestjs 这类框架的核心原理。
5.1 实现路由装饰器
php
function Controller(path: string) {
return function(target:any){
Reflect.defineMetadata("controller_path", path, target);
}
}
function createHttpMethod(method: string) {
return function(path: string) {
return function(target:any,property:string){
Reflect.defineMetadata("route_http_method", method, target,property);
Reflect.defineMetadata("route_path", path, target,property);
}
}
}
const Get = createHttpMethod('GET');
const Post = createHttpMethod('POST');
const Put = createHttpMethod('PUT');
const Delete = createHttpMethod('DELETE');
5.2 使用路由
kotlin
@Controller('/byte')
class ByteController {
@Get('/dance')
dance(ctx:Context) {
ctx.body='byte dance';
}
}
5.3 注册路由
ini
function registerController(controller: any) {
const controllerCls = controller.constructor;//获取构造函数,也就是ByteController类,
const methods = Object.keys(controller.__proto__);//获取原型上定义的全部方法 ["dance"]
const controllerPath = Reflect.getMetadata("controller_path", controllerCls);//获取controller的path
const router = new KoaRouter({prefix: controllerPath});//创建路由router,并且绑定controller的path为前缀prefix路径
for (const method of methods) {
const httpMethod = Reflect.getMetadata("route_http_method", controller, method);//获取http-method
const routePath = Reflect.getMetadata("route_path", controller, method);//获取path
const handle = controller[method].bind(controller);//获取到方法名对应的函数
router.register(routePath, [httpMethod], handle);
}
const app = new Koa();//创建koa应用
app.use(router.routes()).listen(3000);//监听对应应用端口
}
registerController(new ByteController());
5.4 访问验证
访问 http://localhost:3000/byte/dance
返回 byte dance