从装饰器到 nestjs 浅析

一、装饰器简介

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中使用

  1. 安装
css 复制代码
npm install --save-dev @babel/plugin-proposal-decorators
  1. 配置 .babelrc
json 复制代码
{  "plugins": [    ["@babel/plugin-proposal-decorators", { "legacy": true }],  ]}

1.2.2 在typescript中使用

由于装饰器是一项实验性功能,可以通过命令行 或者 tsconfig.json 中增加配置来使用。

  1. 命令行启用
css 复制代码
tsc --target ES5 --experimentalDecorators --emitDecoratorMetadata
  1. 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;
}());

从编译结果可以看出

  1. 实际在执行 __decorate 之前会先从上到下先执行 Thin()/Beautiful()装饰器工厂函数以获得装饰器函数。
  2. 但是最终执行装饰器函数时候是从下向上先执行 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,可以读取和写入装饰器相关的元数据信息,比如方法的参数类型,参数个数等等,常用于解决控制反转,依赖注入。

  1. 安装
css 复制代码
npm i reflect-metadata
  1. 使用
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

相关推荐
田本初8 分钟前
如何修改npm包
前端·npm·node.js
明辉光焱29 分钟前
[Electron]总结:如何创建Electron+Element Plus的项目
前端·javascript·electron
牧码岛1 小时前
Web前端之汉字排序、sort与localeCompare的介绍、编码顺序与字典顺序的区别
前端·javascript·web·web前端
开心工作室_kaic1 小时前
ssm111基于MVC的舞蹈网站的设计与实现+vue(论文+源码)_kaic
前端·vue.js·mvc
晨曦_子画1 小时前
用于在 .NET 中构建 Web API 的 FastEndpoints 入门
前端·.net
慧都小妮子1 小时前
Spire.PDF for .NET【页面设置】演示:在 PDF 文件中添加图像作为页面背景
前端·pdf·.net·spire.pdf
咔咔库奇2 小时前
ES6基础
前端·javascript·es6
Jiaberrr2 小时前
开启鸿蒙开发之旅:交互——点击事件
前端·华为·交互·harmonyos·鸿蒙
bug爱好者2 小时前
如何解决sourcetree 一打开就闪退问题
前端·javascript·vue.js