TypeScript装饰器从入门到应用

2022-09-26 11:01:40 浏览数 (1)

前言

最近两年TypesScript简称“Ts”越来越火了,而且还在持续高涨。相信在不久后的将来,它将会成为我们日常开发中不可缺少的部分。我这么说是有依据的,因为在前端最火的框架中就已经有两个都更好的在支持使用Ts编写代码了,相信已经在使用中的朋友已经尝到香味了。

我所说的这两个框架目前支持最为好的是React,其次就是Vue,前段时间Vue作者也已经发布了Vue 3.0 beta版本,我想离正式发布已经不远了,所以还在使用Vue2的同学现在可以先尝尝鲜,提前了解一下最新版本的变动。同时最好开始学习一下Ts,无论今后你在看源码,还是做需求,我相信你现在的提前准备都是在帮助现在的自己。

正文

什么是装饰器?

装饰器是一种特殊类型的声明,它能够被附加到类声明,方法, 访问符,属性或参数上。装饰器使用 @expression这种形式,expression求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入。

例如,定义一个类装饰器

代码语言:javascript复制
function Contorller (target) {  
  // 可以通过target(类的构造函数)去做些事情
}

使用类装饰器

代码语言:javascript复制
@Contorller
clss Admin {}

使用多个装饰器

一个声明使用多个装饰器

例如, 写在多行的

代码语言:javascript复制
@Contorller
@Contorller1
class Admin {}

又或者写在一行

代码语言:javascript复制
@Contorller @Contorller1
class Admin {}

当多个装饰器应用于一个声明上,它们求值方式与复合函数相似。在这个模型下,当复合Contorller和Contorller1时,复合的结果Contorller(Contorller1(Admin))。

同样,在Ts里,当多个装饰器应用在一个声明上时会进行如下步骤的操作:

  • 由上至下依次对装饰器表达式求值。
  • 求值的结果会被当作函数,由下至上依次调用。

如果是一行的

  • 由左至右依次对装饰器表达式求值。
  • 求值的结果会被当作函数,由右至左依次调用。

工厂函数的装饰器

如果我们要定制一个修饰器如何应用到一个声明上,我们得写一个工厂函数装饰器。 其实就是一个简单的函数,它返回一个表达式,以供装饰器在运行时调用。也可以说是一个函数柯里化

下面举例一个工厂函数类装饰器

代码语言:javascript复制
function Contorller (path) {
  // 返回一个装饰器函数
  return function (target) {
    target.prototype.root = path
  }
}

@Contorller('//www.wujiabk.com')
class Admin {
  getRoot () {
    console.log(this.root)
  }
}

根据上面的例子就很容易理解了,这样一来装饰器就变得更加灵活了,可以传入任何我们需要的参数进行修饰应用。

类装饰器

类装饰器在类声明之前被声明(紧靠着类声明)。类装饰器应用于类构造函数,可以用来监视,修改或替换类定义。

注意:类装饰器不能用在声明文件中( .d.ts),也不能用在任何外部上下文中(比如declare的类)

类装饰器表达式会在运行时当作函数被调用,它唯一的参数就是类的构造函数。

代码语言:javascript复制
// 定义一个类装饰器
function Contorller (target) {
  target.prototype.getName = function () {
    console.log('WuJia')
  }
}

// 使用类装饰器
@Contorller
class Admin {}

// 实例化类
const admin = new Admin()
admin.getName() // 打印 WuJia

上面代码执行步骤是这样的,当Admin类被声明的时候,会执行Contorller装饰器函数,然后我们在装饰器函数内向构造函数的原型上添加了一个getName方法,当类被实例化后,当然就可以去调用我们通过装饰器注入进去的方法啦~

方法装饰器

方法装饰器声明在一个方法的声明之前(紧靠着方法声明)。它会被应用到方法的 属性描述符上,可以用来监视,修改或者替换方法定义。

注意:方法装饰器不能用在声明文件( .d.ts),重载或者任何外部上下文(比如declare的类)中。

方法装饰器表达式会在运行时当作函数被调用,它有三个参数:

  1. 对于静态方法来说是类的构造函数,对于原型方法来说是类的原型对象。
  2. 方法的名字。
  3. 方法的属性描述符{value: any, writable: boolean, enumerable: boolean, configurable: boolean}。

用代码来理解一下

代码语言:javascript复制
function Get (path) {
  return function (target, methodName, descriptor) {
    /* 这里是可以改写方法的
     let fn = attributes.value
     attributes.value = function () { 
       console.log(`改写了了${methodName}方法`)
       将path传入
       fn.call(target, path)
     }
    */
    console.log(target)
    console.log(`method:${methodName}`)
    console.log(`descriptor:${JSON.stringify(descriptor)}`)
  }
}

class Admin {
  @Get('/setname')
  static setName () {}
  @Get('/getName')  
  getName () {}
}

// 输出结果
/*
Admin { getName: [Function] }
method:getName
descriptor:{"writable":true,"enumerable":true,"configurable":true}

{ [Function: Admin] setName: [Function] }
method:setName
desc {"writable":true,"enumerable":true,"configurable":true}
*/

根据打印的结果,可以看到,如果装饰的是静态方法,第一个参数将是一个构造函数;如果装饰的不是一个静态方法,那么第一个参数将会是一个原型对象。

装饰器的实现使用了ES5的 Object.defineProperty 方法,这三个参数也和这个方法的参数一致。装饰器的本质就是一个函数语法糖,通过Object.defineProperty来修改类中一些属性,descriptor参数也是一个对象,是针对key属性的描述符,里面有控制目标对象的该属性是否可写的writable属性等。

访问器装饰器

访问器装饰器声明在一个访问器的声明之前(紧靠着访问器声明)。访问器装饰器应用于访问器的属性描述符并且可以用来监视,修改或替换一个访问器的定义。

注意:访问器装饰器不能用在声明文件中(.d.ts),或者任何外部上下文(比如declare的类)里。

TypeScript不允许为单个成员装饰get和set访问器。相反,该成员的所有装饰器必须应用于按文档顺序指定的第一个访问器。这是因为装饰器适用于属性描述符,它结合了get和set访问器,而不是单独的每个声明。

装饰器表达式会在运行时当作函数被调用,它的参数与方法访问器参数一样,所以就不一一列出了。

下面看一个例子理解一下

代码语言:javascript复制
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; }
}

function configurable(value: boolean) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        descriptor.configurable = value;
    };
}

在声明x,y访问器的时候,调用了configurable装饰器,通过装饰器设置了描述符对象中configurable属性的值

参数装饰器

参数装饰器声明在一个参数声明之前(紧靠着参数声明)。参数装饰器应用于类构造函数或方法声明。

注意:参数装饰器不能用在声明文件(.d.ts),重载或其它外部上下文(比如 declare的类)里。

参数装饰器只能用来监视一个方法的参数是否被传入。

参数装饰器表达式会在运行时当作函数被调用,它有三个参数:

  1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  2. 参数的名字。
  3. 参数在函数参数列表中的索引。

下面看例子

代码语言:javascript复制
function PathParam(paramName: string) {
    return function (target, methodName: string, paramIndex: number) {
        !target.meta && (target.meta = {});
        target.meta[paramIndex] = paramName;
    }
}

class HelloService {
    constructor() { }
    getUser( @PathParam("userId") userId: string) { }
}

console.log(HelloService.prototype.meta); // {'0':'userId'}

在getUser方法中使用了PathParam装饰器,在PathParam装饰器中,通过原型对象去设置了一个meta对象,然后对这个meta对象中通过参数下标和参数名称去添加键值,这样就形成了一个参数map。

属性装饰器

属性装饰器声明在一个属性声明之前(紧靠着属性声明)。属性装饰器不能用在声明文件中(.d.ts),或者任何外部上下文(比如declare的类)里。

注意:属性描述符不会做为参数传入属性装饰器,这与TypeScript是如何初始化属性装饰器的有关。因为目前没有办法在定义一个原型对象的成员时描述一个实例属性,并且没办法监视或修改一个属性的初始化方法。返回值也会被忽略。因此,属性描述符只能用来监视类中是否声明了某个名字的属性。

属性装饰器表达式会在运行时当作函数被调用,它有两个参数:

  1. 对于静态属性来说是类的构造函数,对于原型属性来说是类的原型对象。
  2. 属性的名字。

同样,用一个例子来理解一下

代码语言:javascript复制
function DefaultValue(value: string) {
    return function (target: any, propertyName: string) {
        target[propertyName] = value;
    }
}

class Hello {
    @DefaultValue("world")
    greeting: string;
}
console.log(new Hello().greeting); // 输出: world

在上面代码中,我们给greeting属性添加了一个工厂装饰器DefaultValue,装饰中通过第一参数原型对象和第二参数属性名称给greeting属性做了赋值操作,所以在最后就打印出了world。

装饰器加载顺序

类中不同声明上的装饰器将按以下规定的顺序应用:

  1. 参数装饰器,然后依次是方法装饰器,访问符装饰器,或属性装饰器应用到每个实例成员。
  2. 参数装饰器,然后依次是方法装饰器,访问符装饰器,或属性装饰器应用到每个静态成员。
  3. 参数装饰器应用到构造函数。
  4. 类装饰器应用到类。

应用

上面我们讲了基础部分,下面我们用一个完整demo来应用加强理解,不过demo中我不会运用以上讲解的所有内容。

开始之前,这里先要说一下,我们下面的demo会使用到元数据,使用的是reflect-metadata库做的支持,如果没有使用过元数据的同学还请提前了解。

reflect-metadata仓库地址:https://github.com/rbuckton/reflect-metadata

index.ts

代码语言:javascript复制
import express from 'express'
import router from './router'
import './controller'

const app = express()

app.use(router)

const port = 80
app.listen(port, () => {
  console.log(`http://127.0.0.1:${port}`)
})

使用express开启一个服务,导入路由与接口

router.ts

代码语言:javascript复制
import { Router } from 'express'

export default Router()

导出路由

controller.ts

代码语言:javascript复制
import { Request, Response, NextFunction } from 'express'
import { controller, use, get } from './decorator'

@controller('/api/hd')
export class AdminController {
  @get('/login')
  login(req: Request, res: Response): void {
    res.json({
      success: true,
      code: 200,
      message: '请求成功!',
    })
  }

  @get('/list')
  @use((req: Request, res: Response, next: NextFunction) => {
    console.log('执行了list1中间件')
    next()
  })
  getData(req: Request, res: Response): void {
    res.json({
      success: true,
      code: 200,
      message: '获取成功',
      data: [],
    })
  }
}

这里主要实现接口的逻辑部分,通过装饰器实现了路由注册

decorator.ts

代码语言:javascript复制
import { RequestHandler } from 'express'
import { Methods } from './request'
import router from './router'
import { AdminController } from './contorller' 

export enum Methods {
  get = 'get',
  post = 'post',
}

// 基于类的controller装饰器,注册路由
export const controller = (root: string) => {
  return (target: new (...args: any[]) => {}): void => {
    for (let key in target.prototype) {
      const path: string = Reflect.getMetadata('path', target.prototype, key)
      const method: Methods = Reflect.getMetadata('method', target.prototype, key)
      const middlewares: RequestHandler[] = Reflect.getMetadata('middlewares', target.prototype, key)
      const handler = target.prototype[key]
      if (path && method) {
        root = root.replace(/^//, '')
        const fullPath = root ? `/${root}${path}` : path
        middlewares ? router[method](fullPath, ...middlewares, handler) : router[method](fullPath, handler)
      }
    }
  }
}

// 基于类方法的装饰器,定义路由
const getReuesetDecorator = (method: Methods) => {
  return (path: string) => {
    return (target: AdminController, key: string, decorator: any): void => {
      Reflect.defineMetadata('path', path, target, key)
      Reflect.defineMetadata('method', method, target, key)
    }
  }
}
// get路由
export const get = getReuesetDecorator(Methods.get)
// post路由
export const post = getReuesetDecorator(Methods.post)

// 基于类方法的装饰器,定义路由中间件
export const use = (middleware: RequestHandler) => {
  return (target: AdminController, key: string, decorator: any): void => {
    const originMiddlewares: RequestHandler[] =  Reflect.getMetadata('middlewares', target, key) || []
    originMiddlewares.push(middleware)
    Reflect.defineMetadata('middlewares', originMiddlewares, target, key)
  }
}

装饰器逻辑部分

逻辑分析

在contoller文件中,我们调用了各装饰器,通过装饰器实现了路由的注册,下面我们来分析一下这个过程。

首先,在AdminContorller中,我们在getData方法声明前使用了get和use两个装饰器,这两个装饰器承担的任务分别是设置接口路径,给接口使用的中间件。

我们先看一下get装饰器,在get装饰器中,我们通过Reflect.defineMetadata定义了两个元数据,一个path,一个method,分别设置在了getData上面了,这也是当前这个装饰器做的主要任务。

同样在use装饰器中定义了一个middlewares元数据,也是定义在了getData上,并且通过获取元数据的方式做了一个方法上使用多个中间件的情况处理。

这两个中间件我们可以看作成是在记录我们需要注册的路由并使用到的中间件,而把最后的注册放在类的装饰器contorller身上,我们上面讲了装饰器的执行顺序,因为类装饰器是被最后执行的,并且可以通过类装饰器的target参数获取到所有的方法名,属性名,这样一来我们就可以获取到我们之前已经存好的元数据了,所以就可以直接进行路由动态注册。

在类的装饰器中,我们可以看到遍历了原型对象,这就是我上面所说的我们需要通过原型上面去获取方法名称,这样一来就可以通过方法名称去获取我们已经存好的对应元数据,最后就可以通过router直接进行接口的注册。

contorller装饰器接受了一个参数,其实这个参数我们是用来作为接口的root的,因为在接口开发中,可能业务不止只有一个系统,可能会有多个,比如:一个管理后台,一个app h5,那么我们需要开发的接口就有后台的和h5的,所以就可以去通过contorller的参数进行划分。

结语

以上就是全部内容了,有任何问题欢迎留言指正,一起讨论。

0 人点赞